001package gudusoft.gsqlparser.pp2; 002 003import gudusoft.gsqlparser.pp.para.GFmtOpt; 004import gudusoft.gsqlparser.pp.para.GFmtOptFactory; 005import gudusoft.gsqlparser.pp2.zone.CommentPolicy; 006 007import java.lang.reflect.Field; 008import java.lang.reflect.Modifier; 009import java.util.ArrayList; 010import java.util.List; 011 012/** 013 * Configuration for the pp2 fault-tolerant SQL formatter. 014 * 015 * <p>Composes — does not extend — {@link GFmtOpt}. Composition is deliberate 016 * (plan §5.5): inheritance would couple pp2's option surface to every future 017 * change to {@code GFmtOpt} and force {@code protected} shims. The wrapped 018 * {@code GFmtOpt} carries all of pp's options (case rules, alignment, indent, 019 * etc.); the pp2-specific fields below extend that surface with the knobs 020 * pp2's region/recovery/fallback pipeline needs. 021 * 022 * <p>The {@link #from(GFmtOpt)} factory <b>copies field values by reflection</b> 023 * into a freshly-allocated {@code GFmtOpt} so that: 024 * <ul> 025 * <li>pp2 tuning never leaks back to the caller's {@code GFmtOpt}, and</li> 026 * <li>each {@code Pp2FormatOptions} carries its own {@code sessionId} so 027 * pp's session-keyed caches in {@code FormatterFactory}, 028 * {@code ProcessorFactory}, and {@code MediatorFactory} are not poisoned 029 * by pp2's invocations.</li> 030 * </ul> 031 * 032 * <p>The reflection copier is risk R17's mitigation: when {@code GFmtOpt} gains 033 * a new public instance field, the copier picks it up automatically. The S2 034 * test {@code Pp2FormatOptionsTest} verifies field-by-field equality after 035 * {@code from()}. 036 * 037 * <h2>Field semantics</h2> 038 * 039 * <ul> 040 * <li><b>{@link #tolerantMode}</b> ({@code true} by default) — when 041 * {@code true} the engine never throws on a parse failure; it routes 042 * the region to the lexical fallback. When {@code false} the engine 043 * raises a {@link Pp2ParseException} with the parser diagnostics 044 * attached.</li> 045 * <li><b>{@link #maxLineWidth}</b> (default 120) — soft wrap target for the 046 * lexical island renderer. Hard guarantees are not made; identifier 047 * names and literals are never split.</li> 048 * <li><b>{@link #errorRegionStrategy}</b> (default {@link ErrorRegionStrategy#PRESERVE}) — 049 * how to render a region the engine could not parse and the lexical 050 * fallback flagged as an {@code ERROR_REGION}.</li> 051 * <li><b>{@link #maxErrorRegionSize}</b> (default 10000 chars) — error 052 * regions larger than this are clamped; the overflow is emitted as a 053 * raw-text passthrough with an {@code INFO} diagnostic.</li> 054 * <li><b>{@link #maxRegionParseChars}</b> (default 200000 chars) — safety 055 * valve: regions whose source span exceeds this length skip per-region 056 * parsing entirely and go straight to the fallback renderer.</li> 057 * <li><b>{@link #commentPolicy}</b> (default {@link CommentPolicy#PRESERVE}) — 058 * how to anchor comments across region boundaries.</li> 059 * <li><b>{@link #showIndentMarkers}</b> (default {@code false}) — Delphi-style 060 * {@code |} debug rendering of indent levels in the lexical fallback 061 * output. Diagnostic only.</li> 062 * <li><b>{@link #astOverlayEnabled}</b> (default {@code false}) — feature flag 063 * for the v3 AST overlay annotator. Off in v2; flipping it on in v2 064 * activates the annotator scaffold from slice S33 but does not change 065 * rendering output.</li> 066 * </ul> 067 * 068 * <p>Mutability matches {@code GFmtOpt}'s convention: fields are public and 069 * mutable so existing callers can tune options ergonomically. Concurrent 070 * mutation while a {@code Pp2Formatter} call is in flight is undefined. 071 */ 072public class Pp2FormatOptions { 073 074 /** Strategy for rendering parse-failed, lexical-fallback regions. */ 075 public enum ErrorRegionStrategy { 076 /** Emit the raw source text verbatim. Safest. Default. */ 077 PRESERVE, 078 /** Apply light token spacing only; never reorder or drop tokens. */ 079 LIGHT_FORMAT, 080 /** Apply the full lexical island pipeline; may rearrange whitespace. */ 081 BEST_EFFORT 082 } 083 084 // ---- pp options surface (composed, not inherited) ------------------- 085 086 private final GFmtOpt gfmtOpt; 087 088 // ---- pp2 knobs ------------------------------------------------------ 089 090 /** When {@code true}, parse failures route to fallback; never throws. */ 091 public boolean tolerantMode = true; 092 093 /** Soft line-wrap target (lexical island renderer only). */ 094 public int maxLineWidth = 120; 095 096 /** How to render error regions. */ 097 public ErrorRegionStrategy errorRegionStrategy = ErrorRegionStrategy.PRESERVE; 098 099 /** Error regions larger than this are clamped. */ 100 public int maxErrorRegionSize = 10000; 101 102 /** Regions larger than this skip per-region parsing entirely. */ 103 public int maxRegionParseChars = 200000; 104 105 /** Comment-anchoring policy across region boundaries. */ 106 public CommentPolicy commentPolicy = CommentPolicy.PRESERVE; 107 108 /** Emit {@code |} markers at each indent level (debug output). */ 109 public boolean showIndentMarkers = false; 110 111 /** Feature flag for the v3 AST overlay annotator. */ 112 public boolean astOverlayEnabled = false; 113 114 // ---- construction --------------------------------------------------- 115 116 /** 117 * Internal constructor used by {@link #defaults()} and {@link #from(GFmtOpt)}. 118 * Both factories supply a freshly-allocated {@code GFmtOpt}. 119 * 120 * @throws NullPointerException if {@code gfmtOpt} is null 121 */ 122 Pp2FormatOptions(GFmtOpt gfmtOpt) { 123 if (gfmtOpt == null) { 124 throw new NullPointerException("gfmtOpt"); 125 } 126 this.gfmtOpt = gfmtOpt; 127 } 128 129 /** 130 * Construct with a fresh {@code GFmtOpt} carrying default values. The 131 * underlying {@code GFmtOpt} is allocated via 132 * {@link GFmtOptFactory#newInstance()} so it gets a unique 133 * {@code sessionId} (used by pp's {@code FormatterFactory} caches). 134 */ 135 public static Pp2FormatOptions defaults() { 136 return new Pp2FormatOptions(GFmtOptFactory.newInstance()); 137 } 138 139 /** 140 * Construct from an existing {@code GFmtOpt} by <b>copying its public 141 * instance fields</b> into a freshly-allocated {@code GFmtOpt}. The 142 * caller's instance is not retained; subsequent mutations to it are 143 * invisible to pp2, and pp2's mutations are invisible to the caller. 144 * 145 * <p>The {@code sessionId} field is {@code final} on {@code GFmtOpt}, so 146 * the copy carries the new fresh {@code sessionId} from 147 * {@link GFmtOptFactory#newInstance()} — not the source's. 148 * 149 * @param gfmtOpt must not be {@code null} 150 * @throws NullPointerException if {@code gfmtOpt} is null 151 */ 152 public static Pp2FormatOptions from(GFmtOpt gfmtOpt) { 153 if (gfmtOpt == null) { 154 throw new NullPointerException("gfmtOpt"); 155 } 156 GFmtOpt copy = GFmtOptFactory.newInstance(); 157 copyPublicFields(gfmtOpt, copy); 158 return new Pp2FormatOptions(copy); 159 } 160 161 /** 162 * Reflection-based field copier. Walks every public, non-static, 163 * non-{@code final} field on {@link GFmtOpt} and copies its value from 164 * {@code src} to {@code dst}. Skipping {@code final} fields means 165 * {@code sessionId} (which is {@code final}) is preserved from {@code dst}. 166 * 167 * <p>This is the R17 mitigation: a new public field on {@code GFmtOpt} is 168 * picked up automatically. The S2 test verifies field-by-field equality. 169 */ 170 private static void copyPublicFields(GFmtOpt src, GFmtOpt dst) { 171 List<String> skipped = null; 172 for (Field f : GFmtOpt.class.getFields()) { 173 int mod = f.getModifiers(); 174 if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) continue; 175 try { 176 f.set(dst, f.get(src)); 177 } catch (IllegalAccessException e) { 178 // Should not happen for public fields, but record any holes 179 // rather than swallow them silently. 180 if (skipped == null) skipped = new ArrayList<String>(); 181 skipped.add(f.getName()); 182 } 183 } 184 if (skipped != null && !skipped.isEmpty()) { 185 throw new IllegalStateException( 186 "Pp2FormatOptions.from(): could not copy GFmtOpt fields: " + skipped); 187 } 188 } 189 190 // ---- accessors ------------------------------------------------------ 191 192 /** 193 * Return the wrapped {@code GFmtOpt}. pp2 hands this directly to 194 * {@code FormatterFactory.pp()} when the 195 * {@code gudusoft.gsqlparser.pp2.engine.Pp2Engine} (S16) dispatches a 196 * parseable region to the AST delegate. 197 */ 198 public GFmtOpt toGFmtOpt() { 199 return gfmtOpt; 200 } 201}