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    /**
115     * When {@code true} (default), consecutive top-level statements that share
116     * a physical line in the source (separated only by horizontal whitespace,
117     * with no line break) are placed on their own lines in the output. When
118     * {@code false}, the original inter-statement whitespace is preserved
119     * verbatim — the legacy behaviour, which could leave several statements
120     * running together on one line. Gaps that already contain a line break, or
121     * that contain comments / non-whitespace trivia, are always preserved
122     * verbatim regardless of this flag.
123     */
124    public boolean breakStatementsOnNewLine = true;
125
126    // ---- construction ---------------------------------------------------
127
128    /**
129     * Internal constructor used by {@link #defaults()} and {@link #from(GFmtOpt)}.
130     * Both factories supply a freshly-allocated {@code GFmtOpt}.
131     *
132     * @throws NullPointerException if {@code gfmtOpt} is null
133     */
134    Pp2FormatOptions(GFmtOpt gfmtOpt) {
135        if (gfmtOpt == null) {
136            throw new NullPointerException("gfmtOpt");
137        }
138        this.gfmtOpt = gfmtOpt;
139    }
140
141    /**
142     * Construct with a fresh {@code GFmtOpt} carrying default values. The
143     * underlying {@code GFmtOpt} is allocated via
144     * {@link GFmtOptFactory#newInstance()} so it gets a unique
145     * {@code sessionId} (used by pp's {@code FormatterFactory} caches).
146     */
147    public static Pp2FormatOptions defaults() {
148        return new Pp2FormatOptions(GFmtOptFactory.newInstance());
149    }
150
151    /**
152     * Construct from an existing {@code GFmtOpt} by <b>copying its public
153     * instance fields</b> into a freshly-allocated {@code GFmtOpt}. The
154     * caller's instance is not retained; subsequent mutations to it are
155     * invisible to pp2, and pp2's mutations are invisible to the caller.
156     *
157     * <p>The {@code sessionId} field is {@code final} on {@code GFmtOpt}, so
158     * the copy carries the new fresh {@code sessionId} from
159     * {@link GFmtOptFactory#newInstance()} — not the source's.
160     *
161     * @param gfmtOpt must not be {@code null}
162     * @throws NullPointerException if {@code gfmtOpt} is null
163     */
164    public static Pp2FormatOptions from(GFmtOpt gfmtOpt) {
165        if (gfmtOpt == null) {
166            throw new NullPointerException("gfmtOpt");
167        }
168        GFmtOpt copy = GFmtOptFactory.newInstance();
169        copyPublicFields(gfmtOpt, copy);
170        return new Pp2FormatOptions(copy);
171    }
172
173    /**
174     * Reflection-based field copier. Walks every public, non-static,
175     * non-{@code final} field on {@link GFmtOpt} and copies its value from
176     * {@code src} to {@code dst}. Skipping {@code final} fields means
177     * {@code sessionId} (which is {@code final}) is preserved from {@code dst}.
178     *
179     * <p>This is the R17 mitigation: a new public field on {@code GFmtOpt} is
180     * picked up automatically. The S2 test verifies field-by-field equality.
181     */
182    private static void copyPublicFields(GFmtOpt src, GFmtOpt dst) {
183        List<String> skipped = null;
184        for (Field f : GFmtOpt.class.getFields()) {
185            int mod = f.getModifiers();
186            if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) continue;
187            try {
188                f.set(dst, f.get(src));
189            } catch (IllegalAccessException e) {
190                // Should not happen for public fields, but record any holes
191                // rather than swallow them silently.
192                if (skipped == null) skipped = new ArrayList<String>();
193                skipped.add(f.getName());
194            }
195        }
196        if (skipped != null && !skipped.isEmpty()) {
197            throw new IllegalStateException(
198                "Pp2FormatOptions.from(): could not copy GFmtOpt fields: " + skipped);
199        }
200    }
201
202    // ---- accessors ------------------------------------------------------
203
204    /**
205     * Return the wrapped {@code GFmtOpt}. pp2 hands this directly to
206     * {@code FormatterFactory.pp()} when the
207     * {@code gudusoft.gsqlparser.pp2.engine.Pp2Engine} (S16) dispatches a
208     * parseable region to the AST delegate.
209     */
210    public GFmtOpt toGFmtOpt() {
211        return gfmtOpt;
212    }
213}