001package gudusoft.gsqlparser.pp2.region; 002 003import gudusoft.gsqlparser.TCustomSqlStatement; 004import gudusoft.gsqlparser.TGSqlParser; 005import gudusoft.gsqlparser.TStatementList; 006import gudusoft.gsqlparser.TSyntaxError; 007 008import java.util.ArrayList; 009import java.util.Collections; 010import java.util.List; 011 012/** 013 * Immutable per-region outcome produced by {@link ParseRecoveryEngine}. 014 * 015 * <p>The outcome carries one of three statuses: 016 * 017 * <ul> 018 * <li>{@link Status#AST_OK} — the region's source slice parsed cleanly. 019 * {@link #getStatement()} returns the AST root captured from the 020 * parser. The {@link #getSyntaxErrors()} list is empty.</li> 021 * <li>{@link Status#AST_ERROR} — parse failed (or was skipped by the 022 * {@code maxRegionParseChars} safety valve, or the parser threw). 023 * {@link #getStatement()} is {@code null}. {@link #getSyntaxErrors()} 024 * carries the parser's diagnostics (possibly empty if the engine 025 * could not capture them — e.g., after a {@code Throwable}). The 026 * optional {@link #getEngineNote()} field carries a short message 027 * describing why this region was marked {@code AST_ERROR} when the 028 * parser itself did not report a diagnostic (e.g., region too large, 029 * parser threw).</li> 030 * <li>{@link Status#TRIVIA} — the region contains no solid tokens (only 031 * whitespace and / or comments). No parse was attempted. 032 * {@link #getStatement()} is {@code null}, {@link #getSyntaxErrors()} 033 * is empty.</li> 034 * </ul> 035 * 036 * <p><b>AST lifetime contract.</b> Every {@code AST_OK} outcome carries a 037 * reference to the {@link TGSqlParser} that produced it. That parser owns the 038 * captured AST (statement tree, source-token list, error list) and is the 039 * input S13 ({@code GuardedAstDelegate}) and S16 ({@code Pp2Engine}) hand to 040 * {@code FormatterFactory.pp(parser, opt)} via {@link #getParser()}. 041 * Two scenarios: 042 * 043 * <ul> 044 * <li><b>{@link ParseRecoveryEngine#parseRegion(StatementRange) parseRegion}:</b> 045 * the parser is the pool's single instance. Its state is overwritten 046 * on the <i>next</i> {@code parseRegion} / {@code parseAll} call. 047 * Consume the AST <i>before</i> requesting the next region.</li> 048 * <li><b>{@link ParseRecoveryEngine#parseAll(List) parseAll}:</b> every 049 * {@code AST_OK} outcome carries a freshly-allocated parser 050 * dedicated to that region (the engine probes each range via the 051 * pool, and on success re-parses the slice into a new 052 * {@code TGSqlParser} so {@code parser.sqlstatements} holds exactly 053 * one statement). Outcomes are mutually valid: a caller may iterate 054 * the returned list in any order and consume each AST without 055 * invalidating the rest. The contract is broken by the next 056 * {@code parseAll} / {@code parseRegion} call on the same engine 057 * only for outcomes produced via {@code parseRegion} (which share 058 * the pool); {@code parseAll}'s dedicated parsers outlive the engine 059 * call.</li> 060 * </ul> 061 * 062 * <p>Plan reference: §7.3/S12, §7.4/S12. 063 */ 064public final class RegionParseOutcome { 065 066 public enum Status { 067 /** Parse succeeded; {@link #getStatement()} is non-null. */ 068 AST_OK, 069 /** Parse failed, was skipped, or the parser threw. */ 070 AST_ERROR, 071 /** Region contains no solid tokens. No parse was attempted. */ 072 TRIVIA 073 } 074 075 private final StatementRange range; 076 private final Status status; 077 private final String parsedSql; 078 private final TCustomSqlStatement statement; // nullable 079 private final TGSqlParser parser; // nullable; non-null iff AST_OK 080 private final List<TSyntaxError> syntaxErrors; 081 private final String engineNote; // nullable 082 083 private RegionParseOutcome(StatementRange range, 084 Status status, 085 String parsedSql, 086 TCustomSqlStatement statement, 087 TGSqlParser parser, 088 List<TSyntaxError> syntaxErrors, 089 String engineNote) { 090 if (range == null) throw new NullPointerException("range"); 091 if (status == null) throw new NullPointerException("status"); 092 if (parsedSql == null) throw new NullPointerException("parsedSql"); 093 this.range = range; 094 this.status = status; 095 this.parsedSql = parsedSql; 096 this.statement = statement; 097 this.parser = parser; 098 // Defensive copy: callers (including ParseRecoveryEngine) often 099 // pass the parser's live list; without a copy, a subsequent parse() 100 // that mutates that list would silently change this outcome's 101 // visible diagnostics. 102 this.syntaxErrors = (syntaxErrors == null || syntaxErrors.isEmpty()) 103 ? Collections.<TSyntaxError>emptyList() 104 : Collections.unmodifiableList(new ArrayList<TSyntaxError>(syntaxErrors)); 105 this.engineNote = engineNote; 106 } 107 108 /** 109 * Build a successful outcome. The first statement of {@code stmts} is 110 * captured as the AST root for this region; the caller's per-region parse 111 * is expected to yield exactly one top-level statement, but the engine 112 * tolerates parsers that wrap output in additional containers by reading 113 * {@code stmts.get(0)} and ignoring extras. {@code stmts} may be 114 * {@code null} or empty, in which case the outcome is treated as 115 * {@code AST_ERROR} (parser claimed success but produced no statement). 116 * 117 * <p>{@code parser} is the {@link TGSqlParser} instance that produced the 118 * statement. It is exposed via {@link #getParser()} for S13's 119 * {@code GuardedAstDelegate} which feeds it to 120 * {@code FormatterFactory.pp(parser, opt)}. 121 */ 122 static RegionParseOutcome astOk(StatementRange range, String parsedSql, 123 TStatementList stmts, TGSqlParser parser) { 124 int stmtCount = (stmts == null) ? 0 : stmts.size(); 125 if (stmtCount == 0) { 126 return new RegionParseOutcome(range, Status.AST_ERROR, parsedSql, 127 null, null, null, 128 "parser reported success but produced no statement"); 129 } 130 if (stmtCount > 1) { 131 // Defensive: S13 hands the outcome's parser to 132 // FormatterFactory.pp(parser, opt) which iterates EVERY 133 // statement in parser.sqlstatements. If a single region's 134 // source slice somehow parses into multiple top-level 135 // statements (e.g. the boundary detector's known DECLARE-clause 136 // limitation per the S11 resume), routing this through the AST 137 // path would render siblings the caller did not ask for. Force 138 // the lexical fallback instead. 139 return new RegionParseOutcome(range, Status.AST_ERROR, parsedSql, 140 null, null, null, 141 "region parsed into " + stmtCount + " statements; AST path " 142 + "expects exactly one — routing to lexical fallback"); 143 } 144 if (parser == null) throw new NullPointerException("parser"); 145 return new RegionParseOutcome(range, Status.AST_OK, parsedSql, 146 stmts.get(0), parser, null, null); 147 } 148 149 /** 150 * Build an {@code AST_ERROR} outcome. Used by the engine for parse 151 * failures and safety-valve skips; also useful to tests and downstream 152 * stages that need to package a synthetic error outcome (e.g., a 153 * region the boundary detector emitted but parsing was bypassed). 154 * 155 * <p>{@code errors} may be {@code null} or empty. 156 */ 157 public static RegionParseOutcome astError(StatementRange range, 158 String parsedSql, 159 List<TSyntaxError> errors, 160 String note) { 161 return new RegionParseOutcome(range, Status.AST_ERROR, parsedSql, 162 null, null, errors, note); 163 } 164 165 /** 166 * Build a {@code TRIVIA} outcome (region with no solid tokens — only 167 * whitespace and/or comments). 168 */ 169 public static RegionParseOutcome trivia(StatementRange range, 170 String parsedSql) { 171 return new RegionParseOutcome(range, Status.TRIVIA, parsedSql, 172 null, null, null, null); 173 } 174 175 public StatementRange getRange() { return range; } 176 public Status getStatus() { return status; } 177 178 /** The source text that was (or would have been) handed to the parser. */ 179 public String getParsedSql() { return parsedSql; } 180 181 /** 182 * The captured AST root for {@code AST_OK} outcomes; {@code null} 183 * otherwise. See the class Javadoc for the AST lifetime contract. 184 */ 185 public TCustomSqlStatement getStatement() { return statement; } 186 187 /** 188 * The {@link TGSqlParser} that produced the AST for this outcome. 189 * Non-null only when {@link #getStatus()} is {@link Status#AST_OK}; 190 * {@code null} otherwise. See the class Javadoc for the lifetime 191 * contract; in particular, after the next 192 * {@link ParseRecoveryEngine#parseRegion(StatementRange)} or 193 * {@link ParseRecoveryEngine#parseAll(List)} call the parser handed back 194 * here may have been overwritten in place (for outcomes produced via 195 * the pool) and must not be reused. 196 */ 197 public TGSqlParser getParser() { return parser; } 198 199 /** Immutable list of syntax errors. Empty for {@code AST_OK} and {@code TRIVIA}. */ 200 public List<TSyntaxError> getSyntaxErrors() { return syntaxErrors; } 201 202 /** 203 * Short engine-level note explaining the outcome when the parser itself 204 * did not report a diagnostic. Examples: "region exceeds 205 * maxRegionParseChars", "parser threw: NullPointerException". 206 * {@code null} when not applicable. 207 */ 208 public String getEngineNote() { return engineNote; } 209 210 @Override 211 public String toString() { 212 return "RegionParseOutcome{" + status + " " + range 213 + (statement != null ? " ast=" + statement.getClass().getSimpleName() : "") 214 + " errs=" + syntaxErrors.size() 215 + (engineNote != null ? " note='" + engineNote + "'" : "") 216 + "}"; 217 } 218}