001package gudusoft.gsqlparser.pp2.token;
002
003/**
004 * A half-open {@code [startOffset, endOffset)} span over the original
005 * input SQL string, tagged with what kind of content lives there.
006 *
007 * <p>Used by {@link SourceSpanLedger} to cover every byte of the input.
008 * Immutable.
009 *
010 * <p>Plan reference: §6 ({@code token/SourceSpan.java}) and §10.3 (the
011 * construction sketch).
012 */
013public final class SourceSpan {
014
015    /** Classification of what the span represents. */
016    public enum Kind {
017        /**
018         * A solid lexer token: keyword, identifier, literal, operator,
019         * punctuation. Layout rules may freely re-space around it.
020         */
021        TOKEN,
022
023        /**
024         * Whitespace (including newlines). The assembler may rewrite
025         * trivia spans according to the active layout rules.
026         */
027        TRIVIA,
028
029        /**
030         * A region whose bytes must be emitted verbatim: comments, string
031         * literals, quoted identifiers, hints, {@code --BEGIN_NO_FORMAT}
032         * blocks. Slice S9 expands this classification.
033         */
034        PROTECTED,
035
036        /**
037         * A byte range the tokenizer did not cover — pp2 records the gap
038         * here, never silently drops it. Every {@code RAW_FALLBACK} span
039         * yields a {@code FormatDiagnostic} on the result.
040         */
041        RAW_FALLBACK
042    }
043
044    private final int startOffset;
045    private final int endOffset;
046    private final Kind kind;
047    private final String text;
048
049    public SourceSpan(int startOffset, int endOffset, Kind kind, String text) {
050        if (kind == null) throw new NullPointerException("kind");
051        if (text == null) throw new NullPointerException("text");
052        if (startOffset < 0) {
053            throw new IllegalArgumentException("startOffset < 0: " + startOffset);
054        }
055        if (endOffset < startOffset) {
056            throw new IllegalArgumentException(
057                "endOffset < startOffset: " + endOffset + " < " + startOffset);
058        }
059        if (text.length() != endOffset - startOffset) {
060            throw new IllegalArgumentException(
061                "text length " + text.length() + " does not match span width "
062                + (endOffset - startOffset)
063                + " (start=" + startOffset + " end=" + endOffset + ")");
064        }
065        this.startOffset = startOffset;
066        this.endOffset = endOffset;
067        this.kind = kind;
068        this.text = text;
069    }
070
071    public int getStartOffset() { return startOffset; }
072    public int getEndOffset() { return endOffset; }
073    public Kind getKind() { return kind; }
074    public String getText() { return text; }
075
076    /** Convenience: the half-open width of the span. Equals {@code text.length()}. */
077    public int length() { return endOffset - startOffset; }
078
079    @Override
080    public String toString() {
081        return "SourceSpan{" + kind + " [" + startOffset + ".." + endOffset
082            + "] '" + (text.length() > 40 ? text.substring(0, 40) + "…" : text) + "'}";
083    }
084}