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}