@backbencher/brain 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,1640 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyDocumentScope,
|
|
3
|
+
documentDirScope,
|
|
4
|
+
projectRelativePath
|
|
5
|
+
} from "./chunk-2WXYNLBA.js";
|
|
6
|
+
import {
|
|
7
|
+
ASTEngine
|
|
8
|
+
} from "./chunk-V44PGPA6.js";
|
|
9
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
10
|
+
import "./chunk-SK6YVHFZ.js";
|
|
11
|
+
import "./chunk-2BSKD7O3.js";
|
|
12
|
+
import "./chunk-KAET47ZL.js";
|
|
13
|
+
import "./chunk-KCDECRJY.js";
|
|
14
|
+
import "./chunk-MLKGABMK.js";
|
|
15
|
+
|
|
16
|
+
// src/ast/real-json-analyzer.ts
|
|
17
|
+
import { statSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var VALIDATION_KEYS = /* @__PURE__ */ new Set([
|
|
20
|
+
"minLength",
|
|
21
|
+
"maxLength",
|
|
22
|
+
"minimum",
|
|
23
|
+
"maximum",
|
|
24
|
+
"exclusiveMinimum",
|
|
25
|
+
"exclusiveMaximum",
|
|
26
|
+
"pattern",
|
|
27
|
+
"format",
|
|
28
|
+
"enum",
|
|
29
|
+
"const",
|
|
30
|
+
"multipleOf",
|
|
31
|
+
"minItems",
|
|
32
|
+
"maxItems",
|
|
33
|
+
"uniqueItems",
|
|
34
|
+
"minProperties",
|
|
35
|
+
"maxProperties",
|
|
36
|
+
"additionalProperties"
|
|
37
|
+
]);
|
|
38
|
+
var API_METHOD_KEYS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "trace"]);
|
|
39
|
+
var LSP_INHERITANCE_KEYS = ["extends", "base", "superclass", "superClass", "implements", "mixins"];
|
|
40
|
+
function isDependencyContainerPath(path) {
|
|
41
|
+
const dot = path.lastIndexOf(".");
|
|
42
|
+
const segment = dot >= 0 ? path.slice(dot + 1) : path;
|
|
43
|
+
return segment === "dependencies" || segment === "devDependencies" || segment === "peerDependencies" || segment === "optionalDependencies";
|
|
44
|
+
}
|
|
45
|
+
function isPathReference(value) {
|
|
46
|
+
return value.startsWith("#/") || value.startsWith("#/definitions/") || value.startsWith("#/components/");
|
|
47
|
+
}
|
|
48
|
+
var RealJsonAnalyzer = class {
|
|
49
|
+
analyze(rootNode, content, filePath, context) {
|
|
50
|
+
const rootName = this.documentName(filePath, context?.projectRoot);
|
|
51
|
+
const ctx = {
|
|
52
|
+
filePath,
|
|
53
|
+
projectRoot: typeof context?.projectRoot === "string" ? context.projectRoot : void 0,
|
|
54
|
+
content,
|
|
55
|
+
rootName,
|
|
56
|
+
symbols: [],
|
|
57
|
+
symbolMap: /* @__PURE__ */ new Map(),
|
|
58
|
+
relationships: [],
|
|
59
|
+
imports: [],
|
|
60
|
+
exports: [],
|
|
61
|
+
errors: [],
|
|
62
|
+
requiredByObject: /* @__PURE__ */ new Map(),
|
|
63
|
+
schemaObjects: /* @__PURE__ */ new Set(),
|
|
64
|
+
referenceSymbols: [],
|
|
65
|
+
validationSymbols: [],
|
|
66
|
+
apiEndpointSymbols: [],
|
|
67
|
+
dependencySymbols: [],
|
|
68
|
+
dependencyVersions: /* @__PURE__ */ new Map(),
|
|
69
|
+
overrideCandidates: [],
|
|
70
|
+
emittedPaths: /* @__PURE__ */ new Set(),
|
|
71
|
+
usesReferenceLiterals: false,
|
|
72
|
+
usesClassDeclarations: false,
|
|
73
|
+
typeDeclarations: /* @__PURE__ */ new Map(),
|
|
74
|
+
documentIds: /* @__PURE__ */ new Map(),
|
|
75
|
+
documentAnchors: /* @__PURE__ */ new Map(),
|
|
76
|
+
inheritanceCandidates: [],
|
|
77
|
+
objectCount: 0,
|
|
78
|
+
propertyCount: 0,
|
|
79
|
+
rootNode,
|
|
80
|
+
arrayCount: 0,
|
|
81
|
+
maxDepth: 0,
|
|
82
|
+
hasNullValues: false,
|
|
83
|
+
hasBooleanValues: false,
|
|
84
|
+
hasArrays: false
|
|
85
|
+
};
|
|
86
|
+
this.collectErrors(rootNode, ctx);
|
|
87
|
+
this.collectSchemaFacts(rootNode, ctx);
|
|
88
|
+
this.extractSymbols(rootNode, ctx);
|
|
89
|
+
this.extractRelationships(ctx);
|
|
90
|
+
this.extractManifestExports(ctx);
|
|
91
|
+
this.extractSchemaComposition(ctx);
|
|
92
|
+
this.extractDeclaredInheritance(ctx);
|
|
93
|
+
this.extractPackageDependencies(ctx);
|
|
94
|
+
this.extractNpmScriptCalls(ctx);
|
|
95
|
+
this.extractDuplicateKeyOverrides(ctx);
|
|
96
|
+
const rootSymbol = {
|
|
97
|
+
name: rootName,
|
|
98
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
99
|
+
qualifiedName: rootName,
|
|
100
|
+
range: this.nodeToRange(rootNode),
|
|
101
|
+
scope: "",
|
|
102
|
+
visibility: "public",
|
|
103
|
+
metadata: {
|
|
104
|
+
jsonSpecific: true,
|
|
105
|
+
documentType: this.documentType(ctx),
|
|
106
|
+
hasSchema: ctx.schemaObjects.size > 0 || ctx.symbolMap.has("$schema"),
|
|
107
|
+
isApiSpec: this.isApiSpec(ctx),
|
|
108
|
+
objectCount: ctx.objectCount,
|
|
109
|
+
propertyCount: ctx.propertyCount,
|
|
110
|
+
arrayCount: ctx.arrayCount,
|
|
111
|
+
maxDepth: ctx.maxDepth,
|
|
112
|
+
hasNullValues: ctx.hasNullValues,
|
|
113
|
+
hasBooleanValues: ctx.hasBooleanValues
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
ctx.symbols.push(rootSymbol);
|
|
117
|
+
ctx.exports.push({
|
|
118
|
+
name: rootName,
|
|
119
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
120
|
+
range: rootSymbol.range,
|
|
121
|
+
visibility: "public"
|
|
122
|
+
});
|
|
123
|
+
const frameworks = this.detectFrameworks(ctx);
|
|
124
|
+
const patterns = this.detectPatterns(ctx);
|
|
125
|
+
const structural = this.structuralScores(ctx);
|
|
126
|
+
return {
|
|
127
|
+
symbols: ctx.symbols,
|
|
128
|
+
relationships: ctx.relationships,
|
|
129
|
+
imports: ctx.imports,
|
|
130
|
+
exports: ctx.exports,
|
|
131
|
+
errors: ctx.errors,
|
|
132
|
+
metadata: {
|
|
133
|
+
complexity: ctx.symbols.length + ctx.relationships.length,
|
|
134
|
+
linesOfCode: content.split("\n").length,
|
|
135
|
+
cyclomaticComplexity: 1,
|
|
136
|
+
framework: frameworks[0],
|
|
137
|
+
patterns,
|
|
138
|
+
detectedFrameworks: frameworks,
|
|
139
|
+
frameworkPatterns: [],
|
|
140
|
+
architecturalInsights: this.insights(ctx),
|
|
141
|
+
architecturalScore: structural.dataModelingScore,
|
|
142
|
+
codeQuality: structural.dataModelingScore >= 90 ? "Excellent" : structural.dataModelingScore >= 70 ? "Good" : "Fair",
|
|
143
|
+
hasTypeInference: false,
|
|
144
|
+
hasFrameworkIntelligence: true,
|
|
145
|
+
dataModelingScore: structural.dataModelingScore,
|
|
146
|
+
schemaValidationScore: structural.schemaValidationScore,
|
|
147
|
+
apiDesignScore: this.isApiSpec(ctx) ? 100 : 80,
|
|
148
|
+
analysisVersion: "4.0.0-pure-json-ast"
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
collectErrors(rootNode, ctx) {
|
|
153
|
+
const stack = [rootNode];
|
|
154
|
+
while (stack.length > 0) {
|
|
155
|
+
const node = stack.pop();
|
|
156
|
+
if (node.type === "ERROR" || node.isMissing) {
|
|
157
|
+
ctx.errors.push({
|
|
158
|
+
message: `Syntax ${node.type} detected in JSON AST.`,
|
|
159
|
+
range: this.nodeToRange(node),
|
|
160
|
+
severity: "warning"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const children = node.namedChildren ?? [];
|
|
164
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
165
|
+
const child = children[i];
|
|
166
|
+
if (child) stack.push(child);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Two DOCUMENT-level preconditions, read in the pass that already visits every pair.
|
|
172
|
+
*
|
|
173
|
+
* Both exist because the pass-2 corpus probe found inheritance declared as ordinary
|
|
174
|
+
* object keys, and both are deliberately narrow. `implements` had been RULED on the
|
|
175
|
+
* ground that the grammar cannot express a supertype clause, and the corpus says
|
|
176
|
+
* otherwise: `dart-sdk`'s `lsp_meta_model.json` states 87 interface mix-ins and 66 class
|
|
177
|
+
* supertypes, and duckdb's serialization schemas state 200-odd superclasses. A grammar
|
|
178
|
+
* has no node type for a KEYWORD, which is the same unsound question this lane has now
|
|
179
|
+
* had to retract six times.
|
|
180
|
+
*
|
|
181
|
+
* What keeps this from firing everywhere is that both shapes are announced by the
|
|
182
|
+
* document before any key is interpreted:
|
|
183
|
+
*
|
|
184
|
+
* - LSP's meta-model uses `{"kind":"reference","name":X}`, a discriminated union for
|
|
185
|
+
* "this is a named type". Two documents in the admitted population contain one, and
|
|
186
|
+
* only one of them states inheritance. So the `name` key of an arbitrary JSON object
|
|
187
|
+
* is never read as a type declaration.
|
|
188
|
+
* - duckdb's schemas declare a C++ class with a `class` string. Twenty-one documents,
|
|
189
|
+
* all duckdb.
|
|
190
|
+
*
|
|
191
|
+
* The measurement that would overturn this: a corpus document that states a supertype or
|
|
192
|
+
* an interface mix-in in a shape carrying neither marker. 0 occurrences over 11,713
|
|
193
|
+
* files, which is what `validation/m14/fixtures/json/inheritance-probe.ts` reports.
|
|
194
|
+
*
|
|
195
|
+
* It is folded into `collectSchemaFacts` rather than given its own walk, and the reason is
|
|
196
|
+
* measured. `otp`'s adversarial `json_SUITE_data/n_structure_open_array_object.json` is 50,000
|
|
197
|
+
* unclosed `[` — the grammar recovers it as an ERROR root with 200,000 children — and every
|
|
198
|
+
* extra full-tree walk over that shape costs ~100 s on top of a 249 s baseline, because
|
|
199
|
+
* `namedChild(i)` is not O(1) in this binding. Two walks became one.
|
|
200
|
+
*/
|
|
201
|
+
noteDocumentConventions(key, value, ctx) {
|
|
202
|
+
if (value?.type !== "string") return;
|
|
203
|
+
if (key === "kind") {
|
|
204
|
+
if (this.stripQuotes(value.text) === "reference") ctx.usesReferenceLiterals = true;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (key !== "class" || ctx.usesClassDeclarations) return;
|
|
208
|
+
const declared = this.stripQuotes(value.text);
|
|
209
|
+
if (this.isIdentifierText(declared)) ctx.usesClassDeclarations = true;
|
|
210
|
+
}
|
|
211
|
+
isIdentifierText(text) {
|
|
212
|
+
const first = text[0];
|
|
213
|
+
if (!(first >= "a" && first <= "z" || first >= "A" && first <= "Z" || first === "_")) return false;
|
|
214
|
+
for (let i = 1; i < text.length; i++) {
|
|
215
|
+
const c = text[i];
|
|
216
|
+
const ok = c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_";
|
|
217
|
+
if (!ok) return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Collect schema metadata before symbol extraction so `required` is based on
|
|
223
|
+
* the actual sibling array, not on a key-name convention. Each object is
|
|
224
|
+
* visited once, so this pass is O(number of AST nodes).
|
|
225
|
+
*/
|
|
226
|
+
collectSchemaFacts(rootNode, ctx) {
|
|
227
|
+
const stack = [{ node: rootNode, parentPath: ctx.rootName, parentName: ctx.rootName }];
|
|
228
|
+
while (stack.length > 0) {
|
|
229
|
+
const { node, parentPath } = stack.pop();
|
|
230
|
+
if (node.type === "document") {
|
|
231
|
+
this.pushTopLevelValues(stack, node, parentPath, ctx.rootName);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (node.type === "object") {
|
|
235
|
+
const schemaKeys = /* @__PURE__ */ new Set();
|
|
236
|
+
let requiredNames;
|
|
237
|
+
for (const pair of node.namedChildren ?? []) {
|
|
238
|
+
if (pair?.type !== "pair") continue;
|
|
239
|
+
const key = this.keyText(pair);
|
|
240
|
+
const value = pair.childForFieldName("value");
|
|
241
|
+
if (!key) continue;
|
|
242
|
+
this.noteDocumentConventions(key, value, ctx);
|
|
243
|
+
if (value?.type === "string" && (key === "$id" || key === "$anchor")) {
|
|
244
|
+
const declared = this.stripQuotes(String(value.text ?? ""));
|
|
245
|
+
if (declared.length > 0) {
|
|
246
|
+
if (key === "$id") {
|
|
247
|
+
const base = this.withoutEmptyFragment(declared);
|
|
248
|
+
if (!ctx.documentIds.has(base)) ctx.documentIds.set(base, parentPath);
|
|
249
|
+
} else {
|
|
250
|
+
const paths = ctx.documentAnchors.get(declared);
|
|
251
|
+
if (paths) paths.push(parentPath);
|
|
252
|
+
else ctx.documentAnchors.set(declared, [parentPath]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
schemaKeys.add(key);
|
|
257
|
+
if (key === "required" && value?.type === "array") {
|
|
258
|
+
const required = /* @__PURE__ */ new Set();
|
|
259
|
+
for (const item of value.namedChildren ?? []) {
|
|
260
|
+
if (item?.type === "string") required.add(this.stripQuotes(item.text));
|
|
261
|
+
}
|
|
262
|
+
if (required.size > 0) requiredNames = required;
|
|
263
|
+
}
|
|
264
|
+
if (value?.type === "object") stack.push({ node: value, parentPath: this.childPath(parentPath, key), parentName: key });
|
|
265
|
+
else if (value?.type === "array") stack.push({ node: value, parentPath: this.childPath(parentPath, key), parentName: key });
|
|
266
|
+
}
|
|
267
|
+
let schemaMatches = 0;
|
|
268
|
+
for (const key of schemaKeys) {
|
|
269
|
+
if (key === "$schema" || key === "type" || key === "properties" || key === "required" || key === "items" || key === "definitions" || key === "$defs" || key === "additionalProperties") schemaMatches++;
|
|
270
|
+
}
|
|
271
|
+
if (requiredNames) {
|
|
272
|
+
const requiredOwner = schemaKeys.has("properties") ? this.childPath(parentPath, "properties") : parentPath;
|
|
273
|
+
ctx.requiredByObject.set(requiredOwner, requiredNames);
|
|
274
|
+
}
|
|
275
|
+
if (schemaMatches >= 2) ctx.schemaObjects.add(parentPath);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (node.type === "array") this.pushArrayChildren(stack, node, parentPath, ctx.rootName, ctx.rootName);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
extractSymbols(rootNode, ctx) {
|
|
282
|
+
const stack = [{ node: rootNode, parentPath: ctx.rootName, parentName: ctx.rootName }];
|
|
283
|
+
while (stack.length > 0) {
|
|
284
|
+
const frame = stack.pop();
|
|
285
|
+
const { node, parentPath, parentName } = frame;
|
|
286
|
+
if (node.type === "document") {
|
|
287
|
+
this.pushTopLevelValues(stack, node, parentPath, parentName);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (node.type === "object") {
|
|
291
|
+
this.updateDepth(ctx, parentPath);
|
|
292
|
+
if (frame.emitObject) this.addArrayObjectSymbol(ctx, node, parentPath, parentName);
|
|
293
|
+
this.recordTypeDeclaration(node, parentPath, ctx);
|
|
294
|
+
this.pushNamedChildren(stack, node, parentPath, parentName, frame.duplicateKeys ?? /* @__PURE__ */ new Map());
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (node.type === "array") {
|
|
298
|
+
ctx.arrayCount++;
|
|
299
|
+
ctx.hasArrays = true;
|
|
300
|
+
if (frame.emitObject) this.addArrayElementSymbol(ctx, node, parentPath, parentName);
|
|
301
|
+
this.pushArrayChildren(stack, node, parentPath, parentName, ctx.rootName);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (node.type !== "pair") continue;
|
|
305
|
+
const key = this.keyText(node);
|
|
306
|
+
const value = node.childForFieldName("value");
|
|
307
|
+
if (!key || !value) continue;
|
|
308
|
+
if (frame.duplicateKeys && key.length > 0) {
|
|
309
|
+
const line = node.startPosition.row + 1;
|
|
310
|
+
const prior = frame.duplicateKeys.get(key);
|
|
311
|
+
if (prior === void 0) frame.duplicateKeys.set(key, line);
|
|
312
|
+
else
|
|
313
|
+
ctx.overrideCandidates.push({
|
|
314
|
+
ownerName: parentName,
|
|
315
|
+
ownerPath: parentPath,
|
|
316
|
+
key,
|
|
317
|
+
targetPath: this.childPath(parentPath, key),
|
|
318
|
+
line,
|
|
319
|
+
overridesLine: prior
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const path = this.childPath(parentPath, key);
|
|
323
|
+
const valueType = this.valueType(value);
|
|
324
|
+
const kind = valueType === "object" ? ASTEngine.SymbolKind.Class : ASTEngine.SymbolKind.Property;
|
|
325
|
+
const symbol = {
|
|
326
|
+
name: key,
|
|
327
|
+
kind,
|
|
328
|
+
qualifiedName: path,
|
|
329
|
+
range: this.nodeToRange(node),
|
|
330
|
+
ownerName: parentName,
|
|
331
|
+
ownerKind: parentPath === ctx.rootName ? ASTEngine.SymbolKind.Module : ASTEngine.SymbolKind.Class,
|
|
332
|
+
scope: parentPath,
|
|
333
|
+
visibility: "public",
|
|
334
|
+
returnType: valueType,
|
|
335
|
+
metadata: {
|
|
336
|
+
jsonSpecific: true,
|
|
337
|
+
nodeType: node.type,
|
|
338
|
+
valueType,
|
|
339
|
+
fullPath: path,
|
|
340
|
+
isArray: valueType === "array",
|
|
341
|
+
isObject: valueType === "object",
|
|
342
|
+
isRequired: this.isRequired(ctx, parentPath, key),
|
|
343
|
+
hasValidation: this.hasValidation(ctx, key, value),
|
|
344
|
+
isReference: this.isReference(key, value),
|
|
345
|
+
refPath: this.referencePath(key, value),
|
|
346
|
+
isApiEndpoint: this.isApiEndpoint(key, value),
|
|
347
|
+
depth: this.pathDepth(path, ctx.rootName)
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
if (ctx.emittedPaths.has(path)) continue;
|
|
351
|
+
ctx.emittedPaths.add(path);
|
|
352
|
+
this.addSymbol(ctx, symbol);
|
|
353
|
+
if (valueType === "null") ctx.hasNullValues = true;
|
|
354
|
+
if (valueType === "boolean") ctx.hasBooleanValues = true;
|
|
355
|
+
if (symbol.metadata.isReference) ctx.referenceSymbols.push(symbol);
|
|
356
|
+
if (symbol.metadata.hasValidation) ctx.validationSymbols.push(symbol);
|
|
357
|
+
if (symbol.metadata.isApiEndpoint) ctx.apiEndpointSymbols.push(symbol);
|
|
358
|
+
if (isDependencyContainerPath(parentPath)) {
|
|
359
|
+
ctx.dependencySymbols.push(symbol);
|
|
360
|
+
if (value.type === "string") {
|
|
361
|
+
const range = this.stripQuotes(String(value.text ?? ""));
|
|
362
|
+
if (range.length > 0) ctx.dependencyVersions.set(path, range);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (value.type === "object" || value.type === "array") stack.push({ node: value, parentPath: path, parentName: key });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* INHERITANCE DECLARED AS ORDINARY OBJECT KEYS — the corpus measurement that retracted
|
|
370
|
+
* this lane's `implements` ruling and found `extends` incomplete.
|
|
371
|
+
*
|
|
372
|
+
* `implements` was ruled on a grammar inventory ("the grammar produced 20 distinct node
|
|
373
|
+
* types across the corpus and NONE expresses it") and the corpus says otherwise. Two
|
|
374
|
+
* documents in 11,713 declare types and state their supertypes:
|
|
375
|
+
*
|
|
376
|
+
* dart-sdk/…/lsp_meta_model.json — the Language Server Protocol's own serialisation of
|
|
377
|
+
* its type declarations. 66 structures carry
|
|
378
|
+
* "extends": [{"kind":"reference","name":"TextDocumentPositionParams"}]
|
|
379
|
+
* and 87 carry
|
|
380
|
+
* "mixins": [{"kind":"reference","name":"WorkDoneProgressParams"}, …]
|
|
381
|
+
* `mixins` names the TypeScript INTERFACES a class mixes in, which is `implements`
|
|
382
|
+
* and nothing else: in TypeScript an interface carries no implementation, so there is
|
|
383
|
+
* nothing else for the word to mean.
|
|
384
|
+
*
|
|
385
|
+
* duckdb/src/include/duckdb/storage/serialization/*.json — serialisation schemas for
|
|
386
|
+
* C++ classes. `{ "class": "NotNullConstraint", "base": "Constraint", … }`: `class`
|
|
387
|
+
* declares the type, `base` names its superclass, and every superclass named is
|
|
388
|
+
* declared in the same file.
|
|
389
|
+
*
|
|
390
|
+
* Two shapes are declined, and the corpus says why:
|
|
391
|
+
*
|
|
392
|
+
* - A member descriptor. duckdb writes `{ "id": 200, "name": "info", "type":
|
|
393
|
+
* "AlterInfo*", "base": "ParseInfo" }` — a FIELD whose C++ class derives from
|
|
394
|
+
* `ParseInfo`. It is not a type declaration, and 16 such statements name a target the
|
|
395
|
+
* document does not declare. The rule requires a `class` marker, which a member
|
|
396
|
+
* descriptor does not carry, so all 16 are declined by the marker rather than by a
|
|
397
|
+
* name list.
|
|
398
|
+
* - `"parent"` and a non-string `base`. vega's data files carry `"parent": <number>` on
|
|
399
|
+
* 251 pairs; a number is not a type. The value must be a STRING.
|
|
400
|
+
*
|
|
401
|
+
* Confidence is 1.0 for `extends` and 0.9 for `implements`, and the difference is the
|
|
402
|
+
* honest one: a declared supertype is read structurally in both cases, but the mapping of
|
|
403
|
+
* LSP's word `mixins` onto the ontology's `implements` is a NAMING decision, not a parse
|
|
404
|
+
* fact. It therefore carries `reasonCode`, which is what the honesty contract §4 asks of
|
|
405
|
+
* anything below the deterministic threshold.
|
|
406
|
+
*/
|
|
407
|
+
recordTypeDeclaration(node, path, ctx) {
|
|
408
|
+
const index = this.objectKeyIndex(node);
|
|
409
|
+
if (this.referenceLiteralName(index) !== void 0) return;
|
|
410
|
+
const className = ctx.usesClassDeclarations ? this.stringPairValue(index, "class") : void 0;
|
|
411
|
+
const named = className ?? (ctx.usesReferenceLiterals ? this.lspDeclaredName(index) : void 0);
|
|
412
|
+
if (named !== void 0 && named.length > 0 && !ctx.typeDeclarations.has(named)) ctx.typeDeclarations.set(named, path);
|
|
413
|
+
if (named === void 0 || named.length === 0) return;
|
|
414
|
+
if (className !== void 0) {
|
|
415
|
+
const base = this.stringPairValue(index, "base");
|
|
416
|
+
if (base !== void 0 && base.length > 0) {
|
|
417
|
+
ctx.inheritanceCandidates.push({
|
|
418
|
+
source: path,
|
|
419
|
+
target: base,
|
|
420
|
+
key: "base",
|
|
421
|
+
form: "duckdb_base_class_name",
|
|
422
|
+
label: "extends",
|
|
423
|
+
line: this.pairLine(index, "base")
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
for (const key of LSP_INHERITANCE_KEYS) {
|
|
429
|
+
const value = this.pairValue(index, key);
|
|
430
|
+
if (value?.type !== "array") continue;
|
|
431
|
+
const label = key === "mixins" || key === "implements" ? "implements" : "extends";
|
|
432
|
+
const items = (value.namedChildren ?? []).filter((c) => c.type !== "[");
|
|
433
|
+
for (const item of items) {
|
|
434
|
+
const reference = this.referenceLiteralName(this.objectKeyIndex(item));
|
|
435
|
+
if (reference === void 0) continue;
|
|
436
|
+
ctx.inheritanceCandidates.push({
|
|
437
|
+
source: path,
|
|
438
|
+
target: reference,
|
|
439
|
+
key,
|
|
440
|
+
form: "lsp_reference_literal",
|
|
441
|
+
label,
|
|
442
|
+
line: item.startPosition.row + 1
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Resolve the candidates against the declarations collected in the same walk and emit.
|
|
449
|
+
*
|
|
450
|
+
* A candidate whose target this document never declares produces NO edge. Measured: 16 of
|
|
451
|
+
* the supertype statements name a type declared in a sibling file, and an edge to a symbol
|
|
452
|
+
* this file does not contain is exactly the fabrication the ontology workstream calls the
|
|
453
|
+
* most expensive mistake available.
|
|
454
|
+
*
|
|
455
|
+
* The endpoints are QUALIFIED PATHS, which is the lane's established convention for a
|
|
456
|
+
* relation between two declarations rather than between two keys — the same choice
|
|
457
|
+
* `references` makes, and for the same reason: the thing that inherits is an object in the
|
|
458
|
+
* document, not a key. It has a measured cost, declared in the doc: these endpoints bind by
|
|
459
|
+
* qualified name and not by bare name, so the store cannot resolve them by name.
|
|
460
|
+
*/
|
|
461
|
+
extractDeclaredInheritance(ctx) {
|
|
462
|
+
for (const c of ctx.inheritanceCandidates) {
|
|
463
|
+
const targetPath = ctx.typeDeclarations.get(c.target);
|
|
464
|
+
if (targetPath === void 0) continue;
|
|
465
|
+
if (targetPath === c.source) continue;
|
|
466
|
+
ctx.relationships.push({
|
|
467
|
+
source: c.source,
|
|
468
|
+
target: targetPath,
|
|
469
|
+
type: c.label === "implements" ? ASTEngine.RelationshipType.Implements : ASTEngine.RelationshipType.Extends,
|
|
470
|
+
confidence: c.label === "implements" ? 0.9 : 1,
|
|
471
|
+
line_number: c.line,
|
|
472
|
+
metadata: {
|
|
473
|
+
jsonSpecific: true,
|
|
474
|
+
inheritanceForm: c.form,
|
|
475
|
+
declarationKey: c.key,
|
|
476
|
+
declaredType: c.target,
|
|
477
|
+
reasonCode: c.label === "implements" ? "declared_interface_mixin" : "declared_supertype",
|
|
478
|
+
evidenceClass: "structural",
|
|
479
|
+
endpoints: "qualified_path"
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/** An LSP reference literal: an object with `kind` == "reference" and a `name` string. */
|
|
485
|
+
referenceLiteralName(index) {
|
|
486
|
+
if (this.stringPairValue(index, "kind") !== "reference") return void 0;
|
|
487
|
+
const name = this.stringPairValue(index, "name");
|
|
488
|
+
return name !== void 0 && name.length > 0 ? name : void 0;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The name an LSP-style object declares ITSELF under, or undefined when the object is not a
|
|
492
|
+
* type declaration.
|
|
493
|
+
*
|
|
494
|
+
* Two keys name the type (`name` for a structure or an enumeration, `typeName` for a
|
|
495
|
+
* request or a notification) and one of five makes the object a TYPE rather than one of its
|
|
496
|
+
* members: `properties`, `values`, `extends`, `mixins`, `implements`. A property descriptor
|
|
497
|
+
* (`{"name":"textDocument","type":{"kind":"reference",…}}`) has a `name` and none of those,
|
|
498
|
+
* so it is declined — and it sits inside a declaration in the same file, which is what makes
|
|
499
|
+
* the test necessary rather than tidy.
|
|
500
|
+
*/
|
|
501
|
+
lspDeclaredName(index) {
|
|
502
|
+
if (!index.has("properties") && !index.has("values") && !index.has("extends") && !index.has("mixins") && !index.has("implements")) {
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
return this.stringPairValue(index, "name") ?? this.stringPairValue(index, "typeName");
|
|
506
|
+
}
|
|
507
|
+
/** A key -> pair index for one object, built once. */
|
|
508
|
+
objectKeyIndex(node) {
|
|
509
|
+
const index = /* @__PURE__ */ new Map();
|
|
510
|
+
const children = node?.namedChildren ?? [];
|
|
511
|
+
for (let i = 0; i < children.length; i++) {
|
|
512
|
+
const child = children[i];
|
|
513
|
+
if (child?.type !== "pair") continue;
|
|
514
|
+
const key = this.keyText(child);
|
|
515
|
+
if (key.length === 0) continue;
|
|
516
|
+
if (!index.has(key)) index.set(key, child);
|
|
517
|
+
}
|
|
518
|
+
return index;
|
|
519
|
+
}
|
|
520
|
+
pairValue(index, key) {
|
|
521
|
+
const pair = index.get(key);
|
|
522
|
+
return pair ? pair.childForFieldName("value") : void 0;
|
|
523
|
+
}
|
|
524
|
+
stringPairValue(index, key) {
|
|
525
|
+
const value = this.pairValue(index, key);
|
|
526
|
+
if (value?.type !== "string") return void 0;
|
|
527
|
+
const text = this.stripQuotes(String(value.text ?? ""));
|
|
528
|
+
return text.length > 0 ? text : void 0;
|
|
529
|
+
}
|
|
530
|
+
pairLine(index, key) {
|
|
531
|
+
const pair = index.get(key);
|
|
532
|
+
return pair ? pair.startPosition.row + 1 : 0;
|
|
533
|
+
}
|
|
534
|
+
addSymbol(ctx, symbol) {
|
|
535
|
+
ctx.symbols.push(symbol);
|
|
536
|
+
ctx.symbolMap.set(symbol.name, symbol);
|
|
537
|
+
const fullPath = symbol.metadata?.fullPath;
|
|
538
|
+
if (fullPath) ctx.symbolMap.set(fullPath, symbol);
|
|
539
|
+
if (symbol.kind === ASTEngine.SymbolKind.Class) ctx.objectCount++;
|
|
540
|
+
if (symbol.kind === ASTEngine.SymbolKind.Property) ctx.propertyCount++;
|
|
541
|
+
}
|
|
542
|
+
extractRelationships(ctx) {
|
|
543
|
+
for (const symbol of ctx.symbols) {
|
|
544
|
+
if (symbol.ownerName) {
|
|
545
|
+
ctx.relationships.push({
|
|
546
|
+
source: this.ownerPathOf(symbol),
|
|
547
|
+
target: String(symbol.qualifiedName || symbol.name),
|
|
548
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
549
|
+
confidence: 1,
|
|
550
|
+
line_number: this.declLine(symbol),
|
|
551
|
+
metadata: {
|
|
552
|
+
fullPath: symbol.metadata?.fullPath || "",
|
|
553
|
+
ownerName: symbol.ownerName,
|
|
554
|
+
endpoints: "qualified_path"
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (const ref of ctx.referenceSymbols) {
|
|
560
|
+
const refPath = ref.metadata?.refPath;
|
|
561
|
+
if (!refPath) continue;
|
|
562
|
+
const declaredIdentity = this.documentDeclaredIdentity(ctx, refPath);
|
|
563
|
+
const target = declaredIdentity ?? this.referenceTarget(ctx, refPath);
|
|
564
|
+
const source = String(ref.metadata?.fullPath || ref.name);
|
|
565
|
+
const fileCandidate = this.fileRefCandidate(ctx, refPath);
|
|
566
|
+
const claimedFile = fileCandidate !== null && this.projectCarriesFile(ctx, fileCandidate) ? fileCandidate : null;
|
|
567
|
+
if (this.isFileNamingRef(refPath)) {
|
|
568
|
+
ctx.relationships.push({
|
|
569
|
+
source,
|
|
570
|
+
target: refPath,
|
|
571
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
572
|
+
confidence: 1,
|
|
573
|
+
line_number: this.declLine(ref),
|
|
574
|
+
metadata: {
|
|
575
|
+
jsonSpecific: true,
|
|
576
|
+
importForm: "json_schema_ref",
|
|
577
|
+
resolvedPath: false,
|
|
578
|
+
// the dependency this edge asserts, in the spelling the project's own file list uses
|
|
579
|
+
// — or, where the project does not carry it, the external claim that says so. The
|
|
580
|
+
// fallback is not optional: an `imports` edge with no claim at all is UNRESOLVED and
|
|
581
|
+
// counts against the lane, and "the project does not carry this file" is a statement,
|
|
582
|
+
// not a gap. A path-shaped target carrying `isExternal` is UNVERIFIABLE by the
|
|
583
|
+
// arbiter's owner lookup by design (`verifyExternalClaim` refuses to read a stem as an
|
|
584
|
+
// owner — a rule fixed in `5a611de4b`), which is exactly right for it.
|
|
585
|
+
...claimedFile ? { importProjectFile: claimedFile } : { isExternal: true },
|
|
586
|
+
evidenceClass: "structural"
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
ctx.relationships.push({
|
|
591
|
+
source,
|
|
592
|
+
target,
|
|
593
|
+
type: ASTEngine.RelationshipType.References,
|
|
594
|
+
confidence: target === refPath ? 0.8 : 1,
|
|
595
|
+
line_number: this.declLine(ref),
|
|
596
|
+
metadata: {
|
|
597
|
+
relationshipType: "json_reference",
|
|
598
|
+
refPath,
|
|
599
|
+
// THE EXTERNAL CLAIM, and it is a claim about the CORPUS, so it may only be made where
|
|
600
|
+
// the shape says the target is outside this document. A LOCAL POINTER never is: `#`
|
|
601
|
+
// (the root) and `#/a/b` (a location in this file) are answered by this file. Before
|
|
602
|
+
// this test, `#` was marked external by `!refPath.startsWith("#/")` — nine edges in one
|
|
603
|
+
// vendored meta-schema — and a URI/path-shaped target can never be verified by the
|
|
604
|
+
// gate's owner lookup, so those nine made the lane's verdict unreadable: the arbiter
|
|
605
|
+
// returns a branch that prints no amb/unres/edges totals (measured, 2026-10-09).
|
|
606
|
+
// A pointer that names a location this document does not publish is a BROKEN pointer,
|
|
607
|
+
// not an external one; `reasonCode` says exactly that and the confidence carries it.
|
|
608
|
+
// A `$ref` that names a FILE carries the file claim instead, on both labels, so the two
|
|
609
|
+
// rows for one dependency are adjudicated the same way — and where the project does not
|
|
610
|
+
// carry the file, BOTH labels carry the external claim, so one dependency is never
|
|
611
|
+
// adjudicated two ways.
|
|
612
|
+
// AN IDENTITY THE DOCUMENT DECLARES IS NOT EXTERNAL EITHER: the third same-document
|
|
613
|
+
// form. `$id`/`$anchor` declarations are what make the target a declaration HERE, so the
|
|
614
|
+
// corpus claim that made the edge `ext?` would be a claim this file refutes.
|
|
615
|
+
...claimedFile ? { importProjectFile: claimedFile } : { isExternal: !this.isLocalPointer(refPath) && declaredIdentity === null },
|
|
616
|
+
// Honesty contract §4 point 1: anything below 1.0 carries a machine-readable code.
|
|
617
|
+
// The 0.8 case is exactly one thing — the pointer names a location this document
|
|
618
|
+
// does not declare — so the code says so, and the 1.0 case says the opposite.
|
|
619
|
+
reasonCode: target === refPath ? "ref_target_not_declared_in_document" : "json_schema_ref",
|
|
620
|
+
evidenceClass: "structural"
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
for (const endpoint of ctx.apiEndpointSymbols) {
|
|
625
|
+
ctx.relationships.push({
|
|
626
|
+
// QUALIFIED PATHS on both ends. An endpoint is a KEY of the `paths` object, so it has
|
|
627
|
+
// a position in the document and a path exists for it — `endpoint.qualifiedName` IS
|
|
628
|
+
// that path, and `endpoint.scope` is the `paths` object's own path. This is what
|
|
629
|
+
// separates this site from a manifest dependency's `uses`, whose target is an npm
|
|
630
|
+
// package name and genuinely has no position: see `classifyUnresolved` in
|
|
631
|
+
// validation/m14/fixtures/json/namespace-orphan-probe.ts, which reads the two apart
|
|
632
|
+
// from the discriminator each site publishes.
|
|
633
|
+
source: endpoint.scope || endpoint.ownerName || ctx.rootName,
|
|
634
|
+
target: endpoint.qualifiedName || endpoint.name,
|
|
635
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
636
|
+
confidence: 0.8,
|
|
637
|
+
line_number: this.declLine(endpoint),
|
|
638
|
+
metadata: {
|
|
639
|
+
relationshipType: "json_api_endpoint",
|
|
640
|
+
// Honesty contract §4 point 1. The endpoints are a fact about the tree; the claim
|
|
641
|
+
// that declaring an HTTP method in a `paths` entry is a USE rather than mere
|
|
642
|
+
// containment is a reading of the OpenAPI document shape, so it is 0.8 with a code.
|
|
643
|
+
reasonCode: "openapi_path_entry_declares_http_method",
|
|
644
|
+
evidenceClass: "usage_contextual",
|
|
645
|
+
endpoints: "qualified_path"
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* The OWNER's qualified path, recovered from a child's own path — or the bare `ownerName`
|
|
652
|
+
* when the path does not reduce to one.
|
|
653
|
+
*
|
|
654
|
+
* Two shapes occur and both are handled by string surgery on the child's path, because the
|
|
655
|
+
* path IS the child's `qualifiedName` in this analyzer:
|
|
656
|
+
*
|
|
657
|
+
* an object property — path `ar.dashboard.analytics.unknown`, name `unknown`; the owner is
|
|
658
|
+
* everything before the final `.unknown`, i.e. `ar.dashboard.analytics`.
|
|
659
|
+
*
|
|
660
|
+
* an array item — path `ar.items[0]`, name `ar.items[0]` (see `arrayItemName`: an item is
|
|
661
|
+
* named after the array that holds it plus its index). There is no `.` immediately before
|
|
662
|
+
* the name, so the `.`-suffix test misses it and the `[i]` suffix test catches it, leaving
|
|
663
|
+
* `ar.items`.
|
|
664
|
+
*
|
|
665
|
+
* The fallback returns `ownerName` unchanged, which is what the previous version always
|
|
666
|
+
* emitted. That direction was chosen on purpose: a path can only ever resolve MORE than a
|
|
667
|
+
* bare name (strategy 1 exact-matches it), so an unreducible path degrades to today's
|
|
668
|
+
* behaviour and never to a worse one.
|
|
669
|
+
*/
|
|
670
|
+
ownerPathOf(symbol) {
|
|
671
|
+
const owner = String(symbol.ownerName ?? "");
|
|
672
|
+
if (owner === "") return owner;
|
|
673
|
+
const p = String(symbol.qualifiedName ?? "");
|
|
674
|
+
const n = String(symbol.name ?? "");
|
|
675
|
+
if (n === "" || p === "") return owner;
|
|
676
|
+
if (symbol.metadata?.isArrayItem === true) {
|
|
677
|
+
const bracket = p.lastIndexOf("[");
|
|
678
|
+
if (bracket > 0) return p.slice(0, bracket);
|
|
679
|
+
return owner;
|
|
680
|
+
}
|
|
681
|
+
if (p.length > n.length && p.endsWith(`.${n}`)) return p.slice(0, p.length - n.length - 1);
|
|
682
|
+
if (p.length > n.length && p.endsWith(n)) return p.slice(0, p.length - n.length);
|
|
683
|
+
return owner;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* What a manifest PUBLISHES, as `exports` edges.
|
|
687
|
+
*
|
|
688
|
+
* The lane's first ruling excluded `exports` on a grammar inventory — "no
|
|
689
|
+
* `export_statement` node" — and that is the SAME unsound reasoning that produced
|
|
690
|
+
* the wrong `imports` ruling: a grammar cannot see a CONVENTION, and a Node
|
|
691
|
+
* manifest's `exports` field is a convention expressed as ordinary object keys. The
|
|
692
|
+
* audit found 1 of 36 manifests carrying `exports` and 1 carrying `main`, so the
|
|
693
|
+
* mass is small and the principle is not, and the type is emitted.
|
|
694
|
+
*
|
|
695
|
+
* A published entry point IS an export, and each of its names is exported: a
|
|
696
|
+
* subpath key for `exports`, the field name for `main`, the command name for `bin`.
|
|
697
|
+
*
|
|
698
|
+
* Two silent failures lived in this function before it worked, and both are worth
|
|
699
|
+
* recording because neither would ever have been noticed by reading it. It first
|
|
700
|
+
* reached for a `__node` property nothing set, and then iterated the `document`
|
|
701
|
+
* WRAPPER's children when the top-level `pair`s are one level down inside the root
|
|
702
|
+
* `object`. Both type-check, both compile, both do nothing. The only reason this
|
|
703
|
+
* version works is that it was measured — 24 edges on svelte's manifest, and 0 when
|
|
704
|
+
* either guard was wrong.
|
|
705
|
+
*/
|
|
706
|
+
extractManifestExports(ctx) {
|
|
707
|
+
if (!this.fileEndsWith(ctx.filePath, "package.json")) return;
|
|
708
|
+
const document = ctx.rootNode;
|
|
709
|
+
if (document.type !== "document") return;
|
|
710
|
+
const rootObject = document.namedChildren?.find((c) => c.type === "object");
|
|
711
|
+
if (!rootObject) return;
|
|
712
|
+
const emit = (name, line, parent) => {
|
|
713
|
+
if (name.length === 0) return;
|
|
714
|
+
const target = [ctx.rootName, ...parent ? [parent] : [], name].join(".");
|
|
715
|
+
ctx.relationships.push({
|
|
716
|
+
source: ctx.rootName,
|
|
717
|
+
target,
|
|
718
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
719
|
+
confidence: 1,
|
|
720
|
+
line_number: line,
|
|
721
|
+
metadata: { jsonSpecific: true, exportForm: "manifest_entry_point", evidenceClass: "structural" }
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
for (const pair of rootObject.namedChildren ?? []) {
|
|
725
|
+
if (pair?.type !== "pair") continue;
|
|
726
|
+
const key = this.keyText(pair);
|
|
727
|
+
const value = pair.childForFieldName("value");
|
|
728
|
+
if (!key || !value) continue;
|
|
729
|
+
const line = pair.startPosition.row + 1;
|
|
730
|
+
if (key === "main") {
|
|
731
|
+
if (value.type === "string") emit("main", line);
|
|
732
|
+
} else if (key === "bin") {
|
|
733
|
+
if (value.type === "string") emit("bin", line);
|
|
734
|
+
else {
|
|
735
|
+
for (const binPair of value.namedChildren ?? []) {
|
|
736
|
+
if (binPair?.type !== "pair") continue;
|
|
737
|
+
const binName = this.keyText(binPair);
|
|
738
|
+
if (binName) emit(binName, binPair.startPosition.row + 1, "bin");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} else if (key === "exports") {
|
|
742
|
+
if (value.type === "string") emit("exports", line);
|
|
743
|
+
else {
|
|
744
|
+
for (const sub of value.namedChildren ?? []) {
|
|
745
|
+
if (sub?.type !== "pair") continue;
|
|
746
|
+
const subPath = this.keyText(sub);
|
|
747
|
+
if (subPath) emit(subPath, sub.startPosition.row + 1, "exports");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* JSON Schema COMPOSITION, as `extends`.
|
|
755
|
+
*
|
|
756
|
+
* `allOf` means "must satisfy ALL of these" — a superset-of relationship, and the
|
|
757
|
+
* only schema keyword that means one. `anyOf` and `oneOf` are ALTERNATIVES and have
|
|
758
|
+
* no ontology type, so they are correctly emitted as nothing beyond the `references`
|
|
759
|
+
* their `$ref`s already produce.
|
|
760
|
+
*
|
|
761
|
+
* This was ruled out twice on the same wrong reasoning, and the pattern is now
|
|
762
|
+
* recognisable: a grammar inventory says the PARSER cannot express a construct, which
|
|
763
|
+
* is not the same as the LANGUAGE lacking one. `allOf` is a keyword, and a grammar
|
|
764
|
+
* has no node type for a keyword any more than it has one for a manifest's `exports`
|
|
765
|
+
* field. Measured over the corpus: **1,226 `allOf` `$ref`s** against 88 `anyOf` and
|
|
766
|
+
* 89 `oneOf`, which is the shape the reading predicts — composition is the common
|
|
767
|
+
* case and alternatives are rare.
|
|
768
|
+
*/
|
|
769
|
+
extractSchemaComposition(ctx) {
|
|
770
|
+
for (const ref of ctx.referenceSymbols) {
|
|
771
|
+
const fullPath = ref.metadata?.fullPath;
|
|
772
|
+
if (!fullPath) continue;
|
|
773
|
+
let parent = fullPath.slice(0, fullPath.lastIndexOf("."));
|
|
774
|
+
const bracket = parent.lastIndexOf("[");
|
|
775
|
+
if (bracket > 0 && parent.endsWith("]")) parent = parent.slice(0, bracket);
|
|
776
|
+
if (!parent.endsWith(".allOf")) continue;
|
|
777
|
+
const composer = parent.length > ".allOf".length ? parent.slice(0, parent.length - ".allOf".length) : ctx.rootName;
|
|
778
|
+
const refPath = ref.metadata?.refPath;
|
|
779
|
+
if (!refPath) continue;
|
|
780
|
+
const isLocal = refPath.startsWith("#/");
|
|
781
|
+
const isSiblingFile = !isLocal && this.isFileNamingRef(refPath);
|
|
782
|
+
if (!isLocal && !isSiblingFile) continue;
|
|
783
|
+
const target = isLocal ? this.referenceTarget(ctx, refPath) : refPath;
|
|
784
|
+
const siblingFile = isSiblingFile ? this.fileRefCandidate(ctx, refPath) : null;
|
|
785
|
+
const claimedSibling = siblingFile !== null && this.projectCarriesFile(ctx, siblingFile) ? siblingFile : null;
|
|
786
|
+
ctx.relationships.push({
|
|
787
|
+
source: composer,
|
|
788
|
+
target,
|
|
789
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
790
|
+
confidence: 1,
|
|
791
|
+
line_number: this.declLine(ref),
|
|
792
|
+
metadata: {
|
|
793
|
+
jsonSpecific: true,
|
|
794
|
+
composition: "allOf",
|
|
795
|
+
compositionSite: parent,
|
|
796
|
+
refForm: isLocal ? "intra_document" : "sibling_file",
|
|
797
|
+
resolvedPath: isLocal,
|
|
798
|
+
...claimedSibling ? { importProjectFile: claimedSibling } : isSiblingFile ? { isExternal: true } : {},
|
|
799
|
+
note: "allOf is a superset-of relationship; anyOf/oneOf are alternatives and emit only `references`",
|
|
800
|
+
reasonCode: "json_schema_all_of",
|
|
801
|
+
evidenceClass: "structural",
|
|
802
|
+
endpoints: "qualified_path"
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
extractPackageDependencies(ctx) {
|
|
808
|
+
if (!this.fileEndsWith(ctx.filePath, "package.json") && !this.fileEndsWith(ctx.filePath, "composer.json")) return;
|
|
809
|
+
for (const dependency of ctx.dependencySymbols) {
|
|
810
|
+
ctx.imports.push({
|
|
811
|
+
module: dependency.name,
|
|
812
|
+
symbols: [],
|
|
813
|
+
range: dependency.range,
|
|
814
|
+
type: "dependency",
|
|
815
|
+
resolvedPath: "package-manager"
|
|
816
|
+
});
|
|
817
|
+
ctx.relationships.push({
|
|
818
|
+
source: ctx.rootName,
|
|
819
|
+
target: String(dependency.qualifiedName ?? dependency.name),
|
|
820
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
821
|
+
confidence: 1,
|
|
822
|
+
line_number: dependency.range?.startLine ?? 0,
|
|
823
|
+
metadata: {
|
|
824
|
+
relationshipType: "package_dependency",
|
|
825
|
+
package: dependency.name,
|
|
826
|
+
version: this.dependencyVersion(ctx, dependency)
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* An npm script that runs a SIBLING script, as `calls`.
|
|
833
|
+
*
|
|
834
|
+
* `calls` was ruled out for this lane twice, on a grammar inventory: the JSON
|
|
835
|
+
* grammar has no node type for a function, so it has none for an invocation either.
|
|
836
|
+
* That is the same unsound question that produced the wrong `imports`, `exports` and
|
|
837
|
+
* `extends` rulings, and the corpus settles it. A `package.json` `scripts` object
|
|
838
|
+
* declares its entries as ordinary keys, so both endpoints of the relation are
|
|
839
|
+
* symbols IN THE SAME FILE, and a command string naming one of them is an
|
|
840
|
+
* invocation of it:
|
|
841
|
+
*
|
|
842
|
+
* "scripts": { "pretest": "npm run compile", "compile": "tsc -p ." }
|
|
843
|
+
*
|
|
844
|
+
* `pretest` calls `compile`, and both names resolve. This is not a name-match guess
|
|
845
|
+
* across files — the callee must be a key of the SAME `scripts` object, which is the
|
|
846
|
+
* only condition under which the edge can be checked at all.
|
|
847
|
+
*
|
|
848
|
+
* Three things are declined rather than left silent, each because emitting them would
|
|
849
|
+
* be a fabricated edge:
|
|
850
|
+
*
|
|
851
|
+
* - `npm run <name>` where `<name>` is NOT a key of this `scripts` object. The
|
|
852
|
+
* callee is then a binary, a workspace script in another package, or nothing;
|
|
853
|
+
* `package.json` does not say, so the edge would name a symbol that is not there.
|
|
854
|
+
* Measured separately by `validation/m14/fixtures/json/probes.ts` so the
|
|
855
|
+
* undercount is visible instead of assumed.
|
|
856
|
+
* - a script invoking ITSELF. npm rejects it, and a self-edge carries no order.
|
|
857
|
+
* - npm's IMPLICIT lifecycle hooks (`pre<script>` / `post<script>` around any
|
|
858
|
+
* script). npm runs them without the command naming them, which is real
|
|
859
|
+
* behaviour and is NOT an explicit invocation, so emitting it would attribute an
|
|
860
|
+
* edge to a name the file never wrote in that position.
|
|
861
|
+
*
|
|
862
|
+
* Confidence is 0.8, not 1, and the reason is carried in `reasonCode`: the two
|
|
863
|
+
* endpoints are structural facts but the LINK is read out of a string value by a
|
|
864
|
+
* known-CLI convention, which is exactly the class the honesty contract §4 requires
|
|
865
|
+
* to be below the deterministic threshold and countable.
|
|
866
|
+
*/
|
|
867
|
+
/**
|
|
868
|
+
* A key declared twice in one object, as `overrides`.
|
|
869
|
+
*
|
|
870
|
+
* `overrides` was ruled out for this lane on a grammar inventory — the JSON grammar
|
|
871
|
+
* has no node type for a redefinition — which is the unsound question this lane has
|
|
872
|
+
* now had to retract three times (`imports`, `exports`, `extends`, and `calls` on the
|
|
873
|
+
* same corpus evidence). The construct is measurable and it occurs: 21 NAMED
|
|
874
|
+
* duplicate keys across 11 of 11,713 admitted files, e.g.
|
|
875
|
+
* dart-sdk/pkg/front_end/test/language_versioning/data/
|
|
876
|
+
* package_default_version_is_wrong_2/.dart_tool/package_config.json:8 declaring
|
|
877
|
+
* `languageVersion` twice, and duckdb/data/json/11152.json.
|
|
878
|
+
*
|
|
879
|
+
* The edge shape is the BASH lane's, deliberately, because it is the only shape that
|
|
880
|
+
* works when both declarations share one name: source is the OWNER symbol, target is
|
|
881
|
+
* the repeated key, and `overridesLine` carries the PRIOR declaration's line so the
|
|
882
|
+
* replacement is readable without re-deriving it.
|
|
883
|
+
*
|
|
884
|
+
* Both endpoints are QUALIFIED PATHS, like every other type on this lane. It used to be
|
|
885
|
+
* the last type emitting bare names, and the bare form was ambiguous on EVERY corpus
|
|
886
|
+
* edge: measured over the whole admitted population, 22 of 22 targets matched more than
|
|
887
|
+
* one published symbol by name, against 14 of 22 by path. The 8 that become unique are
|
|
888
|
+
* real gains — a bare `rec2` names both elements of an array and a bare `1` names four
|
|
889
|
+
* keys, so the bare endpoint could bind to a declaration in a DIFFERENT object. The 14
|
|
890
|
+
* that stay ambiguous are the two declarations of ONE key sharing a name AND a path,
|
|
891
|
+
* which is declined below and carried in `overridesLine`. The names are retained in
|
|
892
|
+
* metadata as `ownerName`/`key`, where they read as names.
|
|
893
|
+
*
|
|
894
|
+
* `contains` already emits a row for each declaration, so the pair exists independently
|
|
895
|
+
* of this edge.
|
|
896
|
+
*
|
|
897
|
+
* Three things are declined, each because the edge would assert something the file
|
|
898
|
+
* does not say:
|
|
899
|
+
*
|
|
900
|
+
* - The same key in two DIFFERENT objects of one file. A key is unique only within
|
|
901
|
+
* its object, so this is not a redefinition, and it is the corpus's dominant shape
|
|
902
|
+
* (120,174 occurrences across 7,679 files against 21 real duplicates). Emitting
|
|
903
|
+
* it would have been ~5,700 false positives.
|
|
904
|
+
* - An EMPTY key. 10 occurrences across 5 files, every one a document the grammar
|
|
905
|
+
* already recovered with ERROR nodes; an empty key is not a named member.
|
|
906
|
+
* - Any attempt to name the two declarations apart. They share a name AND a
|
|
907
|
+
* qualified path, so distinguishing them would mean minting a symbol the document
|
|
908
|
+
* does not declare. The line metadata carries the distinction instead.
|
|
909
|
+
*
|
|
910
|
+
* Confidence is 1: two `pair` nodes with one key inside one `object` is a fact about
|
|
911
|
+
* the parse tree, read at the same place the symbol is created.
|
|
912
|
+
*/
|
|
913
|
+
extractDuplicateKeyOverrides(ctx) {
|
|
914
|
+
const seen = /* @__PURE__ */ new Set();
|
|
915
|
+
for (const c of ctx.overrideCandidates) {
|
|
916
|
+
const identity = `${c.ownerPath}\0${c.key}`;
|
|
917
|
+
if (seen.has(identity)) continue;
|
|
918
|
+
seen.add(identity);
|
|
919
|
+
ctx.relationships.push({
|
|
920
|
+
source: c.ownerPath,
|
|
921
|
+
target: c.targetPath,
|
|
922
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
923
|
+
confidence: 1,
|
|
924
|
+
line_number: c.line,
|
|
925
|
+
metadata: {
|
|
926
|
+
jsonSpecific: true,
|
|
927
|
+
redefinition: true,
|
|
928
|
+
overridesLine: c.overridesLine,
|
|
929
|
+
ownerName: c.ownerName,
|
|
930
|
+
key: c.key,
|
|
931
|
+
endpoints: "qualified_path",
|
|
932
|
+
evidenceClass: "structural"
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
extractNpmScriptCalls(ctx) {
|
|
938
|
+
if (!this.fileEndsWith(ctx.filePath, "package.json")) return;
|
|
939
|
+
const document = ctx.rootNode;
|
|
940
|
+
if (document.type !== "document") return;
|
|
941
|
+
const rootObject = document.namedChildren?.find((c) => c.type === "object");
|
|
942
|
+
if (!rootObject) return;
|
|
943
|
+
const scriptsObject = this.topLevelObjectValue(rootObject, "scripts");
|
|
944
|
+
if (!scriptsObject) return;
|
|
945
|
+
const declared = /* @__PURE__ */ new Map();
|
|
946
|
+
for (const pair of scriptsObject.namedChildren ?? []) {
|
|
947
|
+
if (pair?.type !== "pair") continue;
|
|
948
|
+
const key = this.keyText(pair);
|
|
949
|
+
if (key) declared.set(key, pair);
|
|
950
|
+
}
|
|
951
|
+
if (declared.size === 0) return;
|
|
952
|
+
const scriptsPath = this.childPath(ctx.rootName, "scripts");
|
|
953
|
+
for (const [name, pair] of declared) {
|
|
954
|
+
const value = pair.childForFieldName("value");
|
|
955
|
+
if (value?.type !== "string") continue;
|
|
956
|
+
for (const callee of this.npmRunTargets(this.stripQuotes(value.text))) {
|
|
957
|
+
if (callee === name) continue;
|
|
958
|
+
if (!declared.has(callee)) continue;
|
|
959
|
+
ctx.relationships.push({
|
|
960
|
+
source: this.childPath(scriptsPath, name),
|
|
961
|
+
target: this.childPath(scriptsPath, callee),
|
|
962
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
963
|
+
confidence: 0.8,
|
|
964
|
+
line_number: pair.startPosition.row + 1,
|
|
965
|
+
metadata: {
|
|
966
|
+
jsonSpecific: true,
|
|
967
|
+
callForm: "npm_run_sibling_script",
|
|
968
|
+
evidenceClass: "usage_contextual",
|
|
969
|
+
reasonCode: "npm_run_sibling_script",
|
|
970
|
+
endpoints: "qualified_path",
|
|
971
|
+
scriptName: name,
|
|
972
|
+
calleeName: callee
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* The script names a command string invokes through a package runner.
|
|
980
|
+
*
|
|
981
|
+
* Tokenised with a character scan and no regex, because the lane's contract is zero
|
|
982
|
+
* regex literals in this file (`script/m12/json-contract-probe.ts` enforces it) and
|
|
983
|
+
* because a runner name is only meaningful as a whole token: `npm` inside `npx` or
|
|
984
|
+
* a path like `./scripts/npm` is not a runner. A QUOTED target is declined, because
|
|
985
|
+
* the quoting means the shell rewrote the argument and the file no longer states
|
|
986
|
+
* the name plainly.
|
|
987
|
+
*/
|
|
988
|
+
npmRunTargets(command) {
|
|
989
|
+
const RUNNERS = ["npm", "pnpm", "yarn", "bun"];
|
|
990
|
+
const VERBS = ["run", "run-script", "run-s"];
|
|
991
|
+
const SEPARATORS = [" ", " ", "\n", "\r", ";", "&", "|", "(", ")", "<", ">"];
|
|
992
|
+
const tokens = [];
|
|
993
|
+
let current = "";
|
|
994
|
+
let quoted = false;
|
|
995
|
+
let inQuotes = false;
|
|
996
|
+
for (const ch of command) {
|
|
997
|
+
if (ch === '"') {
|
|
998
|
+
inQuotes = !inQuotes;
|
|
999
|
+
quoted = true;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (!inQuotes && SEPARATORS.includes(ch)) {
|
|
1003
|
+
if (current.length > 0) tokens.push({ text: current, quoted });
|
|
1004
|
+
current = "";
|
|
1005
|
+
quoted = false;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
current += ch;
|
|
1009
|
+
}
|
|
1010
|
+
if (current.length > 0) tokens.push({ text: current, quoted });
|
|
1011
|
+
const out = [];
|
|
1012
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1013
|
+
if (!RUNNERS.includes(tokens[i].text)) continue;
|
|
1014
|
+
const verb = tokens[i + 1];
|
|
1015
|
+
if (!verb || verb.quoted || !VERBS.includes(verb.text)) continue;
|
|
1016
|
+
const target = tokens[i + 2];
|
|
1017
|
+
if (!target || target.quoted) continue;
|
|
1018
|
+
out.push(target.text);
|
|
1019
|
+
i += 2;
|
|
1020
|
+
}
|
|
1021
|
+
return out;
|
|
1022
|
+
}
|
|
1023
|
+
/** The object value of a named top-level key, or undefined. */
|
|
1024
|
+
topLevelObjectValue(rootObject, name) {
|
|
1025
|
+
for (const pair of rootObject.namedChildren ?? []) {
|
|
1026
|
+
if (pair?.type !== "pair") continue;
|
|
1027
|
+
if (this.keyText(pair) !== name) continue;
|
|
1028
|
+
const value = pair.childForFieldName("value");
|
|
1029
|
+
if (value?.type === "object") return value;
|
|
1030
|
+
}
|
|
1031
|
+
return void 0;
|
|
1032
|
+
}
|
|
1033
|
+
dependencyVersion(ctx, dependency) {
|
|
1034
|
+
const fullPath = String(dependency.metadata?.fullPath || "");
|
|
1035
|
+
const recorded = ctx.dependencyVersions.get(fullPath);
|
|
1036
|
+
if (recorded !== void 0) return recorded;
|
|
1037
|
+
return "unspecified";
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Does this `$ref` value name ANOTHER FILE rather than something inside this one?
|
|
1041
|
+
*
|
|
1042
|
+
* Three shapes, and they are genuinely different things: a local pointer (`#/a/b`)
|
|
1043
|
+
* names a location in THIS document; a URL names a vocabulary (a JSON-Schema draft
|
|
1044
|
+
* identifier); a relative path with a document extension names a FILE, and that is a
|
|
1045
|
+
* dependency. Decided structurally — an extension and a path separator or a leading
|
|
1046
|
+
* `./` — with no regex, so the lane's zero-regex contract holds.
|
|
1047
|
+
*/
|
|
1048
|
+
/**
|
|
1049
|
+
* DOES THE PROJECT CARRY THE FILE THIS EDGE DEPENDS ON? The precondition of the file claim.
|
|
1050
|
+
*
|
|
1051
|
+
* `candidate` is already project-relative and cannot escape the root (`fileRefCandidate` pops
|
|
1052
|
+
* `..` and returns null when one would leave the project), so the join cannot address anything
|
|
1053
|
+
* outside the project.
|
|
1054
|
+
*
|
|
1055
|
+
* ENOENT / ENOTDIR mean the project does not have it, and the claim is then NOT MADE — the
|
|
1056
|
+
* arbiter would refute it, correctly, because the claim says the project has the file. ANY
|
|
1057
|
+
* OTHER error means this check did not run rather than that the file is absent, and a claim is
|
|
1058
|
+
* not dropped on a check that did not happen: those cases keep the claim, which is exactly the
|
|
1059
|
+
* behaviour every measurement before this one recorded.
|
|
1060
|
+
*
|
|
1061
|
+
* This is a STAT, not a read. The lane's contract is that every decision comes from the TREE
|
|
1062
|
+
* and that the file body is never scanned as text (`BD-regex`, enforced by
|
|
1063
|
+
* `script/m12/json-contract-probe.ts`); asking the filesystem whether a path exists asserts
|
|
1064
|
+
* nothing about that file's contents and adds no regular expression.
|
|
1065
|
+
*/
|
|
1066
|
+
projectCarriesFile(ctx, candidate) {
|
|
1067
|
+
const root = ctx.projectRoot;
|
|
1068
|
+
if (typeof root !== "string" || root.length === 0) return false;
|
|
1069
|
+
try {
|
|
1070
|
+
return statSync(join(root, candidate)).isFile();
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
return !(e?.code === "ENOENT" || e?.code === "ENOTDIR");
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
isFileNamingRef(refPath) {
|
|
1076
|
+
if (refPath.length === 0) return false;
|
|
1077
|
+
if (refPath.startsWith("#/")) return false;
|
|
1078
|
+
if (refPath.startsWith("http://") || refPath.startsWith("https://")) return false;
|
|
1079
|
+
const DOCUMENT_EXTENSIONS = ["json", "jsonc", "json5", "yaml", "yml", "avsc", "avdl", "proto"];
|
|
1080
|
+
const withoutFragment = refPath.split("#")[0];
|
|
1081
|
+
const dot = withoutFragment.lastIndexOf(".");
|
|
1082
|
+
if (dot < 0) return false;
|
|
1083
|
+
const ext = withoutFragment.slice(dot + 1);
|
|
1084
|
+
if (!DOCUMENT_EXTENSIONS.includes(ext)) return false;
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* A `$ref`'s TARGET, and a local pointer is a pointer INTO THIS DOCUMENT.
|
|
1089
|
+
*
|
|
1090
|
+
* The gate found the defect this fixes (`incoming-gate.ts json json 60`): nine `references`
|
|
1091
|
+
* edges whose target was the literal string `#` and whose metadata said `isExternal: true`,
|
|
1092
|
+
* all nine from one file — kubernetes' vendored draft-04 meta-schema
|
|
1093
|
+
* (`.../gnostic-models/jsonschema/schema.json`), whose `definitions.schemaArray.items` is
|
|
1094
|
+
* `{ "$ref": "#" }`. `#` is the JSON-Schema ROOT POINTER ("the schema itself"), so it is the
|
|
1095
|
+
* most local reference a document can write, and BOTH rules below got it wrong for the same
|
|
1096
|
+
* reason: they test `startsWith("#/")`, and a bare `#` is not that prefix.
|
|
1097
|
+
*
|
|
1098
|
+
* the TARGET fell through to the raw `#` — a string no symbol can be named, so the edge
|
|
1099
|
+
* named nothing. It now resolves to `ctx.rootName`, which IS the published
|
|
1100
|
+
* identity of the document root (the module symbol `analyze` pushes first), so
|
|
1101
|
+
* the pointer binds to the thing it points at.
|
|
1102
|
+
* the MARKER (see the emission site) called it EXTERNAL, which is a claim about the corpus
|
|
1103
|
+
* that a pointer into the file's own document cannot support — the same
|
|
1104
|
+
* file-local-class-as-corpus-claim error the fsharp lane retired this week.
|
|
1105
|
+
*
|
|
1106
|
+
* Measured after: the gate reads 29 of 29 `references` resolved, `ext?` 0, `extFAIL` 0 — and
|
|
1107
|
+
* the verdict, which had been returning a branch that prints NO totals, prints them again.
|
|
1108
|
+
*/
|
|
1109
|
+
referenceTarget(ctx, refPath) {
|
|
1110
|
+
if (refPath === "#") return ctx.rootName;
|
|
1111
|
+
if (!refPath.startsWith("#/")) return refPath;
|
|
1112
|
+
const local = [ctx.rootName, ...refPath.slice(2).split("/").map((seg) => this.decodePointerSegment(seg))].join(".");
|
|
1113
|
+
return ctx.symbolMap.has(local) ? local : refPath;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* A `$ref` THAT NAMES AN IDENTITY THIS DOCUMENT DECLARES — or `null`.
|
|
1117
|
+
*
|
|
1118
|
+
* This is the third form a `$ref` can take, and the one whose target was being published
|
|
1119
|
+
* verbatim with a corpus claim it cannot support:
|
|
1120
|
+
*
|
|
1121
|
+
* 1 `#/a/b` a JSON POINTER — a location in this document; `referenceTarget` answers it.
|
|
1122
|
+
* 2 `x.json` a SIBLING FILE — `imports`, with the project's file claim on both labels.
|
|
1123
|
+
* 3 `<uri>` an IDENTITY — `$id` declares a base URI for a schema resource and `$anchor`
|
|
1124
|
+
* declares a fragment name inside one. JSON Schema resolves the reference
|
|
1125
|
+
* against the declared base, and the declaration IS the object carrying the
|
|
1126
|
+
* `$id`; this analyzer already publishes that object's qualified path.
|
|
1127
|
+
*
|
|
1128
|
+
* MEASURED (`validation/m14/fixtures/json/document-identity-census.ts`): 6 corpus `$ref`s in
|
|
1129
|
+
* 2 repositories name an identity their own document declares; 4 of them were this lane's
|
|
1130
|
+
* whole `ext?` class on `--repo=gql-graphql-mesh`.
|
|
1131
|
+
*
|
|
1132
|
+
* The returned path is the DECLARING OBJECT'S `qualifiedName`, i.e. production resolver
|
|
1133
|
+
* strategy 1's exact key — never a basename stem — so this cannot mass-move edges into
|
|
1134
|
+
* `collRes` the way a stem rebase would, and it mints no symbol.
|
|
1135
|
+
*
|
|
1136
|
+
* A fragment is accepted only where the base's resource really is where it lives: for an
|
|
1137
|
+
* `$anchor`, the declaring object must BE the base object or sit UNDER it. A bare anchor name
|
|
1138
|
+
* scopes nothing on its own, so a name declared in another resource of the same document is
|
|
1139
|
+
* refused rather than guessed.
|
|
1140
|
+
*/
|
|
1141
|
+
documentDeclaredIdentity(ctx, refPath) {
|
|
1142
|
+
const hash = refPath.indexOf("#");
|
|
1143
|
+
const base = this.withoutEmptyFragment(hash < 0 ? refPath : refPath.slice(0, hash));
|
|
1144
|
+
if (base.length === 0) return null;
|
|
1145
|
+
const declared = ctx.documentIds.get(base);
|
|
1146
|
+
if (declared === void 0) return null;
|
|
1147
|
+
const fragment = hash < 0 ? "" : refPath.slice(hash + 1);
|
|
1148
|
+
if (fragment.length === 0) return declared;
|
|
1149
|
+
if (fragment.startsWith("/")) return null;
|
|
1150
|
+
const anchorPaths = ctx.documentAnchors.get(fragment);
|
|
1151
|
+
if (!anchorPaths) return null;
|
|
1152
|
+
const underBase = anchorPaths.find((p) => p === declared || p.startsWith(`${declared}.`));
|
|
1153
|
+
return underBase ?? null;
|
|
1154
|
+
}
|
|
1155
|
+
/** One trailing empty fragment is not part of an identity: `…/schema` and `…/schema#` name the
|
|
1156
|
+
* same base URI (RFC 3986 §6.2.2), and `$id`/`$ref` use both spellings. */
|
|
1157
|
+
withoutEmptyFragment(uri) {
|
|
1158
|
+
return uri.endsWith("#") ? uri.slice(0, -1) : uri;
|
|
1159
|
+
}
|
|
1160
|
+
/** RFC 6901: within a pointer segment, `~1` is `/` and `~0` is `~`, decoded in that order. */
|
|
1161
|
+
decodePointerSegment(segment) {
|
|
1162
|
+
if (!segment.includes("~")) return segment;
|
|
1163
|
+
let out = "";
|
|
1164
|
+
for (let i = 0; i < segment.length; i++) {
|
|
1165
|
+
if (segment[i] === "~" && i + 1 < segment.length) {
|
|
1166
|
+
const next = segment[i + 1];
|
|
1167
|
+
if (next === "0") {
|
|
1168
|
+
out += "~";
|
|
1169
|
+
i++;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (next === "1") {
|
|
1173
|
+
out += "/";
|
|
1174
|
+
i++;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
out += segment[i];
|
|
1179
|
+
}
|
|
1180
|
+
return out;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* IS THIS `$ref` A LOCAL POINTER? A pointer into the file's own document — `#` (the root) or
|
|
1184
|
+
* `#/a/b` (a location inside it) — is answered by the document and by nothing else, so it is
|
|
1185
|
+
* neither an import nor an external target. Everything else (`./other.json`,
|
|
1186
|
+
* `schemas/x.json#/defs/Y`, `https://…`) names something the document does not contain.
|
|
1187
|
+
*/
|
|
1188
|
+
isLocalPointer(refPath) {
|
|
1189
|
+
return refPath === "#" || refPath.startsWith("#/");
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* THE FILE A `$ref` NAMES, in the spelling the PROJECT's own file list uses — or `null`.
|
|
1193
|
+
*
|
|
1194
|
+
* `null` is the honest answer whenever the path cannot be derived soundly: no project root was
|
|
1195
|
+
* supplied, the referring file does not sit under the root, the refPath is not a relative
|
|
1196
|
+
* document path (a URL, an absolute path, a bare fragment), or folding `..` escapes the project
|
|
1197
|
+
* (a sibling checkout is not an admitted file, and claiming one would be REFUTED by the gate).
|
|
1198
|
+
* A wrong candidate is worse than none: the arbiter refutes a file claim it cannot find, and a
|
|
1199
|
+
* refutation is counted against the lane exactly as an unresolved edge is.
|
|
1200
|
+
*/
|
|
1201
|
+
fileRefCandidate(ctx, refPath) {
|
|
1202
|
+
if (!this.isFileNamingRef(refPath)) return null;
|
|
1203
|
+
const self = projectRelativePath(ctx.filePath, ctx.projectRoot);
|
|
1204
|
+
if (self === null) return null;
|
|
1205
|
+
const cut = self.lastIndexOf("/");
|
|
1206
|
+
const dir = cut < 0 ? "" : self.slice(0, cut + 1);
|
|
1207
|
+
const noFragment = refPath.split("#")[0];
|
|
1208
|
+
if (noFragment.length === 0) return null;
|
|
1209
|
+
const out = [];
|
|
1210
|
+
for (const seg of (dir + noFragment).split("/")) {
|
|
1211
|
+
if (seg.length === 0 || seg === ".") continue;
|
|
1212
|
+
if (seg === "..") {
|
|
1213
|
+
if (out.length === 0) return null;
|
|
1214
|
+
out.pop();
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
out.push(seg);
|
|
1218
|
+
}
|
|
1219
|
+
return out.length > 0 ? out.join("/") : null;
|
|
1220
|
+
}
|
|
1221
|
+
isReference(key, value) {
|
|
1222
|
+
if (key === "$ref") return true;
|
|
1223
|
+
return value.type === "string" && isPathReference(this.stripQuotes(value.text));
|
|
1224
|
+
}
|
|
1225
|
+
referencePath(key, value) {
|
|
1226
|
+
if (!this.isReference(key, value) || value.type !== "string") return void 0;
|
|
1227
|
+
return this.stripQuotes(value.text);
|
|
1228
|
+
}
|
|
1229
|
+
isRequired(ctx, parentPath, key) {
|
|
1230
|
+
return ctx.requiredByObject.get(parentPath)?.has(key) ?? false;
|
|
1231
|
+
}
|
|
1232
|
+
hasValidation(ctx, key, value) {
|
|
1233
|
+
if (VALIDATION_KEYS.has(key)) return true;
|
|
1234
|
+
if (value.type !== "object") return false;
|
|
1235
|
+
for (const pair of value.namedChildren ?? []) {
|
|
1236
|
+
if (pair?.type === "pair" && VALIDATION_KEYS.has(this.keyText(pair))) return true;
|
|
1237
|
+
}
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
isApiEndpoint(key, value) {
|
|
1241
|
+
if (key.length === 0 || key[0] !== "/") return false;
|
|
1242
|
+
if (value.type !== "object") return false;
|
|
1243
|
+
for (const pair of value.namedChildren ?? []) {
|
|
1244
|
+
if (pair?.type === "pair" && API_METHOD_KEYS.has(this.keyText(pair))) return true;
|
|
1245
|
+
}
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
detectFrameworks(ctx) {
|
|
1249
|
+
const frameworks = [];
|
|
1250
|
+
const file = ctx.filePath.toLowerCase();
|
|
1251
|
+
const has = (name) => ctx.symbolMap.has(name);
|
|
1252
|
+
const dependency = (name) => {
|
|
1253
|
+
for (const symbol of ctx.dependencySymbols) if (symbol.name === name) return true;
|
|
1254
|
+
return false;
|
|
1255
|
+
};
|
|
1256
|
+
if (this.fileEndsWith(file, "package.json")) frameworks.push("npm/Node.js");
|
|
1257
|
+
if (this.fileEndsWith(file, "composer.json")) frameworks.push("Composer/PHP");
|
|
1258
|
+
if (this.fileEndsWith(file, "tsconfig.json") || has("compilerOptions")) frameworks.push("TypeScript");
|
|
1259
|
+
if (has("openapi") || has("swagger")) frameworks.push("OpenAPI/Swagger");
|
|
1260
|
+
if (has("$schema") || ctx.schemaObjects.size > 0) frameworks.push("JSON Schema");
|
|
1261
|
+
if (has("Version") && has("Resources")) frameworks.push("AWS CloudFormation");
|
|
1262
|
+
if (dependency("react") || dependency("@types/react")) frameworks.push("React");
|
|
1263
|
+
if (dependency("vue") || dependency("@vue/core")) frameworks.push("Vue.js");
|
|
1264
|
+
if (dependency("next")) frameworks.push("Next.js");
|
|
1265
|
+
return frameworks;
|
|
1266
|
+
}
|
|
1267
|
+
detectPatterns(ctx) {
|
|
1268
|
+
const patterns = [];
|
|
1269
|
+
if (ctx.schemaObjects.size > 0 || ctx.symbolMap.has("$schema")) patterns.push("JSON Schema Validation");
|
|
1270
|
+
if (ctx.referenceSymbols.length > 0) patterns.push("Reference-based Relationships");
|
|
1271
|
+
if (ctx.apiEndpointSymbols.length > 0) patterns.push("RESTful API Design");
|
|
1272
|
+
if (ctx.hasArrays) patterns.push("Collection Data Types");
|
|
1273
|
+
if (ctx.maxDepth > 3) patterns.push("Deep Nested Structures");
|
|
1274
|
+
if (ctx.symbolMap.has("dependencies") || ctx.symbolMap.has("devDependencies")) patterns.push("Dependency Management");
|
|
1275
|
+
return patterns;
|
|
1276
|
+
}
|
|
1277
|
+
structuralScores(ctx) {
|
|
1278
|
+
let dataModelingScore = 70;
|
|
1279
|
+
if (ctx.objectCount > 0 && ctx.propertyCount / ctx.objectCount >= 2) dataModelingScore += 10;
|
|
1280
|
+
if (ctx.maxDepth <= 4) dataModelingScore += 10;
|
|
1281
|
+
let schemaValidationScore = 50;
|
|
1282
|
+
if (ctx.schemaObjects.size > 0) schemaValidationScore += 25;
|
|
1283
|
+
if (ctx.validationSymbols.length > 0) schemaValidationScore += 15;
|
|
1284
|
+
if (ctx.referenceSymbols.length > 0) schemaValidationScore += 10;
|
|
1285
|
+
return { dataModelingScore: Math.min(100, dataModelingScore), schemaValidationScore: Math.min(100, schemaValidationScore) };
|
|
1286
|
+
}
|
|
1287
|
+
insights(ctx) {
|
|
1288
|
+
const out = [];
|
|
1289
|
+
if (ctx.referenceSymbols.length > 0) out.push(`JSON references detected (${ctx.referenceSymbols.length})`);
|
|
1290
|
+
if (ctx.validationSymbols.length > 0) out.push(`Schema validation keywords detected (${ctx.validationSymbols.length})`);
|
|
1291
|
+
if (ctx.hasArrays) out.push(`Arrays detected (${ctx.arrayCount})`);
|
|
1292
|
+
return out;
|
|
1293
|
+
}
|
|
1294
|
+
valueType(node) {
|
|
1295
|
+
switch (node.type) {
|
|
1296
|
+
case "object":
|
|
1297
|
+
return "object";
|
|
1298
|
+
case "array":
|
|
1299
|
+
return "array";
|
|
1300
|
+
case "string":
|
|
1301
|
+
return "string";
|
|
1302
|
+
case "number":
|
|
1303
|
+
return "number";
|
|
1304
|
+
case "true":
|
|
1305
|
+
case "false":
|
|
1306
|
+
return "boolean";
|
|
1307
|
+
case "null":
|
|
1308
|
+
return "null";
|
|
1309
|
+
default:
|
|
1310
|
+
return "unknown";
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
keyText(pair) {
|
|
1314
|
+
const key = pair.type === "pair" ? pair.childForFieldName("key") : pair;
|
|
1315
|
+
return key ? this.stripQuotes(key.text) : "";
|
|
1316
|
+
}
|
|
1317
|
+
stripQuotes(text) {
|
|
1318
|
+
const value = text.trim();
|
|
1319
|
+
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') return value.slice(1, -1);
|
|
1320
|
+
return value;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* An array item's SYMBOL name and the SYMBOL name of the array that holds it.
|
|
1324
|
+
*
|
|
1325
|
+
* Derived from the array's own QUALIFIED path (minus the module prefix) plus the index,
|
|
1326
|
+
* never from the item's path and never from the array's bare NAME — and the second half of
|
|
1327
|
+
* that is the fix. The array's own NAME is not enough once an array can be an ELEMENT of
|
|
1328
|
+
* another array, because then two different arrays share it:
|
|
1329
|
+
*
|
|
1330
|
+
* { "rows": [ [ {"cell": 1}, {"cell": 2} ] ] }
|
|
1331
|
+
*
|
|
1332
|
+
* The inner array's name arrives as `rows`, the same name the OUTER array has, so its two
|
|
1333
|
+
* elements came out as `rows[0]` and `rows[1]` — names that collide with the outer array's
|
|
1334
|
+
* own element namespace — while their qualified paths said `rows[0][0]` and `rows[0][1]`.
|
|
1335
|
+
* The consequence was not cosmetic: `contains` is emitted from the OWNER symbol, and the
|
|
1336
|
+
* owner was derived from the truncated name, so the document stated
|
|
1337
|
+
*
|
|
1338
|
+
* rows[0] contains rows[0][1]
|
|
1339
|
+
*
|
|
1340
|
+
* between two objects that are not nested at all. Measured over the corpus: 30 of the 37
|
|
1341
|
+
* `contains` edges an independent oracle could not corroborate, and 183 at the whole-population
|
|
1342
|
+
* count before the endpoint change. The two walkers agreed on the defect, which is the case
|
|
1343
|
+
* the honesty contract calls out: agreement is evidence that two implementations agree, not
|
|
1344
|
+
* that either is right.
|
|
1345
|
+
*
|
|
1346
|
+
* Deriving from the array's own NAME is still right for the ordinary case, because a key's
|
|
1347
|
+
* qualified path minus the module prefix IS its name — `a.rows` → `rows` — and it is what
|
|
1348
|
+
* makes a dotted module name survive: `index.svelte.warnings.json` gives the module
|
|
1349
|
+
* `index.svelte.warnings` and the item owner `items`, not `warnings`.
|
|
1350
|
+
*/
|
|
1351
|
+
arrayItemName(arrayPath, rootName, index) {
|
|
1352
|
+
if (arrayPath === rootName) return { item: `${rootName}[${index}]`, holder: rootName };
|
|
1353
|
+
const lastSegment = arrayPath.slice(arrayPath.lastIndexOf(".") + 1);
|
|
1354
|
+
const self = lastSegment.endsWith("]") ? this.afterModuleImpl(arrayPath, rootName) : lastSegment;
|
|
1355
|
+
return { item: `${self}[${index}]`, holder: self };
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* The name a qualified path carries once the module prefix is removed — the ONE place that
|
|
1359
|
+
* strips it, because getting it wrong is invisible in isolation and expensive in aggregate.
|
|
1360
|
+
*
|
|
1361
|
+
* The character at `rootName.length` says what kind of path this is, and the two cases need
|
|
1362
|
+
* OPPOSITE handling:
|
|
1363
|
+
*
|
|
1364
|
+
* `.` the path continues under a KEY, and the key is the name — drop the separator, so
|
|
1365
|
+
* `mod.rows[0]` gives `rows[0]`.
|
|
1366
|
+
* `[` the path hangs off the module by an INDEX and there is no key at all, so the
|
|
1367
|
+
* bracket IS the name's first character — keep it, so `mod[4]` gives `[4]`.
|
|
1368
|
+
*
|
|
1369
|
+
* A fixed `slice(rootName.length + 1)` assumes the first case and therefore eats the `[` in
|
|
1370
|
+
* the second, publishing `4]`, `4][2]`, `4][2][6]`. Measured on a sample of the corpus's
|
|
1371
|
+
* top-level-array documents: 19,708 names over 214 files, 14.9% of that population — a
|
|
1372
|
+
* family (`4][2][6][1]`) no keyed or element name can produce, which is what distinguishes
|
|
1373
|
+
* it from the correct `Statement[0]` spelling that also ends in `]`.
|
|
1374
|
+
*
|
|
1375
|
+
* A path that is neither (no separator at all) returns the path unchanged rather than
|
|
1376
|
+
* chopping a leading character off a name that has no prefix to remove.
|
|
1377
|
+
*/
|
|
1378
|
+
afterModuleImpl(path, rootName) {
|
|
1379
|
+
if (path === rootName) return path;
|
|
1380
|
+
const boundary = path.charAt(rootName.length);
|
|
1381
|
+
if (boundary === "[") return path.slice(rootName.length);
|
|
1382
|
+
if (boundary === ".") return path.slice(rootName.length + 1);
|
|
1383
|
+
return path;
|
|
1384
|
+
}
|
|
1385
|
+
addArrayObjectSymbol(ctx, node, path, itemName) {
|
|
1386
|
+
const bracket = itemName.lastIndexOf("[");
|
|
1387
|
+
const name = itemName;
|
|
1388
|
+
const ownerName = bracket > 0 ? itemName.slice(0, bracket) : ctx.rootName;
|
|
1389
|
+
this.addSymbol(ctx, {
|
|
1390
|
+
name,
|
|
1391
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1392
|
+
qualifiedName: path,
|
|
1393
|
+
range: this.nodeToRange(node),
|
|
1394
|
+
ownerName,
|
|
1395
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1396
|
+
scope: ownerName,
|
|
1397
|
+
visibility: "public",
|
|
1398
|
+
returnType: "object",
|
|
1399
|
+
metadata: {
|
|
1400
|
+
jsonSpecific: true,
|
|
1401
|
+
nodeType: "object",
|
|
1402
|
+
valueType: "object",
|
|
1403
|
+
fullPath: path,
|
|
1404
|
+
isObject: true,
|
|
1405
|
+
isArrayItem: true,
|
|
1406
|
+
depth: this.pathDepth(path, ctx.rootName)
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* The symbol for an array element that is itself an ARRAY.
|
|
1412
|
+
*
|
|
1413
|
+
* Same shape as `addArrayObjectSymbol` and for the same reason: `contains` is emitted from
|
|
1414
|
+
* the OWNER symbol, so an element that owns elements needs a symbol of its own. Its name is
|
|
1415
|
+
* the qualified path minus the module prefix (`rows[0]`), which is the same string the
|
|
1416
|
+
* object-element rule produces, so the two kinds cannot collide on one position.
|
|
1417
|
+
*/
|
|
1418
|
+
addArrayElementSymbol(ctx, node, path, itemName) {
|
|
1419
|
+
const lastSegment = path.slice(path.lastIndexOf(".") + 1);
|
|
1420
|
+
const name = lastSegment.endsWith("]") ? this.afterModuleImpl(path, ctx.rootName) : lastSegment;
|
|
1421
|
+
const bracket = name.lastIndexOf("[");
|
|
1422
|
+
const ownerName = bracket > 0 ? name.slice(0, bracket) : ctx.rootName;
|
|
1423
|
+
this.addSymbol(ctx, {
|
|
1424
|
+
name,
|
|
1425
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1426
|
+
qualifiedName: path,
|
|
1427
|
+
range: this.nodeToRange(node),
|
|
1428
|
+
ownerName,
|
|
1429
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1430
|
+
scope: ownerName,
|
|
1431
|
+
visibility: "public",
|
|
1432
|
+
returnType: "array",
|
|
1433
|
+
metadata: {
|
|
1434
|
+
jsonSpecific: true,
|
|
1435
|
+
nodeType: "array",
|
|
1436
|
+
valueType: "array",
|
|
1437
|
+
fullPath: path,
|
|
1438
|
+
isArray: true,
|
|
1439
|
+
isArrayItem: true,
|
|
1440
|
+
depth: this.pathDepth(path, ctx.rootName)
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
childPath(parentPath, key) {
|
|
1445
|
+
return parentPath ? `${parentPath}.${key}` : key;
|
|
1446
|
+
}
|
|
1447
|
+
pushArrayChildren(stack, node, parentPath, parentName, rootName) {
|
|
1448
|
+
const children = node.namedChildren ?? [];
|
|
1449
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1450
|
+
const child = children[i];
|
|
1451
|
+
if (!child) continue;
|
|
1452
|
+
const itemPath = `${parentPath}[${i}]`;
|
|
1453
|
+
const { item } = this.arrayItemName(parentPath, rootName, i);
|
|
1454
|
+
const isElement = child.type === "object" || child.type === "array";
|
|
1455
|
+
stack.push({
|
|
1456
|
+
node: child,
|
|
1457
|
+
parentPath: itemPath,
|
|
1458
|
+
parentName: isElement ? item : parentName,
|
|
1459
|
+
emitObject: isElement
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Depth of a qualified path below the module, counted in dot-separated segments.
|
|
1465
|
+
*
|
|
1466
|
+
* This reads the same prefix `afterModuleImpl` removes, so it calls that rather than
|
|
1467
|
+
* repeating the arithmetic. The two derivations had drifted independently and the second
|
|
1468
|
+
* one was wrong for the same reason: for a top-level array the character at
|
|
1469
|
+
* `rootName.length` is `[`, so the fixed `+ 1` ate it. It happened to cost nothing here —
|
|
1470
|
+
* both `4][2][6]` and `[4][2][6]` split to one segment — which is exactly why it survived
|
|
1471
|
+
* a fix to the naming rule beside it. Two copies of one derivation is a second chance to be
|
|
1472
|
+
* wrong for free.
|
|
1473
|
+
*/
|
|
1474
|
+
pathDepth(path, rootName) {
|
|
1475
|
+
if (path === rootName) return 0;
|
|
1476
|
+
return this.afterModuleImpl(path, rootName).split(".").length;
|
|
1477
|
+
}
|
|
1478
|
+
updateDepth(ctx, path) {
|
|
1479
|
+
const depth = this.pathDepth(path, ctx.rootName);
|
|
1480
|
+
if (depth > ctx.maxDepth) ctx.maxDepth = depth;
|
|
1481
|
+
}
|
|
1482
|
+
pushNamedChildren(stack, node, parentPath, parentName = parentPath, duplicateKeys) {
|
|
1483
|
+
const children = node.namedChildren ?? [];
|
|
1484
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1485
|
+
const child = children[i];
|
|
1486
|
+
if (child) stack.push({ node: child, parentPath, parentName, duplicateKeys });
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* The node types that are JSON VALUES. A `comment` is a named child of `document` in
|
|
1491
|
+
* this grammar and is not a value: it introduces no key and cannot publish a name. An
|
|
1492
|
+
* `ERROR` node is the grammar's recovery shape and says nothing about how many documents
|
|
1493
|
+
* the file really holds.
|
|
1494
|
+
*/
|
|
1495
|
+
isValueNode(node) {
|
|
1496
|
+
const t = node.type;
|
|
1497
|
+
return t === "object" || t === "array" || t === "string" || t === "number" || t === "true" || t === "false" || t === "null";
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Push a document's top-level values, giving each one its OWN position when the file
|
|
1501
|
+
* holds more than one.
|
|
1502
|
+
*
|
|
1503
|
+
* ## The defect
|
|
1504
|
+
*
|
|
1505
|
+
* Concatenated JSON is real input — cargo/rustfix's recorded compiler diagnostics are
|
|
1506
|
+
* sequences of 2 to 5 top-level objects in one file — and this grammar parses that as ONE
|
|
1507
|
+
* `document` node with N named children. `pushNamedChildren` gave every one of them the
|
|
1508
|
+
* SAME `parentPath`, so all N published `<module>.<key>` and the qualified name stopped
|
|
1509
|
+
* identifying anything:
|
|
1510
|
+
*
|
|
1511
|
+
* name=code kind=class qn=str-lit-type-mismatch.code line 3
|
|
1512
|
+
* name=code kind=class qn=str-lit-type-mismatch.code line 73
|
|
1513
|
+
* name=code kind=class qn=str-lit-type-mismatch.code line 143
|
|
1514
|
+
* name=code kind=property qn=str-lit-type-mismatch.code line 213
|
|
1515
|
+
*
|
|
1516
|
+
* Four symbols under one name, the fourth of a different kind — so the shared arbiter
|
|
1517
|
+
* counts it AMBIGUOUS and the reverse walk picks a winner that is simply the wrong line.
|
|
1518
|
+
* This is the whole of bucket A in the 601-file ambiguity split.
|
|
1519
|
+
*
|
|
1520
|
+
* ## Why the position is conditional
|
|
1521
|
+
*
|
|
1522
|
+
* A document with ONE value keeps `<module>` as its parent path, unchanged. That is 13,835
|
|
1523
|
+
* of 13,951 admitted files and it is the contract every fixture encodes. 358 more hold
|
|
1524
|
+
* one value beside comments or ERROR nodes, and `comment+object` is the commonest
|
|
1525
|
+
* multi-child shape in the corpus at 24 files — gating on `namedChildCount > 1` would
|
|
1526
|
+
* rename all of them for no reason. `top-level-value-positions.ts` measures the split:
|
|
1527
|
+
* 45 files have >=2 values, 358 have 1 value plus extras, 13,477 have exactly one child.
|
|
1528
|
+
*
|
|
1529
|
+
* ## Why the spelling is `<module>[i]` and not a new separator
|
|
1530
|
+
*
|
|
1531
|
+
* It is the spelling `arrayItemName` already gives a top-level array's elements, and a
|
|
1532
|
+
* concatenated document is the same shape written without brackets: N values in sequence
|
|
1533
|
+
* rather than N elements in an array. Reusing it means one convention, not two, and
|
|
1534
|
+
* `afterModuleImpl` already handles the `[` boundary.
|
|
1535
|
+
*/
|
|
1536
|
+
pushTopLevelValues(stack, node, parentPath, parentName) {
|
|
1537
|
+
let valueCount = 0;
|
|
1538
|
+
const children = node.namedChildren ?? [];
|
|
1539
|
+
for (const child of children) {
|
|
1540
|
+
if (child && this.isValueNode(child)) valueCount++;
|
|
1541
|
+
}
|
|
1542
|
+
if (valueCount <= 1) {
|
|
1543
|
+
this.pushNamedChildren(stack, node, parentPath, parentName);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
let position = 0;
|
|
1547
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1548
|
+
const child = children[i];
|
|
1549
|
+
if (!child) continue;
|
|
1550
|
+
if (!this.isValueNode(child)) continue;
|
|
1551
|
+
const name = `${parentPath}[${position}]`;
|
|
1552
|
+
stack.push({ node: child, parentPath: name, parentName: name, emitObject: child.type === "object" });
|
|
1553
|
+
position++;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* A document's IDENTITY, and it is scoped to the file.
|
|
1558
|
+
*
|
|
1559
|
+
* ## The defect this fixes
|
|
1560
|
+
*
|
|
1561
|
+
* The identity was the filesystem BASENAME, and every `package.json` in a project publishes a
|
|
1562
|
+
* module symbol named `package`. Measured over a 601-file stride of the json corpus
|
|
1563
|
+
* (`validation/m14/measurements/json-ambiguity-split-601-after.json`): 90 colliding target names
|
|
1564
|
+
* across 1,218 collision symbols, and the `package.*` family — `package.dependencies` alone
|
|
1565
|
+
* carries 3 declarations — is the bucket that file scoping resolves on its own. So the `contains`
|
|
1566
|
+
* edge asserting that `package` owns `dependencies` could not be walked in either direction:
|
|
1567
|
+
* the shared resolver binds a bare name only under `HAVING COUNT(*) = 1`, and an exact-key match
|
|
1568
|
+
* has to be unique too.
|
|
1569
|
+
*
|
|
1570
|
+
* ## The rule
|
|
1571
|
+
*
|
|
1572
|
+
* Scoping by the project-relative directory chain, dot-joined, basename last. Dot-joined because
|
|
1573
|
+
* every downstream consumer already reads a dotted string as a path; basename last because the
|
|
1574
|
+
* leaf of every member path is computed by slicing a fixed number of characters off the module
|
|
1575
|
+
* prefix, so moving the basename would break that arithmetic. The full rule and its three
|
|
1576
|
+
* properties are in `src/ast/project-scoped-identity.ts`, which is shared on purpose.
|
|
1577
|
+
*
|
|
1578
|
+
* ## What it does NOT do
|
|
1579
|
+
*
|
|
1580
|
+
* It does not make the BASENAME key unique — `package` is still published by every package.json,
|
|
1581
|
+
* because the basename is the readable name a human uses. It makes the QUALIFIED PATH unique,
|
|
1582
|
+
* and that path is what `contains` emits and what exact-match resolution binds.
|
|
1583
|
+
*/
|
|
1584
|
+
documentName(filePath, projectRoot) {
|
|
1585
|
+
const slash = filePath.lastIndexOf("/");
|
|
1586
|
+
const base = slash >= 0 ? filePath.slice(slash + 1) : filePath;
|
|
1587
|
+
const lower = base.toLowerCase();
|
|
1588
|
+
for (const suffix of [".json", ".jsonc", ".json5"]) {
|
|
1589
|
+
if (lower.endsWith(suffix)) return this.scoped(base.slice(0, base.length - suffix.length) || "document", filePath, projectRoot);
|
|
1590
|
+
}
|
|
1591
|
+
return this.scoped(base || "document", filePath, projectRoot);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Fold the project-relative directory chain onto the document's stem.
|
|
1595
|
+
*
|
|
1596
|
+
* One place, so the scope cannot be applied on one return path and missed on another — the
|
|
1597
|
+
* early-return shape this replaced is exactly how a document keeps its basename on the branch
|
|
1598
|
+
* that strips an extension and gets scoped on the branch that does not.
|
|
1599
|
+
*/
|
|
1600
|
+
scoped(stem, filePath, projectRoot) {
|
|
1601
|
+
return applyDocumentScope(stem, documentDirScope(filePath, projectRoot));
|
|
1602
|
+
}
|
|
1603
|
+
fileEndsWith(filePath, suffix) {
|
|
1604
|
+
return filePath.toLowerCase().endsWith(suffix.toLowerCase());
|
|
1605
|
+
}
|
|
1606
|
+
documentType(ctx) {
|
|
1607
|
+
if (this.fileEndsWith(ctx.filePath, "package.json")) return "npm_package";
|
|
1608
|
+
if (this.fileEndsWith(ctx.filePath, "composer.json")) return "composer_package";
|
|
1609
|
+
if (this.isApiSpec(ctx)) return "openapi_spec";
|
|
1610
|
+
if (ctx.schemaObjects.size > 0 || ctx.symbolMap.has("$schema")) return "json_schema";
|
|
1611
|
+
return "generic_json";
|
|
1612
|
+
}
|
|
1613
|
+
isApiSpec(ctx) {
|
|
1614
|
+
return (ctx.symbolMap.has("openapi") || ctx.symbolMap.has("swagger")) && ctx.symbolMap.has("paths");
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* The 1-based line a symbol was DECLARED on, for the `line_number` every
|
|
1618
|
+
* relationship must carry. M60 requires a site line on every edge, and this
|
|
1619
|
+
* analyzer emitted NONE: the string `line_number` did not occur in the file, so
|
|
1620
|
+
* every JSON edge in the index was stored with a NULL line and no consumer —
|
|
1621
|
+
* the graph panel included — could point at where an edge came from. Symbols
|
|
1622
|
+
* carried `range` all along, so the position was known and simply not published.
|
|
1623
|
+
*/
|
|
1624
|
+
declLine(symbol) {
|
|
1625
|
+
return symbol?.range?.startLine ?? 0;
|
|
1626
|
+
}
|
|
1627
|
+
nodeToRange(node) {
|
|
1628
|
+
return {
|
|
1629
|
+
startLine: node.startPosition.row + 1,
|
|
1630
|
+
startColumn: node.startPosition.column + 1,
|
|
1631
|
+
endLine: node.endPosition.row + 1,
|
|
1632
|
+
endColumn: node.endPosition.column + 1,
|
|
1633
|
+
startIndex: node.startIndex,
|
|
1634
|
+
endIndex: node.endIndex
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
export {
|
|
1639
|
+
RealJsonAnalyzer
|
|
1640
|
+
};
|