@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,2085 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-V44PGPA6.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import {
|
|
8
|
+
BINDING_SEP
|
|
9
|
+
} from "./chunk-KAET47ZL.js";
|
|
10
|
+
import "./chunk-KCDECRJY.js";
|
|
11
|
+
import "./chunk-MLKGABMK.js";
|
|
12
|
+
|
|
13
|
+
// src/ast/real-julia-analyzer.ts
|
|
14
|
+
import { readFileSync } from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
var JULIA_PACKAGE_CACHE = /* @__PURE__ */ new Map();
|
|
17
|
+
function projectTomlName(text) {
|
|
18
|
+
let inTable = false;
|
|
19
|
+
for (const rawLine of text.split("\n")) {
|
|
20
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
21
|
+
const t = line.trim();
|
|
22
|
+
if (!t || t.startsWith("#")) continue;
|
|
23
|
+
if (t.startsWith("[")) {
|
|
24
|
+
inTable = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (inTable) continue;
|
|
28
|
+
const eq = t.indexOf("=");
|
|
29
|
+
if (eq < 0 || t.slice(0, eq).trim() !== "name") continue;
|
|
30
|
+
let value = t.slice(eq + 1).trim();
|
|
31
|
+
if (!value) return "";
|
|
32
|
+
const quote = value[0];
|
|
33
|
+
if (quote === '"' || quote === "'") {
|
|
34
|
+
const close = value.indexOf(quote, 1);
|
|
35
|
+
return close > 0 ? value.slice(1, close).trim() : "";
|
|
36
|
+
}
|
|
37
|
+
const hash = value.indexOf("#");
|
|
38
|
+
if (hash >= 0) value = value.slice(0, hash);
|
|
39
|
+
return value.trim();
|
|
40
|
+
}
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
function juliaPackageName(projectRoot) {
|
|
44
|
+
if (!projectRoot) return "";
|
|
45
|
+
const hit = JULIA_PACKAGE_CACHE.get(projectRoot);
|
|
46
|
+
if (hit !== void 0) return hit;
|
|
47
|
+
let name = "";
|
|
48
|
+
try {
|
|
49
|
+
name = projectTomlName(readFileSync(path.join(projectRoot, "Project.toml"), "utf8"));
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
JULIA_PACKAGE_CACHE.set(projectRoot, name);
|
|
53
|
+
return name;
|
|
54
|
+
}
|
|
55
|
+
var TYPE_DECLARATIONS = /* @__PURE__ */ new Set(["abstract_definition", "primitive_definition", "struct_definition"]);
|
|
56
|
+
var GLOBAL_SCOPE = "global";
|
|
57
|
+
var IMPLICIT_OR_CONSTANT = /* @__PURE__ */ new Set([
|
|
58
|
+
"nothing",
|
|
59
|
+
"missing",
|
|
60
|
+
"undef",
|
|
61
|
+
"ARGS",
|
|
62
|
+
"ENV",
|
|
63
|
+
"C_NULL",
|
|
64
|
+
"stdout",
|
|
65
|
+
"stderr",
|
|
66
|
+
"stdin"
|
|
67
|
+
]);
|
|
68
|
+
var SCOPE_TRANSPARENT_MACROS = /* @__PURE__ */ new Set(["eval", "static", "sync", "async", "time"]);
|
|
69
|
+
var SCOPE_INTRODUCING_NODES = /* @__PURE__ */ new Set([
|
|
70
|
+
"function_definition",
|
|
71
|
+
"macro_definition",
|
|
72
|
+
"let_statement",
|
|
73
|
+
"for_statement",
|
|
74
|
+
"while_statement",
|
|
75
|
+
"do_clause",
|
|
76
|
+
"quote_expression",
|
|
77
|
+
"quote_statement",
|
|
78
|
+
"struct_definition",
|
|
79
|
+
"abstract_definition",
|
|
80
|
+
"primitive_definition",
|
|
81
|
+
"arrow_function_expression",
|
|
82
|
+
"comprehension_expression"
|
|
83
|
+
]);
|
|
84
|
+
function stripSigil(text, sigil) {
|
|
85
|
+
const t = text.trim();
|
|
86
|
+
return t.startsWith(sigil) ? t.slice(sigil.length) : t;
|
|
87
|
+
}
|
|
88
|
+
function hasLayout(text) {
|
|
89
|
+
for (const ch of text) if (ch.trim() === "" || ch === "#") return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function macroNameOf(text) {
|
|
93
|
+
const t = stripSigil(text, "@");
|
|
94
|
+
for (let i = 0; i < t.length; i++) {
|
|
95
|
+
const c = t.charCodeAt(i);
|
|
96
|
+
if (c === 10 || c === 13 || c === 9 || c === 32) {
|
|
97
|
+
const first = t.slice(0, i);
|
|
98
|
+
return hasLayout(first) ? "" : first;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return hasLayout(t) ? "" : t;
|
|
102
|
+
}
|
|
103
|
+
function collapseSpace(text) {
|
|
104
|
+
const words = [];
|
|
105
|
+
let word = "";
|
|
106
|
+
for (const ch of text) {
|
|
107
|
+
if (ch.trim() === "") {
|
|
108
|
+
if (word) {
|
|
109
|
+
words.push(word);
|
|
110
|
+
word = "";
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
word += ch;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (word) words.push(word);
|
|
117
|
+
return words.join(" ");
|
|
118
|
+
}
|
|
119
|
+
var CONDITIONAL_OR_GENERATED = /* @__PURE__ */ new Set([
|
|
120
|
+
// conditional
|
|
121
|
+
"if_statement",
|
|
122
|
+
"else_clause",
|
|
123
|
+
"elseif_clause",
|
|
124
|
+
"while_statement",
|
|
125
|
+
"for_statement",
|
|
126
|
+
"try_statement",
|
|
127
|
+
"catch_clause",
|
|
128
|
+
"finally_clause",
|
|
129
|
+
"ternary_expression",
|
|
130
|
+
"macrocall_expression",
|
|
131
|
+
// generated, not declared here
|
|
132
|
+
"quote_expression",
|
|
133
|
+
"quote_statement"
|
|
134
|
+
]);
|
|
135
|
+
var LOCAL_BINDING_SCOPE = /* @__PURE__ */ new Set([
|
|
136
|
+
"function_definition",
|
|
137
|
+
"macro_definition",
|
|
138
|
+
"do_clause",
|
|
139
|
+
"let_statement",
|
|
140
|
+
"arrow_function_expression"
|
|
141
|
+
]);
|
|
142
|
+
var RealJuliaAnalyzer = class {
|
|
143
|
+
/**
|
|
144
|
+
* Call sites under an `@ccall`, recorded when the macro is visited and consulted when the
|
|
145
|
+
* nested `call_expression` is emitted. Instance state rather than a local because the macro
|
|
146
|
+
* case and the call case are different methods, and CLEARED at the top of every `analyze()` so
|
|
147
|
+
* a reused instance cannot carry one file's C symbols into the next file's graph — which would
|
|
148
|
+
* mark an unrelated call as foreign and is precisely the kind of cross-file bleed a shared
|
|
149
|
+
* analyzer instance invites.
|
|
150
|
+
*/
|
|
151
|
+
foreignMarked = /* @__PURE__ */ new Set();
|
|
152
|
+
/** Set while the walk is inside a `macro_definition`, whose body is QUOTED code. */
|
|
153
|
+
inMacroBody = false;
|
|
154
|
+
/**
|
|
155
|
+
* `call_expression` nodes that are the LEFT-HAND SIDE of a short-form declaration, i.e.
|
|
156
|
+
* `f(x) = …` rather than an invocation. Julia writes a function definition with the same node
|
|
157
|
+
* type as a call, so without this the declaration reads as the function calling itself.
|
|
158
|
+
* Cleared per `analyze()` for the same reason as `foreignMarked`.
|
|
159
|
+
*/
|
|
160
|
+
declSites = /* @__PURE__ */ new Set();
|
|
161
|
+
/**
|
|
162
|
+
* `type_head` nodes belonging to a type DECLARATION, so a type is not recorded as referencing
|
|
163
|
+
* itself. Cleared per `analyze()` like the other per-file sets.
|
|
164
|
+
*/
|
|
165
|
+
typeHeadSites = /* @__PURE__ */ new Set();
|
|
166
|
+
/**
|
|
167
|
+
* Declaration nodes that REPLACE an earlier declaration of the same method, keyed by node id.
|
|
168
|
+
*
|
|
169
|
+
* COLLECTED IN A PRE-PASS for the same reason `declaredTypes` is: the question "has this exact
|
|
170
|
+
* method already been defined in this file?" is only answerable once the whole file has been
|
|
171
|
+
* seen, and answering it during a single walk would classify the FIRST declaration as the
|
|
172
|
+
* override and leave the second — the one that actually replaces it — unrecorded.
|
|
173
|
+
*
|
|
174
|
+
* M60/julia `overrides`. The lane's file header declined this type on the grounds that julia has
|
|
175
|
+
* multiple dispatch rather than overriding, and the argument is sound for the case it names: two
|
|
176
|
+
* methods of the same name with DIFFERENT signatures are different methods, both live, and
|
|
177
|
+
* neither replaces the other. It does not cover the case julia actually has. Julia's method table
|
|
178
|
+
* holds exactly one entry per (name, signature) tuple, so a second declaration of the SAME pair
|
|
179
|
+
* REPLACES the first — julia itself says so, printing `WARNING: Method definition f(Int) in
|
|
180
|
+
* module Main at line 12 overwritten at line 40` under `--warn-overwrite=yes`. That is a
|
|
181
|
+
* replacement, and it is the only sense in which the language has one, which is the same
|
|
182
|
+
* criterion the bash lane's `overrides` ruling used to convict this very omission
|
|
183
|
+
* (`script/m14/ontology-coverage-gate.ts` header, BD8b).
|
|
184
|
+
*
|
|
185
|
+
* MEASURED: filled in from `validation/m14/fixtures/julia/census.ts` over the admitted corpus —
|
|
186
|
+
* see `docs/analyzers/julia.md` for the counts and the probe that produces them.
|
|
187
|
+
*/
|
|
188
|
+
redefinedAt = /* @__PURE__ */ new Map();
|
|
189
|
+
/**
|
|
190
|
+
* The kind of every symbol this file declares, by name. Populated at each `symbols.push`, which is
|
|
191
|
+
* the single point every declaration in this analyzer goes through, so the table cannot fall behind
|
|
192
|
+
* the symbol list. Cleared per `analyze()` like the other per-file state.
|
|
193
|
+
*
|
|
194
|
+
* It exists for one question: does this file declare `owner` as a module or a type, and `target` as
|
|
195
|
+
* a method? See `ownsAMethodTable`.
|
|
196
|
+
*/
|
|
197
|
+
declaredSymbolKinds = /* @__PURE__ */ new Map();
|
|
198
|
+
/**
|
|
199
|
+
* THE FOURTH PARAMETER IS NEW, AND ITS ABSENCE WAS A WIRING GAP OF EXACTLY THE KIND THIS PROGRAMME
|
|
200
|
+
* HAS NOW FOUND THREE TIMES: `engine.ts` passes an `AnalyzeContext` to every analyzer, python's once
|
|
201
|
+
* stopped at three parameters so the companion "never reached it at all", and this analyzer did the
|
|
202
|
+
* same — the context was available at every call site and dropped here. It is ADDITIVE: a caller that
|
|
203
|
+
* passes nothing gets the previous behaviour byte for byte, which is what the five fixtures assert.
|
|
204
|
+
*/
|
|
205
|
+
analyze(rootNode, content, filePath, context) {
|
|
206
|
+
this.inMacroBody = false;
|
|
207
|
+
this.foreignMarked.clear();
|
|
208
|
+
this.declSites.clear();
|
|
209
|
+
this.typeHeadSites.clear();
|
|
210
|
+
this.redefinedAt.clear();
|
|
211
|
+
this.declaredSymbolKinds.clear();
|
|
212
|
+
const symbols = [];
|
|
213
|
+
const pushSym = (s) => {
|
|
214
|
+
if (s.ownerName && !s.qualifiedName) s.qualifiedName = `${s.ownerName}.${s.name}`;
|
|
215
|
+
else if (!s.ownerName && !fileDeclaresModule && packageName) s.qualifiedName = `${packageName}.${s.name}`;
|
|
216
|
+
symbols.push(s);
|
|
217
|
+
this.declaredSymbolKinds.set(s.name, s.kind);
|
|
218
|
+
};
|
|
219
|
+
const memberKey = (owner, name) => owner ? `${owner}.${name}` : name;
|
|
220
|
+
const relationships = [];
|
|
221
|
+
const imports = [];
|
|
222
|
+
const exports = [];
|
|
223
|
+
const errors = [];
|
|
224
|
+
const frameworks = /* @__PURE__ */ new Set();
|
|
225
|
+
const declaredTypes = /* @__PURE__ */ new Set();
|
|
226
|
+
const declaredModules = /* @__PURE__ */ new Set();
|
|
227
|
+
{
|
|
228
|
+
const collect = (n) => {
|
|
229
|
+
if (TYPE_DECLARATIONS.has(n.type)) {
|
|
230
|
+
const head = this.firstNamedChildOfType(n, "type_head");
|
|
231
|
+
const name = head ? this.typeHeadName(head) : "";
|
|
232
|
+
if (name) declaredTypes.add(name);
|
|
233
|
+
}
|
|
234
|
+
if (n.type === "module_definition") {
|
|
235
|
+
const nm = n.childForFieldName("name");
|
|
236
|
+
const m = nm ? nm.text.trim() : "";
|
|
237
|
+
if (m) declaredModules.add(m);
|
|
238
|
+
}
|
|
239
|
+
for (const c of this.namedChildren(n)) collect(c);
|
|
240
|
+
};
|
|
241
|
+
collect(rootNode);
|
|
242
|
+
}
|
|
243
|
+
let fileDeclaresModule = false;
|
|
244
|
+
{
|
|
245
|
+
const scan = (n) => {
|
|
246
|
+
if (n.type === "module_definition") fileDeclaresModule = true;
|
|
247
|
+
for (const c of this.namedChildren(n)) if (!fileDeclaresModule) scan(c);
|
|
248
|
+
};
|
|
249
|
+
scan(rootNode);
|
|
250
|
+
}
|
|
251
|
+
const packageName = juliaPackageName(context?.projectRoot);
|
|
252
|
+
const bindingsByScope = /* @__PURE__ */ new Map();
|
|
253
|
+
{
|
|
254
|
+
const bindName = (b) => {
|
|
255
|
+
if (!b) return "";
|
|
256
|
+
if (b.type === "identifier") return b.text.trim();
|
|
257
|
+
if (b.type === "typed_expression" || b.type === "unary_typed_expression") {
|
|
258
|
+
const ks = this.namedChildren(b);
|
|
259
|
+
if (ks.length < 2) return "";
|
|
260
|
+
return bindName(ks[0]);
|
|
261
|
+
}
|
|
262
|
+
if (b.type === "splat_expression" || b.type === "named_argument") {
|
|
263
|
+
const inner = this.pos(b, 0);
|
|
264
|
+
return inner ? bindName(inner) : "";
|
|
265
|
+
}
|
|
266
|
+
if (b.type === "juxtaposition_expression" || b.type === "parenthesized_expression") {
|
|
267
|
+
const inner = this.pos(b, 0);
|
|
268
|
+
return inner ? bindName(inner) : "";
|
|
269
|
+
}
|
|
270
|
+
return "";
|
|
271
|
+
};
|
|
272
|
+
const add = (scope, n) => {
|
|
273
|
+
if (!scope || !n) return;
|
|
274
|
+
const set = bindingsByScope.get(scope) ?? /* @__PURE__ */ new Set();
|
|
275
|
+
set.add(n);
|
|
276
|
+
bindingsByScope.set(scope, set);
|
|
277
|
+
};
|
|
278
|
+
const addParams = (call, scope) => {
|
|
279
|
+
if (!call || !scope) return;
|
|
280
|
+
const args = this.namedChildren(call).find((c) => c.type === "argument_list");
|
|
281
|
+
if (!args) return;
|
|
282
|
+
for (const a of this.namedChildren(args)) add(scope, bindName(a));
|
|
283
|
+
};
|
|
284
|
+
const scopeOf = (n) => {
|
|
285
|
+
if (n.type === "module_definition") {
|
|
286
|
+
const nn = n.childForFieldName("name");
|
|
287
|
+
return nn ? nn.text.trim() : "";
|
|
288
|
+
}
|
|
289
|
+
if (n.type === "struct_definition" || n.type === "abstract_definition" || n.type === "primitive_definition") {
|
|
290
|
+
const head = this.firstNamedChildOfType(n, "type_head");
|
|
291
|
+
return head ? this.typeHeadName(head) : "";
|
|
292
|
+
}
|
|
293
|
+
if (n.type === "function_definition" || n.type === "macro_definition") {
|
|
294
|
+
const sig = this.firstNamedChildOfType(n, "signature");
|
|
295
|
+
return sig ? this.signatureName(sig) : "";
|
|
296
|
+
}
|
|
297
|
+
if (n.type === "assignment") {
|
|
298
|
+
const lhs = this.pos(n, 0);
|
|
299
|
+
if (!lhs) return "";
|
|
300
|
+
const u = this.unwrapWhere(lhs);
|
|
301
|
+
return u.type === "call_expression" ? this.calleeName(u) ?? "" : "";
|
|
302
|
+
}
|
|
303
|
+
return "";
|
|
304
|
+
};
|
|
305
|
+
const collect = (n, scope) => {
|
|
306
|
+
const inner = scopeOf(n) || scope;
|
|
307
|
+
if (n.type === "signature") {
|
|
308
|
+
let cur = n;
|
|
309
|
+
for (let i = 0; i < 4 && cur && cur.type !== "call_expression"; i++) {
|
|
310
|
+
cur = this.namedChildren(cur).find(
|
|
311
|
+
(c) => c.type === "call_expression" || c.type === "where_expression" || c.type === "parenthesized_expression"
|
|
312
|
+
) ?? null;
|
|
313
|
+
}
|
|
314
|
+
addParams(cur && cur.type === "call_expression" ? cur : null, inner);
|
|
315
|
+
}
|
|
316
|
+
if (n.type === "assignment") {
|
|
317
|
+
const lhs = this.pos(n, 0);
|
|
318
|
+
if (lhs) {
|
|
319
|
+
const u = this.unwrapWhere(lhs);
|
|
320
|
+
addParams(u.type === "call_expression" ? u : null, inner);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const c of this.namedChildren(n)) collect(c, inner);
|
|
324
|
+
};
|
|
325
|
+
collect(rootNode, "");
|
|
326
|
+
}
|
|
327
|
+
{
|
|
328
|
+
const firstDecl = /* @__PURE__ */ new Map();
|
|
329
|
+
const scan = (n, scope, scopeAt, localDepth) => {
|
|
330
|
+
let childScope = scope;
|
|
331
|
+
let childScopeAt = scopeAt;
|
|
332
|
+
let childLocal = localDepth;
|
|
333
|
+
if (TYPE_DECLARATIONS.has(n.type)) {
|
|
334
|
+
const head = this.firstNamedChildOfType(n, "type_head");
|
|
335
|
+
const name = head ? this.typeHeadName(head) : "";
|
|
336
|
+
if (name) {
|
|
337
|
+
childScope = name;
|
|
338
|
+
childScopeAt = n.startPosition.row + 1;
|
|
339
|
+
}
|
|
340
|
+
} else if (n.type === "module_definition") {
|
|
341
|
+
const nameNode = n.childForFieldName("name");
|
|
342
|
+
const name = nameNode ? nameNode.text.trim() : "";
|
|
343
|
+
if (name) {
|
|
344
|
+
childScope = name;
|
|
345
|
+
childScopeAt = n.startPosition.row + 1;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (LOCAL_BINDING_SCOPE.has(n.type)) childLocal = localDepth + 1;
|
|
349
|
+
const site = this.declarationSiteOf(n);
|
|
350
|
+
if (site && site.name && site.signature && localDepth === 0) {
|
|
351
|
+
const key = `${childScope}@${childScopeAt}|${site.name}|${site.signature}`;
|
|
352
|
+
const prior = firstDecl.get(key);
|
|
353
|
+
const safe = this.isUnconditionalPosition(n);
|
|
354
|
+
if (prior && prior.safe && safe) {
|
|
355
|
+
this.redefinedAt.set(n.id, { name: site.name, replacesLine: prior.line, signature: site.signature, scope: childScope });
|
|
356
|
+
}
|
|
357
|
+
if (!prior) firstDecl.set(key, { line: n.startPosition.row + 1, safe });
|
|
358
|
+
}
|
|
359
|
+
for (const c of this.namedChildren(n)) scan(c, childScope, childScopeAt, childLocal);
|
|
360
|
+
};
|
|
361
|
+
scan(rootNode, "", 0, 0);
|
|
362
|
+
}
|
|
363
|
+
let activeScope = "";
|
|
364
|
+
let containerScope = true;
|
|
365
|
+
let depth = 0;
|
|
366
|
+
const walkStack = [{ node: rootNode, activeScope: "", containerScope: true, inMacroBody: false, isExit: false }];
|
|
367
|
+
while (walkStack.length > 0) {
|
|
368
|
+
const frame = walkStack.pop();
|
|
369
|
+
const node = frame.node;
|
|
370
|
+
if (frame.isExit) {
|
|
371
|
+
depth--;
|
|
372
|
+
activeScope = frame.activeScope;
|
|
373
|
+
containerScope = frame.containerScope;
|
|
374
|
+
this.inMacroBody = frame.inMacroBody;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const scope = frame.activeScope;
|
|
378
|
+
containerScope = frame.containerScope;
|
|
379
|
+
const parentContainerScope = containerScope;
|
|
380
|
+
const parentInMacroBody = this.inMacroBody;
|
|
381
|
+
switch (node.type) {
|
|
382
|
+
// ---------------------------------------------------------------- modules
|
|
383
|
+
case "module_definition": {
|
|
384
|
+
const nameNode = node.childForFieldName("name");
|
|
385
|
+
const name = nameNode ? this.nameOf(nameNode) : "";
|
|
386
|
+
if (name) {
|
|
387
|
+
pushSym({
|
|
388
|
+
name,
|
|
389
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
390
|
+
range: this.range(node),
|
|
391
|
+
complexity: this.complexity(node),
|
|
392
|
+
ownerName: scope || void 0,
|
|
393
|
+
metadata: { juliaSpecific: true, isBareModule: this.firstNamedChildType(node) === "baremodule" }
|
|
394
|
+
});
|
|
395
|
+
if (scope && containerScope) relationships.push(this.rel(scope, ASTEngine.RelationshipType.Contains, memberKey(scope, name), node, 1));
|
|
396
|
+
activeScope = name;
|
|
397
|
+
containerScope = true;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
// ---------------------------------------------------------------- type declarations
|
|
402
|
+
case "abstract_definition":
|
|
403
|
+
case "primitive_definition":
|
|
404
|
+
case "struct_definition": {
|
|
405
|
+
const nameNode = this.firstNamedChildOfType(node, "type_head");
|
|
406
|
+
const headInner = nameNode ? this.pos(nameNode, 0) : null;
|
|
407
|
+
if (headInner && headInner.type === "interpolation_expression") break;
|
|
408
|
+
const name = nameNode ? this.typeHeadName(nameNode) : "";
|
|
409
|
+
if (!name) break;
|
|
410
|
+
const kind = node.type === "struct_definition" ? ASTEngine.SymbolKind.Class : ASTEngine.SymbolKind.Interface;
|
|
411
|
+
pushSym({
|
|
412
|
+
name,
|
|
413
|
+
kind,
|
|
414
|
+
range: this.range(node),
|
|
415
|
+
complexity: this.complexity(node),
|
|
416
|
+
ownerName: scope || void 0,
|
|
417
|
+
metadata: { juliaSpecific: true, declarationForm: node.type, isMutable: this.hasToken(node, "mutable") }
|
|
418
|
+
});
|
|
419
|
+
declaredTypes.add(name);
|
|
420
|
+
if (scope && containerScope) relationships.push(this.rel(scope, ASTEngine.RelationshipType.Contains, memberKey(scope, name), node, 1));
|
|
421
|
+
if (nameNode) this.typeHeadSites.add(nameNode.id);
|
|
422
|
+
activeScope = name;
|
|
423
|
+
containerScope = true;
|
|
424
|
+
for (const sup of this.supertypesOf(node)) {
|
|
425
|
+
if (sup === name) continue;
|
|
426
|
+
relationships.push(this.rel(name, ASTEngine.RelationshipType.Extends, sup, node, 1, { juliaSpecific: true, isSubtype: true }));
|
|
427
|
+
}
|
|
428
|
+
for (const field of this.structFields(node)) {
|
|
429
|
+
pushSym({
|
|
430
|
+
name: field.name,
|
|
431
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
432
|
+
range: this.range(field.node),
|
|
433
|
+
complexity: this.complexity(field.node),
|
|
434
|
+
ownerName: name,
|
|
435
|
+
metadata: { juliaSpecific: true, isStructField: true }
|
|
436
|
+
});
|
|
437
|
+
relationships.push(this.rel(name, ASTEngine.RelationshipType.Contains, memberKey(name, field.name), field.node, 1));
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
// ---------------------------------------------------------------- functions
|
|
442
|
+
case "function_definition":
|
|
443
|
+
case "macro_definition": {
|
|
444
|
+
const sig = this.firstNamedChildOfType(node, "signature");
|
|
445
|
+
const name = sig ? this.signatureName(sig) : "";
|
|
446
|
+
if (!name) break;
|
|
447
|
+
if (sig) {
|
|
448
|
+
let callable = this.pos(sig, 0);
|
|
449
|
+
for (let i = 0; i < 4 && callable && callable.type !== "call_expression"; i++) {
|
|
450
|
+
callable = this.pos(callable, 0);
|
|
451
|
+
}
|
|
452
|
+
if (callable && callable.type === "call_expression") {
|
|
453
|
+
this.declSites.add(callable.id);
|
|
454
|
+
const callee = this.pos(callable, 0);
|
|
455
|
+
this.typeHeadSites.add(callee ? callee.id : callable.id);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const isMacro = node.type === "macro_definition";
|
|
459
|
+
const wasInMacro = this.inMacroBody;
|
|
460
|
+
if (isMacro) this.inMacroBody = true;
|
|
461
|
+
pushSym({
|
|
462
|
+
name,
|
|
463
|
+
kind: isMacro ? ASTEngine.SymbolKind.Macro : ASTEngine.SymbolKind.Function,
|
|
464
|
+
range: this.range(node),
|
|
465
|
+
complexity: this.complexity(node),
|
|
466
|
+
ownerName: scope || void 0,
|
|
467
|
+
metadata: { juliaSpecific: true, isMacro, hasBlockBody: !!this.firstNamedChildOfType(node, "compound_statement") }
|
|
468
|
+
});
|
|
469
|
+
if (scope && containerScope) relationships.push(this.rel(scope, ASTEngine.RelationshipType.Contains, memberKey(scope, name), node, 1));
|
|
470
|
+
activeScope = name;
|
|
471
|
+
containerScope = false;
|
|
472
|
+
const ovr = this.overrideEdgeFor(node, scope);
|
|
473
|
+
if (ovr) relationships.push(ovr);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
// ---------------------------------------------------------------- SHORT-FORM function definition
|
|
477
|
+
//
|
|
478
|
+
// `f(x) = expr` and `A.B(x) = expr` are NOT `function_definition` nodes. The grammar
|
|
479
|
+
// gives `(assignment (call_expression (identifier) (argument_list …)) (operator) rhs)`, so
|
|
480
|
+
// Julia's single most common way of writing a function has no node of its own. Handling
|
|
481
|
+
// only `function … end` found 4 functions in a sample where the file has 7, and — worse —
|
|
482
|
+
// attributed every call INSIDE a short-form body to the enclosing module, because the scope
|
|
483
|
+
// never advanced. Julia's own stdlib writes this way far more often than the long form.
|
|
484
|
+
case "assignment": {
|
|
485
|
+
let lhs = this.unwrapWhere(this.pos(node, 0) ?? node);
|
|
486
|
+
if (lhs.type === "typed_expression") {
|
|
487
|
+
const inner = this.pos(lhs, 0);
|
|
488
|
+
if (inner && inner.type === "call_expression") lhs = inner;
|
|
489
|
+
}
|
|
490
|
+
if (lhs.type === "parametrized_type_expression") {
|
|
491
|
+
const wrapper = node.parent ? node.parent.type : "";
|
|
492
|
+
if (wrapper === "const_statement" || wrapper === "global_statement" || wrapper === "local_statement") break;
|
|
493
|
+
const base = this.pos(lhs, 0);
|
|
494
|
+
const alias = base && base.type === "identifier" ? base.text.trim() : "";
|
|
495
|
+
if (!alias || IMPLICIT_OR_CONSTANT.has(alias)) break;
|
|
496
|
+
if (!containerScope) break;
|
|
497
|
+
pushSym({
|
|
498
|
+
name: alias,
|
|
499
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
500
|
+
range: this.range(node),
|
|
501
|
+
complexity: this.complexity(node),
|
|
502
|
+
ownerName: scope || void 0,
|
|
503
|
+
metadata: { juliaSpecific: true, declarationForm: "parametric_alias" }
|
|
504
|
+
});
|
|
505
|
+
if (scope) relationships.push(this.rel(scope, ASTEngine.RelationshipType.Contains, memberKey(scope, alias), node, 1));
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
if (lhs.type !== "call_expression") {
|
|
509
|
+
const wrapper = node.parent ? node.parent.type : "";
|
|
510
|
+
if (wrapper === "const_statement" || wrapper === "global_statement" || wrapper === "local_statement") break;
|
|
511
|
+
if (!containerScope) break;
|
|
512
|
+
const binding = this.plainModuleBinding(node);
|
|
513
|
+
if (!binding || IMPLICIT_OR_CONSTANT.has(binding)) break;
|
|
514
|
+
pushSym({
|
|
515
|
+
name: binding,
|
|
516
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
517
|
+
range: this.range(node),
|
|
518
|
+
complexity: this.complexity(node),
|
|
519
|
+
ownerName: scope || void 0,
|
|
520
|
+
metadata: { juliaSpecific: true, declarationForm: "module_assignment" }
|
|
521
|
+
});
|
|
522
|
+
if (scope) relationships.push(this.rel(scope, ASTEngine.RelationshipType.Contains, memberKey(scope, binding), node, 1));
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
const name = this.calleeName(lhs);
|
|
526
|
+
if (!name || IMPLICIT_OR_CONSTANT.has(name)) break;
|
|
527
|
+
pushSym({
|
|
528
|
+
name,
|
|
529
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
530
|
+
range: this.range(node),
|
|
531
|
+
complexity: this.complexity(node),
|
|
532
|
+
ownerName: scope || void 0,
|
|
533
|
+
metadata: { juliaSpecific: true, declarationForm: "short_form_assignment", hasBlockBody: false }
|
|
534
|
+
});
|
|
535
|
+
this.declSites.add(lhs.id);
|
|
536
|
+
if (scope && containerScope) relationships.push(this.rel(scope, ASTEngine.RelationshipType.Contains, memberKey(scope, name), node, 1));
|
|
537
|
+
const ovr = this.overrideEdgeFor(node, scope);
|
|
538
|
+
if (ovr) relationships.push(ovr);
|
|
539
|
+
activeScope = name;
|
|
540
|
+
containerScope = false;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
// ---------------------------------------------------------------- const / global / local
|
|
544
|
+
case "const_statement":
|
|
545
|
+
case "global_statement":
|
|
546
|
+
case "local_statement": {
|
|
547
|
+
const assign = this.firstNamedChildOfType(node, "assignment");
|
|
548
|
+
if (!assign) break;
|
|
549
|
+
const lhs = this.pos(assign, 0);
|
|
550
|
+
if (!lhs || lhs.type === "call_expression") break;
|
|
551
|
+
const name = this.nameOf(lhs);
|
|
552
|
+
if (!name || IMPLICIT_OR_CONSTANT.has(name)) break;
|
|
553
|
+
const isGlobal = node.type === "global_statement" || this.hasToken(node, "global");
|
|
554
|
+
const owner = isGlobal ? this.enclosingModuleName(node) : scope;
|
|
555
|
+
if (this.inMacroBody && node.type !== "const_statement") break;
|
|
556
|
+
pushSym({
|
|
557
|
+
name,
|
|
558
|
+
kind: node.type === "const_statement" ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable,
|
|
559
|
+
range: this.range(node),
|
|
560
|
+
complexity: this.complexity(node),
|
|
561
|
+
ownerName: owner || void 0,
|
|
562
|
+
metadata: { juliaSpecific: true, binding: node.type.replace("_statement", "") }
|
|
563
|
+
});
|
|
564
|
+
if (owner && (isGlobal || containerScope)) relationships.push(this.rel(owner, ASTEngine.RelationshipType.Contains, memberKey(owner, name), node, 1));
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
// ---------------------------------------------------------------- imports
|
|
568
|
+
case "import_statement":
|
|
569
|
+
case "using_statement": {
|
|
570
|
+
for (const t of this.importTargets(node)) {
|
|
571
|
+
imports.push({
|
|
572
|
+
module: t.name,
|
|
573
|
+
items: t.items,
|
|
574
|
+
importType: node.type === "import_statement" ? "import" : "using",
|
|
575
|
+
lineNumber: node.startPosition.row + 1,
|
|
576
|
+
isExternal: !t.name.startsWith(".")
|
|
577
|
+
});
|
|
578
|
+
relationships.push(this.rel("root", ASTEngine.RelationshipType.References, t.name, node, 1, { juliaSpecific: true, isImport: true, importForm: node.type }));
|
|
579
|
+
relationships.push(
|
|
580
|
+
this.rel(scope || "root", ASTEngine.RelationshipType.Imports, t.name, node, 1, {
|
|
581
|
+
juliaSpecific: true,
|
|
582
|
+
importForm: node.type,
|
|
583
|
+
isExternal: !t.name.startsWith("."),
|
|
584
|
+
importRenamed: t.renamed === true
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case "export_statement":
|
|
591
|
+
case "public_statement": {
|
|
592
|
+
if (this.namedChildren(node)[0]?.type === "ERROR") break;
|
|
593
|
+
for (const child of this.namedChildren(node)) {
|
|
594
|
+
const name = this.exportableName(child);
|
|
595
|
+
if (!name) continue;
|
|
596
|
+
exports.push({
|
|
597
|
+
name,
|
|
598
|
+
// The kind is deliberately NOT asserted. `export X` exports a BINDING whose kind
|
|
599
|
+
// depends on what X turns out to be — a struct in one file, a function in another —
|
|
600
|
+
// and Julia allows the export to precede the declaration. The first version pushed a
|
|
601
|
+
// `SymbolKind.Function` for every `export`, which produced a SECOND `Circle` beside
|
|
602
|
+
// the real `class:Circle`, and the dedup did not merge them because its key is
|
|
603
|
+
// `kind:owner:name` and the kinds differed. So `export` records an export and marks
|
|
604
|
+
// nothing: the declaration is the declaration.
|
|
605
|
+
type: node.type === "export_statement" ? "value" : "type",
|
|
606
|
+
lineNumber: node.startPosition.row + 1,
|
|
607
|
+
metadata: { juliaSpecific: true, exportForm: node.type, declaresNothing: true }
|
|
608
|
+
});
|
|
609
|
+
for (const sym of symbols) {
|
|
610
|
+
if (sym.name !== name) continue;
|
|
611
|
+
sym.metadata = { ...sym.metadata ?? {}, isExported: true, exportForm: node.type };
|
|
612
|
+
}
|
|
613
|
+
relationships.push(
|
|
614
|
+
this.rel(scope || "root", ASTEngine.RelationshipType.Exports, name, node, 1, {
|
|
615
|
+
juliaSpecific: true,
|
|
616
|
+
exportForm: node.type,
|
|
617
|
+
declaresNothing: true
|
|
618
|
+
})
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
// ---------------------------------------------------------------- two call shapes with no `call_expression`
|
|
624
|
+
//
|
|
625
|
+
// Both were INVISIBLE: the walk recursed into them, found nothing to declare, and returned no
|
|
626
|
+
// edge, so a real invocation in Julia's two most idiomatic forms simply did not appear in the
|
|
627
|
+
// graph. Found by classifying every node type the analyzer names against every node type the
|
|
628
|
+
// grammar produces — 53 produced types the analyzer never mentions, and these two are calls.
|
|
629
|
+
//
|
|
630
|
+
// MEASURED on the corpus: `adjoint_expression` occurs in 22 files and
|
|
631
|
+
// `broadcast_call_expression` in 74.
|
|
632
|
+
case "adjoint_expression": {
|
|
633
|
+
if (this.namedChildren(node).length === 1) {
|
|
634
|
+
relationships.push(this.rel(
|
|
635
|
+
scope || GLOBAL_SCOPE,
|
|
636
|
+
ASTEngine.RelationshipType.Calls,
|
|
637
|
+
"adjoint",
|
|
638
|
+
node,
|
|
639
|
+
1,
|
|
640
|
+
{ juliaSpecific: true, isOperatorApplication: true, spelling: "postfix_prime" }
|
|
641
|
+
));
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case "broadcast_call_expression": {
|
|
646
|
+
const callee = this.calleeName(node);
|
|
647
|
+
if (callee && !IMPLICIT_OR_CONSTANT.has(callee)) {
|
|
648
|
+
relationships.push(this.rel(
|
|
649
|
+
scope || GLOBAL_SCOPE,
|
|
650
|
+
ASTEngine.RelationshipType.Calls,
|
|
651
|
+
callee,
|
|
652
|
+
node,
|
|
653
|
+
1,
|
|
654
|
+
{ juliaSpecific: true, isBroadcastCall: true }
|
|
655
|
+
));
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
// ---------------------------------------------------------------- calls and construction
|
|
660
|
+
case "call_expression": {
|
|
661
|
+
if (this.declSites.has(node.id)) break;
|
|
662
|
+
const callee = this.calleeName(node);
|
|
663
|
+
if (!callee || IMPLICIT_OR_CONSTANT.has(callee)) break;
|
|
664
|
+
if (callee === "include") {
|
|
665
|
+
const part = this.includePart(node);
|
|
666
|
+
if (part) {
|
|
667
|
+
relationships.push(this.rel(scope || GLOBAL_SCOPE, ASTEngine.RelationshipType.PartOf, part.target, node, 1, {
|
|
668
|
+
partRole: "library",
|
|
669
|
+
partPath: part.path,
|
|
670
|
+
partSymbol: part.target,
|
|
671
|
+
isJuliaInclude: true
|
|
672
|
+
}));
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
relationships.push(this.rel(scope || GLOBAL_SCOPE, ASTEngine.RelationshipType.Calls, callee, node, 1, {
|
|
676
|
+
juliaSpecific: true,
|
|
677
|
+
isInclude: true,
|
|
678
|
+
isDynamicInclude: true
|
|
679
|
+
}));
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
const dot = callee.indexOf(".");
|
|
683
|
+
const qualifier = dot > 0 ? callee.slice(0, dot) : "";
|
|
684
|
+
const ctorLeaf = dot > 0 ? callee.slice(callee.lastIndexOf(".") + 1) : callee;
|
|
685
|
+
const sameFileCtor = dot > 0 && declaredModules.has(qualifier) && declaredTypes.has(ctorLeaf);
|
|
686
|
+
const isCtor = declaredTypes.has(callee) || sameFileCtor || TYPE_DECLARATIONS.has(callee);
|
|
687
|
+
const foreign = callee === "ccall" ? this.ccallTarget(node) : null;
|
|
688
|
+
const meta = isCtor ? { juliaSpecific: true, isTypeConstruction: true } : foreign ? { juliaSpecific: true, isForeignCall: true, foreignSymbol: foreign.symbol, foreignLibrary: foreign.library, via: "ccall" } : this.foreignMarked.has(this.textKey(node, callee)) ? { juliaSpecific: true, isForeignCall: true, via: "@ccall" } : { juliaSpecific: true };
|
|
689
|
+
{
|
|
690
|
+
relationships.push(this.rel(
|
|
691
|
+
scope || GLOBAL_SCOPE,
|
|
692
|
+
isCtor ? ASTEngine.RelationshipType.Instantiates : ASTEngine.RelationshipType.Calls,
|
|
693
|
+
sameFileCtor ? ctorLeaf : callee,
|
|
694
|
+
node,
|
|
695
|
+
isCtor ? 0.9 : 1,
|
|
696
|
+
meta
|
|
697
|
+
));
|
|
698
|
+
}
|
|
699
|
+
if (foreign) {
|
|
700
|
+
relationships.push(this.rel(
|
|
701
|
+
scope || GLOBAL_SCOPE,
|
|
702
|
+
ASTEngine.RelationshipType.Calls,
|
|
703
|
+
foreign.symbol,
|
|
704
|
+
node,
|
|
705
|
+
1,
|
|
706
|
+
{
|
|
707
|
+
juliaSpecific: true,
|
|
708
|
+
isForeignCall: true,
|
|
709
|
+
foreignSymbol: foreign.symbol,
|
|
710
|
+
foreignLibrary: foreign.library,
|
|
711
|
+
via: "ccall"
|
|
712
|
+
}
|
|
713
|
+
));
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
// ---------------------------------------------------------------- macro calls
|
|
718
|
+
case "macrocall_expression": {
|
|
719
|
+
const nameNode = this.firstNamedChildOfType(node, "macro_identifier");
|
|
720
|
+
const name = nameNode ? macroNameOf(nameNode.text) : "";
|
|
721
|
+
if (name === "ccall") this.markForeignCallees(node);
|
|
722
|
+
if (!name) break;
|
|
723
|
+
const decorated = this.decoratedDeclarations(node);
|
|
724
|
+
if (decorated.length > 0) {
|
|
725
|
+
for (const d of decorated) {
|
|
726
|
+
relationships.push(this.rel(d.name, ASTEngine.RelationshipType.Decorates, name, node, 1, {
|
|
727
|
+
decoratorName: name,
|
|
728
|
+
decoratedKind: d.kind,
|
|
729
|
+
isMacroDecorator: true
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
relationships.push(this.rel(scope || GLOBAL_SCOPE, ASTEngine.RelationshipType.Calls, name, node, 1, { juliaSpecific: true, isMacroCall: true }));
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
// ---------------------------------------------------------------- type annotations
|
|
738
|
+
case "unary_typed_expression":
|
|
739
|
+
case "typed_expression": {
|
|
740
|
+
const typedKids = this.namedChildren(node);
|
|
741
|
+
const anonymous = node.type === "unary_typed_expression" || node.text.trimStart().startsWith("::");
|
|
742
|
+
const typeNode = anonymous ? typedKids[0] : typedKids.length >= 2 ? typedKids[1] : typedKids[0];
|
|
743
|
+
const typeName = typeNode ? this.typeRefName(typeNode) : "";
|
|
744
|
+
if (typeName) relationships.push(this.rel(scope || GLOBAL_SCOPE, ASTEngine.RelationshipType.References, typeName, node, 1, { juliaSpecific: true, isTypeReference: true }));
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "parametrized_type_expression": {
|
|
748
|
+
for (const inner of this.namedChildren(node)) {
|
|
749
|
+
if (inner.type !== "curly_expression") continue;
|
|
750
|
+
for (const b of this.namedChildren(inner)) {
|
|
751
|
+
for (const link of this.subtypeChainLinks(b)) {
|
|
752
|
+
relationships.push(this.rel(
|
|
753
|
+
link.from,
|
|
754
|
+
ASTEngine.RelationshipType.Extends,
|
|
755
|
+
link.to,
|
|
756
|
+
link.at,
|
|
757
|
+
1,
|
|
758
|
+
{ juliaSpecific: true, isSubtype: true, isWhereBound: true }
|
|
759
|
+
));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (this.withinTypeHead(node)) break;
|
|
764
|
+
const baseNode = this.pos(node, 0);
|
|
765
|
+
const base = baseNode ? this.fieldPath(baseNode) : "";
|
|
766
|
+
if (base) relationships.push(this.rel(scope || GLOBAL_SCOPE, ASTEngine.RelationshipType.References, base, node, 1, { juliaSpecific: true, isTypeReference: true }));
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "where_expression": {
|
|
770
|
+
const whereKids = this.namedChildren(node);
|
|
771
|
+
const lastWhereKid = whereKids[whereKids.length - 1];
|
|
772
|
+
for (const child of whereKids) {
|
|
773
|
+
if (child.type === "curly_expression") {
|
|
774
|
+
for (const inner of this.namedChildren(child)) {
|
|
775
|
+
for (const link of this.subtypeChainLinks(inner)) {
|
|
776
|
+
relationships.push(this.rel(link.from, ASTEngine.RelationshipType.Extends, link.to, link.at, 1, { juliaSpecific: true, isSubtype: true, isWhereBound: true }));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (child.type !== "binary_expression") continue;
|
|
782
|
+
if (child !== lastWhereKid && node.parent?.type !== "curly_expression") continue;
|
|
783
|
+
for (const link of this.subtypeChainLinks(child)) {
|
|
784
|
+
relationships.push(this.rel(link.from, ASTEngine.RelationshipType.Extends, link.to, link.at, 1, { juliaSpecific: true, isSubtype: true, isWhereBound: true }));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
// ---------------------------------------------------------------- field access
|
|
790
|
+
case "field_expression": {
|
|
791
|
+
if (node.parent?.type === "field_expression") break;
|
|
792
|
+
const value = node.childForFieldName("value");
|
|
793
|
+
if (!value) break;
|
|
794
|
+
if (value.type === "field_expression") {
|
|
795
|
+
const path2 = this.fieldPath(node);
|
|
796
|
+
if (path2) {
|
|
797
|
+
relationships.push(this.rel(
|
|
798
|
+
scope || GLOBAL_SCOPE,
|
|
799
|
+
ASTEngine.RelationshipType.References,
|
|
800
|
+
path2,
|
|
801
|
+
node,
|
|
802
|
+
0.95,
|
|
803
|
+
{ juliaSpecific: true, isQualifiedReference: true }
|
|
804
|
+
));
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
const member = this.namedChildren(node).find((c) => c.type === "identifier" && c.id !== value.id) ?? this.namedChildren(node).find((c) => c.type === "identifier" && c.id !== value.id) ?? this.namedChildren(node).find((c) => c.id !== value.id && (c.type === "quote_expression" || c.type === "operator"));
|
|
809
|
+
if (!member) break;
|
|
810
|
+
const memberName = member.type === "quote_expression" ? this.quotedName(member) : member.text.trim();
|
|
811
|
+
if (!memberName) break;
|
|
812
|
+
relationships.push(this.rel(
|
|
813
|
+
scope || GLOBAL_SCOPE,
|
|
814
|
+
ASTEngine.RelationshipType.Uses,
|
|
815
|
+
memberName,
|
|
816
|
+
node,
|
|
817
|
+
0.9,
|
|
818
|
+
{ juliaSpecific: true, isFieldRead: true, isOperatorMember: member.type !== "identifier" }
|
|
819
|
+
));
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
walkStack.push({ node, activeScope: scope, containerScope: parentContainerScope, inMacroBody: parentInMacroBody, isExit: true });
|
|
824
|
+
const childScope = activeScope;
|
|
825
|
+
let opaqueArgs = null;
|
|
826
|
+
const opaqueArgsOf = (parent) => {
|
|
827
|
+
const set = /* @__PURE__ */ new Set();
|
|
828
|
+
for (const c of this.namedChildren(parent)) {
|
|
829
|
+
if (c.type === "macro_identifier" || c.type === "macro_argument_list") continue;
|
|
830
|
+
if (!this.directDeclarationArg(c)) set.add(c.id);
|
|
831
|
+
}
|
|
832
|
+
return set;
|
|
833
|
+
};
|
|
834
|
+
if (node.type === "macrocall_expression" && !this.macroIsScopeTransparent(node)) {
|
|
835
|
+
opaqueArgs = opaqueArgsOf(node);
|
|
836
|
+
}
|
|
837
|
+
if (node.type === "macro_argument_list" && node.parent && node.parent.type === "macrocall_expression" && !this.macroIsScopeTransparent(node.parent)) {
|
|
838
|
+
opaqueArgs = opaqueArgsOf(node);
|
|
839
|
+
}
|
|
840
|
+
depth++;
|
|
841
|
+
const fieldTypeChild = node.type === "typed_expression" && node.parent && node.parent.type === "struct_definition" ? this.pos(node, 1) : null;
|
|
842
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
843
|
+
const c = node.namedChild(i);
|
|
844
|
+
if (!c) continue;
|
|
845
|
+
const typePositionChild = (node.type === "struct_definition" || node.type === "abstract_definition" || node.type === "primitive_definition") && c.type === "type_head" || (fieldTypeChild ? c.id === fieldTypeChild.id : false);
|
|
846
|
+
walkStack.push({ node: c, activeScope: childScope, containerScope: typePositionChild || opaqueArgs?.has(c.id) ? false : containerScope, inMacroBody: this.inMacroBody, isExit: false });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const symbolMap = /* @__PURE__ */ new Map();
|
|
850
|
+
for (const s of symbols) symbolMap.set(`${s.kind}:${s.ownerName || ""}:${s.name}`, s);
|
|
851
|
+
const uniqueSymbols = [...symbolMap.values()];
|
|
852
|
+
const declsByName = /* @__PURE__ */ new Map();
|
|
853
|
+
for (const s of uniqueSymbols) {
|
|
854
|
+
const list = declsByName.get(s.name) ?? [];
|
|
855
|
+
list.push(s);
|
|
856
|
+
declsByName.set(s.name, list);
|
|
857
|
+
}
|
|
858
|
+
if (bindingsByScope.size > 0) {
|
|
859
|
+
for (const r of relationships) {
|
|
860
|
+
if (r.type !== ASTEngine.RelationshipType.Calls && r.type !== ASTEngine.RelationshipType.References) continue;
|
|
861
|
+
const tgt = String(r.target ?? "");
|
|
862
|
+
if (!tgt || tgt.includes(".") || tgt.includes(BINDING_SEP)) continue;
|
|
863
|
+
if (!bindingsByScope.get(String(r.source ?? ""))?.has(tgt)) continue;
|
|
864
|
+
r.target = `${r.source}${BINDING_SEP}${tgt}`;
|
|
865
|
+
r.metadata = { ...r.metadata ?? {}, localBinding: true };
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
for (const r of relationships) {
|
|
869
|
+
if (r.type !== ASTEngine.RelationshipType.References) continue;
|
|
870
|
+
const tgt = String(r.target ?? "");
|
|
871
|
+
if (!tgt || tgt.includes(".")) continue;
|
|
872
|
+
const decls = declsByName.get(tgt);
|
|
873
|
+
if (!decls || decls.length !== 1) continue;
|
|
874
|
+
const q = decls[0].qualifiedName;
|
|
875
|
+
if (typeof q === "string" && q) r.target = q;
|
|
876
|
+
}
|
|
877
|
+
const relMap = /* @__PURE__ */ new Map();
|
|
878
|
+
for (const r of relationships) {
|
|
879
|
+
const key = `${r.type}|${r.source}->${r.target}`;
|
|
880
|
+
const prev = relMap.get(key);
|
|
881
|
+
if (prev) {
|
|
882
|
+
if (prev.line_number === void 0 && r.line_number !== void 0) prev.line_number = r.line_number;
|
|
883
|
+
} else relMap.set(key, r);
|
|
884
|
+
}
|
|
885
|
+
const uniqueRelationships = [...relMap.values()];
|
|
886
|
+
this.classifyEvidenceTier(uniqueRelationships);
|
|
887
|
+
const includeOwners = new Set(
|
|
888
|
+
uniqueSymbols.filter((s) => s.name === "include" && s.kind !== ASTEngine.SymbolKind.Module).map((s) => s.ownerName ?? "")
|
|
889
|
+
);
|
|
890
|
+
if (includeOwners.size > 0) {
|
|
891
|
+
for (const r of uniqueRelationships) {
|
|
892
|
+
const md = r.metadata;
|
|
893
|
+
if (!md || md.isJuliaInclude !== true) continue;
|
|
894
|
+
const src = String(r.source ?? "");
|
|
895
|
+
const shadowed = [...includeOwners].some((owner) => owner === "" || src === owner || src.startsWith(owner + "."));
|
|
896
|
+
if (!shadowed) continue;
|
|
897
|
+
r.type = ASTEngine.RelationshipType.Calls;
|
|
898
|
+
r.target = "include";
|
|
899
|
+
r.metadata = { juliaSpecific: true, isInclude: true, isShadowedInclude: true };
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const declaredNames = new Set(uniqueSymbols.map((s) => s.name));
|
|
903
|
+
for (const r of uniqueRelationships) {
|
|
904
|
+
if (r.type !== ASTEngine.RelationshipType.Imports) continue;
|
|
905
|
+
const md = r.metadata;
|
|
906
|
+
if (!md || md.isExternal !== true) continue;
|
|
907
|
+
const tgt = String(r.target ?? "");
|
|
908
|
+
const dot = tgt.indexOf(".");
|
|
909
|
+
const dotted = dot > 0;
|
|
910
|
+
const root = dotted ? tgt.slice(0, dot) : tgt;
|
|
911
|
+
if (dotted || declaredNames.has(root)) md.isExternal = false;
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
language: "julia",
|
|
915
|
+
symbols: uniqueSymbols,
|
|
916
|
+
relationships: uniqueRelationships,
|
|
917
|
+
imports,
|
|
918
|
+
exports,
|
|
919
|
+
errors,
|
|
920
|
+
metadata: {
|
|
921
|
+
complexity: uniqueSymbols.length,
|
|
922
|
+
linesOfCode: content.split("\n").length,
|
|
923
|
+
declaredTypes: [...declaredTypes]
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
// ------------------------------------------------------------------ helpers
|
|
928
|
+
/**
|
|
929
|
+
* The declaration this node carries, as the pair julia's method table keys on: a NAME and a
|
|
930
|
+
* SIGNATURE. Returns null for a node that declares no method.
|
|
931
|
+
*
|
|
932
|
+
* The signature is the WHOLE argument list, normalised for whitespace, plus the `where` clause
|
|
933
|
+
* when the declaration has one — because `f(x) where {T} = …` and `f(x) where {S} = …` are two
|
|
934
|
+
* DIFFERENT methods and `f(x) = …` and `f(x, y) = …` are two different methods, while
|
|
935
|
+
* `f(x) = 1` and `f( x ) = 2` are the same one. Keying on the name alone would conflate every
|
|
936
|
+
* method of a generic function, and a generic function's methods are precisely the ones the
|
|
937
|
+
* `overrides` argument in this file's header says are NOT overrides.
|
|
938
|
+
*
|
|
939
|
+
* Both declaration FORMS are read, because julia has two and the corpus uses both heavily: the
|
|
940
|
+
* long `function f(x) … end` (whose `signature` holds the call) and the short `f(x) = …` (whose
|
|
941
|
+
* assignment's left-hand side is the call, possibly behind a `where`, a return-type annotation
|
|
942
|
+
* and parentheses — the same wrappers `unwrapWhere` already knows about).
|
|
943
|
+
*/
|
|
944
|
+
declarationSiteOf(node) {
|
|
945
|
+
if (node.type === "function_definition" || node.type === "macro_definition") {
|
|
946
|
+
const sig = this.firstNamedChildOfType(node, "signature");
|
|
947
|
+
if (!sig) return null;
|
|
948
|
+
const name2 = this.signatureName(sig);
|
|
949
|
+
return name2 ? { name: name2, signature: this.methodSignatureKey(sig) } : null;
|
|
950
|
+
}
|
|
951
|
+
if (node.type !== "assignment") return null;
|
|
952
|
+
const rawLhs = this.pos(node, 0) ?? node;
|
|
953
|
+
let lhs = this.unwrapWhere(rawLhs);
|
|
954
|
+
if (lhs.type === "typed_expression") {
|
|
955
|
+
const inner = this.pos(lhs, 0);
|
|
956
|
+
if (inner && inner.type === "call_expression") lhs = inner;
|
|
957
|
+
}
|
|
958
|
+
if (lhs.type !== "call_expression") return null;
|
|
959
|
+
const name = this.calleeName(lhs);
|
|
960
|
+
if (!name || IMPLICIT_OR_CONSTANT.has(name)) return null;
|
|
961
|
+
return { name, signature: this.methodSignatureKey(rawLhs, lhs) };
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* The `(name, signature)` half that distinguishes two methods of the same generic function.
|
|
965
|
+
*
|
|
966
|
+
* `root` is the node the declaration's text hangs off — a `signature` for the long form, the
|
|
967
|
+
* assignment's un-unwrapped left-hand side for the short one — and `call` is the callable inside
|
|
968
|
+
* it. Both are needed because they are not the same node: `where` wraps the call in one and the
|
|
969
|
+
* other has no `where` at all, so a single argument could only ever capture one of the two shapes.
|
|
970
|
+
*/
|
|
971
|
+
methodSignatureKey(root, call) {
|
|
972
|
+
const callNode = call ?? (root.type === "call_expression" ? root : this.namedChildren(root).find((c) => c.type === "call_expression"));
|
|
973
|
+
if (!callNode) return "";
|
|
974
|
+
const args = this.firstNamedChildOfType(callNode, "argument_list");
|
|
975
|
+
const whereNode = root.type === "where_expression" ? root : this.namedChildren(root).find((c) => c.type === "where_expression");
|
|
976
|
+
if (!args && !whereNode) return "";
|
|
977
|
+
return `${args ? collapseSpace(args.text) : ""}${whereNode ? `|where ${collapseSpace(whereNode.text)}` : ""}`;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Is this declaration reached unconditionally, with text julia itself wrote?
|
|
981
|
+
*
|
|
982
|
+
* Walks ANCESTORS rather than the parent, because the conditional node is not always adjacent:
|
|
983
|
+
* a declaration inside `else` sits under `else_clause` and `if_statement`, and a declaration
|
|
984
|
+
* inside `catch` sits under `catch_clause` and `try_statement`, so a parent-only test reads
|
|
985
|
+
* `try_statement` as unconditional. The whole list is what the test is — adding a construct to
|
|
986
|
+
* it makes the edge class narrower, which is the direction that errs toward refusing rather than
|
|
987
|
+
* toward asserting.
|
|
988
|
+
*
|
|
989
|
+
* `macrocall_expression` and the quote forms are in the set because their content is not julia text
|
|
990
|
+
* in this file: `@eval f(x) = …` and `quote f(x) = … end` are code GENERATED at expansion, and the
|
|
991
|
+
* tree shows the generated text, not what the file declares.
|
|
992
|
+
*
|
|
993
|
+
* `&&` and `||` are not in the set — they are `binary_expression` nodes, which are the most common
|
|
994
|
+
* node in julia and cannot be listed wholesale — so the short-circuit case is tested by OPERATOR.
|
|
995
|
+
* In `cond && function f() … end` the right operand runs only when `cond` holds, and a declaration
|
|
996
|
+
* there is as conditional as one inside an `if`. Only the RIGHT operand is conditional; the left
|
|
997
|
+
* one always runs, which is why the test looks at the second OPERAND rather than both.
|
|
998
|
+
*
|
|
999
|
+
* `&&` is the live operator and `||` is not: measured over the 971 admitted corpus files there are
|
|
1000
|
+
* 11,549 `&&` binary_expressions and 5,145 `||` ones, and of the `&&` set exactly ONE has a
|
|
1001
|
+
* declaration node as its right operand — `julia/JuliaSyntax/src/core/parse_stream.jl:316`,
|
|
1002
|
+
*
|
|
1003
|
+
* VERSION < v"1.11.0-DEV.753" && function ParseStream(io::IOBuffer; version=VERSION)
|
|
1004
|
+
*
|
|
1005
|
+
* which is a version-guarded method definition and is exactly the shape that must not publish an
|
|
1006
|
+
* edge. One is not zero, so this is a predicate the grammar can reach rather than a safety net
|
|
1007
|
+
* that never fires; `||` is left out because no `||` in the corpus has one, and a branch that
|
|
1008
|
+
* cannot match is what honesty contract §2's L2 audit exists to report.
|
|
1009
|
+
*
|
|
1010
|
+
* THE OPERAND INDEX IS NOT 1. The operator is a NAMED child of `binary_expression` in this
|
|
1011
|
+
* grammar, so the operand list is `[left, operator, right]` over named children and the right
|
|
1012
|
+
* operand sits at index 2. The first version read index 1, compared it to the declaration's own
|
|
1013
|
+
* node id, never matched, and published `overrides` for the one guarded definition in the corpus
|
|
1014
|
+
* — found by running the fixture rather than by reading the code, which is the only way a
|
|
1015
|
+
* predicate that is wrong in the direction of asserting anything gets caught.
|
|
1016
|
+
*/
|
|
1017
|
+
isUnconditionalPosition(node) {
|
|
1018
|
+
let cur = node;
|
|
1019
|
+
for (let i = 0; i < 24 && cur; i++) {
|
|
1020
|
+
if (CONDITIONAL_OR_GENERATED.has(cur.type)) return false;
|
|
1021
|
+
if (cur.type === "binary_expression" && this.shortCircuitOperator(cur) === "&&") {
|
|
1022
|
+
const named = this.namedChildren(cur).filter((c) => c.type !== "operator");
|
|
1023
|
+
const right = named.length > 1 ? named[named.length - 1] : null;
|
|
1024
|
+
if (right && right.id === node.id) return false;
|
|
1025
|
+
}
|
|
1026
|
+
cur = cur.parent ?? null;
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
/** The operator token of a `binary_expression`, or "" — in this grammar the operator is a NAMED
|
|
1031
|
+
* child of type `operator`, the same shape `binOp` reads in the census instrument. */
|
|
1032
|
+
shortCircuitOperator(node) {
|
|
1033
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1034
|
+
const c = node.child(i);
|
|
1035
|
+
if (c && c.type === "operator") return c.text.trim();
|
|
1036
|
+
}
|
|
1037
|
+
return "";
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* The `overrides` edge for a declaration that replaces an earlier one.
|
|
1041
|
+
*
|
|
1042
|
+
* ## The edge shape, and why it is not `name -> name`
|
|
1043
|
+
*
|
|
1044
|
+
* A Julia method-table replacement has both of its endpoints at the SAME symbol: the earlier
|
|
1045
|
+
* declaration and the later one are both named `f` with the same signature, so the second
|
|
1046
|
+
* definition replaces the first IN PLACE — it acquires no supertype, no owner and no new name. A
|
|
1047
|
+
* self-loop `f -overrides-> f` is therefore the most literally accurate shape available, and it is
|
|
1048
|
+
* exactly the shape this codebase already ruled out three times:
|
|
1049
|
+
*
|
|
1050
|
+
* - `src/ast/real-php-analyzer.ts:2496` — "UNRESOLVABLE and emit NO edge (a self-loop is always
|
|
1051
|
+
* fabrication, and a …)"
|
|
1052
|
+
* - `src/lsp/rich_hover.ts:222` — "Degenerate self-loop rows … are not connections and not
|
|
1053
|
+
* usages: skip them."
|
|
1054
|
+
* - `src/intelligence/patterns/orphans.ts:818` — a self-loop `overrides` row is listed under
|
|
1055
|
+
* "REFUSED EVIDENCE", and the rake `listen` regression is the case it cites: a self-loop row
|
|
1056
|
+
* was the only thing standing between a real method and a dead-code report.
|
|
1057
|
+
*
|
|
1058
|
+
* A corpus full of rows every consumer discards is not coverage; it is volume that makes the
|
|
1059
|
+
* signal harder to see. So the SOURCE is the enclosing DECLARED scope — a `module` or a type, a
|
|
1060
|
+
* symbol this file really declares — and the TARGET is the method whose entry is replaced.
|
|
1061
|
+
* `module M … f(x) = 1; f(x) = 2 … end` gives `M -overrides-> f` with both ends binding to real
|
|
1062
|
+
* symbols in the same file, which is the same shape the bash lane settled on for the same
|
|
1063
|
+
* construct (`real-bash-analyzer.ts:322-348`, BD8b) and the shape
|
|
1064
|
+
* `src/ast/override-retarget.ts` is written to consume.
|
|
1065
|
+
*
|
|
1066
|
+
* ## Why the module-less case is REFUSED rather than published as `global`
|
|
1067
|
+
*
|
|
1068
|
+
* 71.9% of Julia corpus files declare no module at all — measured, see this file's `GLOBAL_SCOPE`
|
|
1069
|
+
* note — so the refused case is the MAJORITY one and it is worth being explicit that it is a
|
|
1070
|
+
* refusal rather than an oversight. Publishing `global -overrides-> f` would give the edge a
|
|
1071
|
+
* source that resolves to nothing, which is the same dangling identity this file's `contains` case
|
|
1072
|
+
* declines to invent, and `global -calls-> f` is already the largest unresolvable-source bucket in
|
|
1073
|
+
* the census. The edge is dropped instead, and the drop is counted by the census rather than left
|
|
1074
|
+
* invisible.
|
|
1075
|
+
*
|
|
1076
|
+
* Confidence 1.0: this is a byte comparison of two declaration signatures in one file, which is a
|
|
1077
|
+
* parse fact and not a guess. `evidenceClass` is `structural` for the same reason (see
|
|
1078
|
+
* `classifyEvidenceTier`), so the edge is never counted as a heuristic.
|
|
1079
|
+
*/
|
|
1080
|
+
overrideEdgeFor(node, scope) {
|
|
1081
|
+
const hit = this.redefinedAt.get(node.id);
|
|
1082
|
+
if (!hit) return null;
|
|
1083
|
+
if (!scope || scope === hit.name) return null;
|
|
1084
|
+
if (!this.ownsAMethodTable(scope, hit.name)) return null;
|
|
1085
|
+
return this.rel(scope, ASTEngine.RelationshipType.Overrides, hit.name, node, 1, {
|
|
1086
|
+
juliaSpecific: true,
|
|
1087
|
+
isMethodRedefinition: true,
|
|
1088
|
+
redefinition: true,
|
|
1089
|
+
via: "identical_signature_redefinition",
|
|
1090
|
+
replacesLine: hit.replacesLine,
|
|
1091
|
+
signature: hit.signature.slice(0, 120),
|
|
1092
|
+
declaredIn: scope
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Does this file declare `owner` as a module or a type — the two things whose bodies hold a method
|
|
1097
|
+
* table?
|
|
1098
|
+
*
|
|
1099
|
+
* Answered from the symbol table rather than from the walk's `activeScope`, because `activeScope`
|
|
1100
|
+
* names FUNCTIONS too and a function is not a method-table owner. The symbol kinds are the
|
|
1101
|
+
* analyzer's own: `SymbolKind.Module` for a `module`/`baremodule`, `SymbolKind.Interface` for an
|
|
1102
|
+
* `abstract type`/`primitive type` and `SymbolKind.Class` for a `struct`. Cost is a scan of the
|
|
1103
|
+
* file's own symbols, once per admitted override edge, over a list that is already in memory.
|
|
1104
|
+
*/
|
|
1105
|
+
ownsAMethodTable(owner, target) {
|
|
1106
|
+
const syms = this.declaredSymbolKinds;
|
|
1107
|
+
if (!syms) return false;
|
|
1108
|
+
const kind = syms.get(owner);
|
|
1109
|
+
if (kind !== ASTEngine.SymbolKind.Module && kind !== ASTEngine.SymbolKind.Interface && kind !== ASTEngine.SymbolKind.Class) return false;
|
|
1110
|
+
const t = syms.get(target);
|
|
1111
|
+
return t === ASTEngine.SymbolKind.Function || t === ASTEngine.SymbolKind.Macro;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* The one place a relationship is constructed.
|
|
1115
|
+
*
|
|
1116
|
+
* `type` is `ASTEngine.RelationshipType` and NOT `RelationshipType | string`, and that narrowing is
|
|
1117
|
+
* the point of this entry rather than a typing preference. The lane used to pass serialized string
|
|
1118
|
+
* literals ("contains", "extends"), which type-checked because the parameter accepted `string` — and
|
|
1119
|
+
* which `script/m14/verify-doc-truth-emissions.ts` cannot see: its header declares the limit
|
|
1120
|
+
* outright, "A serialized kind emitted by assigning a STRING literal would be invisible to this
|
|
1121
|
+
* gate", and it derives a lane's emitted surface from `RelationshipType.X` references in code. So
|
|
1122
|
+
* every relationship this analyzer published was outside the one gate whose whole job is to catch a
|
|
1123
|
+
* doc claiming a channel the analyzer does not fill. Naming the enum member makes the surface
|
|
1124
|
+
* readable by that gate and makes a typo in a type label a compile error rather than a row nothing
|
|
1125
|
+
* groups.
|
|
1126
|
+
*
|
|
1127
|
+
* The serialized values are identical to the literals that were here (`RelationshipType.Contains`
|
|
1128
|
+
* is `"contains"`, `RelationshipType.Imports` is `"brain_import"`), so this changes the label's
|
|
1129
|
+
* provenance and nothing about the bytes stored.
|
|
1130
|
+
*/
|
|
1131
|
+
rel(source, type, target, node, confidence, metadata) {
|
|
1132
|
+
return {
|
|
1133
|
+
source,
|
|
1134
|
+
target,
|
|
1135
|
+
type,
|
|
1136
|
+
confidence,
|
|
1137
|
+
line_number: node.startPosition.row + 1,
|
|
1138
|
+
metadata: { juliaSpecific: true, ...metadata ?? {} }
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Is this macro ARGUMENT itself a declaration — the form a decorator applies to?
|
|
1143
|
+
*
|
|
1144
|
+
* The same shapes `decoratedDeclarations` admits, asked from the other side: a long-form
|
|
1145
|
+
* definition, or a short-form `f(x) = …` assignment whose left-hand side unwraps to a call. Used
|
|
1146
|
+
* to keep `@kwdef struct S` a module member while everything inside `@testset begin … end` is not.
|
|
1147
|
+
*/
|
|
1148
|
+
/** Is this macro one whose arguments are evaluated in the scope they are written in? */
|
|
1149
|
+
macroIsScopeTransparent(macrocall) {
|
|
1150
|
+
const m = this.firstNamedChildOfType(macrocall, "macro_identifier");
|
|
1151
|
+
return SCOPE_TRANSPARENT_MACROS.has(m ? macroNameOf(m.text) : "");
|
|
1152
|
+
}
|
|
1153
|
+
directDeclarationArg(arg) {
|
|
1154
|
+
if (arg.type === "function_definition" || arg.type === "macro_definition" || arg.type === "struct_definition" || arg.type === "abstract_definition" || arg.type === "primitive_definition" || arg.type === "module_definition") return true;
|
|
1155
|
+
if (arg.type !== "assignment") return false;
|
|
1156
|
+
const lhs = this.pos(arg, 0);
|
|
1157
|
+
const u = lhs ? this.unwrapWhere(lhs) : null;
|
|
1158
|
+
return !!u && u.type === "call_expression";
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* The declarations a macro is APPLIED TO, read from its own `macro_argument_list`.
|
|
1162
|
+
*
|
|
1163
|
+
* `@doc "…" function g` puts the declaration after a string; `@kwdef mutable struct S` puts it
|
|
1164
|
+
* first; `@inline h(x) = x` is the short form, whose declaration is the ASSIGNMENT's left-hand
|
|
1165
|
+
* call. The scan is over the argument list, not the first child, because a macro that decorates a
|
|
1166
|
+
* declaration can also carry a docstring or a configuration expression before it. A macro with no
|
|
1167
|
+
* declaration argument returns [] and stays a `calls` edge — `@time f()` runs a call, it does not
|
|
1168
|
+
* decorate a declaration.
|
|
1169
|
+
*/
|
|
1170
|
+
decoratedDeclarations(call) {
|
|
1171
|
+
const args = this.namedChildren(call).find((c) => c.type === "macro_argument_list");
|
|
1172
|
+
if (!args) return [];
|
|
1173
|
+
if (this.namedChildren(args).length > 4) return [];
|
|
1174
|
+
const out = [];
|
|
1175
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1176
|
+
const add = (name, kind) => {
|
|
1177
|
+
const n = name.trim();
|
|
1178
|
+
if (!n || seen.has(n)) return;
|
|
1179
|
+
seen.add(n);
|
|
1180
|
+
out.push({ name: n, kind });
|
|
1181
|
+
};
|
|
1182
|
+
for (const arg of this.namedChildren(args)) {
|
|
1183
|
+
if (arg.type === "function_definition" || arg.type === "macro_definition") {
|
|
1184
|
+
const sig = this.firstNamedChildOfType(arg, "signature");
|
|
1185
|
+
add(sig ? this.signatureName(sig) : "", arg.type);
|
|
1186
|
+
} else if (arg.type === "struct_definition" || arg.type === "abstract_definition" || arg.type === "primitive_definition") {
|
|
1187
|
+
const head = this.firstNamedChildOfType(arg, "type_head");
|
|
1188
|
+
add(head ? this.typeHeadName(head) : "", arg.type);
|
|
1189
|
+
} else if (arg.type === "module_definition") {
|
|
1190
|
+
const nameNode = arg.childForFieldName("name");
|
|
1191
|
+
add(nameNode ? nameNode.text : "", arg.type);
|
|
1192
|
+
} else if (arg.type === "assignment") {
|
|
1193
|
+
const lhs = this.pos(arg, 0);
|
|
1194
|
+
const u = lhs ? this.unwrapWhere(lhs) : null;
|
|
1195
|
+
if (u && u.type === "call_expression") add(this.calleeName(u) ?? "", "short_function");
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return out;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* The PART an `include("path")` names, or null when the argument is not one literal string.
|
|
1202
|
+
*
|
|
1203
|
+
* THE TARGET IS THE BASENAME WITH ITS EXTENSION (`b.jl`), the spelling bash's BD2 module symbol
|
|
1204
|
+
* uses, and the path as written is kept in `metadata.partPath`. A path is not a symbol name; the
|
|
1205
|
+
* arbiter resolves target STRINGS against symbols, so the basename is the only spelling a file's
|
|
1206
|
+
* own module symbol could carry. The written path is evidence, never a silent replacement.
|
|
1207
|
+
*
|
|
1208
|
+
* REJECTED, each because it does not name a part: a string with an interpolation or an escape
|
|
1209
|
+
* sequence (the bytes are not the path), a path ending at a separator (no basename), `.`/`..`, and
|
|
1210
|
+
* every non-`string_literal` argument (`joinpath(...)`, a variable, a `raw"…"`). A rejected
|
|
1211
|
+
* literal is a DYNAMIC include and stays a call with `isDynamicInclude` — a different, true fact.
|
|
1212
|
+
*/
|
|
1213
|
+
includePart(call) {
|
|
1214
|
+
const args = this.namedChildren(call).find((c) => c.type === "argument_list");
|
|
1215
|
+
if (!args) return null;
|
|
1216
|
+
const first = this.namedChildren(args)[0];
|
|
1217
|
+
if (!first || first.type !== "string_literal") return null;
|
|
1218
|
+
if (this.namedChildren(first).some((c) => c.type === "string_interpolation" || c.type === "escape_sequence")) return null;
|
|
1219
|
+
const text = first.text;
|
|
1220
|
+
let body = "";
|
|
1221
|
+
if (text.startsWith('"""') && text.endsWith('"""') && text.length >= 6) body = text.slice(3, -3);
|
|
1222
|
+
else if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) body = text.slice(1, -1);
|
|
1223
|
+
else return null;
|
|
1224
|
+
if (!body || body.endsWith("/") || body.endsWith("\\")) return null;
|
|
1225
|
+
const cut = Math.max(body.lastIndexOf("/"), body.lastIndexOf("\\"));
|
|
1226
|
+
const target = body.slice(cut + 1);
|
|
1227
|
+
if (!target || target === "." || target === "..") return null;
|
|
1228
|
+
return { path: body, target };
|
|
1229
|
+
}
|
|
1230
|
+
range(node) {
|
|
1231
|
+
return {
|
|
1232
|
+
startLine: node.startPosition.row + 1,
|
|
1233
|
+
startColumn: node.startPosition.column + 1,
|
|
1234
|
+
endLine: node.endPosition.row + 1,
|
|
1235
|
+
endColumn: node.endPosition.column + 1,
|
|
1236
|
+
startIndex: node.startIndex,
|
|
1237
|
+
endIndex: node.endIndex
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* This grammar is almost entirely POSITIONAL.
|
|
1242
|
+
*
|
|
1243
|
+
* Measured from the vendored `src/node-types.json`: **7 of 151 named node types carry field
|
|
1244
|
+
* names** — `module_definition.name`, `field_expression.value`, `if_statement.condition` /
|
|
1245
|
+
* `.alternative`, `elseif_clause.condition`, `while_statement.condition`, and the two prefixed
|
|
1246
|
+
* string/command literals. Every other node, including `binary_expression`, `typed_expression`,
|
|
1247
|
+
* `signature`, `call_expression` and `type_head`, must be navigated by child ORDER.
|
|
1248
|
+
*
|
|
1249
|
+
* The first version of this analyzer reached for `childForFieldName("right")` and
|
|
1250
|
+
* `childForFieldName("name")` throughout, which returns null for a node that has no such
|
|
1251
|
+
* field. It did not throw and it did not crash — it silently emitted **zero `extends` edges**
|
|
1252
|
+
* and **zero struct fields** for a corpus that has thousands of both, and the symptom was a
|
|
1253
|
+
* plausible-looking symbol list. That is the F1 node-type-literal class of defect the C lane's
|
|
1254
|
+
* pin record warns about ("written from memory of a different tree-sitter release, which F1
|
|
1255
|
+
* caught only because the vocabulary happened to be checked"), and the reason `pos()` exists
|
|
1256
|
+
* here rather than another `childForFieldName`.
|
|
1257
|
+
*/
|
|
1258
|
+
pos(node, index) {
|
|
1259
|
+
const c = node.namedChild(index);
|
|
1260
|
+
return c ?? null;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The two operands of a `binary_expression`.
|
|
1264
|
+
*
|
|
1265
|
+
* `binary_expression` here is `(left) (operator) (right)` where the OPERATOR IS A NAMED CHILD —
|
|
1266
|
+
* measured, not assumed: `isNamed=true`, `type="operator"`, sitting at index 1. So the naive
|
|
1267
|
+
* "left is child 0, right is child 1" is wrong twice over: index 1 is the operator, not the
|
|
1268
|
+
* right operand, and an operand can itself be an operator token. Right is therefore the LAST
|
|
1269
|
+
* named child, and left the first, which holds for every binary shape in the grammar
|
|
1270
|
+
* (`a + b`, `a::T`, `a <: B`, `a where {B}`).
|
|
1271
|
+
*
|
|
1272
|
+
* Both halves of this were wrong in the first version — `tokenText` scanned only ANONYMOUS
|
|
1273
|
+
* children for the operator, so it never found it, and `pos(node,1)` handed back the operator
|
|
1274
|
+
* where the right operand belonged. The visible effect was **zero `extends` edges** over a
|
|
1275
|
+
* corpus where `<:` is everywhere, on a file that parsed with zero ERROR nodes.
|
|
1276
|
+
*/
|
|
1277
|
+
binOperands(node) {
|
|
1278
|
+
const named = this.namedChildren(node).filter((c) => c.type !== "operator");
|
|
1279
|
+
return {
|
|
1280
|
+
op: this.tokenText(node, "operator"),
|
|
1281
|
+
left: named[0] ?? null,
|
|
1282
|
+
right: named.length > 1 ? named[named.length - 1] : null
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* A `<:` / `>:` CHAIN states one subtype fact per ADJACENT PAIR, and both ends must be NAMES.
|
|
1287
|
+
*
|
|
1288
|
+
* `A where LB <: X <: UB` parses as `(binary (binary LB <: X) <: UB)`. The previous code read
|
|
1289
|
+
* `left.text` for the source, so it published an `extends` edge whose SOURCE was `LB <: X` — a
|
|
1290
|
+
* source fragment is not a symbol, and the edge can resolve to nothing. MEASURED on the whole
|
|
1291
|
+
* 1,985-file population by `direct-oracle-check.ts`: 25 of the 25 unexplained `extends` rows carry
|
|
1292
|
+
* either a fragment source (`LB <: X`, `X <: f()`, `S <: V`) or a doubled target (`UB.UB`), and
|
|
1293
|
+
* every one of the first ten is in `julia/JuliaLowering/test/typedefs_ir.jl` — a file of deliberate
|
|
1294
|
+
* lowering-error probes whose `where` chains the grammar recovers around. The same defect occurs
|
|
1295
|
+
* in a WELL-FORMED chain, which is the reason it is fixed rather than filed as an ERROR-shape
|
|
1296
|
+
* artefact: a fragment source is a lie the graph tells on any file that writes the chain.
|
|
1297
|
+
*
|
|
1298
|
+
* The rule, stated rather than inferred: flatten the chain left to right, emit `a -> b` and
|
|
1299
|
+
* `b -> c`, take each source with `typeHeadName` (never `.text`), and take each target through
|
|
1300
|
+
* `boundTypeName`, which REFUSES a call/literal bound — `X <: f() <: Z` is a lowering error, and
|
|
1301
|
+
* naming `f` as a type would invent a relation the source says is invalid.
|
|
1302
|
+
*/
|
|
1303
|
+
subtypeChainLinks(node) {
|
|
1304
|
+
const out = [];
|
|
1305
|
+
const link = (left, right, at) => {
|
|
1306
|
+
const from = left ? this.boundTypeName(left) : "";
|
|
1307
|
+
const to = right ? this.boundTypeName(right) : "";
|
|
1308
|
+
if (from && to && from !== to) out.push({ from, to, at });
|
|
1309
|
+
};
|
|
1310
|
+
const walk = (n) => {
|
|
1311
|
+
if (n.type !== "binary_expression") return;
|
|
1312
|
+
const { op, left, right } = this.binOperands(n);
|
|
1313
|
+
if (op !== "<:" && op !== ">:") return;
|
|
1314
|
+
let fromNode = left;
|
|
1315
|
+
if (left && left.type === "binary_expression") {
|
|
1316
|
+
const inner = this.binOperands(left);
|
|
1317
|
+
if (inner.op === "<:" || inner.op === ">:") {
|
|
1318
|
+
walk(left);
|
|
1319
|
+
fromNode = inner.right;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
link(fromNode, right, n);
|
|
1323
|
+
};
|
|
1324
|
+
walk(node);
|
|
1325
|
+
return out;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* The name a subtype BOUND states, or "" when the tree states none.
|
|
1329
|
+
*
|
|
1330
|
+
* `typeHeadName` accepts a call expression through its first-identifier fallback, which is right
|
|
1331
|
+
* for a declaration head (`f(x) = …`) and wrong for a bound: `A where X <: f() <: Z` is a
|
|
1332
|
+
* lowering error, and publishing `f` as the supertype would invent a type relation the source
|
|
1333
|
+
* explicitly rejects. A bound is an identifier, a dotted path, or a parameterised/wrapped form of
|
|
1334
|
+
* one — anything else is refused.
|
|
1335
|
+
*/
|
|
1336
|
+
boundTypeName(node) {
|
|
1337
|
+
if (!node) return "";
|
|
1338
|
+
if (node.type === "identifier" || node.type === "operator") return node.text.trim();
|
|
1339
|
+
if (node.type === "macro_identifier") return macroNameOf(node.text);
|
|
1340
|
+
if (node.type === "field_expression") return this.fieldPath(node);
|
|
1341
|
+
if (node.type === "parametrized_type_expression" || node.type === "where_expression" || node.type === "parenthesized_expression" || node.type === "curly_expression" || node.type === "type_head") {
|
|
1342
|
+
return this.boundTypeName(this.pos(node, 0));
|
|
1343
|
+
}
|
|
1344
|
+
return "";
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* The operator token of a `binary_expression`. It is a NAMED child of type `operator` in this
|
|
1348
|
+
* grammar — verified by walking the children of a real `struct Circle{T<:Real} <: Shape` —
|
|
1349
|
+
* so a scan restricted to anonymous children finds nothing and the operator reads as empty.
|
|
1350
|
+
*/
|
|
1351
|
+
tokenText(node, token) {
|
|
1352
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1353
|
+
const c = node.child(i);
|
|
1354
|
+
if (c && c.type === token) return c.text.trim();
|
|
1355
|
+
}
|
|
1356
|
+
return "";
|
|
1357
|
+
}
|
|
1358
|
+
namedChildren(node) {
|
|
1359
|
+
const out = [];
|
|
1360
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1361
|
+
const c = node.namedChild(i);
|
|
1362
|
+
if (c) out.push(c);
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
firstNamedChildOfType(node, type) {
|
|
1367
|
+
for (const c of this.namedChildren(node)) if (c.type === type) return c;
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
firstNamedChildType(node) {
|
|
1371
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1372
|
+
const c = node.child(i);
|
|
1373
|
+
if (c && c.isNamed) return c.type;
|
|
1374
|
+
}
|
|
1375
|
+
return "";
|
|
1376
|
+
}
|
|
1377
|
+
hasToken(node, token) {
|
|
1378
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1379
|
+
const c = node.child(i);
|
|
1380
|
+
if (c && !c.isNamed && c.type === token) return true;
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* `type_head` is positional: `Name`, `Name{Params}`, or `Name{T} <: Super`.
|
|
1386
|
+
*
|
|
1387
|
+
* The `<:` case is the trap. `struct Circle{T<:Real} <: Shape` makes the head
|
|
1388
|
+
* `(binary_expression (parametrized_type_expression Circle{T<:Real}) <: (identifier Shape))`, so
|
|
1389
|
+
* the first identifier ANYWHERE in that subtree is **Shape** — the supertype — and the first
|
|
1390
|
+
* version named every such struct after its supertype: one `class:Shape` with
|
|
1391
|
+
* `contains -> radius`, and no `extends` edge, because the name and the supertype had been
|
|
1392
|
+
* swapped by the same mistake. A declared name is the subtree's LEFT-most name, so a binary
|
|
1393
|
+
* expression contributes its left operand and the supertype is read separately by
|
|
1394
|
+
* `supertypesOf`. That is a plausible-looking wrong answer, which is the whole difficulty: the
|
|
1395
|
+
* symbol list looked like a valid parse of a valid file.
|
|
1396
|
+
*/
|
|
1397
|
+
typeRefName(node) {
|
|
1398
|
+
if (node.type === "interpolation_expression") {
|
|
1399
|
+
const inner = this.pos(node, 0);
|
|
1400
|
+
return inner ? this.fieldPath(inner) : "";
|
|
1401
|
+
}
|
|
1402
|
+
return this.typeHeadName(node);
|
|
1403
|
+
}
|
|
1404
|
+
typeHeadName(node) {
|
|
1405
|
+
if (node.type === "interpolation_expression") return "";
|
|
1406
|
+
if (node.type === "identifier") return node.text.trim();
|
|
1407
|
+
if (node.type === "macro_identifier") return macroNameOf(node.text);
|
|
1408
|
+
if (node.type === "operator") return node.text.trim();
|
|
1409
|
+
if (node.type === "binary_expression") {
|
|
1410
|
+
const { left } = this.binOperands(node);
|
|
1411
|
+
return left ? this.typeHeadName(left) : "";
|
|
1412
|
+
}
|
|
1413
|
+
if (node.type === "field_expression") {
|
|
1414
|
+
const path2 = this.fieldPath(node);
|
|
1415
|
+
if (path2) return path2;
|
|
1416
|
+
}
|
|
1417
|
+
const first = this.pos(node, 0);
|
|
1418
|
+
if (first && first.type === "identifier") return first.text.trim();
|
|
1419
|
+
if (first && first.type === "interpolation_expression") return "";
|
|
1420
|
+
const direct = this.firstNamedChildOfType(node, "identifier");
|
|
1421
|
+
if (direct) return direct.text.trim();
|
|
1422
|
+
for (const c of this.namedChildren(node)) {
|
|
1423
|
+
if (c.type === "curly_expression" || c.type === "parametrized_type_expression" || c.type === "binary_expression" || c.type === "field_expression") {
|
|
1424
|
+
const inner = this.typeHeadName(c);
|
|
1425
|
+
if (inner) return inner;
|
|
1426
|
+
}
|
|
1427
|
+
if (c.type === "where_expression") {
|
|
1428
|
+
const inner = this.typeHeadName(c);
|
|
1429
|
+
if (inner) return inner;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return "";
|
|
1433
|
+
}
|
|
1434
|
+
/** `signature` is positional: an identifier, a call, an argument list, a typed expr, or a where-expr. */
|
|
1435
|
+
/**
|
|
1436
|
+
* Strip a `where` clause to the callable underneath it. The callable is ALWAYS `pos(0)`.
|
|
1437
|
+
*
|
|
1438
|
+
* `f(x::T) where {T} = body` is `(assignment (where_expression (call_expression …) {T}) …)` and
|
|
1439
|
+
* `function f(x::T) where T … end` is `(function_definition (signature (where_expression
|
|
1440
|
+
* (call_expression …) T)) …)`. The corpus audit is what found this: `Compiler/test/inline.jl:664`
|
|
1441
|
+
* declares `g41299(f::Tf, args::Vararg{Any,N}) where {Tf,N}`, nothing unwrapped the wrapper, so
|
|
1442
|
+
* the declaration was never published and the call to it came out UNRESOLVED against a runtime
|
|
1443
|
+
* that resolves it perfectly. Worse, the TYPE VARIABLE was read as the function's name.
|
|
1444
|
+
*
|
|
1445
|
+
* And note the two forms differ: `where {T}` puts a `curly_expression` after the call, but
|
|
1446
|
+
* BARE `where T` puts the type variable there as a plain sibling `identifier`. That is why
|
|
1447
|
+
* scanning for an identifier is unsound here — for `where T` it finds `T`, the parameter, and
|
|
1448
|
+
* returns it as the function's name, fabricating a function called `T`. Positional access is
|
|
1449
|
+
* not a workaround for this grammar; it is the only correct navigation.
|
|
1450
|
+
*/
|
|
1451
|
+
unwrapWhere(node) {
|
|
1452
|
+
const WRAPPERS = /* @__PURE__ */ new Set(["where_expression", "curly_expression", "typed_expression", "parenthesized_expression"]);
|
|
1453
|
+
let cur = node;
|
|
1454
|
+
for (let guard = 0; guard < 8 && WRAPPERS.has(cur.type); guard++) {
|
|
1455
|
+
const first = this.pos(cur, 0);
|
|
1456
|
+
if (!first) break;
|
|
1457
|
+
cur = first;
|
|
1458
|
+
}
|
|
1459
|
+
return cur;
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* The nearest enclosing `module … end`, by name. Used by the `global` rule: the keyword declares a
|
|
1463
|
+
* MODULE binding at any nesting, so the owner is this module and not the nearest container.
|
|
1464
|
+
*/
|
|
1465
|
+
enclosingModuleName(node) {
|
|
1466
|
+
let cur = node.parent ?? null;
|
|
1467
|
+
while (cur) {
|
|
1468
|
+
if (cur.type === "module_definition") {
|
|
1469
|
+
const nm = cur.childForFieldName("name");
|
|
1470
|
+
return nm ? nm.text.trim() : "";
|
|
1471
|
+
}
|
|
1472
|
+
cur = cur.parent ?? null;
|
|
1473
|
+
}
|
|
1474
|
+
return "";
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* The binding name a PLAIN module-scope assignment declares, or "" when this is not one.
|
|
1478
|
+
*
|
|
1479
|
+
* Julia admits a top-level assignment with no `const` (`x = 1`) and it creates a global binding in
|
|
1480
|
+
* the module, which is what `contains` is for. Three things disqualify a node here, and each is a
|
|
1481
|
+
* different fact:
|
|
1482
|
+
*
|
|
1483
|
+
* - it is not written in a module's own scope (a `let`, a loop, a short-form body, a type body);
|
|
1484
|
+
* - its left side is not a NAME: `A.x = 1` assigns a field of another module, `a, b = 1, 2`
|
|
1485
|
+
* destructures (the grammar's tuple LHS), and a call/`where` head is a method definition;
|
|
1486
|
+
* - the name is a constant Julia implies (`nothing`, `missing`, …) rather than a binding.
|
|
1487
|
+
*
|
|
1488
|
+
* A `typed_expression` left side (`x::Int = 0`) is read through to its first child, the same rule
|
|
1489
|
+
* the `const` case applies: the annotation is a reference, not part of the name.
|
|
1490
|
+
*/
|
|
1491
|
+
plainModuleBinding(node) {
|
|
1492
|
+
if (!this.inModuleBindingScope(node)) return "";
|
|
1493
|
+
const lhs = this.pos(node, 0);
|
|
1494
|
+
if (!lhs) return "";
|
|
1495
|
+
const head = lhs.type === "typed_expression" ? this.pos(lhs, 0) : lhs;
|
|
1496
|
+
if (!head) return "";
|
|
1497
|
+
if (head.type === "identifier" || head.type === "operator" || head.type === "prefixed_string_literal") {
|
|
1498
|
+
return head.text.trim();
|
|
1499
|
+
}
|
|
1500
|
+
return "";
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Is this node written in a MODULE's own scope — not inside a body, loop, `let` or quote?
|
|
1504
|
+
*
|
|
1505
|
+
* The scope-introducing constructs are Julia's own list, not a heuristic: a binding written inside
|
|
1506
|
+
* any of them is local to it. `begin`, `if` and `try` are transparent here (`if` is documented as
|
|
1507
|
+
* introducing no scope; a top-level `begin` is the module body continuing). A SHORT-FORM FUNCTION
|
|
1508
|
+
* is an `assignment` whose left side is a call, so the ancestor test has to recognise that shape
|
|
1509
|
+
* as a body — otherwise `f(x) = (y = 1; y)` would publish `y` as a member of the module.
|
|
1510
|
+
*/
|
|
1511
|
+
inModuleBindingScope(node) {
|
|
1512
|
+
let cur = node.parent ?? null;
|
|
1513
|
+
while (cur) {
|
|
1514
|
+
if (cur.type === "module_definition") return true;
|
|
1515
|
+
if (SCOPE_INTRODUCING_NODES.has(cur.type)) return false;
|
|
1516
|
+
if (cur.type === "assignment") {
|
|
1517
|
+
const head = this.unwrapWhere(this.pos(cur, 0) ?? cur);
|
|
1518
|
+
const inner = head.type === "typed_expression" ? this.pos(head, 0) : head;
|
|
1519
|
+
if (inner && inner.type === "call_expression") return false;
|
|
1520
|
+
}
|
|
1521
|
+
cur = cur.parent ?? null;
|
|
1522
|
+
}
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
/** The `call_expression` at a definition's head, through the wrappers a head can carry. */
|
|
1526
|
+
definitionCall(head) {
|
|
1527
|
+
let cur = head;
|
|
1528
|
+
for (let i = 0; i < 8 && cur; i++) {
|
|
1529
|
+
if (cur.type === "call_expression") return cur;
|
|
1530
|
+
if (cur.type === "where_expression") cur = this.unwrapWhere(cur);
|
|
1531
|
+
else if (cur.type === "signature" || cur.type === "typed_expression" || cur.type === "parenthesized_expression" || cur.type === "macro_argument_list") cur = this.pos(cur, 0);
|
|
1532
|
+
else return null;
|
|
1533
|
+
}
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
signatureName(sig) {
|
|
1537
|
+
if (sig.type === "typed_expression") {
|
|
1538
|
+
const callable = this.pos(sig, 0);
|
|
1539
|
+
if (callable) return this.signatureName(callable);
|
|
1540
|
+
}
|
|
1541
|
+
if (sig.type === "macrocall_expression") {
|
|
1542
|
+
const m = this.firstNamedChildOfType(sig, "macro_identifier");
|
|
1543
|
+
return m ? macroNameOf(m.text) : "";
|
|
1544
|
+
}
|
|
1545
|
+
if (sig.type === "call_expression") return this.calleeName(sig) ?? "";
|
|
1546
|
+
if (sig.type === "where_expression") {
|
|
1547
|
+
const callable = this.unwrapWhere(sig);
|
|
1548
|
+
if (callable.type === "call_expression") return this.calleeName(callable) ?? "";
|
|
1549
|
+
}
|
|
1550
|
+
const direct = this.firstNamedChildOfType(sig, "identifier");
|
|
1551
|
+
if (direct) return direct.text.trim();
|
|
1552
|
+
const call = this.firstNamedChildOfType(sig, "call_expression");
|
|
1553
|
+
if (call) return this.calleeName(call) ?? "";
|
|
1554
|
+
for (const c of this.namedChildren(sig)) {
|
|
1555
|
+
if (c.type === "where_expression" || c.type === "typed_expression" || c.type === "parenthesized_expression") {
|
|
1556
|
+
const inner = this.signatureName(c);
|
|
1557
|
+
if (inner) return inner;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return "";
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
/**
|
|
1564
|
+
* Is this node inside the `type_head` of a type DECLARATION?
|
|
1565
|
+
*
|
|
1566
|
+
* Checked by walking ancestors rather than the immediate parent, because the depth varies with
|
|
1567
|
+
* the shape of the head: `struct Circle <: Shape` puts the `parametrized_type_expression` two
|
|
1568
|
+
* levels down (`type_head > binary_expression > …`) while `struct Circle{T} <: Shape` puts it
|
|
1569
|
+
* three, and a parent-only test silently missed the second. A guard that only sometimes fires
|
|
1570
|
+
* is worse than none, because it leaves a self-reference edge on exactly the generic types that
|
|
1571
|
+
* most need to be right.
|
|
1572
|
+
*/
|
|
1573
|
+
withinTypeHead(node) {
|
|
1574
|
+
let cur = node;
|
|
1575
|
+
while (cur) {
|
|
1576
|
+
if (this.typeHeadSites.has(cur.id)) return true;
|
|
1577
|
+
if (cur.type === "function_definition" || cur.type === "macro_definition" || cur.type === "module_definition" || cur.type === "source_file") return false;
|
|
1578
|
+
cur = cur.parent ?? null;
|
|
1579
|
+
}
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
/** The C symbol a `ccall(...)` call targets, with its library when the two-tuple form gives one. */
|
|
1583
|
+
ccallTarget(call) {
|
|
1584
|
+
const args = this.firstNamedChildOfType(call, "argument_list");
|
|
1585
|
+
const first = args ? this.pos(args, 0) : null;
|
|
1586
|
+
if (!first) return null;
|
|
1587
|
+
if (first.type === "quote_expression") {
|
|
1588
|
+
const sym = this.pos(first, 0);
|
|
1589
|
+
return sym ? { symbol: stripSigil(sym.text, ":"), library: null } : null;
|
|
1590
|
+
}
|
|
1591
|
+
if (first.type === "tuple_expression") {
|
|
1592
|
+
const elem = (q) => {
|
|
1593
|
+
if (!q) return "";
|
|
1594
|
+
if (q.type === "quote_expression") {
|
|
1595
|
+
const inner = this.pos(q, 0);
|
|
1596
|
+
return inner ? stripSigil(inner.text.trim(), ":") : "";
|
|
1597
|
+
}
|
|
1598
|
+
return q.text.trim();
|
|
1599
|
+
};
|
|
1600
|
+
const parts = this.namedChildren(first).map(elem);
|
|
1601
|
+
if (parts.length >= 2 && parts[0] && parts[1]) return { symbol: parts[0], library: parts[1] };
|
|
1602
|
+
if (parts.length === 1 && parts[0]) return { symbol: parts[0], library: null };
|
|
1603
|
+
}
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
/** Stable key for a call site, so a `@ccall` target can be recognised again when it is emitted. */
|
|
1607
|
+
textKey(node, callee) {
|
|
1608
|
+
return `${node.startIndex}:${callee}`;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Record every call site under an `@ccall` as a call to a FOREIGN symbol.
|
|
1612
|
+
*
|
|
1613
|
+
* `@ccall strlen(s)::Csize_t` puts the C function in an ordinary `call_expression` inside
|
|
1614
|
+
* `macro_argument_list`, so without this the emit site cannot tell it from `f(s)` and asserts a
|
|
1615
|
+
* Julia binding. Collected by node start index rather than by name, because two different C
|
|
1616
|
+
* symbols can share a name and only the call SITE says which one this edge is.
|
|
1617
|
+
*/
|
|
1618
|
+
markForeignCallees(macroNode) {
|
|
1619
|
+
const args = this.firstNamedChildOfType(macroNode, "macro_argument_list");
|
|
1620
|
+
if (!args) return;
|
|
1621
|
+
const visit = (n) => {
|
|
1622
|
+
if (n.type === "call_expression") {
|
|
1623
|
+
const callee = this.calleeName(n);
|
|
1624
|
+
if (callee) this.foreignMarked.add(this.textKey(n, callee));
|
|
1625
|
+
}
|
|
1626
|
+
for (const c of this.namedChildren(n)) visit(c);
|
|
1627
|
+
};
|
|
1628
|
+
visit(args);
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* The dotted path of a `field_expression` chain — this grammar's qualified access.
|
|
1632
|
+
*
|
|
1633
|
+
* MEASURED, and the measurement contradicted the code. There is NO `scoped_identifier` node in
|
|
1634
|
+
* this grammar: scanning 400 corpus files for `scoped_identifier` under a `call_expression`
|
|
1635
|
+
* callee found **0**, while `field_expression` accounted for every qualified callee. `Core
|
|
1636
|
+
* .Intrinsics.bitcast` is `field_expression(field_expression(Core, Intrinsics), bitcast)` —
|
|
1637
|
+
* left-nested, so the path is built by recursion, not by listing segments.
|
|
1638
|
+
*
|
|
1639
|
+
* The `scoped_identifier` branch this replaced was dead code written from a different grammar,
|
|
1640
|
+
* and because it never matched, `calleeName` returned null and the analyzer emitted **nothing at
|
|
1641
|
+
* all** for a file of qualified calls: zero symbols, zero relationships, no error. A silent empty
|
|
1642
|
+
* graph is the worst failure shape there is — it reads as "nothing to see here" rather than
|
|
1643
|
+
* "this analyzer is blind".
|
|
1644
|
+
*
|
|
1645
|
+
* The dotted path is kept WHOLE on purpose. Collapsing it to the last segment was measured to
|
|
1646
|
+
* invent merges: over 400 files, 1,352 bare segments appear behind a qualified path and **72 of
|
|
1647
|
+
* them are reachable by more than one distinct path**. `parse` alone is `Meta.parse`,
|
|
1648
|
+
* `Base.parse`, `TOML.parse` and `Dates.parse` — four unrelated functions. `bitcast` is
|
|
1649
|
+
* `Base.bitcast`, `Core.bitcast` and `Intrinsics.bitcast`. `isreadable` is
|
|
1650
|
+
* `Filesystem.isreadable`, `Base.isreadable` and `Sys.isreadable`. One node per bare name would
|
|
1651
|
+
* silently fuse them, and a merged node makes every edge into and out of it a statement about
|
|
1652
|
+
* the wrong function. In Julia the qualified path IS the identity of the binding.
|
|
1653
|
+
*
|
|
1654
|
+
* `Base.:(==)` puts the operator in a `quote_expression`, so the last segment can be either.
|
|
1655
|
+
*/
|
|
1656
|
+
/**
|
|
1657
|
+
* THE NAME A NODE DECLARES, or "" when the node names nothing.
|
|
1658
|
+
*
|
|
1659
|
+
* One resolver for every position that must publish a NAME — a binding's left-hand side, a
|
|
1660
|
+
* `where` bound's parameter — because the alternative is what this lane shipped: a `.text.trim()`
|
|
1661
|
+
* at each site, each of which is correct for the shapes its author had in mind and a source SPAN
|
|
1662
|
+
* for every other shape. The defect is not that any one site was careless; it is that "take the
|
|
1663
|
+
* node's text" is a rule that has no answer to "what if the node is not a name".
|
|
1664
|
+
*
|
|
1665
|
+
* The walk descends the LEFT-most positional child through every wrapper this grammar can place
|
|
1666
|
+
* around a declared name, and STOPS at a node it can spell:
|
|
1667
|
+
*
|
|
1668
|
+
* typed_expression `x::Int` -> `x` (child 1 is the type, a reference)
|
|
1669
|
+
* binary_expression `T<:Real` -> `T` (left operand; the right is a bound)
|
|
1670
|
+
* parametrized_type `Foo{T}` -> `Foo` (the base name, NOT the parameter list)
|
|
1671
|
+
* field_expression `A.B` -> `A.B` (the whole path is the identity)
|
|
1672
|
+
* parenthesized/wrapped forms -> their first child
|
|
1673
|
+
*
|
|
1674
|
+
* and returns "" for anything else — an `ERROR` node from a broken parse, a `tuple_expression`
|
|
1675
|
+
* (which declares SEVERAL names and is therefore not one name), a literal, a comment. A refusal
|
|
1676
|
+
* costs a symbol the grammar could not spell; publishing the node's text instead costs a key
|
|
1677
|
+
* that no lookup can ever match, which is strictly worse.
|
|
1678
|
+
*
|
|
1679
|
+
* MEASURED on the whole corpus by `validation/m14/fixtures/julia/key-shape-census.ts`.
|
|
1680
|
+
*/
|
|
1681
|
+
nameOf(node, depth = 0) {
|
|
1682
|
+
if (depth > 8) return "";
|
|
1683
|
+
switch (node.type) {
|
|
1684
|
+
case "identifier":
|
|
1685
|
+
case "operator":
|
|
1686
|
+
return node.text.trim();
|
|
1687
|
+
case "macro_identifier":
|
|
1688
|
+
return stripSigil(node.text, "@");
|
|
1689
|
+
case "scoped_identifier":
|
|
1690
|
+
case "field_expression":
|
|
1691
|
+
case "prefixed_string_literal":
|
|
1692
|
+
return node.type === "prefixed_string_literal" ? node.text.trim() : this.fieldPath(node);
|
|
1693
|
+
case "quote_expression":
|
|
1694
|
+
return this.quotedName(node);
|
|
1695
|
+
case "typed_expression":
|
|
1696
|
+
case "unary_typed_expression":
|
|
1697
|
+
case "binary_expression":
|
|
1698
|
+
case "parenthesized_expression":
|
|
1699
|
+
case "where_expression":
|
|
1700
|
+
case "parametrized_type_expression":
|
|
1701
|
+
case "curly_expression": {
|
|
1702
|
+
const first = this.pos(node, 0);
|
|
1703
|
+
return first ? this.nameOf(first, depth + 1) : "";
|
|
1704
|
+
}
|
|
1705
|
+
default:
|
|
1706
|
+
return "";
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* The name a `quote_expression` spells — the OPERATOR for a quoted operator (`:(==)` → `==`), the
|
|
1711
|
+
* quoted symbol itself for a Symbol value (`:foo`), or "" when the node's text is a SPAN rather
|
|
1712
|
+
* than a spelling.
|
|
1713
|
+
*
|
|
1714
|
+
* The span rule is the whole point of the guard below. A `quote_expression` recovered over a
|
|
1715
|
+
* broken region spans the reparsed remainder of the file —
|
|
1716
|
+
* `julia/base/broadcast.jl`'s `:.>:` followed by the next line's `Expr(...)` came out as
|
|
1717
|
+
* `":.>:\n Expr"` and was published as a FUNCTION's name, with seven relationships
|
|
1718
|
+
* sourced from it. A node whose text carries layout is not a name at all, so this refuses: the
|
|
1719
|
+
* inner symbol is not read either, because when the wrapper spans a region the parser did not
|
|
1720
|
+
* understand, the "inner operator" is whatever token it recovered on and spelling it would be a
|
|
1721
|
+
* fabricated name rather than a missing one.
|
|
1722
|
+
*/
|
|
1723
|
+
quotedName(node) {
|
|
1724
|
+
const t = node.text.trim();
|
|
1725
|
+
if (hasLayout(t)) return "";
|
|
1726
|
+
const inner = this.pos(node, 0);
|
|
1727
|
+
const plain = (n) => n && (n.type === "operator" || n.type === "identifier") && !hasLayout(n.text) ? n.text.trim() : "";
|
|
1728
|
+
if (inner) {
|
|
1729
|
+
const direct = plain(inner);
|
|
1730
|
+
if (direct) return direct;
|
|
1731
|
+
if (inner.type === "parenthesized_expression") {
|
|
1732
|
+
const op = plain(this.pos(inner, 0));
|
|
1733
|
+
if (op) return op;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return t;
|
|
1737
|
+
}
|
|
1738
|
+
fieldPath(node) {
|
|
1739
|
+
if (node.type === "identifier") return node.text.trim();
|
|
1740
|
+
if (node.type === "parametrized_type_expression") {
|
|
1741
|
+
const base = this.pos(node, 0);
|
|
1742
|
+
return base ? this.fieldPath(base) : "";
|
|
1743
|
+
}
|
|
1744
|
+
if (node.type === "quote_expression") return this.quotedName(node);
|
|
1745
|
+
if (node.type === "macro_identifier") return macroNameOf(node.text);
|
|
1746
|
+
if (node.type === "prefixed_string_literal") return node.text.trim();
|
|
1747
|
+
if (node.type === "operator") return node.text.trim();
|
|
1748
|
+
if (node.type === "typed_expression" || node.type === "unary_typed_expression") {
|
|
1749
|
+
const inner = this.pos(node, 0);
|
|
1750
|
+
return inner ? this.fieldPath(inner) : "";
|
|
1751
|
+
}
|
|
1752
|
+
if (node.type === "where_expression" || node.type === "parenthesized_expression") {
|
|
1753
|
+
const inner = this.pos(node, 0);
|
|
1754
|
+
return inner ? this.fieldPath(inner) : "";
|
|
1755
|
+
}
|
|
1756
|
+
if (node.type === "field_expression") {
|
|
1757
|
+
const kids = this.namedChildren(node);
|
|
1758
|
+
const receiver = kids[0];
|
|
1759
|
+
const member = kids[kids.length - 1];
|
|
1760
|
+
if (!receiver || !member) return "";
|
|
1761
|
+
const head = this.fieldPath(receiver);
|
|
1762
|
+
const tail = this.fieldPath(member);
|
|
1763
|
+
return head && tail ? `${head}.${tail}` : head || tail || "";
|
|
1764
|
+
}
|
|
1765
|
+
return "";
|
|
1766
|
+
}
|
|
1767
|
+
/** The callee of a call: a bare name, or the WHOLE dotted path when it is qualified. */
|
|
1768
|
+
calleeName(call) {
|
|
1769
|
+
if (call.type === "where_expression") {
|
|
1770
|
+
const callable = this.unwrapWhere(call);
|
|
1771
|
+
if (callable !== call) return this.calleeName(callable);
|
|
1772
|
+
}
|
|
1773
|
+
const calleeNode = this.pos(call, 0);
|
|
1774
|
+
if (calleeNode) {
|
|
1775
|
+
if (calleeNode.type === "parenthesized_expression") {
|
|
1776
|
+
const inner = this.pos(calleeNode, 0);
|
|
1777
|
+
let base = inner;
|
|
1778
|
+
for (let i = 0; i < 4 && base && (base.type === "where_expression" || base.type === "parenthesized_expression"); i++) {
|
|
1779
|
+
base = this.pos(base, 0);
|
|
1780
|
+
}
|
|
1781
|
+
if (base && (base.type === "typed_expression" || base.type === "unary_typed_expression" || base.type === "interpolation_expression")) return null;
|
|
1782
|
+
if (base && this.subtreeHasInterpolation(base)) return null;
|
|
1783
|
+
}
|
|
1784
|
+
const path2 = this.fieldPath(calleeNode);
|
|
1785
|
+
if (path2) return path2;
|
|
1786
|
+
}
|
|
1787
|
+
for (const c of this.namedChildren(call)) {
|
|
1788
|
+
if (c.type === "where_expression" || c.type === "parenthesized_expression") {
|
|
1789
|
+
const inner = this.calleeName(c);
|
|
1790
|
+
if (inner) return inner;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Does this subtree splice a runtime value (`$x`) into a NAME?
|
|
1797
|
+
*
|
|
1798
|
+
* A splice is not a spelling: `(Base.$fJ)(… ) = …` declares whichever binding the loop variable
|
|
1799
|
+
* holds, and the source never names it. Walking the subtree rather than testing the top node is
|
|
1800
|
+
* the point — the splice sits under a `field_expression` member, one level below the paren that
|
|
1801
|
+
* `calleeName` inspects.
|
|
1802
|
+
*/
|
|
1803
|
+
subtreeHasInterpolation(node) {
|
|
1804
|
+
if (node.type === "interpolation_expression") return true;
|
|
1805
|
+
for (const c of this.namedChildren(node)) if (this.subtreeHasInterpolation(c)) return true;
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* The supertype a type head STATES — the head's own `<:`, and never a comparison inside it.
|
|
1810
|
+
*
|
|
1811
|
+
* THE HEAD IS A WALL, NOT A ROAD, and this function used to descend it looking for `<:` at any
|
|
1812
|
+
* depth. Two facts made every one of those edges wrong in a different way:
|
|
1813
|
+
*
|
|
1814
|
+
* - a `<:` inside a supertype EXPRESSION is a COMPARISON, not a subtype of the declared type.
|
|
1815
|
+
* `struct S <: (S <: Real ? Any : Real)` published `S extends Real` — the CONDITION's right
|
|
1816
|
+
* operand read as the supertype, `1 of 1` corpus sites in `julia/test/typegroup.jl:665`.
|
|
1817
|
+
* - a `<:` chain inside a `where` clause bounds a PARAMETER (`(AbstractArray{T, N} where T <:
|
|
1818
|
+
* Integer where N)`), which the `where_expression` case below publishes with `T` as the
|
|
1819
|
+
* source; descending here also published `Foo extends N` and `Foo extends Integer`.
|
|
1820
|
+
*
|
|
1821
|
+
* Both were found by the direct oracle (`validation/m14/fixtures/julia/direct-oracle-check.ts`),
|
|
1822
|
+
* whose own rule has been "the head's top-level binary, and the operand must NAME a type" since
|
|
1823
|
+
* it was written — a second derivation of the same fact that disagreed, which is the only reason
|
|
1824
|
+
* a reader could see it.
|
|
1825
|
+
*
|
|
1826
|
+
* A PARAMETER's bound is still published, by the `parametrized_type_expression`/
|
|
1827
|
+
* `where_expression` cases, with the parameter as the source. That is a fact about `T`, not about
|
|
1828
|
+
* the type, and it is a different edge.
|
|
1829
|
+
*/
|
|
1830
|
+
supertypesOf(node) {
|
|
1831
|
+
const head = this.firstNamedChildOfType(node, "type_head");
|
|
1832
|
+
if (!head) return [];
|
|
1833
|
+
const bin = this.firstNamedChildOfType(head, "binary_expression");
|
|
1834
|
+
if (!bin) return [];
|
|
1835
|
+
const { op, right } = this.binOperands(bin);
|
|
1836
|
+
if (op !== "<:" && op !== ">:" || !right) return [];
|
|
1837
|
+
const name = this.supertypeName(right);
|
|
1838
|
+
return name ? [name] : [];
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* The NAME a supertype operand spells, or "" when the operand is a computed VALUE.
|
|
1842
|
+
*
|
|
1843
|
+
* MEASURED over the whole 1,985-file population (validation/m14/measurements/julia-supertype-
|
|
1844
|
+
* shapes.json): 1,313 of the 1,324 `<:` sites in a type head are a NAME — identifier 891,
|
|
1845
|
+
* `parametrized_type_expression` 346 (`AbstractVector{T}`), `field_expression` 76
|
|
1846
|
+
* (`Base.AbstractSet`) — and the remaining 11 are the shapes below, every one of which this
|
|
1847
|
+
* function REFUSES rather than guesses:
|
|
1848
|
+
*
|
|
1849
|
+
* - `$typ <: $sup` (3, `julia/stdlib/LibGit2/src/types.jl:1084`): a runtime VALUE. The old
|
|
1850
|
+
* fallback published the interpolation's own identifier, `typ extends sup` — 3 edges saying a
|
|
1851
|
+
* type is a subtype of a loop variable.
|
|
1852
|
+
* - a statement BLOCK `(global g; g(::S) = 1; Any)` (2, `julia/test/typegroup.jl:652`): the
|
|
1853
|
+
* value is its last statement, `Any`, which happens to be a type — but the operand as written
|
|
1854
|
+
* states no name, and the block also DEFINES a method, which the walker published as
|
|
1855
|
+
* `contains S -> g`.
|
|
1856
|
+
* - `typeof(Core.apply_type)` (1), `0x01` (1), `@NamedTuple{x::Int}` (1), a TERNARY (1), and a
|
|
1857
|
+
* `where` chain (2) — a call, a literal, a macro call, a conditional and a UnionAll. The old
|
|
1858
|
+
* fallback read the call's CALLEE as the supertype (`Foo extends typeof`,
|
|
1859
|
+
* `julia/test/errorshow.jl:1540`) and the first bare identifier it could find inside the
|
|
1860
|
+
* others.
|
|
1861
|
+
*
|
|
1862
|
+
* A parenthesised NAME is still a name (`struct S <: (Any)`), and a `where`/`parametrized` wrapper
|
|
1863
|
+
* is unwrapped to the type it is built from (`Tuple{T} where T` names `Tuple`, which is what the
|
|
1864
|
+
* oracle derives and what a reader can follow). What is inside a CURLY parameter list is never
|
|
1865
|
+
* read as the supertype: those are the type's ARGUMENTS.
|
|
1866
|
+
*/
|
|
1867
|
+
supertypeName(node) {
|
|
1868
|
+
switch (node.type) {
|
|
1869
|
+
case "identifier":
|
|
1870
|
+
case "operator":
|
|
1871
|
+
return node.text.trim();
|
|
1872
|
+
case "field_expression":
|
|
1873
|
+
return this.fieldPath(node) || "";
|
|
1874
|
+
case "parametrized_type_expression":
|
|
1875
|
+
case "curly_expression":
|
|
1876
|
+
case "parenthesized_expression":
|
|
1877
|
+
case "where_expression": {
|
|
1878
|
+
const inner = this.pos(node, 0);
|
|
1879
|
+
return inner ? this.supertypeName(inner) : "";
|
|
1880
|
+
}
|
|
1881
|
+
default:
|
|
1882
|
+
return "";
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/** A struct body's `x::T` entries. The grammar has no dedicated field node for them. */
|
|
1886
|
+
structFields(node) {
|
|
1887
|
+
const out = [];
|
|
1888
|
+
const fieldName = (c) => {
|
|
1889
|
+
if (c.type === "typed_expression") return this.pos(c, 0)?.text.trim() ?? "";
|
|
1890
|
+
if (c.type === "where_expression") {
|
|
1891
|
+
const inner = this.pos(c, 0);
|
|
1892
|
+
return inner ? fieldName(inner) : "";
|
|
1893
|
+
}
|
|
1894
|
+
if (c.type === "identifier") return c.text.trim();
|
|
1895
|
+
if (c.type === "macrocall_expression") {
|
|
1896
|
+
const args = this.namedChildren(c).find((n) => n.type === "macro_argument_list");
|
|
1897
|
+
if (!args) return "";
|
|
1898
|
+
for (const a of this.namedChildren(args)) {
|
|
1899
|
+
const nm = fieldName(a);
|
|
1900
|
+
if (nm) return nm;
|
|
1901
|
+
}
|
|
1902
|
+
return "";
|
|
1903
|
+
}
|
|
1904
|
+
if (c.type === "binary_expression") {
|
|
1905
|
+
const { left } = this.binOperands(c);
|
|
1906
|
+
if (left && left.type === "typed_expression") return this.pos(left, 0)?.text.trim() ?? "";
|
|
1907
|
+
return "";
|
|
1908
|
+
}
|
|
1909
|
+
if (c.type === "const_statement") {
|
|
1910
|
+
const inner = this.namedChildren(c).find((n) => n.type === "assignment" || n.type === "typed_expression" || n.type === "identifier");
|
|
1911
|
+
return inner ? fieldName(inner) : "";
|
|
1912
|
+
}
|
|
1913
|
+
if (c.type === "assignment") {
|
|
1914
|
+
const lhs = this.pos(c, 0);
|
|
1915
|
+
if (!lhs) return "";
|
|
1916
|
+
if (lhs.type === "typed_expression") return this.pos(lhs, 0)?.text.trim() ?? "";
|
|
1917
|
+
if (lhs.type === "identifier") return lhs.text.trim();
|
|
1918
|
+
}
|
|
1919
|
+
return "";
|
|
1920
|
+
};
|
|
1921
|
+
for (const c of this.namedChildren(node)) {
|
|
1922
|
+
const name = fieldName(c);
|
|
1923
|
+
if (name) out.push({ name, node: c });
|
|
1924
|
+
}
|
|
1925
|
+
return out;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* `import A: b, c` / `using A, B` — the module and the names brought in.
|
|
1929
|
+
*
|
|
1930
|
+
* ## WHY THIS IS NOW STATELESS, and what it cost to leave it stateful
|
|
1931
|
+
*
|
|
1932
|
+
* The previous version walked the statement's named children IN ORDER carrying a `pending`
|
|
1933
|
+
* variable, and one of its branches read: *"a bare identifier when something is already pending,
|
|
1934
|
+
* with no items yet, and a different name — that is an ITEM of the pending target."*
|
|
1935
|
+
*
|
|
1936
|
+
* That branch cannot tell a second MODULE from an item of the first. `using A, B` names two
|
|
1937
|
+
* modules, and the corpus's second most common multi-path import shape by volume is exactly that:
|
|
1938
|
+
* 32 two-path and 12 three-path statements in a 400-file draw (`validation/m14/fixtures/julia/
|
|
1939
|
+
* import-export-shapes.ts`, which enumerates every observed shape with counts). The consequence was
|
|
1940
|
+
* silent and total for the tail of a long list — `DataFrames.jl/src/DataFrames.jl:3-19` is one
|
|
1941
|
+
* `using` statement naming **17 modules** and this function published ONE of them, calling the
|
|
1942
|
+
* other sixteen "items" of `Base.Sort`.
|
|
1943
|
+
*
|
|
1944
|
+
* Measured over the whole admitted corpus by the oracle this change was written against
|
|
1945
|
+
* (`script/m14/oracles/julia-imports.ts`, an independent derivation sharing no rule with this one):
|
|
1946
|
+
* **167 of 440 `imports` edges were missing on a 200-file draw — 38% of the type.** Every one of
|
|
1947
|
+
* the `imports` RELATIONSHIPS, every `ImportInfo[]` row and every `references` row carrying
|
|
1948
|
+
* `isImport: true` lost the same dependencies, because all three are written from this one walk.
|
|
1949
|
+
*
|
|
1950
|
+
* The previous instrument could not have found it. `census.ts`'s construct-versus-edge
|
|
1951
|
+
* reconciliation counts FILES containing the construct against files whose edge fired, and a file
|
|
1952
|
+
* whose FIRST module produced an edge passed — the per-file granularity cannot see a missing second
|
|
1953
|
+
* target. A differential against an independent derivation can, and did.
|
|
1954
|
+
*
|
|
1955
|
+
* ## THE RULE, WHICH IS ABOUT THE CHILD AND NOT ITS SIBLINGS
|
|
1956
|
+
*
|
|
1957
|
+
* Every named child is classified on its own, and the question asked of each is "is this child a
|
|
1958
|
+
* module path, or is it a name inside a path's list?" — answerable from the child's own position in
|
|
1959
|
+
* ITS parent, with no knowledge of what came before. Two siblings of the same type therefore cannot
|
|
1960
|
+
* be confused for one another, which is the whole defect class.
|
|
1961
|
+
*
|
|
1962
|
+
* Shapes, all measured over the corpus rather than read out of `grammar.js`:
|
|
1963
|
+
*
|
|
1964
|
+
* `using A` / `import A.B.C` bare path → `identifier` | `scoped_identifier` | `import_path`
|
|
1965
|
+
* `using A, B` two bare paths → two of the above as SIBLINGS — the fixed case
|
|
1966
|
+
* `using A: x, y` path + names → `selected_import`, module is its child 0
|
|
1967
|
+
* `import A as B` renamed import → `import_alias`, module is its child 0
|
|
1968
|
+
*
|
|
1969
|
+
* The last one was dropped entirely before: the `import_alias` branch required `pending`, and a
|
|
1970
|
+
* statement whose ONLY named child is an `import_alias` never had one, so `import ChainRulesCore as
|
|
1971
|
+
* CRC` (`StaticArrays.jl/ext/StaticArraysChainRulesCoreExt.jl`) published nothing at all.
|
|
1972
|
+
*
|
|
1973
|
+
* `import_path` and `scoped_identifier` are taken WHOLE, leading dots and all: the dots are what
|
|
1974
|
+
* distinguishes `using .Foo` from `using ..Foo`, and a dotted path's last segment is not the module
|
|
1975
|
+
* a statement imports (`using Base.GMP.MPZ` imports `Base.GMP.MPZ`, not `MPZ`).
|
|
1976
|
+
*
|
|
1977
|
+
* ## What this function still does not do
|
|
1978
|
+
*
|
|
1979
|
+
* 45 of the corpus's 3,570 import/export statements carry an ERROR descendant, and almost all of
|
|
1980
|
+
* them are legitimate julia the grammar cannot fully parse — `import .Base.Math.@horner`
|
|
1981
|
+
* (`julia/base/special/log.jl:12`, in julia's own stdlib), `import ..test_mod.@old_hyg` (32
|
|
1982
|
+
* statements in `julia/JuliaLowering/test/scopes.jl`), `import B.@mac as @mac2`. The module path
|
|
1983
|
+
* around the unparsed part still parses, so taking the child's whole text records the dependency
|
|
1984
|
+
* that exists. **Refusing these would trade 45 real edges for 2 fabricated ones, which is why the
|
|
1985
|
+
* ERROR rule is applied to `export`/`public` only and for a different reason** — see the
|
|
1986
|
+
* `export_statement` case, where the statement node ITSELF is wrong rather than one part of it.
|
|
1987
|
+
*/
|
|
1988
|
+
importTargets(node) {
|
|
1989
|
+
const out = [];
|
|
1990
|
+
for (const c of this.namedChildren(node)) {
|
|
1991
|
+
if (c.type === "selected_import" || c.type === "import_alias") {
|
|
1992
|
+
const inner = this.namedChildren(c);
|
|
1993
|
+
if (!inner.length) continue;
|
|
1994
|
+
const modName = inner[0].text.trim();
|
|
1995
|
+
if (!modName) continue;
|
|
1996
|
+
const items = c.type === "selected_import" ? inner.slice(1).map((x) => x.text.trim()).filter((x) => x.length > 0) : [];
|
|
1997
|
+
out.push({ name: modName, items, renamed: c.type === "import_alias" || void 0 });
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (c.type === "import_path" || c.type === "identifier" || c.type === "scoped_identifier") {
|
|
2001
|
+
const name = c.text.trim();
|
|
2002
|
+
if (name) out.push({ name, items: [] });
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
return out;
|
|
2006
|
+
}
|
|
2007
|
+
exportableName(node) {
|
|
2008
|
+
if (node.type === "identifier") return node.text.trim();
|
|
2009
|
+
if (node.type === "operator") return node.text.trim();
|
|
2010
|
+
if (node.type === "scoped_identifier") {
|
|
2011
|
+
const segs = this.namedChildren(node).filter((x) => x.type === "identifier");
|
|
2012
|
+
return segs.length ? segs[segs.length - 1].text.trim() : node.text.trim();
|
|
2013
|
+
}
|
|
2014
|
+
if (node.type === "macro_identifier") return macroNameOf(node.text);
|
|
2015
|
+
if (node.type === "call_expression") return this.calleeName(node) ?? "";
|
|
2016
|
+
if (node.type === "tuple_expression" || node.type === "parenthesized_expression") {
|
|
2017
|
+
for (const c of this.namedChildren(node)) {
|
|
2018
|
+
const n = this.exportableName(c);
|
|
2019
|
+
if (n) return n;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return "";
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Evidence tier, the same rules as the swift/dart/csharp/objc family.
|
|
2026
|
+
*/
|
|
2027
|
+
classifyEvidenceTier(rels) {
|
|
2028
|
+
for (const r of rels) {
|
|
2029
|
+
const md = r.metadata ?? {};
|
|
2030
|
+
const t = String(r.type ?? "");
|
|
2031
|
+
if (t === "contains") {
|
|
2032
|
+
md.evidenceClass = "structural";
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (t === "imports" || t === "exports" || t === "references") {
|
|
2036
|
+
md.evidenceClass = "import_based";
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (t === "overrides") {
|
|
2040
|
+
md.evidenceClass = "structural";
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
if (t === "calls" || t === "uses" || t === "instantiates") {
|
|
2044
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.85 ? "usage_contextual" : "inferred";
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (t === "extends" || t === "implements") {
|
|
2048
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Cyclomatic complexity, counted over the tree.
|
|
2055
|
+
*
|
|
2056
|
+
* The walk is GUARDED at 200,000 node visits, and that guard is inherited from the other lanes
|
|
2057
|
+
* rather than invented — the objective-c review measured its own guard at 3.33% of the cap on
|
|
2058
|
+
* the worst corpus file, so the bound is real but never approached there. A truncated value
|
|
2059
|
+
* would be indistinguishable from a true one in the artifact, which is why the cap exists at all
|
|
2060
|
+
* and why its margin is measured rather than assumed.
|
|
2061
|
+
*/
|
|
2062
|
+
complexity(root) {
|
|
2063
|
+
let c = 1;
|
|
2064
|
+
const decisions = /* @__PURE__ */ new Set(["if_statement", "elseif_clause", "for_statement", "while_statement"]);
|
|
2065
|
+
const stack = [root];
|
|
2066
|
+
let guard = 0;
|
|
2067
|
+
while (stack.length > 0 && guard++ < 2e5) {
|
|
2068
|
+
const n = stack.pop();
|
|
2069
|
+
if (decisions.has(n.type)) c++;
|
|
2070
|
+
else if (n.type === "binary_expression") {
|
|
2071
|
+
const opNode = this.pos(n, 1);
|
|
2072
|
+
const op = opNode && opNode.type === "operator" ? opNode.text.trim() : "";
|
|
2073
|
+
if (op === "&&" || op === "||") c++;
|
|
2074
|
+
}
|
|
2075
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2076
|
+
const ch = n.namedChild(i);
|
|
2077
|
+
if (ch) stack.push(ch);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return c;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
export {
|
|
2084
|
+
RealJuliaAnalyzer
|
|
2085
|
+
};
|