001package gudusoft.gsqlparser.pp2.render; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.pp.logger.PPLogger; 005import gudusoft.gsqlparser.pp2.Pp2FormatOptions; 006import gudusoft.gsqlparser.pp2.RendererId; 007import gudusoft.gsqlparser.pp2.dialect.DialectRegistry; 008import gudusoft.gsqlparser.pp2.island.BlockScopeDetector; 009import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator; 010import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult; 011import gudusoft.gsqlparser.pp2.island.IslandRecognizer; 012import gudusoft.gsqlparser.pp2.island.SqlScopeDetector; 013import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult; 014import gudusoft.gsqlparser.pp2.layout.LayoutContext; 015import gudusoft.gsqlparser.pp2.layout.LayoutRulePipeline; 016import gudusoft.gsqlparser.pp2.layout.rules.AlignmentRules; 017import gudusoft.gsqlparser.pp2.layout.rules.AndOrConcatRules; 018import gudusoft.gsqlparser.pp2.layout.rules.CaseModificationRules; 019import gudusoft.gsqlparser.pp2.layout.rules.CaseRules; 020import gudusoft.gsqlparser.pp2.layout.rules.ClauseLinebreakRules; 021import gudusoft.gsqlparser.pp2.layout.rules.IndentRules; 022import gudusoft.gsqlparser.pp2.layout.rules.JoinRules; 023import gudusoft.gsqlparser.pp2.layout.rules.ParenAndCommaRules; 024import gudusoft.gsqlparser.pp2.layout.rules.SetOperatorRules; 025import gudusoft.gsqlparser.pp2.layout.rules.SpacingRules; 026import gudusoft.gsqlparser.pp2.region.RegionParseOutcome; 027import gudusoft.gsqlparser.pp2.region.StatementRange; 028import gudusoft.gsqlparser.pp2.token.Pp2Token; 029import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 030import gudusoft.gsqlparser.pp2.token.TokenEquivalence; 031 032import java.util.ArrayList; 033import java.util.List; 034 035/** 036 * The full lexical island renderer: runs the S18-S29 analysis + layout pipeline 037 * over a region's tokens and emits the formatted text via {@link Pp2OutputWriter}. 038 * 039 * <h2>Pipeline</h2> 040 * 041 * <ol> 042 * <li>Slice a region-local {@link Pp2TokenStream} from the region's token range.</li> 043 * <li>Run S20 SQL scope, S19 block scope, S21 clause annotation, and S22 island 044 * recognition over the slice.</li> 045 * <li>Build a {@link LayoutContext} (with those analyses + the vendor's 046 * {@link gudusoft.gsqlparser.pp2.dialect.DialectStrategy}) and run the full 047 * {@link LayoutRulePipeline}: spacing, clause/set-operator linebreaks, 048 * JOIN/paren/comma, CASE/AND-OR, indent/alignment, and case modification.</li> 049 * <li>Render with {@link Pp2OutputWriter}.</li> 050 * </ol> 051 * 052 * <h2>Fall-through contract</h2> 053 * 054 * <p>Per {@link RegionRenderer}, this returns {@code null} (so the engine falls 055 * back to the conservative renderer) on any {@link Throwable}, or if a 056 * {@link TokenEquivalence} content guard detects that the formatted output lost 057 * a solid token. Content preservation and never-throw are thus upheld even if a 058 * layout rule misbehaves — the conservative renderer remains the safety net. 059 * 060 * <p>Plan reference: §5.2, §7.3/S31, §7.4/S31, §10.4. 061 */ 062public final class LexicalIslandRenderer implements RegionRenderer { 063 064 private final EDbVendor vendor; 065 private final LayoutRulePipeline pipeline; 066 private final Pp2OutputWriter writer = new Pp2OutputWriter(); 067 private final SqlScopeDetector sqlScope = new SqlScopeDetector(); 068 private final BlockScopeDetector blockScope = new BlockScopeDetector(); 069 private final ClauseScopeAnnotator clauseScope = new ClauseScopeAnnotator(); 070 private final IslandRecognizer islands = new IslandRecognizer(); 071 072 private long successCount; 073 private long fallthroughCount; 074 075 public LexicalIslandRenderer(EDbVendor vendor) { 076 if (vendor == null) throw new NullPointerException("vendor"); 077 this.vendor = vendor; 078 // Stateless rules, registered low-to-high priority (the conflict 079 // resolver enforces precedence regardless, but this reads naturally). 080 this.pipeline = new LayoutRulePipeline() 081 .register(new SpacingRules()) 082 .register(new ClauseLinebreakRules()) 083 .register(new SetOperatorRules()) 084 .register(new JoinRules()) 085 .register(new ParenAndCommaRules()) 086 .register(new CaseRules()) 087 .register(new AndOrConcatRules()) 088 .register(new IndentRules()) 089 .register(new AlignmentRules()) 090 .register(new CaseModificationRules()); 091 } 092 093 public long getSuccessCount() { return successCount; } 094 public long getFallthroughCount() { return fallthroughCount; } 095 096 @Override 097 public RendererId id() { return RendererId.LEXICAL_ISLAND; } 098 099 @Override 100 public String render(RegionParseOutcome outcome, Pp2TokenStream stream, 101 Pp2FormatOptions opts) { 102 if (outcome == null) throw new NullPointerException("outcome"); 103 if (opts == null) throw new NullPointerException("opts"); 104 try { 105 return renderInternal(outcome, stream, opts); 106 } catch (Throwable t) { 107 fallthroughCount++; 108 PPLogger.error(t); 109 PPLogger.info("LexicalIslandRenderer: threw; falling through to " 110 + "conservative renderer. range=" + outcome.getRange()); 111 return null; 112 } 113 } 114 115 private String renderInternal(RegionParseOutcome outcome, Pp2TokenStream stream, 116 Pp2FormatOptions opts) { 117 StatementRange range = outcome.getRange(); 118 int start = range.getStartTokenIndex(); 119 int end = Math.min(range.getEndTokenIndex(), stream.size()); 120 if (start >= end) { 121 // A collapsed range is only a valid empty render when the region's 122 // source is genuinely empty; otherwise fall through so the 123 // conservative renderer preserves the region's content. 124 String original = outcome.getParsedSql(); 125 return (original == null || original.trim().isEmpty()) ? "" : null; 126 } 127 128 // Copy the region's tokens (same TSourceToken reference, copied roles + 129 // whitespace) into fresh Pp2Token wrappers, so the S19-S22 analyses and 130 // layout rules annotate these copies — never the engine's shared token 131 // objects. This keeps island annotations from leaking into the 132 // conservative fallback (on a guard failure) or into other regions. 133 List<Pp2Token> source = stream.toReadOnlyList(); 134 List<Pp2Token> regionTokens = new ArrayList<Pp2Token>(end - start); 135 for (int i = start; i < end; i++) { 136 Pp2Token o = source.get(i); 137 regionTokens.add(new Pp2Token(o.getSourceToken(), 138 o.getPrecedingBlanks(), o.getPrecedingLinebreaks(), o.getRoles())); 139 } 140 Pp2TokenStream sub = Pp2TokenStream.ofTokens(regionTokens); 141 142 SqlScopeResult sql = sqlScope.detect(sub); 143 ClauseScopeResult clause = clauseScope.annotate(sub, sql); 144 LayoutContext ctx = new LayoutContext(sub, opts) 145 .withSqlScope(sql) 146 .withBlockScope(blockScope.detect(sub)) 147 .withClauseScope(clause) 148 .withIslands(islands.recognize(sub, sql, clause, opts)) 149 .withDialect(DialectRegistry.forVendor(vendor)); 150 pipeline.run(ctx); 151 String output = writer.write(ctx); 152 153 // Content guard: never let layout drop a solid token. On failure the 154 // engine falls back to the content-preserving conservative renderer. 155 String original = outcome.getParsedSql(); 156 if (original != null 157 && !TokenEquivalence.equalsModuloFormatting(original, output, opts, vendor, true)) { 158 fallthroughCount++; 159 PPLogger.info("LexicalIslandRenderer: content guard failed; falling " 160 + "through to conservative renderer. range=" + range); 161 return null; 162 } 163 successCount++; 164 return output; 165 } 166}