001package gudusoft.gsqlparser.pp2.token; 002 003import gudusoft.gsqlparser.TSourceToken; 004import gudusoft.gsqlparser.TSourceTokenList; 005 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.Iterator; 009import java.util.List; 010import java.util.NoSuchElementException; 011 012/** 013 * Immutable, indexed view of a {@link TSourceTokenList} as a sequence of 014 * {@link Pp2Token}s. 015 * 016 * <p>The constructor copies token <i>references</i> from the input 017 * {@code TSourceTokenList} into a private {@link ArrayList} — the source 018 * list is never mutated, and the resulting stream cannot be structurally 019 * mutated (no add/remove/clear). Per-token role bookkeeping on individual 020 * {@link Pp2Token}s remains writable because pp2 stages annotate roles 021 * incrementally (see {@code TokenRole}). 022 * 023 * <p>Two factory paths are supported: 024 * <ul> 025 * <li>{@link #fromSourceTokenList(TSourceTokenList)} — basic wrap with 026 * zero {@code precedingBlanks}/{@code precedingLinebreaks}. Used by 027 * early Phase-1 callers and tests.</li> 028 * <li>{@link #ofTokens(List)} — internal entry point used by 029 * {@code Pp2TokenStreamBuilder} (S7), which folds whitespace into the 030 * next solid token's preceding-whitespace counts before constructing 031 * the stream.</li> 032 * </ul> 033 * 034 * <p>Probe A7 (slice S1) established that {@code TGSqlParser} reuses the 035 * same {@code TSourceTokenList} reference across calls. This stream's 036 * copy-on-construct contract is the reason that finding is not a footgun: 037 * the next re-tokenization cannot invalidate an already-built stream's 038 * <i>structure</i>. 039 * 040 * <h2>Two-tier stability contract</h2> 041 * 042 * <ul> 043 * <li><b>Structural stability</b> (guaranteed): after a stream is built, 044 * {@link #size()} and the per-index {@link Pp2Token} wrapper 045 * references are stable. Re-tokenizing the source parser does not 046 * grow, shrink, or re-shuffle the stream, and never replaces a 047 * wrapper reference at a given index. {@link #toReadOnlyList()} 048 * returns this stable structure.</li> 049 * <li><b>Text stability</b> (NOT guaranteed): {@code Pp2Token.getText()} 050 * delegates live to the wrapped {@code TSourceToken}. If the parser 051 * re-tokenizes a different SQL and re-uses the underlying 052 * {@code TSourceToken} fields, the live text returned by an existing 053 * wrapper changes. Neither {@link #toReadOnlyList()} nor any other 054 * method in this class snapshots text. Callers that must outlive a 055 * parser re-tokenization MUST snapshot text into their own 056 * {@code List<String>} before the second tokenize call (see 057 * {@code Pp2TokenStreamTest#textSnapshotSurvivesParserReuse}).</li> 058 * </ul> 059 */ 060public final class Pp2TokenStream implements Iterable<Pp2Token> { 061 062 private final List<Pp2Token> tokens; 063 064 /** Internal constructor. Use {@link #ofTokens(List)} or factories. */ 065 private Pp2TokenStream(List<Pp2Token> tokens) { 066 // Defensive copy into a fresh ArrayList so the caller cannot 067 // structurally mutate the stream after construction. 068 this.tokens = new ArrayList<Pp2Token>(tokens); 069 } 070 071 /** 072 * Build a stream from a previously-prepared list of {@link Pp2Token}s. 073 * The supplied list is copied; subsequent caller mutations do not leak. 074 * 075 * @throws NullPointerException if {@code tokens} or any element is null 076 */ 077 public static Pp2TokenStream ofTokens(List<Pp2Token> tokens) { 078 if (tokens == null) throw new NullPointerException("tokens"); 079 for (int i = 0; i < tokens.size(); i++) { 080 if (tokens.get(i) == null) { 081 throw new NullPointerException("tokens[" + i + "]"); 082 } 083 } 084 return new Pp2TokenStream(tokens); 085 } 086 087 /** 088 * Build a stream by wrapping each {@link TSourceToken} in the input 089 * list as a {@link Pp2Token} with zero preceding whitespace. 090 * 091 * <p>Smarter folding of whitespace into {@code precedingBlanks}/ 092 * {@code precedingLinebreaks} is the job of 093 * {@code Pp2TokenStreamBuilder} (S7); this factory is the minimal 094 * Phase-1 wrap. 095 * 096 * <p>Contract: the input {@code TSourceTokenList} is not mutated. The 097 * stream holds the same {@code TSourceToken} references as the input, 098 * verifiable in tests by reference + text equality before vs after 099 * construction. 100 * 101 * <p>A null element in the source list is treated as a parser-side bug 102 * and triggers a {@link NullPointerException} — silently skipping would 103 * shift the per-index mapping and hide corruption. 104 * 105 * @throws NullPointerException if {@code source} is null or contains a 106 * null element 107 */ 108 public static Pp2TokenStream fromSourceTokenList(TSourceTokenList source) { 109 if (source == null) throw new NullPointerException("source"); 110 List<Pp2Token> wrapped = new ArrayList<Pp2Token>(source.size()); 111 for (int i = 0; i < source.size(); i++) { 112 TSourceToken t = source.get(i); 113 if (t == null) { 114 throw new NullPointerException("source[" + i + "] — " 115 + "null entry in TSourceTokenList; pp2 treats this as " 116 + "parser corruption rather than silently shrinking the stream"); 117 } 118 wrapped.add(new Pp2Token(t)); 119 } 120 return new Pp2TokenStream(wrapped); 121 } 122 123 public int size() { 124 return tokens.size(); 125 } 126 127 public boolean isEmpty() { 128 return tokens.isEmpty(); 129 } 130 131 /** 132 * @throws IndexOutOfBoundsException if {@code index} is out of range 133 */ 134 public Pp2Token get(int index) { 135 return tokens.get(index); 136 } 137 138 /** Unmodifiable view of the underlying token list. */ 139 public List<Pp2Token> toReadOnlyList() { 140 return Collections.unmodifiableList(tokens); 141 } 142 143 @Override 144 public Iterator<Pp2Token> iterator() { 145 return new Iterator<Pp2Token>() { 146 int idx = 0; 147 @Override public boolean hasNext() { return idx < tokens.size(); } 148 @Override public Pp2Token next() { 149 if (idx >= tokens.size()) throw new NoSuchElementException(); 150 return tokens.get(idx++); 151 } 152 @Override public void remove() { 153 throw new UnsupportedOperationException("Pp2TokenStream is immutable"); 154 } 155 }; 156 } 157 158 @Override 159 public String toString() { 160 return "Pp2TokenStream{size=" + tokens.size() + "}"; 161 } 162}