001package gudusoft.gsqlparser.ir.semantic;
002
003import gudusoft.gsqlparser.nodes.TParseTreeNode;
004
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.List;
008import java.util.Objects;
009
010/**
011 * Structured rejection from the SemanticIR builder. Carries a stable
012 * {@link DiagnosticCode}, centrally-derived {@link DiagnosticCategory}, a
013 * {@link Severity}, the rendered user-visible {@link #getMessage() message},
014 * an optional primary {@link SourceSpan}, immutable secondary locations, and
015 * optional exact vendor-error enrichment.
016 *
017 * <p><b>API status:</b> read-only diagnostic getters are part of Join Analysis
018 * Consumption Profile v1. Consumers should treat code, category, severity,
019 * locations, and exact vendor enrichment as structured data. Construction and
020 * copy helpers are producer-oriented APIs.
021 *
022 * <p>Diagnostics are constructed via the {@link #error} factory
023 * methods and wrapped in
024 * {@code SemanticIRBuilder.SemanticIRBuildException} when the builder
025 * cannot continue. External callers should pattern-match on
026 * {@link #getCode()} rather than parsing {@link #getMessage()}
027 * substrings — message text remains user-visible English and may
028 * change without notice; the enum values are the public contract.
029 */
030public final class Diagnostic {
031
032    private final DiagnosticCode code;
033    private final DiagnosticCategory category;
034    private final Severity severity;
035    private final String message;
036    private final SourceSpan span;
037    private final List<RelatedLocation> relatedLocations;
038    private final VendorError vendorError;
039
040    private Diagnostic(DiagnosticCode code, Severity severity,
041                       String message, SourceSpan span,
042                       List<RelatedLocation> relatedLocations,
043                       VendorError vendorError) {
044        this.code = Objects.requireNonNull(code, "code");
045        this.category = DiagnosticDescriptorCatalog.categoryOf(code);
046        this.severity = Objects.requireNonNull(severity, "severity");
047        this.message = Objects.requireNonNull(message, "message");
048        this.span = span;
049        List<RelatedLocation> locations = relatedLocations == null
050                ? Collections.<RelatedLocation>emptyList()
051                : new ArrayList<>(relatedLocations);
052        for (RelatedLocation location : locations) {
053            Objects.requireNonNull(location, "relatedLocations must not contain null");
054        }
055        this.relatedLocations = Collections.unmodifiableList(locations);
056        validateVendorError(category, vendorError);
057        this.vendorError = vendorError;
058    }
059
060    /**
061     * Construct an {@link Severity#ERROR ERROR}-severity diagnostic
062     * with no span anchor.
063     */
064    public static Diagnostic error(DiagnosticCode code, String message) {
065        return builder(code, Severity.ERROR, message).build();
066    }
067
068    /**
069     * Construct an {@link Severity#ERROR ERROR}-severity diagnostic
070     * whose span is derived from {@code anchor} via
071     * {@link SourceSpan#of(TParseTreeNode)}. Null-safe: when
072     * {@code anchor} (or its boundary tokens) is {@code null}, the
073     * resulting span is {@code null}.
074     */
075    public static Diagnostic error(DiagnosticCode code, String message,
076                                   TParseTreeNode anchor) {
077        return builder(code, Severity.ERROR, message).anchor(anchor).build();
078    }
079
080    /**
081     * Construct an {@link Severity#ERROR ERROR}-severity diagnostic
082     * with an explicit (already-computed) source span. Most callers
083     * should prefer {@link #error(DiagnosticCode, String, TParseTreeNode)}
084     * which derives the span from an AST node.
085     */
086    public static Diagnostic errorWithSpan(DiagnosticCode code, String message,
087                                           SourceSpan span) {
088        return builder(code, Severity.ERROR, message).span(span).build();
089    }
090
091    /**
092     * Slice 77 — construct a {@link Severity#WARN WARN}-severity
093     * diagnostic with no span anchor. Warnings are advisory: the
094     * analyzer continues, the IR is built, and
095     * {@code AnalysisResult.isSuccessful()} still returns {@code true}.
096     */
097    public static Diagnostic warn(DiagnosticCode code, String message) {
098        return builder(code, Severity.WARN, message).build();
099    }
100
101    /**
102     * Slice 77 — construct a {@link Severity#WARN WARN}-severity
103     * diagnostic whose span is derived from {@code anchor}.
104     */
105    public static Diagnostic warn(DiagnosticCode code, String message,
106                                  TParseTreeNode anchor) {
107        return builder(code, Severity.WARN, message).anchor(anchor).build();
108    }
109
110    /**
111     * Construct a {@link Severity#WARN WARN}-severity diagnostic with an
112     * explicit (already-computed) source span — the WARN counterpart of
113     * {@link #errorWithSpan}. Used by the builder's degrade paths, which
114     * derive the span from a fallback anchor rather than a single node.
115     */
116    public static Diagnostic warnWithSpan(DiagnosticCode code, String message,
117                                          SourceSpan span) {
118        return builder(code, Severity.WARN, message).span(span).build();
119    }
120
121    /**
122     * Begin constructing a diagnostic. Category is intentionally absent from
123     * this API: it is derived centrally from {@link DiagnosticCode}.
124     */
125    public static Builder builder(DiagnosticCode code, Severity severity,
126                                  String message) {
127        return new Builder(code, severity, message);
128    }
129
130    public DiagnosticCode getCode() {
131        return code;
132    }
133
134    public DiagnosticCategory getCategory() {
135        return category;
136    }
137
138    public Severity getSeverity() {
139        return severity;
140    }
141
142    public String getMessage() {
143        return message;
144    }
145
146    /** @return the source span, or {@code null} when no AST anchor was available. */
147    public SourceSpan getSpan() {
148        return span;
149    }
150
151    /** @return immutable secondary source locations, never {@code null}. */
152    public List<RelatedLocation> getRelatedLocations() {
153        return relatedLocations;
154    }
155
156    /** @return exact vendor enrichment, or {@code null} when not available. */
157    public VendorError getVendorError() {
158        return vendorError;
159    }
160
161    /** Return an immutable copy with a new primary span. */
162    public Diagnostic withSpan(SourceSpan newSpan) {
163        return new Diagnostic(code, severity, message, newSpan,
164                relatedLocations, vendorError);
165    }
166
167    /** Return an immutable copy with one additional secondary location. */
168    public Diagnostic withRelatedLocation(RelatedLocation location) {
169        Objects.requireNonNull(location, "location");
170        List<RelatedLocation> locations = new ArrayList<>(relatedLocations);
171        locations.add(location);
172        return new Diagnostic(code, severity, message, span, locations, vendorError);
173    }
174
175    /**
176     * Return an immutable copy enriched with an exact vendor error.
177     * Unsupported/degraded/advisory categories reject this operation.
178     */
179    public Diagnostic withVendorError(VendorError error) {
180        return new Diagnostic(code, severity, message, span,
181                relatedLocations, Objects.requireNonNull(error, "error"));
182    }
183
184    private static void validateVendorError(DiagnosticCategory category,
185                                            VendorError vendorError) {
186        if (vendorError == null) return;
187        if (category != DiagnosticCategory.SYNTAX_ERROR
188                && category != DiagnosticCategory.DIALECT_SEMANTIC_ERROR) {
189            throw new IllegalArgumentException("vendorError is not valid for " + category);
190        }
191    }
192
193    @Override
194    public boolean equals(Object o) {
195        if (this == o) return true;
196        if (!(o instanceof Diagnostic)) return false;
197        Diagnostic that = (Diagnostic) o;
198        return code == that.code
199                && category == that.category
200                && severity == that.severity
201                && message.equals(that.message)
202                && Objects.equals(span, that.span)
203                && relatedLocations.equals(that.relatedLocations)
204                && Objects.equals(vendorError, that.vendorError);
205    }
206
207    @Override
208    public int hashCode() {
209        return Objects.hash(code, category, severity, message, span,
210                relatedLocations, vendorError);
211    }
212
213    @Override
214    public String toString() {
215        StringBuilder sb = new StringBuilder();
216        sb.append('[').append(code.name()).append("] ").append(message);
217        if (span != null) {
218            sb.append(" at ").append(span);
219        }
220        return sb.toString();
221    }
222
223    /** Mutable construction helper; the built {@link Diagnostic} is immutable. */
224    public static final class Builder {
225        private final DiagnosticCode code;
226        private final Severity severity;
227        private final String message;
228        private SourceSpan span;
229        private final List<RelatedLocation> relatedLocations = new ArrayList<>();
230        private VendorError vendorError;
231
232        private Builder(DiagnosticCode code, Severity severity, String message) {
233            this.code = Objects.requireNonNull(code, "code");
234            this.severity = Objects.requireNonNull(severity, "severity");
235            this.message = Objects.requireNonNull(message, "message");
236        }
237
238        public Builder span(SourceSpan value) {
239            this.span = value;
240            return this;
241        }
242
243        public Builder anchor(TParseTreeNode value) {
244            this.span = SourceSpan.of(value);
245            return this;
246        }
247
248        public Builder addRelatedLocation(RelatedLocation value) {
249            relatedLocations.add(Objects.requireNonNull(value, "value"));
250            return this;
251        }
252
253        public Builder vendorError(VendorError value) {
254            this.vendorError = Objects.requireNonNull(value, "value");
255            return this;
256        }
257
258        public Diagnostic build() {
259            return new Diagnostic(code, severity, message, span,
260                    relatedLocations, vendorError);
261        }
262    }
263}