001package gudusoft.gsqlparser.lineage.v2.validation; 002 003import gudusoft.gsqlparser.lineage.v2.Finding; 004import gudusoft.gsqlparser.lineage.v2.identity.IdentityV1; 005import gudusoft.gsqlparser.lineage.v2.identity.PathV1; 006 007import java.nio.ByteBuffer; 008import java.nio.CharBuffer; 009import java.nio.charset.CharacterCodingException; 010import java.nio.charset.CharsetDecoder; 011import java.nio.charset.CoderResult; 012import java.nio.charset.CodingErrorAction; 013import java.nio.charset.StandardCharsets; 014import java.security.MessageDigest; 015import java.security.NoSuchAlgorithmException; 016import java.util.ArrayList; 017import java.util.Arrays; 018import java.util.Collections; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.TreeMap; 026 027import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.intRef; 028import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.items; 029 030/** 031 * Validation stage {@code SEMANTIC} (specification Section 14.2) as the reference runner implements it 032 * ({@code conformance/run.py}, {@code stage_semantic}): module presence, Facts without an Occurrence, 033 * declaration bases, document-local identities, the proof/binding/coverage contracts of Sections 4.3 and 4.4, 034 * source embedding, PATH-V1 nesting, FROM-item row propagation, spans against the source text, the column 035 * ordinals of the ExplainGraph, and the FOLD-V1 checks of a {@link FoldCheck} when one is given. 036 * 037 * @since 4.2.17 038 */ 039public final class SemanticStage { 040 041 /** The FOLD-V1 {@code PROJECTOR} checks, run when the document has an ExplainGraph. */ 042 public interface FoldCheck { 043 void check(DocumentIndex index, List<Finding> findings, List<String> notes); 044 } 045 046 private static final Map<String, String> MODULE_OF_MEMBER = new TreeMap<String, String>(); 047 048 static { 049 MODULE_OF_MEMBER.put("explainGraph", "explain"); 050 MODULE_OF_MEMBER.put("evidence", "evidence"); 051 MODULE_OF_MEMBER.put("dynamicSqlSites", "dynamic-sql"); 052 MODULE_OF_MEMBER.put("expressionSpans", "exact-spans"); 053 MODULE_OF_MEMBER.put("reproducibilityManifest", "reproducibility"); 054 MODULE_OF_MEMBER.put("typedSteps", "typed-steps"); 055 MODULE_OF_MEMBER.put("routineCalls", "routines"); 056 MODULE_OF_MEMBER.put("wildcardSelections", "wildcards"); 057 MODULE_OF_MEMBER.put("structuralSignatures", "structural-signature"); 058 MODULE_OF_MEMBER.put("refinements", "join-semantics"); 059 MODULE_OF_MEMBER.put("naryFacts", "nary-facts"); 060 } 061 062 private static final List<String> LOCAL_ONLY_KINDS = Arrays.asList("QUERY_RESULT", "TEMP_TABLE", "TABLE_VARIABLE", 063 "CTE", "VARIABLE", "PARAMETER"); 064 private static final List<String> LOCAL_OR_OPAQUE_KINDS = Arrays.asList("SEQUENCE", "STAGE", "STORAGE_LOCATION"); 065 private static final List<String> DECLARING_BASES = Arrays.asList("CATALOG_VERIFIED", "DDL_IN_INPUT", 066 "DERIVED_OUTPUT", "ROUTINE_DECLARATION", "LOCAL_DECLARATION"); 067 private static final List<String> ROW_SOURCE_KINDS = Arrays.asList("RELATION", "TABLE", "VIEW", "MATERIALIZED_VIEW", 068 "EXTERNAL_TABLE", "SYNONYM", "TEMP_TABLE", "TABLE_VARIABLE", "CTE"); 069 private static final List<String> QUERY_BLOCK_STEPS = Arrays.asList("SELECT", "SUBQUERY", "CTE", "BRANCH"); 070 private static final List<String> QUERY_STATEMENT_ROOTS = Arrays.asList("SELECT", "INSERT", "CREATE_TABLE", 071 "CREATE_VIEW"); 072 073 private final SourceTextProvider sources; 074 private final FoldCheck fold; 075 076 /** 077 * @param sources where the text of NAMED source units is found; {@link SourceTextProvider#NONE} for none 078 * @param fold the FOLD-V1 checks, or null to skip them 079 */ 080 public SemanticStage(SourceTextProvider sources, FoldCheck fold) { 081 this.sources = sources == null ? SourceTextProvider.NONE : sources; 082 this.fold = fold; 083 } 084 085 /** 086 * The findings of the stage for a document that passed stage {@code SCHEMA}. 087 * 088 * @param deferred findings stage SCHEMA deferred to this stage 089 * @param notes receives what the stage could not check 090 */ 091 public List<Finding> run(Map<?, ?> doc, List<Finding> deferred, List<String> notes) { 092 List<Finding> findings = new ArrayList<Finding>(deferred); 093 DocumentIndex index = new DocumentIndex(doc); 094 Set<String> modules = new HashSet<String>(); 095 if (doc.get("modules") instanceof List<?>) { 096 for (Object module : (List<?>) doc.get("modules")) { 097 if (module instanceof String) { 098 modules.add((String) module); 099 } 100 } 101 } 102 for (String[] content : moduleContent(doc, index)) { 103 if (!modules.contains(content[0])) { 104 findings.add(new Finding("SEMANTIC_MODULE_NOT_DECLARED", content[1], "module " + content[0] + " is not listed")); 105 } 106 } 107 108 // Section 3.2, rule 1: every published Fact has at least one Occurrence. 109 Set<Long> asserted = new HashSet<Long>(); 110 for (Map<?, ?> occurrence : index.all("occurrence")) { 111 asserted.add(intRef(occurrence.get("factRef"))); 112 } 113 for (Map<?, ?> fact : index.all("fact")) { 114 if (!asserted.contains(intRef(fact.get("ref")))) { 115 findings.add(new Finding("SEMANTIC_FACT_WITHOUT_OCCURRENCE", "facts ref " + fact.get("ref"), "")); 116 } 117 } 118 119 declarationBases(index, findings); 120 checkIdentities(index, findings); 121 checkContracts(doc, index, findings); 122 checkSourceEmbedding(doc, index, findings); 123 checkPaths(doc, index, findings); 124 checkRowPropagation(index, findings); 125 checkSpans(index, findings, notes); 126 checkColumnOrdinals(index, findings); 127 if (index.hasGraph()) { 128 if (fold != null) { 129 fold.check(index, findings, notes); 130 } else { 131 notes.add("explainGraph: no FOLD-V1 check configured; facts, candidates and write bounds not checked" 132 + " against the graph"); 133 } 134 } 135 return findings; 136 } 137 138 // --------------------------------------------------------------------------------------------- 139 140 private static List<String[]> moduleContent(Map<?, ?> doc, DocumentIndex index) { 141 List<String[]> found = new ArrayList<String[]>(); 142 for (Map.Entry<String, String> entry : MODULE_OF_MEMBER.entrySet()) { 143 if (doc.containsKey(entry.getKey())) { 144 found.add(new String[] {entry.getValue(), entry.getKey()}); 145 } 146 } 147 for (Map<?, ?> occurrence : index.all("occurrence")) { 148 if (occurrence.containsKey("supportPathRefs")) { 149 found.add(new String[] {"explain", "occurrences ref " + occurrence.get("ref") + " supportPathRefs"}); 150 } 151 if (occurrence.containsKey("evidenceRefs")) { 152 found.add(new String[] {"evidence", "occurrences ref " + occurrence.get("ref") + " evidenceRefs"}); 153 } 154 } 155 for (String kind : new String[] {"fact", "factCandidate"}) { 156 for (Map<?, ?> obj : index.all(kind)) { 157 if ("DATASET_DEPENDENCY".equals(obj.get("kind")) || obj.containsKey("datasetDependency")) { 158 found.add(new String[] {"dataset-dependency", kind + "s ref " + obj.get("ref")}); 159 } 160 if ("CALL".equals(obj.get("kind"))) { 161 found.add(new String[] {"routines", kind + "s ref " + obj.get("ref")}); 162 } 163 } 164 } 165 for (Map<?, ?> endpoint : index.all("endpoint")) { 166 if (endpoint.containsKey("pathSegments") || endpoint.containsKey("displayPath")) { 167 found.add(new String[] {"nested-fields", "endpoints ref " + endpoint.get("ref")}); 168 } 169 if (endpoint.containsKey("routineNamespace") || endpoint.containsKey("routineKind") 170 || endpoint.containsKey("signature")) { 171 found.add(new String[] {"routines", "endpoints ref " + endpoint.get("ref")}); 172 } 173 } 174 for (Map<?, ?> unit : index.all("sourceUnit")) { 175 if (unit.containsKey("originMap")) { 176 found.add(new String[] {"exact-spans", "sourceUnits ref " + unit.get("ref") + " originMap"}); 177 } 178 } 179 for (Map<?, ?> set : index.all("candidateSet")) { 180 if (set.containsKey("details")) { 181 found.add(new String[] {"evidence", "candidate set ref " + set.get("ref") + " details"}); 182 } 183 } 184 for (Object[] position : positions(index)) { 185 if (position[1] instanceof Map<?, ?> && ((Map<?, ?>) position[1]).containsKey("utf16Offset0")) { 186 found.add(new String[] {"exact-spans", position[0] + ".utf16Offset0"}); 187 } 188 } 189 return found; 190 } 191 192 /** (where, position) for every position of the core and the ExplainGraph. */ 193 private static List<Object[]> positions(DocumentIndex index) { 194 List<Object[]> out = new ArrayList<Object[]>(); 195 for (Map<?, ?> statement : index.all("statement")) { 196 for (String end : new String[] {"start", "end"}) { 197 out.add(new Object[] {"statements ref " + statement.get("ref") + "." + end, statement.get(end)}); 198 } 199 } 200 List<Object[]> spans = new ArrayList<Object[]>(); 201 for (Map<?, ?> unit : index.all("sourceUnit")) { 202 spans.add(new Object[] {"sourceUnits ref " + unit.get("ref") + ".siteSpan", unit.get("siteSpan")}); 203 } 204 for (Map<?, ?> node : index.all("node")) { 205 spans.add(new Object[] {"explainGraph.nodes ref " + node.get("ref") + ".span", node.get("span")}); 206 } 207 for (Map<?, ?> diagnostic : index.all("diagnostic")) { 208 spans.add(new Object[] {"diagnostics ref " + diagnostic.get("ref") + ".span", diagnostic.get("span")}); 209 } 210 for (Object[] span : spans) { 211 if (span[1] instanceof Map<?, ?>) { 212 for (String end : new String[] {"start", "end"}) { 213 out.add(new Object[] {span[0] + "." + end, ((Map<?, ?>) span[1]).get(end)}); 214 } 215 } 216 } 217 for (Map<?, ?> edge : index.all("microEdge")) { 218 out.add(new Object[] {"explainGraph.microEdges ref " + edge.get("ref") + ".referenceStart", 219 edge.get("referenceStart")}); 220 } 221 return out; 222 } 223 224 private static boolean isField(DocumentIndex index, Object ref) { 225 Map<?, ?> endpoint = index.get("endpoint", ref); 226 return endpoint != null && "FIELD".equals(endpoint.get("kind")); 227 } 228 229 private static boolean sideIsField(DocumentIndex index, Object end) { 230 if (!(end instanceof Map<?, ?>)) { 231 return false; 232 } 233 Map<?, ?> side = (Map<?, ?>) end; 234 if (side.containsKey("endpointRef")) { 235 return isField(index, side.get("endpointRef")); 236 } 237 List<?> members = candidateRefs(side.get("candidateSet")); 238 if (members.isEmpty()) { 239 return false; 240 } 241 for (Object member : members) { 242 if (!isField(index, member)) { 243 return false; 244 } 245 } 246 return true; 247 } 248 249 private static List<?> candidateRefs(Object set) { 250 Object refs = set instanceof Map<?, ?> ? ((Map<?, ?>) set).get("candidateRefs") : null; 251 return refs instanceof List<?> ? (List<?>) refs : Collections.emptyList(); 252 } 253 254 /** Section 4.2: the binding of every field endpoint carries declarationBasis. */ 255 private static void declarationBases(DocumentIndex index, List<Finding> findings) { 256 for (Map<?, ?> occurrence : index.all("occurrence")) { 257 Map<?, ?> fact = index.get("fact", occurrence.get("factRef")); 258 Map<?, ?> candidate = index.get("factCandidate", occurrence.get("factCandidateRef")); 259 boolean[] fields; 260 if (fact != null) { 261 fields = new boolean[] {isField(index, fact.get("sourceRef")), isField(index, fact.get("targetRef"))}; 262 } else if (candidate != null) { 263 fields = new boolean[] {sideIsField(index, candidate.get("source")), 264 sideIsField(index, candidate.get("target"))}; 265 } else { 266 continue; 267 } 268 String[] members = {"sourceBinding", "targetBinding"}; 269 for (int i = 0; i < 2; i++) { 270 Object binding = occurrence.get(members[i]); 271 if (fields[i] && binding instanceof Map<?, ?> && !((Map<?, ?>) binding).containsKey("declarationBasis")) { 272 findings.add(new Finding("SEMANTIC_DECLARATION_BASIS_MISSING", 273 "occurrences ref " + occurrence.get("ref") + " " + members[i], "")); 274 } 275 } 276 } 277 for (Map<?, ?> edge : index.all("microEdge")) { 278 Object[][] sides = {{"sourceBinding", edge.get("sourceRef")}, {"targetBinding", edge.get("targetRef")}}; 279 for (Object[] side : sides) { 280 Object binding = edge.get(side[0]); 281 if (binding instanceof Map<?, ?> && isField(index, side[1]) 282 && !((Map<?, ?>) binding).containsKey("declarationBasis")) { 283 findings.add(new Finding("SEMANTIC_DECLARATION_BASIS_MISSING", 284 "explainGraph.microEdges ref " + edge.get("ref") + " " + side[0], "")); 285 } 286 } 287 } 288 } 289 290 /** 291 * Whether the owner chain of an endpoint reaches an endpoint of one kind. Each endpoint is walked once, so a 292 * document is checked in time linear in its endpoints; a chain that ends at a missing owner, or loops without 293 * meeting the kind, does not reach it. 294 */ 295 static final class OwnerChains { 296 private final DocumentIndex index; 297 private final String kind; 298 /** For an endpoint ref: whether it, or an endpoint on its owner chain, has the kind. */ 299 private final Map<Long, Boolean> selfOrChain = new HashMap<Long, Boolean>(); 300 301 OwnerChains(DocumentIndex index, String kind) { 302 this.index = index; 303 this.kind = kind; 304 } 305 306 boolean reaches(Map<?, ?> endpoint) { 307 Long owner = intRef(endpoint.get("ownerRef")); 308 return owner != null && selfOrChain(owner); 309 } 310 311 private boolean selfOrChain(Long start) { 312 List<Long> path = new ArrayList<Long>(); 313 Set<Long> onPath = new HashSet<Long>(); 314 Long current = start; 315 boolean result; 316 while (true) { 317 Boolean known = selfOrChain.get(current); 318 if (known != null) { 319 result = known; 320 break; 321 } 322 Map<?, ?> endpoint = index.get("endpoint", current); 323 if (endpoint == null || !onPath.add(current)) { 324 result = false; 325 break; 326 } 327 if (kind.equals(endpoint.get("kind"))) { 328 selfOrChain.put(current, Boolean.TRUE); 329 result = true; 330 break; 331 } 332 path.add(current); 333 current = intRef(endpoint.get("ownerRef")); 334 if (current == null) { 335 result = false; 336 break; 337 } 338 } 339 for (Long ref : path) { 340 selfOrChain.put(ref, result); 341 } 342 return result; 343 } 344 } 345 346 /** Section 3.2, rule 6: endpoints whose identity is document-local. */ 347 private static void checkIdentities(DocumentIndex index, List<Finding> findings) { 348 OwnerChains queryResults = new OwnerChains(index, "QUERY_RESULT"); 349 for (Map<?, ?> endpoint : index.all("endpoint")) { 350 Object identity = endpoint.get("identity"); 351 Object status = identity instanceof Map<?, ?> ? ((Map<?, ?>) identity).get("status") : null; 352 Object kind = endpoint.get("kind"); 353 String where = "endpoints ref " + endpoint.get("ref"); 354 if (LOCAL_ONLY_KINDS.contains(kind) && !"LOCAL".equals(status)) { 355 findings.add(new Finding("SEMANTIC_LOCAL_IDENTITY_REQUIRED", where, kind + " declares " + status)); 356 } else if (LOCAL_OR_OPAQUE_KINDS.contains(kind) && !"LOCAL".equals(status) 357 && !"OPAQUE_PRODUCER_LOCAL".equals(status)) { 358 findings.add(new Finding("SEMANTIC_LOCAL_IDENTITY_REQUIRED", where, kind + " declares " + status)); 359 } else if (!"LOCAL".equals(status) && queryResults.reaches(endpoint)) { 360 findings.add(new Finding("SEMANTIC_LOCAL_IDENTITY_REQUIRED", where, 361 "owned by a QUERY_RESULT but declares " + status)); 362 } 363 } 364 } 365 366 private static List<Object[]> bindings(DocumentIndex index) { 367 List<Object[]> out = new ArrayList<Object[]>(); 368 for (Map<?, ?> occurrence : index.all("occurrence")) { 369 for (String member : new String[] {"sourceBinding", "targetBinding"}) { 370 if (occurrence.get(member) instanceof Map<?, ?>) { 371 out.add(new Object[] {"occurrences ref " + occurrence.get("ref") + " " + member, occurrence.get(member)}); 372 } 373 } 374 } 375 for (Map<?, ?> edge : index.all("microEdge")) { 376 for (String member : new String[] {"sourceBinding", "targetBinding"}) { 377 if (edge.get(member) instanceof Map<?, ?>) { 378 out.add(new Object[] {"explainGraph.microEdges ref " + edge.get("ref") + " " + member, edge.get(member)}); 379 } 380 } 381 } 382 return out; 383 } 384 385 /** 386 * The ExplainGraph candidate sets that the first (source side) or last (target side) micro-edge of an Occurrence's 387 * support paths binds through, one per set, by ref. 388 */ 389 private static List<Map<?, ?>> walkedSets(DocumentIndex index, Map<?, ?> occurrence, String side) { 390 TreeMap<Long, Map<?, ?>> out = new TreeMap<Long, Map<?, ?>>(); 391 for (Object pathRef : occurrence.get("supportPathRefs") instanceof List<?> 392 ? (List<?>) occurrence.get("supportPathRefs") : Collections.emptyList()) { 393 Map<?, ?> path = index.get("supportPath", pathRef); 394 List<?> refs = path != null && path.get("microEdgeRefs") instanceof List<?> ? (List<?>) path.get("microEdgeRefs") 395 : Collections.emptyList(); 396 Map<?, ?> edge = refs.isEmpty() ? null 397 : index.get("microEdge", "sourceBinding".equals(side) ? refs.get(0) : refs.get(refs.size() - 1)); 398 Object binding = edge == null ? null : edge.get(side); 399 if (ambiguous(binding)) { 400 Map<?, ?> set = index.get("candidateSet", ((Map<?, ?>) binding).get("candidateSetRef")); 401 if (set != null) { 402 out.put(DocumentIndex.intRef(set.get("ref")), set); 403 } 404 } 405 } 406 return new ArrayList<Map<?, ?>>(out.values()); 407 } 408 409 /** A binding with status CANDIDATE or UNRESOLVED. */ 410 private static boolean ambiguous(Object binding) { 411 return binding instanceof Map<?, ?> && ("CANDIDATE".equals(((Map<?, ?>) binding).get("status")) 412 || "UNRESOLVED".equals(((Map<?, ?>) binding).get("status"))); 413 } 414 415 /** The proof, binding and coverage rules of Sections 4.3 and 4.4 that relate two statuses or two objects. */ 416 private static void checkContracts(Map<?, ?> doc, DocumentIndex index, List<Finding> findings) { 417 // Section 4.3, rule 1: a Fact holds whichever candidate each reference denotes (rule 4); its Occurrence at an 418 // ambiguous reference is ASSUMED and names a candidate set of the document's candidateSets (in L2, the Endpoint 419 // projection of the set its walks read or write through), and for each side on which its binding is ambiguous 420 // the Fact has another Occurrence of the same statement whose binding on that side is not (fold-v1, Sections 421 // 7.1 and 8.1) 422 Map<String, Map<?, ?>> rootSets = new HashMap<String, Map<?, ?>>(); 423 for (Map<?, ?> set : DocumentIndex.items(doc.get("candidateSets"))) { 424 rootSets.put(String.valueOf(set.get("ref")), set); 425 } 426 Map<String, Set<Object>> settled = new HashMap<String, Set<Object>>(); 427 for (Map<?, ?> occurrence : index.all("occurrence")) { 428 for (String side : new String[] {"sourceBinding", "targetBinding"}) { 429 if (occurrence.containsKey("factRef") && !ambiguous(occurrence.get(side))) { 430 String key = side + " " + occurrence.get("factRef") + " " + occurrence.get("statementRef"); 431 if (!settled.containsKey(key)) { 432 settled.put(key, new HashSet<Object>()); 433 } 434 settled.get(key).add(occurrence.get("ref")); 435 } 436 } 437 } 438 for (Map<?, ?> occurrence : index.all("occurrence")) { 439 String where = "occurrences ref " + occurrence.get("ref"); 440 List<Object> statuses = new ArrayList<Object>(); 441 for (String member : new String[] {"sourceBinding", "targetBinding"}) { 442 if (occurrence.get(member) instanceof Map<?, ?>) { 443 statuses.add(((Map<?, ?>) occurrence.get(member)).get("status")); 444 } 445 } 446 boolean ambiguous = statuses.contains("CANDIDATE") || statuses.contains("UNRESOLVED"); 447 if (occurrence.containsKey("factRef") && ambiguous) { 448 if (!"ASSUMED".equals(occurrence.get("proofStatus"))) { 449 findings.add(new Finding("SEMANTIC_BINDING_STATUS_MISMATCH", where, 450 "an Occurrence of a Fact with a CANDIDATE or UNRESOLVED binding is not ASSUMED")); 451 } 452 for (String side : new String[] {"sourceBinding", "targetBinding"}) { 453 Object binding = occurrence.get(side); 454 if (!ambiguous(binding)) { 455 continue; 456 } 457 Set<Object> others = settled.get(side + " " + occurrence.get("factRef") + " " 458 + occurrence.get("statementRef")); 459 if (others == null || others.isEmpty() 460 || (others.size() == 1 && others.contains(occurrence.get("ref")))) { 461 findings.add(new Finding("SEMANTIC_BINDING_STATUS_MISMATCH", where, 462 "a Fact rests on Occurrences with a CANDIDATE or UNRESOLVED " + side)); 463 } 464 Map<?, ?> root = rootSets.get(String.valueOf(((Map<?, ?>) binding).get("candidateSetRef"))); 465 if (root == null) { 466 findings.add(new Finding("SEMANTIC_BINDING_STATUS_MISMATCH", where, 467 "the candidate set of an Occurrence of a Fact is not in candidateSets")); 468 continue; 469 } 470 for (Map<?, ?> graphSet : walkedSets(index, occurrence, side)) { 471 List<Long> members = new ArrayList<Long>(); 472 int total = 0; 473 for (Object r : graphSet.get("candidateRefs") instanceof List<?> 474 ? (List<?>) graphSet.get("candidateRefs") : Collections.emptyList()) { 475 if (index.get("endpoint", r) != null) { 476 members.add(DocumentIndex.intRef(r)); 477 } 478 total++; 479 } 480 boolean complete = "COMPLETE".equals(graphSet.get("membership")) && members.size() >= 2 481 && members.size() == total; 482 List<Long> listed = new ArrayList<Long>(); 483 for (Object r : root.get("candidateRefs") instanceof List<?> 484 ? (List<?>) root.get("candidateRefs") : Collections.emptyList()) { 485 listed.add(DocumentIndex.intRef(r)); 486 } 487 Collections.sort(members); 488 Collections.sort(listed); 489 if (!members.equals(listed) 490 || !(complete ? "COMPLETE" : "INCOMPLETE").equals(root.get("membership"))) { 491 findings.add(new Finding("SEMANTIC_BINDING_STATUS_MISMATCH", where, "the candidate set " 492 + root.get("ref") + " is not the Endpoint projection of set " + graphSet.get("ref"))); 493 } 494 } 495 } 496 } 497 if (occurrence.containsKey("factCandidateRef") && !ambiguous) { 498 findings.add(new Finding("SEMANTIC_BINDING_STATUS_MISMATCH", where, 499 "an Occurrence of a FactCandidate carries no CANDIDATE or UNRESOLVED binding")); 500 } 501 if ("PROVEN".equals(occurrence.get("proofStatus")) && statuses.contains("ASSUMED")) { 502 findings.add(new Finding("SEMANTIC_PROOF_STRONGER_THAN_BINDING", where, "PROVEN with an ASSUMED binding")); 503 } 504 } 505 for (Object[] item : bindings(index)) { 506 Map<?, ?> binding = (Map<?, ?>) item[1]; 507 Set<String> bases = new HashSet<String>(); 508 if (binding.get("bases") instanceof List<?>) { 509 for (Object base : (List<?>) binding.get("bases")) { 510 if (base instanceof String) { 511 bases.add((String) base); 512 } 513 } 514 } 515 Object status = binding.get("status"); 516 if (bases.contains("PRODUCER_DEFAULT") && "PROVEN".equals(status)) { 517 findings.add(new Finding("SEMANTIC_BINDING_BASIS_VIOLATION", (String) item[0], 518 "PROVEN on a PRODUCER_DEFAULT basis")); 519 } 520 boolean declaring = false; 521 for (String base : DECLARING_BASES) { 522 declaring |= bases.contains(base); 523 } 524 if (bases.contains("SINGLE_SOURCE_SCOPE") && !declaring 525 && ("PROVEN".equals(status) || !bases.contains("USAGE_INFERRED"))) { 526 findings.add(new Finding("SEMANTIC_BINDING_BASIS_VIOLATION", (String) item[0], 527 "SINGLE_SOURCE_SCOPE alone must be ASSUMED or weaker, with USAGE_INFERRED")); 528 } 529 if (bases.contains("LOCAL_COLUMN_ASSUMED") && (!"ASSUMED".equals(status) 530 || !bases.contains("SINGLE_SOURCE_SCOPE") || !bases.contains("USAGE_INFERRED"))) { 531 findings.add(new Finding("SEMANTIC_BINDING_BASIS_VIOLATION", (String) item[0], 532 "LOCAL_COLUMN_ASSUMED must be ASSUMED, with SINGLE_SOURCE_SCOPE and USAGE_INFERRED")); 533 } 534 } 535 for (Map<?, ?> set : index.all("candidateSet")) { 536 Object refs = set.get("candidateRefs"); 537 int size = refs instanceof List<?> ? ((List<?>) refs).size() : 0; 538 if ("COMPLETE".equals(set.get("membership")) && size < 2) { 539 findings.add(new Finding("SEMANTIC_CANDIDATE_SET_INVALID", "candidate set ref " + set.get("ref"), 540 "membership COMPLETE with fewer than two members")); 541 } 542 } 543 decidedReferences(index, findings); 544 for (Map<?, ?> tfo : index.all("tfo")) { 545 String where = "targetFieldOccurrences ref " + tfo.get("ref"); 546 List<Map<?, ?>> inputs = items(tfo.get("inputs")); 547 Object completeness = tfo.get("upstreamCompleteness"); 548 int disposition = (tfo.containsKey("upstreamDisposition") ? 1 : 0) 549 + (tfo.containsKey("upstreamDispositionReason") ? 1 : 0); 550 if (inputs.isEmpty() && "COMPLETE".equals(completeness) && disposition != 2) { 551 findings.add(new Finding("SEMANTIC_ZERO_UPSTREAM_INVALID", where, 552 "no inputs, COMPLETE, without upstreamDisposition and its reason")); 553 } else if (inputs.isEmpty() && !"COMPLETE".equals(completeness) 554 && (!"UNKNOWN".equals(completeness) || disposition > 0)) { 555 findings.add(new Finding("SEMANTIC_ZERO_UPSTREAM_INVALID", where, 556 "no inputs and lowered: must be UNKNOWN without a disposition")); 557 } else if (!inputs.isEmpty() && disposition > 0) { 558 findings.add(new Finding("SEMANTIC_ZERO_UPSTREAM_INVALID", where, "inputs and an upstreamDisposition")); 559 } 560 boolean viaCandidate = false; 561 boolean unknownInput = false; 562 for (Map<?, ?> input : inputs) { 563 viaCandidate |= input.containsKey("factCandidateRef"); 564 unknownInput |= "UNKNOWN_INPUT".equals(input.get("kind")); 565 } 566 if ("COMPLETE".equals(completeness) && viaCandidate) { 567 findings.add(new Finding("SEMANTIC_COVERAGE_NOT_LOWERED", where, 568 "a FIELD_REF input through a FactCandidate, yet COMPLETE")); 569 } 570 if ("COMPLETE".equals(completeness) && unknownInput) { 571 findings.add(new Finding("SEMANTIC_COVERAGE_NOT_LOWERED", where, "an UNKNOWN_INPUT input, yet COMPLETE")); 572 } 573 } 574 List<Object[]> lowered = new ArrayList<Object[]>(); 575 for (Map<?, ?> unit : index.all("sourceUnit")) { 576 lowered.add(new Object[] {"sourceUnits ref " + unit.get("ref"), !"COMPLETE".equals(unit.get("analysisCompleteness")), 577 unit}); 578 } 579 for (Map<?, ?> statement : index.all("statement")) { 580 lowered.add(new Object[] {"statements ref " + statement.get("ref"), 581 !"COMPLETE".equals(statement.get("analysisCompleteness")), statement}); 582 } 583 List<Object[]> features = new ArrayList<Object[]>(); 584 features.add(new Object[] {"featureCompleteness", doc.get("featureCompleteness")}); 585 for (Map<?, ?> unit : index.all("sourceUnit")) { 586 features.add(new Object[] {"sourceUnits ref " + unit.get("ref") + " featureCompleteness", 587 unit.get("featureCompleteness")}); 588 } 589 for (Map<?, ?> statement : index.all("statement")) { 590 features.add(new Object[] {"statements ref " + statement.get("ref") + " featureCompleteness", 591 statement.get("featureCompleteness")}); 592 } 593 for (Object[] collection : features) { 594 for (Map<?, ?> feature : items(collection[1])) { 595 lowered.add(new Object[] {collection[0] + " " + feature.get("feature"), 596 !"COMPLETE".equals(feature.get("status")), feature}); 597 } 598 } 599 for (Map<?, ?> tfo : index.all("tfo")) { 600 lowered.add(new Object[] {"targetFieldOccurrences ref " + tfo.get("ref"), 601 !"COMPLETE".equals(tfo.get("upstreamCompleteness")), tfo}); 602 } 603 for (Map<?, ?> node : index.all("node")) { 604 Object completeness = node.get("upstreamCompleteness"); 605 lowered.add(new Object[] {"explainGraph.nodes ref " + node.get("ref"), 606 "PARTIAL".equals(completeness) || "UNKNOWN".equals(completeness), node}); 607 } 608 for (Object[] item : lowered) { 609 Object refs = ((Map<?, ?>) item[2]).get("diagnosticRefs"); 610 if ((Boolean) item[1] && !truthy(refs)) { 611 findings.add(new Finding("SEMANTIC_COVERAGE_DIAGNOSTIC_MISSING", (String) item[0], 612 "lowered without a diagnostic")); 613 } 614 } 615 } 616 617 /** Python truthiness of a JSON value. */ 618 static boolean truthy(Object value) { 619 if (value == null || Boolean.FALSE.equals(value)) { 620 return false; 621 } 622 if (value instanceof List<?>) { 623 return !((List<?>) value).isEmpty(); 624 } 625 if (value instanceof Map<?, ?>) { 626 return !((Map<?, ?>) value).isEmpty(); 627 } 628 if (value instanceof String) { 629 return !((String) value).isEmpty(); 630 } 631 if (value instanceof Number) { 632 return ((Number) value).longValue() != 0; 633 } 634 return true; 635 } 636 637 /** Section 12.3: the embedded text agrees with header.sourceEmbedding. */ 638 private static void checkSourceEmbedding(Map<?, ?> doc, DocumentIndex index, List<Finding> findings) { 639 Object header = doc.get("header"); 640 Object embedding = header instanceof Map<?, ?> ? ((Map<?, ?>) header).get("sourceEmbedding") : null; 641 String need = "FULL".equals(embedding) ? "DECODED_TEXT_SHA256" 642 : "REDACTED".equals(embedding) ? "REDACTED_TEXT_SHA256" : null; 643 for (Map<?, ?> unit : index.all("sourceUnit")) { 644 String where = "sourceUnits ref " + unit.get("ref"); 645 Set<Object> domains = new HashSet<Object>(); 646 for (Map<?, ?> digest : items(unit.get("digests"))) { 647 domains.add(digest.get("domain")); 648 } 649 if ("NONE".equals(embedding) && unit.containsKey("text")) { 650 findings.add(new Finding("SEMANTIC_SOURCE_EMBEDDING_MISMATCH", where, "text under sourceEmbedding NONE")); 651 } 652 if (need != null && (!unit.containsKey("text") || !domains.contains(need))) { 653 findings.add(new Finding("SEMANTIC_SOURCE_EMBEDDING_MISMATCH", where, 654 "sourceEmbedding " + embedding + " needs text and a " + need + " digest")); 655 } 656 } 657 } 658 659 /** path-v1.md: every path of an analysis context that declares sqllr-path-1 follows PATH-V1. */ 660 private static void checkPaths(Map<?, ?> doc, final DocumentIndex index, List<Finding> findings) { 661 final Map<Long, Object> contexts = new LinkedHashMap<Long, Object>(); 662 for (Map<?, ?> context : items(doc.get("analysisContexts"))) { 663 contexts.put(intRef(context.get("ref")), context.get("pathProfile")); 664 } 665 Set<Object> profiles = new HashSet<Object>(contexts.values()); 666 final Object fallback = !contexts.isEmpty() && profiles.size() == 1 667 && profiles.contains(IdentityV1.PATH_PROFILE_V1) ? IdentityV1.PATH_PROFILE_V1 : null; 668 List<Object[]> paths = new ArrayList<Object[]>(); 669 for (Map<?, ?> occurrence : index.all("occurrence")) { 670 paths.add(new Object[] {"occurrences ref " + occurrence.get("ref") + " path", occurrence.get("path"), 671 profileOf(index, contexts, fallback, occurrence.get("statementRef"))}); 672 } 673 for (Map<?, ?> scope : index.all("scope")) { 674 if (scope.containsKey("path")) { 675 paths.add(new Object[] {"scopes ref " + scope.get("ref") + " path", scope.get("path"), 676 profileOf(index, contexts, fallback, scope.get("statementRef"))}); 677 } 678 } 679 for (Map<?, ?> unit : index.all("sourceUnit")) { 680 if (unit.containsKey("sitePath")) { 681 paths.add(new Object[] {"sourceUnits ref " + unit.get("ref") + " sitePath", unit.get("sitePath"), 682 profileOf(index, contexts, fallback, unit.get("siteStatementRef"))}); 683 } 684 } 685 for (Map<?, ?> node : index.all("node")) { 686 paths.add(new Object[] {"explainGraph.nodes ref " + node.get("ref") + " path", node.get("path"), 687 profileOf(index, contexts, fallback, node.get("statementRef"))}); 688 } 689 for (Map<?, ?> edge : index.all("microEdge")) { 690 if (edge.containsKey("referencePath")) { 691 paths.add(new Object[] {"explainGraph.microEdges ref " + edge.get("ref") + " referencePath", 692 edge.get("referencePath"), profileOf(index, contexts, fallback, edge.get("statementRef"))}); 693 } 694 } 695 for (Object[] path : paths) { 696 if (IdentityV1.PATH_PROFILE_V1.equals(path[2])) { 697 List<String> problems = PathV1.violations(path[1]); 698 if (!problems.isEmpty()) { 699 findings.add(new Finding("SEMANTIC_PATH_PROFILE_VIOLATION", (String) path[0], path[1] + ": " 700 + problems.get(0))); 701 } 702 } 703 } 704 } 705 706 private static Object profileOf(DocumentIndex index, Map<Long, Object> contexts, Object fallback, Object statementRef) { 707 Map<?, ?> statement = index.get("statement", statementRef); 708 Map<?, ?> unit = statement == null ? null : index.get("sourceUnit", statement.get("sourceUnitRef")); 709 Long context = unit == null ? null : intRef(unit.get("analysisContextRef")); 710 return contexts.containsKey(context) ? contexts.get(context) : fallback; 711 } 712 713 static List<String> stepCodes(Object path) { 714 List<String> out = new ArrayList<String>(); 715 if (path instanceof String) { 716 for (String step : ((String) path).split("/", -1)) { 717 int bracket = step.indexOf('['); 718 out.add(bracket < 0 ? step : step.substring(0, bracket)); 719 } 720 } 721 return out; 722 } 723 724 static Map<?, ?> topOwner(DocumentIndex index, Map<?, ?> endpoint) { 725 Set<Long> seen = new HashSet<Long>(); 726 while (endpoint != null && "FIELD".equals(endpoint.get("kind"))) { 727 Long owner = intRef(endpoint.get("ownerRef")); 728 if (owner == null || !seen.add(owner)) { 729 return null; 730 } 731 endpoint = index.get("endpoint", owner); 732 } 733 return endpoint; 734 } 735 736 /** (source refs, target refs, kind) of the Fact or FactCandidate of an Occurrence. */ 737 static Object[] subjectEnds(DocumentIndex index, Map<?, ?> occurrence) { 738 Map<?, ?> fact = index.get("fact", occurrence.get("factRef")); 739 if (fact != null) { 740 return new Object[] {Collections.singletonList(fact.get("sourceRef")), 741 Collections.singletonList(fact.get("targetRef")), fact.get("kind")}; 742 } 743 Map<?, ?> candidate = index.get("factCandidate", occurrence.get("factCandidateRef")); 744 if (candidate == null) { 745 candidate = Collections.emptyMap(); 746 } 747 List<List<Object>> ends = new ArrayList<List<Object>>(); 748 for (String side : new String[] {"source", "target"}) { 749 Map<?, ?> end = candidate.get(side) instanceof Map<?, ?> ? (Map<?, ?>) candidate.get(side) 750 : Collections.emptyMap(); 751 if (end.containsKey("endpointRef")) { 752 ends.add(Collections.singletonList(end.get("endpointRef"))); 753 } else { 754 Object set = end.get("candidateSet"); 755 ends.add(new ArrayList<Object>(truthy(set) ? candidateRefs(set) : Collections.emptyList())); 756 } 757 } 758 return new Object[] {ends.get(0), ends.get(1), candidate.get("kind")}; 759 } 760 761 /** 762 * fold-v1.md, Section 4.4: a COMPLETE SELECT, INSERT or CREATE statement that reads a field of a relation 763 * inside a query block also asserts ROW Facts (or FactCandidates) from that relation's ROW_SET. 764 */ 765 @SuppressWarnings("unchecked") 766 private static void checkRowPropagation(DocumentIndex index, List<Finding> findings) { 767 Map<Long, List<Map<?, ?>>> byStatement = new HashMap<Long, List<Map<?, ?>>>(); 768 for (Map<?, ?> occurrence : index.all("occurrence")) { 769 Long statement = intRef(occurrence.get("statementRef")); 770 List<Map<?, ?>> list = byStatement.get(statement); 771 if (list == null) { 772 list = new ArrayList<Map<?, ?>>(); 773 byStatement.put(statement, list); 774 } 775 list.add(occurrence); 776 } 777 // a routine that returns a set of rows owns a ROW_SET (spec Section 7.3): a call in FROM that reads its fields 778 // reads its rows too; a routine that returns one value or one row owns none and is no row source 779 Set<Long> rowSetOwners = new HashSet<Long>(); 780 for (Map<?, ?> endpoint : index.all("endpoint")) { 781 if ("ROW_SET".equals(endpoint.get("kind")) && endpoint.get("ownerRef") != null) { 782 rowSetOwners.add(intRef(endpoint.get("ownerRef"))); 783 } 784 } 785 for (Map<?, ?> statement : index.all("statement")) { 786 if (!"COMPLETE".equals(statement.get("analysisCompleteness"))) { 787 continue; 788 } 789 List<Map<?, ?>> occurrences = byStatement.get(intRef(statement.get("ref"))); 790 if (occurrences == null) { 791 occurrences = Collections.emptyList(); 792 } 793 boolean unresolved = false; 794 for (Map<?, ?> occurrence : occurrences) { 795 Object[] ends = subjectEnds(index, occurrence); 796 for (int side = 0; side < 2; side++) { 797 for (Object ref : (List<Object>) ends[side]) { 798 unresolved |= index.get("endpoint", ref) == null; 799 } 800 } 801 } 802 if (unresolved) { 803 continue; // a reference that does not resolve is reported at stage REFERENCE 804 } 805 Set<Long> propagated = new HashSet<Long>(); 806 Map<Long, Object> read = new TreeMap<Long, Object>(); 807 for (Map<?, ?> occurrence : occurrences) { 808 Object[] ends = subjectEnds(index, occurrence); 809 for (Object source : (List<Object>) ends[0]) { 810 Map<?, ?> endpoint = index.get("endpoint", source); 811 if (endpoint == null) { 812 endpoint = Collections.emptyMap(); 813 } 814 if ("ROW".equals(ends[2]) && "ROW_SET".equals(endpoint.get("kind"))) { 815 propagated.add(intRef(endpoint.get("ownerRef"))); 816 } 817 List<String> codes = stepCodes(occurrence.get("path")); 818 boolean inBlock = false; 819 for (String code : codes) { 820 inBlock |= QUERY_BLOCK_STEPS.contains(code); 821 } 822 if ("FIELD".equals(endpoint.get("kind")) && !codes.isEmpty() 823 && QUERY_STATEMENT_ROOTS.contains(codes.get(0)) && inBlock) { 824 Map<?, ?> owner = topOwner(index, endpoint); 825 if (owner != null && (ROW_SOURCE_KINDS.contains(owner.get("kind")) 826 || ("ROUTINE".equals(owner.get("kind")) 827 && rowSetOwners.contains(intRef(owner.get("ref")))))) { 828 Long ownerRef = intRef(owner.get("ref")); 829 if (!read.containsKey(ownerRef)) { 830 read.put(ownerRef, occurrence.get("ref")); 831 } 832 } 833 } 834 } 835 } 836 for (Map.Entry<Long, Object> entry : read.entrySet()) { 837 if (!propagated.contains(entry.getKey())) { 838 findings.add(new Finding("SEMANTIC_ROW_PROPAGATION_MISSING", "statements ref " + statement.get("ref"), 839 "fields of endpoint ref " + entry.getKey() + " are read (occurrences ref " + entry.getValue() 840 + ") but no ROW Fact leaves its ROW_SET")); 841 } 842 } 843 } 844 } 845 846 // --------------------------------------------------------------------------------------------- 847 // Spans (Sections 7.8 and 12.2) 848 // --------------------------------------------------------------------------------------------- 849 850 /** 851 * The text of a source unit, already checked to be UTF-8: the line and column (in code points, 1-based) of a 852 * UTF-8 offset. It keeps the bytes, the offset of every line start and the code points before every 64th byte, 853 * so its size is about that of the text and a lookup reads at most 63 bytes. 854 */ 855 static final class SourceText { 856 private static final int STRIDE = 64; 857 private final byte[] data; 858 private final int[] lineStarts; 859 private final int[] codePointsBefore; 860 861 SourceText(byte[] data) { 862 this.data = data; 863 int lines = 1; 864 for (byte b : data) { 865 if (b == '\n') { 866 lines++; 867 } 868 } 869 lineStarts = new int[lines]; 870 codePointsBefore = new int[data.length / STRIDE + 1]; 871 int line = 1; 872 int codePoints = 0; 873 for (int i = 0; i < data.length; i++) { 874 if (i % STRIDE == 0) { 875 codePointsBefore[i / STRIDE] = codePoints; 876 } 877 if (!continuation(data[i])) { 878 codePoints++; 879 } 880 if (data[i] == '\n') { 881 lineStarts[line++] = i + 1; 882 } 883 } 884 if (data.length % STRIDE == 0) { 885 codePointsBefore[data.length / STRIDE] = codePoints; 886 } 887 } 888 889 private static boolean continuation(byte b) { 890 return (b & 0xc0) == 0x80; 891 } 892 893 private int codePointsBefore(int offset) { 894 int count = codePointsBefore[offset / STRIDE]; 895 for (int i = offset - offset % STRIDE; i < offset; i++) { 896 if (!continuation(data[i])) { 897 count++; 898 } 899 } 900 return count; 901 } 902 903 /** {line, column} of a UTF-8 offset, or null when it is outside the text or inside a character. */ 904 long[] position(long offset) { 905 if (offset < 0 || offset > data.length || (offset < data.length && continuation(data[(int) offset]))) { 906 return null; 907 } 908 int at = (int) offset; 909 int low = 0; 910 int high = lineStarts.length - 1; 911 while (low < high) { 912 int mid = (low + high + 1) >>> 1; 913 if (lineStarts[mid] <= at) { 914 low = mid; 915 } else { 916 high = mid - 1; 917 } 918 } 919 return new long[] {low + 1, codePointsBefore(at) - codePointsBefore(lineStarts[low]) + 1}; 920 } 921 } 922 923 private SourceText sourceText(Map<?, ?> unit, List<String> notes) { 924 if (!"NAMED".equals(unit.get("identityKind"))) { 925 return null; 926 } 927 Object namespace = unit.get("namespace"); 928 Object name = unit.get("logicalName"); 929 if (!(namespace instanceof String) || !(name instanceof String)) { 930 return null; 931 } 932 byte[] data = sources.read((String) namespace, (String) name); 933 if (data == null) { 934 notes.add("source unit ref " + unit.get("ref") + ": " + name + " not available; spans not checked against text"); 935 return null; 936 } 937 String digest = sha256(data); 938 Map<Object, Object> declared = new HashMap<Object, Object>(); 939 for (Map<?, ?> d : items(unit.get("digests"))) { 940 declared.put(d.get("domain"), d.get("value")); 941 } 942 for (String domain : new String[] {"RAW_BYTES_SHA256", "DECODED_TEXT_SHA256"}) { 943 if (declared.containsKey(domain) && !digest.equals(declared.get(domain))) { 944 notes.add("source unit ref " + unit.get("ref") + ": " + domain + " does not match " + name 945 + "; spans not checked against text"); 946 return null; 947 } 948 } 949 if (data.length >= 3 && (data[0] & 0xff) == 0xef && (data[1] & 0xff) == 0xbb && (data[2] & 0xff) == 0xbf) { 950 return null; 951 } 952 try { 953 CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT) 954 .onUnmappableCharacter(CodingErrorAction.REPORT); 955 ByteBuffer in = ByteBuffer.wrap(data); 956 CharBuffer out = CharBuffer.allocate(4096); 957 CoderResult result; 958 do { 959 // through Buffer: CharBuffer.clear() exists only from Java 9, and a build on a newer JDK without 960 // --release 8 would bind to it (NoSuchMethodError on Java 8) 961 ((java.nio.Buffer) out).clear(); 962 result = decoder.decode(in, out, true); 963 if (result.isError()) { 964 result.throwException(); 965 } 966 } while (result.isOverflow()); 967 do { 968 ((java.nio.Buffer) out).clear(); 969 result = decoder.flush(out); 970 } while (result.isOverflow()); 971 return new SourceText(data); 972 } catch (CharacterCodingException e) { 973 notes.add("source unit ref " + unit.get("ref") + ": " + name + " is not UTF-8; spans not checked against text"); 974 return null; 975 } 976 } 977 978 private static String sha256(byte[] data) { 979 try { 980 byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); 981 StringBuilder out = new StringBuilder(); 982 for (byte b : digest) { 983 out.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16)); 984 } 985 return out.toString(); 986 } catch (NoSuchAlgorithmException e) { 987 throw new IllegalStateException(e); 988 } 989 } 990 991 private static Long offsetOf(Object position) { 992 return position instanceof Map<?, ?> ? intRef(((Map<?, ?>) position).get("utf8ByteOffset0")) : null; 993 } 994 995 private static void checkPosition(SourceText text, Object position, String where, List<Finding> findings) { 996 Long offset = offsetOf(position); 997 if (offset == null || text == null) { 998 return; 999 } 1000 long[] expected = text.position(offset); 1001 if (expected == null) { 1002 findings.add(new Finding("SEMANTIC_INVALID_SPAN", where, "offset " + offset 1003 + " is outside the text or inside a character")); 1004 return; 1005 } 1006 Map<?, ?> p = (Map<?, ?>) position; 1007 Long line = intRef(p.get("line1")); 1008 Long column = intRef(p.get("column1")); 1009 if (line == null || column == null || line != expected[0] || column != expected[1]) { 1010 findings.add(new Finding("SEMANTIC_INVALID_SPAN", where, "line/column " + p.get("line1") + "/" 1011 + p.get("column1") + " disagree with offset " + offset)); 1012 } 1013 } 1014 1015 private static void checkSpan(Map<Long, SourceText> texts, Object unitRef, Object start, Object end, String where, 1016 List<Finding> findings) { 1017 Long a = offsetOf(start); 1018 Long b = offsetOf(end); 1019 if (a != null && b != null && b < a) { 1020 findings.add(new Finding("SEMANTIC_INVALID_SPAN", where, "ends before it starts")); 1021 return; 1022 } 1023 SourceText text = texts.get(intRef(unitRef)); 1024 checkPosition(text, start, where + ".start", findings); 1025 checkPosition(text, end, where + ".end", findings); 1026 } 1027 1028 private void checkSpans(DocumentIndex index, List<Finding> findings, List<String> notes) { 1029 Map<Long, SourceText> texts = new HashMap<Long, SourceText>(); 1030 for (Map<?, ?> unit : index.all("sourceUnit")) { 1031 texts.put(intRef(unit.get("ref")), sourceText(unit, notes)); 1032 } 1033 for (Map<?, ?> statement : index.all("statement")) { 1034 checkSpan(texts, statement.get("sourceUnitRef"), statement.get("start"), statement.get("end"), 1035 "statements ref " + statement.get("ref"), findings); 1036 } 1037 for (Map<?, ?> node : index.all("node")) { 1038 Map<?, ?> span = node.get("span") instanceof Map<?, ?> ? (Map<?, ?>) node.get("span") : Collections.emptyMap(); 1039 checkSpan(texts, span.get("sourceUnitRef"), span.get("start"), span.get("end"), 1040 "explainGraph.nodes ref " + node.get("ref") + " span", findings); 1041 } 1042 for (Map<?, ?> edge : index.all("microEdge")) { 1043 Map<?, ?> statement = index.get("statement", edge.get("statementRef")); 1044 if (edge.containsKey("referenceStart")) { 1045 checkPosition(texts.get(statement == null ? null : intRef(statement.get("sourceUnitRef"))), 1046 edge.get("referenceStart"), "explainGraph.microEdges ref " + edge.get("ref") + " referenceStart", 1047 findings); 1048 } 1049 } 1050 for (Map<?, ?> diagnostic : index.all("diagnostic")) { 1051 if (diagnostic.get("span") instanceof Map<?, ?>) { 1052 Map<?, ?> span = (Map<?, ?>) diagnostic.get("span"); 1053 checkSpan(texts, span.get("sourceUnitRef"), span.get("start"), span.get("end"), 1054 "diagnostics ref " + diagnostic.get("ref") + " span", findings); 1055 } 1056 } 1057 } 1058 1059 /** canonical-json-v1 Section 5 and Section 7.7, item 4: the columns of one result set carry 0..n-1. */ 1060 private static void checkColumnOrdinals(DocumentIndex index, List<Finding> findings) { 1061 Map<Long, List<Object>> columns = new TreeMap<Long, List<Object>>(); 1062 for (Map<?, ?> node : index.all("node")) { 1063 Long owner = intRef(node.get("ownerRef")); 1064 if ("FIELD".equals(node.get("kind")) && owner != null) { 1065 List<Object> list = columns.get(owner); 1066 if (list == null) { 1067 list = new ArrayList<Object>(); 1068 columns.put(owner, list); 1069 } 1070 list.add(node.get("ordinal")); 1071 } 1072 } 1073 for (Map.Entry<Long, List<Object>> entry : columns.entrySet()) { 1074 List<Long> ordinals = new ArrayList<Long>(); 1075 for (Object ordinal : entry.getValue()) { 1076 if (ordinal instanceof Boolean) { 1077 ordinals.add(((Boolean) ordinal).booleanValue() ? 1L : 0L); // Python: bool is an int 1078 } else if (intRef(ordinal) != null) { 1079 ordinals.add(intRef(ordinal)); 1080 } 1081 } 1082 Collections.sort(ordinals); 1083 boolean ok = ordinals.size() == entry.getValue().size(); 1084 for (int i = 0; ok && i < ordinals.size(); i++) { 1085 ok = ordinals.get(i) == i; 1086 } 1087 if (!ok) { 1088 findings.add(new Finding("SEMANTIC_ARRAY_KIND_VIOLATION", "explainGraph.nodes owner " + entry.getKey(), 1089 "column ordinals " + entry.getValue() + " are not 0..n-1")); 1090 } 1091 } 1092 } 1093 1094 /** 1095 * fold-v1, Sections 4.3 and 7.1: a candidate set decided by another reference. decidedBy appears in 1096 * explainGraph.candidateSets, naming a deciding set of the graph, and at the ends of FactCandidates, naming a set of 1097 * the document's candidateSets; never on a set of the document's candidateSets. The deciding set is not itself 1098 * decided; the alignment pairs every candidate exactly once with an alternative of the deciding set, each 1099 * alternative at most once; a COMPLETE decided set needs a COMPLETE deciding set whose every alternative is paired. 1100 */ 1101 private static void decidedReferences(DocumentIndex index, List<Finding> findings) { 1102 Map<Long, Map<?, ?>> graphSets = new java.util.TreeMap<Long, Map<?, ?>>(); 1103 for (Map<?, ?> set : DocumentIndex.items(index.graph().get("candidateSets"))) { 1104 Long ref = DocumentIndex.intRef(set.get("ref")); 1105 if (ref != null) { 1106 graphSets.put(ref, set); 1107 } 1108 } 1109 Map<Long, Map<?, ?>> rootSets = new java.util.TreeMap<Long, Map<?, ?>>(); 1110 for (Map<?, ?> set : DocumentIndex.items(index.document().get("candidateSets"))) { 1111 Long ref = DocumentIndex.intRef(set.get("ref")); 1112 if (ref != null) { 1113 rootSets.put(ref, set); 1114 } 1115 if (set.containsKey("decidedBy")) { 1116 findings.add(new Finding("SEMANTIC_CANDIDATE_SET_INVALID", "candidate set ref " + set.get("ref"), 1117 "decidedBy on a set of the document's candidateSets")); 1118 } 1119 } 1120 List<Object[]> decided = new ArrayList<Object[]>(); 1121 for (Map<?, ?> set : graphSets.values()) { 1122 decided.add(new Object[] {set, graphSets}); 1123 } 1124 for (Map<?, ?> candidate : index.all("factCandidate")) { 1125 for (String side : new String[] {"source", "target"}) { 1126 Object end = candidate.get(side); 1127 Object set = end instanceof Map<?, ?> ? ((Map<?, ?>) end).get("candidateSet") : null; 1128 if (set instanceof Map<?, ?>) { 1129 decided.add(new Object[] {set, rootSets}); 1130 } 1131 } 1132 } 1133 for (Object[] item : decided) { 1134 Map<?, ?> set = (Map<?, ?>) item[0]; 1135 @SuppressWarnings("unchecked") 1136 Map<Long, Map<?, ?>> pool = (Map<Long, Map<?, ?>>) item[1]; 1137 if (!(set.get("decidedBy") instanceof Map<?, ?>)) { 1138 continue; 1139 } 1140 Map<?, ?> decidedBy = (Map<?, ?>) set.get("decidedBy"); 1141 Map<?, ?> deciding = pool.get(DocumentIndex.intRef(decidedBy.get("candidateSetRef"))); 1142 List<Long> members = refs(set.get("candidateRefs")); 1143 List<Long> alternatives = deciding == null ? Collections.<Long>emptyList() : refs(deciding.get("candidateRefs")); 1144 List<Long> paired = new ArrayList<Long>(); 1145 List<Long> chosen = new ArrayList<Long>(); 1146 for (Map<?, ?> pair : DocumentIndex.items(decidedBy.get("alignment"))) { 1147 paired.add(DocumentIndex.intRef(pair.get("candidateRef"))); 1148 chosen.add(DocumentIndex.intRef(pair.get("alternativeRef"))); 1149 } 1150 String problem = null; 1151 if (deciding == null || deciding == set || deciding.containsKey("decidedBy")) { 1152 problem = pool == graphSets ? "decidedBy names no deciding set of the graph (one without decidedBy)" 1153 : "decidedBy of a FactCandidate end names no set of the document's candidateSets"; 1154 } else if (!sortedCopy(paired).equals(sortedCopy(members))) { 1155 problem = "the alignment does not pair every candidate exactly once"; 1156 } else if (new java.util.HashSet<Long>(chosen).size() != chosen.size() 1157 || !alternatives.containsAll(chosen)) { 1158 problem = "an alternative of the alignment is not one of the deciding set's, or is paired twice"; 1159 } else if ("COMPLETE".equals(set.get("membership")) && (!"COMPLETE".equals(deciding.get("membership")) 1160 || !new java.util.HashSet<Long>(chosen).equals(new java.util.HashSet<Long>(alternatives)))) { 1161 problem = "membership COMPLETE, but the deciding set is INCOMPLETE or an alternative has no candidate"; 1162 } 1163 if (problem != null) { 1164 findings.add(new Finding("SEMANTIC_CANDIDATE_SET_INVALID", "candidate set ref " + set.get("ref"), 1165 problem)); 1166 } 1167 } 1168 } 1169 1170 private static List<Long> refs(Object list) { 1171 List<Long> out = new ArrayList<Long>(); 1172 if (list instanceof List<?>) { 1173 for (Object r : (List<?>) list) { 1174 out.add(DocumentIndex.intRef(r)); 1175 } 1176 } 1177 return out; 1178 } 1179 1180 private static List<Long> sortedCopy(List<Long> list) { 1181 List<Long> out = new ArrayList<Long>(); 1182 for (Long l : list) { 1183 out.add(l == null ? Long.valueOf(Long.MIN_VALUE) : l); 1184 } 1185 Collections.sort(out); 1186 return out; 1187 } 1188}