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) — reserved for 041 * a future strict-mode switch. The current engine does not read this 042 * field and always recovers ordinary parse failures.</li> 043 * <li><b>{@link #maxLineWidth}</b> (default 120) — reserved soft-wrap target. 044 * The current pp2 renderers do not read this field.</li> 045 * <li><b>{@link #errorRegionStrategy}</b> (default {@link ErrorRegionStrategy#PRESERVE}) — 046 * reserved strategy selector for lexical {@code ERROR_REGION}s. The 047 * current engine does not read this field.</li> 048 * <li><b>{@link #maxErrorRegionSize}</b> (default 10000 chars) — caps the 049 * token span the island recognizer annotates as an {@code ERROR_REGION}. 050 * The current layout pipeline records that boundary but does not use it 051 * to select a different renderer.</li> 052 * <li><b>{@link #maxRegionParseChars}</b> (default 200000 chars) — safety 053 * valve: regions whose source span exceeds this length skip per-region 054 * parsing entirely and go straight to the fallback renderer.</li> 055 * <li><b>{@link #commentPolicy}</b> (default {@link CommentPolicy#PRESERVE}) — 056 * preserves comments across region boundaries. The alternative 057 * re-anchor/reflow policies remain experimental and are not fully 058 * implemented by the region assembler.</li> 059 * <li><b>{@link #showIndentMarkers}</b> (default {@code false}) — reserved 060 * diagnostic option. The current pp2 renderers do not read this field.</li> 061 * <li><b>{@link #astOverlayEnabled}</b> (default {@code false}) — feature flag 062 * for the v3 AST overlay annotator. Off in v2; flipping it on in v2 063 * activates the annotator scaffold from slice S33 but does not change 064 * rendering output.</li> 065 * </ul> 066 * 067 * <p>Mutability matches {@code GFmtOpt}'s convention: fields are public and 068 * mutable so existing callers can tune options ergonomically. Concurrent 069 * mutation while a {@code Pp2Formatter} call is in flight is undefined. 070 */ 071public class Pp2FormatOptions { 072 073 /** Reserved strategies for rendering parse-failed lexical regions. */ 074 public enum ErrorRegionStrategy { 075 /** Planned raw-source preservation strategy. Default reserved value. */ 076 PRESERVE, 077 /** Planned light token-spacing strategy. */ 078 LIGHT_FORMAT, 079 /** Planned full lexical-island strategy. */ 080 BEST_EFFORT 081 } 082 083 // ---- pp options surface (composed, not inherited) ------------------- 084 085 private final GFmtOpt gfmtOpt; 086 087 // ---- pp2 knobs ------------------------------------------------------ 088 089 /** Reserved strict-mode switch; the current engine always recovers parse failures. */ 090 public boolean tolerantMode = true; 091 092 /** Reserved soft line-wrap target; currently has no effect. */ 093 public int maxLineWidth = 120; 094 095 /** Reserved error-region strategy selector; currently has no effect. */ 096 public ErrorRegionStrategy errorRegionStrategy = ErrorRegionStrategy.PRESERVE; 097 098 /** Maximum character span annotated as an ERROR_REGION by island recognition. */ 099 public int maxErrorRegionSize = 10000; 100 101 /** Regions larger than this skip per-region parsing entirely. */ 102 public int maxRegionParseChars = 200000; 103 104 /** Comment policy; PRESERVE is implemented, alternatives remain experimental. */ 105 public CommentPolicy commentPolicy = CommentPolicy.PRESERVE; 106 107 /** Reserved indent-marker option; currently has no effect. */ 108 public boolean showIndentMarkers = false; 109 110 /** Feature flag for the v3 AST overlay annotator. */ 111 public boolean astOverlayEnabled = false; 112 113 /** 114 * Whether a parseable input is handed straight to {@code pp}. 115 * 116 * <p>When {@code true} (the default) and the whole input parses, pp2 first 117 * formats the whole document with {@code FormatterFactory.pp} using these 118 * same options, and returns <b>that text unchanged</b> as an {@code OK} 119 * result provided it is token-equivalent to the input. Only when the 120 * whole-document parse fails, or pp throws, or pp's output drops or 121 * rewrites a token, does the regional pipeline run. 122 * 123 * <p>This is what makes pp2 byte-identical to pp on parseable SQL: not an 124 * imitation of pp's layout rules, but pp itself. Set it to {@code false} 125 * only to exercise the regional pipeline directly (the regional pipeline 126 * reparses statement ranges independently, and its reassembled text was 127 * not byte-identical to whole-document pp on 60 of 1000 parseable corpus 128 * inputs — which is why this path exists). 129 */ 130 public boolean wholeDocumentFastPath = true; 131 132 /** 133 * When {@code true} (default), consecutive top-level statements that share 134 * a physical line in the source (separated only by horizontal whitespace, 135 * with no line break) are placed on their own lines in the output. When 136 * {@code false}, the original inter-statement whitespace is preserved 137 * verbatim — the legacy behaviour, which could leave several statements 138 * running together on one line. Gaps that already contain a line break, or 139 * that contain comments / non-whitespace trivia, are always preserved 140 * verbatim regardless of this flag. 141 */ 142 public boolean breakStatementsOnNewLine = true; 143 144 /** 145 * Transformations of tokens or comments that the caller asked for and that 146 * pp2's content guards must therefore accept, in their exact expected form 147 * (see {@link gudusoft.gsqlparser.pp2.token.TransformPolicy}). Default: 148 * none, the plain content check. 149 */ 150 public gudusoft.gsqlparser.pp2.token.TransformPolicy transformPolicy = 151 gudusoft.gsqlparser.pp2.token.TransformPolicy.NONE; 152 153 // ---- construction --------------------------------------------------- 154 155 /** 156 * Internal constructor used by {@link #defaults()} and {@link #from(GFmtOpt)}. 157 * Both factories supply a freshly-allocated {@code GFmtOpt}. 158 * 159 * @throws NullPointerException if {@code gfmtOpt} is null 160 */ 161 Pp2FormatOptions(GFmtOpt gfmtOpt) { 162 if (gfmtOpt == null) { 163 throw new NullPointerException("gfmtOpt"); 164 } 165 this.gfmtOpt = gfmtOpt; 166 } 167 168 /** 169 * Construct with a fresh {@code GFmtOpt} carrying default values. The 170 * underlying {@code GFmtOpt} is allocated via 171 * {@link GFmtOptFactory#newInstance()} so it gets a unique 172 * {@code sessionId} (used by pp's {@code FormatterFactory} caches). 173 */ 174 public static Pp2FormatOptions defaults() { 175 return new Pp2FormatOptions(GFmtOptFactory.newInstance()); 176 } 177 178 /** 179 * Construct from an existing {@code GFmtOpt} by <b>copying its public 180 * instance fields</b> into a freshly-allocated {@code GFmtOpt}. The 181 * caller's instance is not retained; subsequent mutations to it are 182 * invisible to pp2, and pp2's mutations are invisible to the caller. 183 * 184 * <p>The {@code sessionId} field is {@code final} on {@code GFmtOpt}, so 185 * the copy carries the new fresh {@code sessionId} from 186 * {@link GFmtOptFactory#newInstance()} — not the source's. 187 * 188 * @param gfmtOpt must not be {@code null} 189 * @throws NullPointerException if {@code gfmtOpt} is null 190 */ 191 /** 192 * Wrap an existing {@code GFmtOpt} <b>without copying</b>: pp2 formats 193 * with this very instance and its {@code sessionId}. Use it when the 194 * options must stay registered in {@link GFmtOptFactory} under that id, 195 * because parts of pp re-read their options by session id; the caller 196 * owns the instance and releases it ({@link GFmtOptFactory#remove}). 197 * 198 * @param gfmtOpt must not be {@code null} 199 * @throws NullPointerException if {@code gfmtOpt} is null 200 */ 201 public static Pp2FormatOptions wrap(GFmtOpt gfmtOpt) { 202 return new Pp2FormatOptions(gfmtOpt); 203 } 204 205 public static Pp2FormatOptions from(GFmtOpt gfmtOpt) { 206 if (gfmtOpt == null) { 207 throw new NullPointerException("gfmtOpt"); 208 } 209 GFmtOpt copy = GFmtOptFactory.newInstance(); 210 copyPublicFields(gfmtOpt, copy); 211 return new Pp2FormatOptions(copy); 212 } 213 214 /** 215 * Reflection-based field copier. Walks every public, non-static, 216 * non-{@code final} field on {@link GFmtOpt} and copies its value from 217 * {@code src} to {@code dst}. Skipping {@code final} fields means 218 * {@code sessionId} (which is {@code final}) is preserved from {@code dst}. 219 * 220 * <p>This is the R17 mitigation: a new public field on {@code GFmtOpt} is 221 * picked up automatically. The S2 test verifies field-by-field equality. 222 */ 223 private static void copyPublicFields(GFmtOpt src, GFmtOpt dst) { 224 List<String> skipped = null; 225 for (Field f : GFmtOpt.class.getFields()) { 226 int mod = f.getModifiers(); 227 if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) continue; 228 try { 229 f.set(dst, f.get(src)); 230 } catch (IllegalAccessException e) { 231 // Should not happen for public fields, but record any holes 232 // rather than swallow them silently. 233 if (skipped == null) skipped = new ArrayList<String>(); 234 skipped.add(f.getName()); 235 } 236 } 237 if (skipped != null && !skipped.isEmpty()) { 238 throw new IllegalStateException( 239 "Pp2FormatOptions.from(): could not copy GFmtOpt fields: " + skipped); 240 } 241 } 242 243 // ---- accessors ------------------------------------------------------ 244 245 /** 246 * Return the wrapped {@code GFmtOpt}. pp2 hands this directly to 247 * {@code FormatterFactory.pp()} when the 248 * {@code gudusoft.gsqlparser.pp2.engine.Pp2Engine} (S16) dispatches a 249 * parseable region to the AST delegate. 250 */ 251 public GFmtOpt toGFmtOpt() { 252 return gfmtOpt; 253 } 254}