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}