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}