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 */
028public final class SourceSpan {
029
030    private final long startLine;
031    private final long startColumn;
032    private final long endLine;
033    private final long endColumn;
034
035    private SourceSpan(long startLine, long startColumn,
036                       long endLine, long endColumn) {
037        this.startLine = startLine;
038        this.startColumn = startColumn;
039        this.endLine = endLine;
040        this.endColumn = endColumn;
041    }
042
043    /**
044     * Derive a span from the AST node's start and end tokens.
045     * Null-safe: returns {@code null} when {@code node} is {@code null}
046     * or either boundary token is {@code null}.
047     */
048    public static SourceSpan of(TParseTreeNode node) {
049        if (node == null) return null;
050        return of(node.getStartToken(), node.getEndToken());
051    }
052
053    /**
054     * Derive a span from explicit start / end tokens. End position is
055     * computed as one past the last character of {@code end} so the
056     * resulting interval is half-open {@code [start, end)}.
057     *
058     * <p>Null-safe: returns {@code null} when either token is
059     * {@code null}.
060     */
061    public static SourceSpan of(TSourceToken start, TSourceToken end) {
062        if (start == null || end == null) return null;
063        String endText = end.getAstext();
064        if (endText == null) endText = "";
065        long endLine = end.lineNo;
066        long endColumn = end.columnNo + endText.length();
067        // Multi-line token: advance the line count and recompute the
068        // trailing column from the last newline.
069        int lastNl = endText.lastIndexOf('\n');
070        if (lastNl >= 0) {
071            long extraLines = 0;
072            for (int i = 0; i < endText.length(); i++) {
073                if (endText.charAt(i) == '\n') extraLines++;
074            }
075            endLine = end.lineNo + extraLines;
076            endColumn = endText.length() - lastNl - 1 + 1;
077            // +1 keeps the half-open convention (column index of the
078            // position one past the last char on the final line; the
079            // final line starts at column 1 by GSP convention).
080        }
081        return new SourceSpan(start.lineNo, start.columnNo, endLine, endColumn);
082    }
083
084    public long getStartLine() {
085        return startLine;
086    }
087
088    public long getStartColumn() {
089        return startColumn;
090    }
091
092    public long getEndLine() {
093        return endLine;
094    }
095
096    public long getEndColumn() {
097        return endColumn;
098    }
099
100    @Override
101    public boolean equals(Object o) {
102        if (this == o) return true;
103        if (!(o instanceof SourceSpan)) return false;
104        SourceSpan that = (SourceSpan) o;
105        return startLine == that.startLine
106                && startColumn == that.startColumn
107                && endLine == that.endLine
108                && endColumn == that.endColumn;
109    }
110
111    @Override
112    public int hashCode() {
113        return Objects.hash(startLine, startColumn, endLine, endColumn);
114    }
115
116    @Override
117    public String toString() {
118        return startLine + ":" + startColumn + "-" + endLine + ":" + endColumn;
119    }
120}