001package gudusoft.gsqlparser.ir.semantic; 002 003import gudusoft.gsqlparser.TSourceToken; 004import gudusoft.gsqlparser.nodes.TParseTreeNode; 005 006import java.util.Objects; 007 008/** 009 * Half-open source-text range, {@code [startLine:startColumn, 010 * endLine:endColumn)}. Used by {@link Diagnostic} to point at the 011 * offending AST node when a reject site has one in scope. 012 * 013 * <p>{@code startLine} / {@code startColumn} mark the first character 014 * of the anchor (1-based as populated by GSP's lexer via 015 * {@link TSourceToken#lineNo} / {@link TSourceToken#columnNo}). 016 * {@code endLine} / {@code endColumn} mark the position 017 * <strong>one past</strong> the last character. For a single-line 018 * single-token anchor of length {@code L}, {@code endColumn == 019 * startColumn + L}. Multi-line tokens (e.g. triple-quoted strings) 020 * are handled by counting newlines in the end token's text. 021 * 022 * <p>The class is intentionally null-tolerant at the factory level: 023 * {@link #of(TParseTreeNode)} returns {@code null} when the node or 024 * either of its boundary tokens is {@code null}. Diagnostic factories 025 * pass this {@code null} through to {@link Diagnostic#getSpan()} 026 * unchanged. 027 * 028 * <p><b>API status:</b> read-only consumption of analyzer-produced spans is 029 * part of Join Analysis Consumption Profile v1. Factories are 030 * producer-oriented and are outside that profile. 031 */ 032public final class SourceSpan { 033 034 private final long startLine; 035 private final long startColumn; 036 private final long endLine; 037 private final long endColumn; 038 039 private SourceSpan(long startLine, long startColumn, 040 long endLine, long endColumn) { 041 this.startLine = startLine; 042 this.startColumn = startColumn; 043 this.endLine = endLine; 044 this.endColumn = endColumn; 045 } 046 047 /** 048 * Construct an explicit half-open source span. Coordinates are 1-based, 049 * and the end must not precede the start. 050 */ 051 public static SourceSpan of(long startLine, long startColumn, 052 long endLine, long endColumn) { 053 if (startLine < 1 || startColumn < 1 || endLine < 1 || endColumn < 1) { 054 throw new IllegalArgumentException("source coordinates must be positive"); 055 } 056 if (endLine < startLine 057 || (endLine == startLine && endColumn < startColumn)) { 058 throw new IllegalArgumentException("span end must not precede span start"); 059 } 060 return new SourceSpan(startLine, startColumn, endLine, endColumn); 061 } 062 063 /** 064 * Derive a span from the AST node's start and end tokens. 065 * Null-safe: returns {@code null} when {@code node} is {@code null} 066 * or either boundary token is {@code null}. 067 */ 068 public static SourceSpan of(TParseTreeNode node) { 069 if (node == null) return null; 070 return of(node.getStartToken(), node.getEndToken()); 071 } 072 073 /** 074 * Derive a span from explicit start / end tokens. End position is 075 * computed as one past the last character of {@code end} so the 076 * resulting interval is half-open {@code [start, end)}. 077 * 078 * <p>Null-safe: returns {@code null} when either token is 079 * {@code null}. 080 */ 081 public static SourceSpan of(TSourceToken start, TSourceToken end) { 082 if (start == null || end == null) return null; 083 String endText = end.getAstext(); 084 if (endText == null) endText = ""; 085 long endLine = end.lineNo; 086 long endColumn = end.columnNo + endText.length(); 087 // Multi-line token: advance the line count and recompute the 088 // trailing column from the last newline. 089 int lastNl = endText.lastIndexOf('\n'); 090 if (lastNl >= 0) { 091 long extraLines = 0; 092 for (int i = 0; i < endText.length(); i++) { 093 if (endText.charAt(i) == '\n') extraLines++; 094 } 095 endLine = end.lineNo + extraLines; 096 endColumn = endText.length() - lastNl - 1 + 1; 097 // +1 keeps the half-open convention (column index of the 098 // position one past the last char on the final line; the 099 // final line starts at column 1 by GSP convention). 100 } 101 return new SourceSpan(start.lineNo, start.columnNo, endLine, endColumn); 102 } 103 104 public long getStartLine() { 105 return startLine; 106 } 107 108 public long getStartColumn() { 109 return startColumn; 110 } 111 112 public long getEndLine() { 113 return endLine; 114 } 115 116 public long getEndColumn() { 117 return endColumn; 118 } 119 120 @Override 121 public boolean equals(Object o) { 122 if (this == o) return true; 123 if (!(o instanceof SourceSpan)) return false; 124 SourceSpan that = (SourceSpan) o; 125 return startLine == that.startLine 126 && startColumn == that.startColumn 127 && endLine == that.endLine 128 && endColumn == that.endColumn; 129 } 130 131 @Override 132 public int hashCode() { 133 return Objects.hash(startLine, startColumn, endLine, endColumn); 134 } 135 136 @Override 137 public String toString() { 138 return startLine + ":" + startColumn + "-" + endLine + ":" + endColumn; 139 } 140}