001package gudusoft.gsqlparser.ir.semantic; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.List; 006import java.util.Objects; 007 008/** 009 * Atomic result of one Semantic IR build invocation. 010 * 011 * <p>The program, complete diagnostics, and recovery sidecar are published 012 * together so callers cannot accidentally observe a recovered program while 013 * missing the error that made it invalid for strict publication. A fatal 014 * rejection has a null program and at least one ERROR diagnostic. A normal 015 * successful build has a non-null program, no ERROR diagnostics, and empty 016 * recovery metadata. 017 * 018 * <p><b>API status: advanced/preview.</b> Prefer 019 * {@link SqlSemanticAnalyzer} for the supported SQL-text pipeline. Direct 020 * builder callers should use this atomic result instead of pairing a legacy 021 * {@code build} call with thread-local diagnostic draining. 022 */ 023public final class SemanticBuildResult { 024 025 private final SemanticProgram recoveredProgram; 026 private final List<Diagnostic> diagnostics; 027 private final RecoveryMetadata recoveryMetadata; 028 private final boolean hasErrors; 029 030 private SemanticBuildResult(SemanticProgram recoveredProgram, 031 List<Diagnostic> diagnostics, 032 RecoveryMetadata recoveryMetadata) { 033 this.recoveredProgram = recoveredProgram; 034 List<Diagnostic> copy = new ArrayList<>( 035 Objects.requireNonNull(diagnostics, "diagnostics")); 036 boolean error = false; 037 for (Diagnostic diagnostic : copy) { 038 Objects.requireNonNull(diagnostic, 039 "diagnostics must not contain null"); 040 if (diagnostic.getSeverity() == Severity.ERROR) { 041 error = true; 042 } 043 } 044 this.diagnostics = Collections.unmodifiableList(copy); 045 this.recoveryMetadata = Objects.requireNonNull( 046 recoveryMetadata, "recoveryMetadata"); 047 this.hasErrors = error; 048 049 if (recoveredProgram == null && !error) { 050 throw new IllegalArgumentException( 051 "a result without a program must contain an ERROR diagnostic"); 052 } 053 if (recoveredProgram == null && !recoveryMetadata.isEmpty()) { 054 throw new IllegalArgumentException( 055 "recovery metadata requires a recovered program"); 056 } 057 for (RecoveryMetadata.Entry entry : recoveryMetadata.getEntries()) { 058 if (!copy.contains(entry.getRootDiagnostic())) { 059 throw new IllegalArgumentException( 060 "recovery root diagnostic must be present in diagnostics"); 061 } 062 } 063 } 064 065 /** Construct a result with a built program and optional diagnostics. */ 066 public static SemanticBuildResult completed( 067 SemanticProgram program, List<Diagnostic> diagnostics) { 068 SemanticBuildResult result = new SemanticBuildResult( 069 Objects.requireNonNull(program, "program"), diagnostics, 070 RecoveryMetadata.empty()); 071 if (result.hasErrors()) { 072 throw new IllegalArgumentException( 073 "a completed result cannot contain ERROR diagnostics; " 074 + "use recovered with explicit metadata"); 075 } 076 return result; 077 } 078 079 /** Construct a fatal result. At least one diagnostic must be an ERROR. */ 080 public static SemanticBuildResult rejected(List<Diagnostic> diagnostics) { 081 return new SemanticBuildResult( 082 null, diagnostics, RecoveryMetadata.empty()); 083 } 084 085 /** 086 * Construct a result that retains a program plus explicit recovery 087 * associations. The diagnostics must contain every entry's root 088 * diagnostic. 089 */ 090 public static SemanticBuildResult recovered( 091 SemanticProgram recoveredProgram, List<Diagnostic> diagnostics, 092 RecoveryMetadata recoveryMetadata) { 093 if (Objects.requireNonNull(recoveryMetadata, "recoveryMetadata").isEmpty()) { 094 throw new IllegalArgumentException( 095 "a recovered result requires non-empty recovery metadata"); 096 } 097 return new SemanticBuildResult( 098 Objects.requireNonNull(recoveredProgram, "recoveredProgram"), 099 diagnostics, recoveryMetadata); 100 } 101 102 /** 103 * Return the built program, including a compatibility recovery program 104 * when diagnostics contain an ERROR; null only for a fatal rejection. 105 */ 106 public SemanticProgram getRecoveredProgram() { 107 return recoveredProgram; 108 } 109 110 /** Complete immutable diagnostics for this invocation. */ 111 public List<Diagnostic> getDiagnostics() { 112 return diagnostics; 113 } 114 115 /** Non-null recovery association sidecar. */ 116 public RecoveryMetadata getRecoveryMetadata() { 117 return recoveryMetadata; 118 } 119 120 public boolean hasErrors() { 121 return hasErrors; 122 } 123 124 /** 125 * True when a program is retained despite an ERROR or when a producer 126 * explicitly recorded recovered subjects. 127 */ 128 public boolean hasRecoveredInvalidSemantics() { 129 return recoveredProgram != null 130 && (hasErrors || !recoveryMetadata.isEmpty()); 131 } 132}