@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,3944 @@
|
|
|
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-dart-analyzer.ts
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
16
|
+
var DART_CITATION_ROOTS = /* @__PURE__ */ new Set([
|
|
17
|
+
"override",
|
|
18
|
+
"deprecated",
|
|
19
|
+
"Deprecated",
|
|
20
|
+
"pragma",
|
|
21
|
+
"deprecated_member_use",
|
|
22
|
+
"mustCallSuper",
|
|
23
|
+
"protected",
|
|
24
|
+
"required",
|
|
25
|
+
"immutable",
|
|
26
|
+
"factory",
|
|
27
|
+
"optionalTypeArgs",
|
|
28
|
+
"visibleForTesting",
|
|
29
|
+
"visibleForOverriding",
|
|
30
|
+
"doNotStore",
|
|
31
|
+
"doNotSubmit",
|
|
32
|
+
"mustBeOverridden",
|
|
33
|
+
"redeclare",
|
|
34
|
+
"nonVirtual",
|
|
35
|
+
"recordUse",
|
|
36
|
+
"target",
|
|
37
|
+
"Target",
|
|
38
|
+
"Metadata",
|
|
39
|
+
"Object",
|
|
40
|
+
"String",
|
|
41
|
+
"int",
|
|
42
|
+
"double",
|
|
43
|
+
"bool",
|
|
44
|
+
"List",
|
|
45
|
+
"Map",
|
|
46
|
+
"Set",
|
|
47
|
+
"Future",
|
|
48
|
+
"FutureOr",
|
|
49
|
+
"Stream",
|
|
50
|
+
"Function",
|
|
51
|
+
"Iterable",
|
|
52
|
+
"Duration",
|
|
53
|
+
"DateTime",
|
|
54
|
+
"Exception",
|
|
55
|
+
"Error",
|
|
56
|
+
"StackTrace",
|
|
57
|
+
"RegExp",
|
|
58
|
+
"Uri",
|
|
59
|
+
"BigInt",
|
|
60
|
+
"Symbol",
|
|
61
|
+
"Type",
|
|
62
|
+
"Null",
|
|
63
|
+
"Never",
|
|
64
|
+
"void",
|
|
65
|
+
"dynamic",
|
|
66
|
+
"print",
|
|
67
|
+
"identical",
|
|
68
|
+
"identityHashCode"
|
|
69
|
+
]);
|
|
70
|
+
var DART_CITATION_TYPE_SHAPED_LABELS = /* @__PURE__ */ new Set([
|
|
71
|
+
"calls",
|
|
72
|
+
"uses",
|
|
73
|
+
"instantiates",
|
|
74
|
+
"extends",
|
|
75
|
+
"implements",
|
|
76
|
+
"overrides",
|
|
77
|
+
"mixes_in",
|
|
78
|
+
"part_of",
|
|
79
|
+
// `renders` names a CLASS (the component a `build` method returns), so its target is a
|
|
80
|
+
// type-shaped claim exactly like `extends`/`implements` are.
|
|
81
|
+
"renders"
|
|
82
|
+
]);
|
|
83
|
+
function stripWhitespace(text) {
|
|
84
|
+
let out = "";
|
|
85
|
+
for (let i = 0; i < text.length; i++) {
|
|
86
|
+
const c = text.charCodeAt(i);
|
|
87
|
+
const ws = c === 32 || c === 9 || c === 10 || c === 13 || c === 11 || c === 12 || c >= 160 && (c === 160 || c === 5760 || c >= 8192 && c <= 8202 || c === 8232 || c === 8233 || c === 8239 || c === 8287 || c === 12288 || c === 65279);
|
|
88
|
+
if (!ws) out += text[i];
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function isDottedIdentifier(s) {
|
|
93
|
+
const identStart = (c) => c >= 97 && c <= 122 || c >= 65 && c <= 90 || c === 95 || c === 36;
|
|
94
|
+
const identPart = (c) => c >= 97 && c <= 122 || c >= 65 && c <= 90 || c >= 48 && c <= 57 || c === 95 || c === 36;
|
|
95
|
+
if (s.length === 0 || !identStart(s.charCodeAt(0))) return false;
|
|
96
|
+
let prevDot = false;
|
|
97
|
+
for (let i = 1; i < s.length; i++) {
|
|
98
|
+
const c = s.charCodeAt(i);
|
|
99
|
+
if (c === 46) {
|
|
100
|
+
if (prevDot) return false;
|
|
101
|
+
prevDot = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!identPart(c)) return false;
|
|
105
|
+
prevDot = false;
|
|
106
|
+
}
|
|
107
|
+
return !prevDot;
|
|
108
|
+
}
|
|
109
|
+
function isPlainIdentifier(s) {
|
|
110
|
+
if (s.length === 0) return false;
|
|
111
|
+
const start = s.charCodeAt(0);
|
|
112
|
+
if (!(start >= 97 && start <= 122 || start >= 65 && start <= 90 || start === 95 || start === 36)) return false;
|
|
113
|
+
for (let i = 1; i < s.length; i++) {
|
|
114
|
+
const c = s.charCodeAt(i);
|
|
115
|
+
if (!(c >= 97 && c <= 122 || c >= 65 && c <= 90 || c >= 48 && c <= 57 || c === 95 || c === 36)) return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function stripNullableSuffix(text) {
|
|
120
|
+
let end = text.length;
|
|
121
|
+
while (end > 0 && text.charCodeAt(end - 1) === 63) end--;
|
|
122
|
+
return end === text.length ? text : text.slice(0, end);
|
|
123
|
+
}
|
|
124
|
+
function typeNameWithoutArguments(text) {
|
|
125
|
+
const lt = text.indexOf("<");
|
|
126
|
+
if (lt <= 0) return text;
|
|
127
|
+
if (text.charCodeAt(text.length - 1) !== 62) return text;
|
|
128
|
+
let depth = 0;
|
|
129
|
+
for (let i = lt; i < text.length; i++) {
|
|
130
|
+
const c = text.charCodeAt(i);
|
|
131
|
+
if (c === 60) depth++;
|
|
132
|
+
else if (c === 62) {
|
|
133
|
+
depth--;
|
|
134
|
+
if (depth === 0 && i !== text.length - 1) return text;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (depth !== 0) return text;
|
|
138
|
+
const head = text.slice(0, lt);
|
|
139
|
+
return isPlainIdentifier(head) ? head : text;
|
|
140
|
+
}
|
|
141
|
+
function unquote(text) {
|
|
142
|
+
if (text.length >= 2) {
|
|
143
|
+
const first = text[0];
|
|
144
|
+
if ((first === '"' || first === "'") && text[text.length - 1] === first) {
|
|
145
|
+
return text.slice(1, -1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
function stripCommentMarkers(text) {
|
|
151
|
+
if (text.startsWith("///")) return text.slice(3).trimStart();
|
|
152
|
+
if (text.startsWith("//")) return text.slice(2).trimStart();
|
|
153
|
+
if (text.startsWith("/*")) {
|
|
154
|
+
const end = text.endsWith("*/") ? text.length - 2 : text.length;
|
|
155
|
+
return text.slice(2, end).trim();
|
|
156
|
+
}
|
|
157
|
+
return text.trim();
|
|
158
|
+
}
|
|
159
|
+
function isCapitalized(text) {
|
|
160
|
+
if (text.length === 0) return false;
|
|
161
|
+
const code = text.charCodeAt(0);
|
|
162
|
+
return code >= 65 && code <= 90;
|
|
163
|
+
}
|
|
164
|
+
var IMPLICIT_DYNAMIC_TYPE = "dynamic";
|
|
165
|
+
var DART_UNNAMED_EXTENSION = "unnamed_extension";
|
|
166
|
+
var DART_TYPE_MENTION_DOUBLE_COUNTED = /* @__PURE__ */ new Set([
|
|
167
|
+
// heritage — `extends` / `implements` / `with` (extractClassHeritage)
|
|
168
|
+
"superclass",
|
|
169
|
+
"interfaces",
|
|
170
|
+
"mixins",
|
|
171
|
+
// a mixin `on`-base is an `extends` edge (DARTD14), and a mixin-application
|
|
172
|
+
// class publishes its base and its with-list through the same extractor
|
|
173
|
+
"mixin_declaration",
|
|
174
|
+
"mixin_application",
|
|
175
|
+
// construction is `instantiates`
|
|
176
|
+
"new_expression",
|
|
177
|
+
"const_object_expression",
|
|
178
|
+
"constructor_invocation",
|
|
179
|
+
"redirecting_factory_constructor_signature",
|
|
180
|
+
// `class Box<T>` DECLARES a type variable; it does not mention a type
|
|
181
|
+
"type_parameter",
|
|
182
|
+
// parse damage: not a fact about a well-formed program
|
|
183
|
+
"ERROR"
|
|
184
|
+
]);
|
|
185
|
+
var R7_PLACEHOLDER_NAME = "unknown";
|
|
186
|
+
var DART_OBJECT_TYPE = "Object";
|
|
187
|
+
var DART_OBJECT_INTERFACE_MEMBERS = /* @__PURE__ */ new Set([
|
|
188
|
+
"toString",
|
|
189
|
+
"hashCode",
|
|
190
|
+
"runtimeType",
|
|
191
|
+
"noSuchMethod",
|
|
192
|
+
"operator =="
|
|
193
|
+
]);
|
|
194
|
+
function scopeOwnerName(scopes) {
|
|
195
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
196
|
+
if (scopes[i].kind === "class" /* Class */) return scopes[i].name;
|
|
197
|
+
}
|
|
198
|
+
return void 0;
|
|
199
|
+
}
|
|
200
|
+
function scopeString(scopes) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
for (let i = 1; i < scopes.length; i++) {
|
|
203
|
+
const s = scopes[i];
|
|
204
|
+
if (s.kind !== "global" /* Global */) parts.push(s.name);
|
|
205
|
+
}
|
|
206
|
+
return parts.join(".");
|
|
207
|
+
}
|
|
208
|
+
var DARt_COMPANION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
209
|
+
function dartCompanionFor(projectIndex) {
|
|
210
|
+
if (!projectIndex?.typeMembers) return void 0;
|
|
211
|
+
const key = projectIndex;
|
|
212
|
+
const hit = DARt_COMPANION_CACHE.get(key);
|
|
213
|
+
if (hit) return hit;
|
|
214
|
+
const out = /* @__PURE__ */ new Map();
|
|
215
|
+
const closure = (t, seen) => {
|
|
216
|
+
const cached = out.get(t);
|
|
217
|
+
if (cached) return cached;
|
|
218
|
+
if (seen.has(t)) return /* @__PURE__ */ new Set();
|
|
219
|
+
seen.add(t);
|
|
220
|
+
const members = new Set(projectIndex.typeMembers.get(t) ?? []);
|
|
221
|
+
for (const b of projectIndex.typeBases?.get(t) ?? []) for (const m of closure(b, seen)) members.add(m);
|
|
222
|
+
out.set(t, members);
|
|
223
|
+
return members;
|
|
224
|
+
};
|
|
225
|
+
for (const t of projectIndex.typeKinds?.keys() ?? []) closure(t, /* @__PURE__ */ new Set());
|
|
226
|
+
DARt_COMPANION_CACHE.set(key, out);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
var DART_PROJECT_ROOT_CACHE = /* @__PURE__ */ new Map();
|
|
230
|
+
var DART_PROJECT_FACTS_CACHE = /* @__PURE__ */ new Map();
|
|
231
|
+
var DART_WALK_SKIP = /* @__PURE__ */ new Set([
|
|
232
|
+
".git",
|
|
233
|
+
".hg",
|
|
234
|
+
".svn",
|
|
235
|
+
"node_modules",
|
|
236
|
+
".dart_tool",
|
|
237
|
+
".symlinks",
|
|
238
|
+
"build",
|
|
239
|
+
"coverage",
|
|
240
|
+
".pub-cache",
|
|
241
|
+
"out"
|
|
242
|
+
]);
|
|
243
|
+
function dartProjectRootOf(filePath) {
|
|
244
|
+
const start = path.dirname(path.resolve(filePath));
|
|
245
|
+
const seen = [];
|
|
246
|
+
let dir = start;
|
|
247
|
+
for (let i = 0; i < 64; i++) {
|
|
248
|
+
const cached = DART_PROJECT_ROOT_CACHE.get(dir);
|
|
249
|
+
if (cached !== void 0) {
|
|
250
|
+
for (const d of seen) DART_PROJECT_ROOT_CACHE.set(d, cached);
|
|
251
|
+
return cached;
|
|
252
|
+
}
|
|
253
|
+
seen.push(dir);
|
|
254
|
+
if (existsSync(path.join(dir, ".git"))) {
|
|
255
|
+
if (DART_PROJECT_ROOT_CACHE.size > 2e4) DART_PROJECT_ROOT_CACHE.clear();
|
|
256
|
+
for (const d of seen) DART_PROJECT_ROOT_CACHE.set(d, dir);
|
|
257
|
+
return dir;
|
|
258
|
+
}
|
|
259
|
+
const parent = path.dirname(dir);
|
|
260
|
+
if (parent === dir) break;
|
|
261
|
+
dir = parent;
|
|
262
|
+
}
|
|
263
|
+
if (DART_PROJECT_ROOT_CACHE.size > 2e4) DART_PROJECT_ROOT_CACHE.clear();
|
|
264
|
+
for (const d of seen) DART_PROJECT_ROOT_CACHE.set(d, null);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function isDartLibraryName(text) {
|
|
268
|
+
if (text.length === 0) return false;
|
|
269
|
+
const first = text.charCodeAt(0);
|
|
270
|
+
if (first < 97 || first > 122) return false;
|
|
271
|
+
for (let i = 1; i < text.length; i++) {
|
|
272
|
+
const c = text.charCodeAt(i);
|
|
273
|
+
const ok = c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95;
|
|
274
|
+
if (!ok) return false;
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
function dartPubspecName(file) {
|
|
279
|
+
let text;
|
|
280
|
+
try {
|
|
281
|
+
text = readFileSync(file, "utf8");
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
let lineStart = 0;
|
|
286
|
+
for (let i = 0; i <= text.length; i++) {
|
|
287
|
+
if (i < text.length && text.charCodeAt(i) !== 10) continue;
|
|
288
|
+
const line = text.slice(lineStart, i);
|
|
289
|
+
lineStart = i + 1;
|
|
290
|
+
if (!line.startsWith("name:")) continue;
|
|
291
|
+
let v = 5;
|
|
292
|
+
while (v < line.length && (line.charCodeAt(v) === 32 || line.charCodeAt(v) === 9)) v++;
|
|
293
|
+
let end = v;
|
|
294
|
+
while (end < line.length) {
|
|
295
|
+
const c = line.charCodeAt(end);
|
|
296
|
+
if (c === 32 || c === 9 || c === 13 || c === 35) break;
|
|
297
|
+
end++;
|
|
298
|
+
}
|
|
299
|
+
const value = line.slice(v, end);
|
|
300
|
+
return isDartLibraryName(value) ? value : null;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
function dartProjectFacts(projectRoot) {
|
|
305
|
+
const hit = DART_PROJECT_FACTS_CACHE.get(projectRoot);
|
|
306
|
+
if (hit) return hit;
|
|
307
|
+
const facts = {
|
|
308
|
+
packageRoots: /* @__PURE__ */ new Map(),
|
|
309
|
+
sdkLib: existsSync(path.join(projectRoot, "sdk", "lib")),
|
|
310
|
+
dartLibs: /* @__PURE__ */ new Map()
|
|
311
|
+
};
|
|
312
|
+
const stack = [projectRoot];
|
|
313
|
+
while (stack.length > 0) {
|
|
314
|
+
const dir = stack.pop();
|
|
315
|
+
let entries;
|
|
316
|
+
try {
|
|
317
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
318
|
+
} catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (entry.isDirectory()) {
|
|
323
|
+
if (DART_WALK_SKIP.has(entry.name)) continue;
|
|
324
|
+
stack.push(path.join(dir, entry.name));
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (entry.name !== "pubspec.yaml" || !entry.isFile()) continue;
|
|
328
|
+
const name = dartPubspecName(path.join(dir, entry.name));
|
|
329
|
+
if (name === null || facts.packageRoots.has(name)) continue;
|
|
330
|
+
const rel = path.relative(projectRoot, dir);
|
|
331
|
+
facts.packageRoots.set(name, rel.split(path.sep).join("/"));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (DART_PROJECT_FACTS_CACHE.size >= 64) DART_PROJECT_FACTS_CACHE.clear();
|
|
335
|
+
DART_PROJECT_FACTS_CACHE.set(projectRoot, facts);
|
|
336
|
+
return facts;
|
|
337
|
+
}
|
|
338
|
+
function dartRepoRelPath(projectRoot, absPath) {
|
|
339
|
+
const rel = path.relative(projectRoot, path.resolve(absPath));
|
|
340
|
+
if (rel.length === 0 || path.isAbsolute(rel)) return null;
|
|
341
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`)) return null;
|
|
342
|
+
return rel.split(path.sep).join("/");
|
|
343
|
+
}
|
|
344
|
+
function dartJoinInside(baseRel, tail) {
|
|
345
|
+
const base = baseRel.length === 0 ? "" : `${baseRel}/`;
|
|
346
|
+
const norm = path.posix.normalize(`${base}${tail}`);
|
|
347
|
+
if (norm.length === 0 || norm === "." || norm === ".." || norm.startsWith("../")) return null;
|
|
348
|
+
return norm;
|
|
349
|
+
}
|
|
350
|
+
function dartImportTargetPath(projectRoot, filePath, uri) {
|
|
351
|
+
if (uri.length === 0) return null;
|
|
352
|
+
if (uri.startsWith("package:")) {
|
|
353
|
+
const rest = uri.slice(8);
|
|
354
|
+
const slash = rest.indexOf("/");
|
|
355
|
+
if (slash <= 0 || slash === rest.length - 1) return null;
|
|
356
|
+
const pkg = rest.slice(0, slash);
|
|
357
|
+
const sub = rest.slice(slash + 1);
|
|
358
|
+
if (!isDartLibraryName(pkg)) return null;
|
|
359
|
+
const packageRoot = dartProjectFacts(projectRoot).packageRoots.get(pkg);
|
|
360
|
+
if (packageRoot === void 0) return null;
|
|
361
|
+
return dartJoinInside(packageRoot, `lib/${sub}`);
|
|
362
|
+
}
|
|
363
|
+
if (uri.startsWith("dart:")) {
|
|
364
|
+
const lib = uri.slice(5);
|
|
365
|
+
if (!isDartLibraryName(lib)) return null;
|
|
366
|
+
const facts = dartProjectFacts(projectRoot);
|
|
367
|
+
if (!facts.sdkLib) return null;
|
|
368
|
+
const cached = facts.dartLibs.get(lib);
|
|
369
|
+
if (cached !== void 0) return cached;
|
|
370
|
+
const rel = `sdk/lib/${lib}/${lib}.dart`;
|
|
371
|
+
const found = existsSync(path.join(projectRoot, rel)) ? rel : null;
|
|
372
|
+
facts.dartLibs.set(lib, found);
|
|
373
|
+
return found;
|
|
374
|
+
}
|
|
375
|
+
if (uri.indexOf(":") >= 0) return null;
|
|
376
|
+
const abs = path.resolve(path.dirname(path.resolve(filePath)), uri);
|
|
377
|
+
return dartRepoRelPath(projectRoot, abs);
|
|
378
|
+
}
|
|
379
|
+
function isCapitalisedIdentifier(name) {
|
|
380
|
+
if (name.length === 0) return false;
|
|
381
|
+
const first = name.charCodeAt(0);
|
|
382
|
+
if (first < 65 || first > 90) return false;
|
|
383
|
+
for (let i = 1; i < name.length; i++) {
|
|
384
|
+
const c = name.charCodeAt(i);
|
|
385
|
+
if (c >= 65 && c <= 90) continue;
|
|
386
|
+
if (c >= 97 && c <= 122) continue;
|
|
387
|
+
if (c >= 48 && c <= 57) continue;
|
|
388
|
+
if (c === 95 || c === 36) continue;
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
var RealDartAnalyzer = class _RealDartAnalyzer {
|
|
394
|
+
analyze(rootNode, content, filePath, context) {
|
|
395
|
+
this.scopeDeclIndex.clear();
|
|
396
|
+
this.methodComplexityCache.clear();
|
|
397
|
+
const symbols = [];
|
|
398
|
+
const relationships = [];
|
|
399
|
+
const imports = [];
|
|
400
|
+
const exports = [];
|
|
401
|
+
const errors = [];
|
|
402
|
+
const frameworks = { Dart: true };
|
|
403
|
+
const patterns = {};
|
|
404
|
+
const scopes = [{ name: "__global__", kind: "global" /* Global */ }];
|
|
405
|
+
const library = this.libraryName(rootNode, filePath);
|
|
406
|
+
const moduleName = library.name;
|
|
407
|
+
const projectRoot = dartProjectRootOf(filePath);
|
|
408
|
+
const librarySite = {
|
|
409
|
+
moduleName,
|
|
410
|
+
filePath,
|
|
411
|
+
projectRoot,
|
|
412
|
+
moduleIdentity: projectRoot === null ? null : dartRepoRelPath(projectRoot, filePath)
|
|
413
|
+
};
|
|
414
|
+
symbols.push({
|
|
415
|
+
name: moduleName,
|
|
416
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
417
|
+
// THE LIBRARY'S URI. Strategy 1 of the shared resolver matches `qualified_name` exactly,
|
|
418
|
+
// so publishing the path here is what lets an import edge name this file. Unset when the
|
|
419
|
+
// file is not inside a checkout: then the engine's own synthesis (`<scope>.<name>`) is
|
|
420
|
+
// what it always was, and the edges keep their specifier targets (see LIBRARY IDENTITY).
|
|
421
|
+
...librarySite.moduleIdentity === null ? {} : { qualifiedName: librarySite.moduleIdentity },
|
|
422
|
+
range: library.directive ? this.nodeToRange(library.directive) : { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startIndex: 0, endIndex: 0 },
|
|
423
|
+
visibility: "public",
|
|
424
|
+
scope: moduleName,
|
|
425
|
+
metadata: { nodeType: library.directive ? library.directive.type : "implicit_library" }
|
|
426
|
+
});
|
|
427
|
+
if (rootNode.hasError) {
|
|
428
|
+
const errStack = [rootNode];
|
|
429
|
+
while (errStack.length > 0) {
|
|
430
|
+
const n = errStack.pop();
|
|
431
|
+
if (n.type === "ERROR" || n.isMissing) {
|
|
432
|
+
errors.push({
|
|
433
|
+
message: `Syntax ${n.type} detected. The Brain Engine is bypassing this broken node.`,
|
|
434
|
+
range: this.nodeToRange(n),
|
|
435
|
+
severity: "warning"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
439
|
+
const child = n.namedChild(i);
|
|
440
|
+
if (child) errStack.push(child);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const stack = [{ node: rootNode, scopes, functionDepth: 0 }];
|
|
445
|
+
let cyclomaticComplexity = 1;
|
|
446
|
+
const seenMembers = /* @__PURE__ */ new Map();
|
|
447
|
+
const nameLines = /* @__PURE__ */ new Map();
|
|
448
|
+
while (stack.length > 0) {
|
|
449
|
+
const { node, scopes: frameScopes, functionDepth } = stack.pop();
|
|
450
|
+
const scopeStack = [...frameScopes];
|
|
451
|
+
let nextFunctionDepth = functionDepth;
|
|
452
|
+
const t = node.type;
|
|
453
|
+
if (t === "if_statement" || t === "for_statement" || t === "while_statement" || t === "do_statement" || t === "switch_statement_case" || t === "switch_expression_case" || t === "catch_clause" || t === "conditional_expression" || t === "logical_and_expression" || t === "logical_or_expression") {
|
|
454
|
+
cyclomaticComplexity++;
|
|
455
|
+
}
|
|
456
|
+
switch (t) {
|
|
457
|
+
case "class_definition": {
|
|
458
|
+
const name = node.childForFieldName("name")?.text || (this.findChildByType(node, "mixin_application_class") ? this.findDescendantByType(node, "identifier")?.text : void 0);
|
|
459
|
+
if (name) {
|
|
460
|
+
const comments = this.collectComments(node);
|
|
461
|
+
const isSealed = this.hasModifier(node, "sealed");
|
|
462
|
+
const isBase = this.hasModifier(node, "base");
|
|
463
|
+
const isInterfaceClass = this.hasModifier(node, "interface");
|
|
464
|
+
const isFinalClass = this.hasModifier(node, "final");
|
|
465
|
+
const isAbstract = this.hasModifier(node, "abstract") || isSealed;
|
|
466
|
+
symbols.push({
|
|
467
|
+
name,
|
|
468
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
469
|
+
range: this.nodeToRange(node),
|
|
470
|
+
scope: scopeString(scopeStack),
|
|
471
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
472
|
+
comments,
|
|
473
|
+
isAbstract,
|
|
474
|
+
genericParameters: this.extractTypeParameters(node),
|
|
475
|
+
metadata: { isClass: true, isAbstract, isSealed, isBase, isInterfaceClass, isFinalClass }
|
|
476
|
+
});
|
|
477
|
+
scopeStack.push({ name, kind: "class" /* Class */ });
|
|
478
|
+
this.extractClassHeritage(node, name, relationships, patterns);
|
|
479
|
+
this.extractRenderRelationships(node, name, relationships);
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case "mixin_declaration": {
|
|
484
|
+
const name = node.childForFieldName("name")?.text || this.findChildByType(node, "identifier")?.text;
|
|
485
|
+
if (name) {
|
|
486
|
+
const comments = this.collectComments(node);
|
|
487
|
+
patterns["Mixins"] = true;
|
|
488
|
+
symbols.push({
|
|
489
|
+
name,
|
|
490
|
+
kind: ASTEngine.SymbolKind.Mixin,
|
|
491
|
+
range: this.nodeToRange(node),
|
|
492
|
+
scope: scopeString(scopeStack),
|
|
493
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
494
|
+
comments,
|
|
495
|
+
metadata: { isMixin: true }
|
|
496
|
+
});
|
|
497
|
+
scopeStack.push({ name, kind: "class" /* Class */ });
|
|
498
|
+
this.extractClassHeritage(node, name, relationships, patterns);
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case "extension_declaration": {
|
|
503
|
+
const targetType = this.extensionTargetType(node);
|
|
504
|
+
const name = this.extensionPublishedName(node);
|
|
505
|
+
if (name) {
|
|
506
|
+
const comments = this.collectComments(node);
|
|
507
|
+
patterns["Extensions"] = true;
|
|
508
|
+
symbols.push({
|
|
509
|
+
name,
|
|
510
|
+
kind: ASTEngine.SymbolKind.Extension,
|
|
511
|
+
range: this.nodeToRange(node),
|
|
512
|
+
scope: scopeString(scopeStack),
|
|
513
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
514
|
+
comments,
|
|
515
|
+
metadata: { isExtension: true, targetType, isUnnamedExtension: !node.childForFieldName("name") }
|
|
516
|
+
});
|
|
517
|
+
scopeStack.push({ name, kind: "class" /* Class */ });
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "extension_type_declaration": {
|
|
522
|
+
const name = node.childForFieldName("name")?.text;
|
|
523
|
+
if (name) {
|
|
524
|
+
const comments = this.collectComments(node);
|
|
525
|
+
const rep = this.findChildByType(node, "representation_declaration");
|
|
526
|
+
const repType = this.representationTypeName(rep);
|
|
527
|
+
patterns["Extensions"] = true;
|
|
528
|
+
symbols.push({
|
|
529
|
+
name,
|
|
530
|
+
kind: ASTEngine.SymbolKind.Extension,
|
|
531
|
+
range: this.nodeToRange(node),
|
|
532
|
+
scope: scopeString(scopeStack),
|
|
533
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
534
|
+
comments,
|
|
535
|
+
metadata: { isExtension: true, isExtensionType: true, representationType: repType }
|
|
536
|
+
});
|
|
537
|
+
scopeStack.push({ name, kind: "class" /* Class */ });
|
|
538
|
+
const repName = rep?.childForFieldName("name")?.text;
|
|
539
|
+
if (rep && repName && repName !== "_") {
|
|
540
|
+
const repRange = this.nodeToRange(rep);
|
|
541
|
+
const repNameNode = rep.childForFieldName("name");
|
|
542
|
+
if (repNameNode) nameLines.set(dartNameLineKey(repName, repRange), this.nodeLine(repNameNode));
|
|
543
|
+
symbols.push({
|
|
544
|
+
name: repName,
|
|
545
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
546
|
+
range: repRange,
|
|
547
|
+
scope: name,
|
|
548
|
+
ownerName: name,
|
|
549
|
+
ownerKind: ASTEngine.SymbolKind.Extension,
|
|
550
|
+
visibility: repName.startsWith("_") ? "private" : "public",
|
|
551
|
+
metadata: { isRepresentationField: true, dataType: repType }
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
this.extractClassHeritage(node, name, relationships, patterns);
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case "enum_declaration": {
|
|
559
|
+
const name = node.childForFieldName("name")?.text;
|
|
560
|
+
if (name) {
|
|
561
|
+
const comments = this.collectComments(node);
|
|
562
|
+
symbols.push({
|
|
563
|
+
name,
|
|
564
|
+
kind: ASTEngine.SymbolKind.Enum,
|
|
565
|
+
range: this.nodeToRange(node),
|
|
566
|
+
scope: scopeString(scopeStack),
|
|
567
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
568
|
+
comments,
|
|
569
|
+
metadata: { isEnum: true }
|
|
570
|
+
});
|
|
571
|
+
const body = this.findChildByType(node, "enum_body");
|
|
572
|
+
if (body) {
|
|
573
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
574
|
+
const c = body.namedChild(i);
|
|
575
|
+
if (c && c.type === "enum_constant") {
|
|
576
|
+
const mname = c.childForFieldName("name")?.text;
|
|
577
|
+
if (mname) {
|
|
578
|
+
symbols.push({
|
|
579
|
+
name: mname,
|
|
580
|
+
kind: ASTEngine.SymbolKind.EnumMember,
|
|
581
|
+
range: this.nodeToRange(c),
|
|
582
|
+
scope: name,
|
|
583
|
+
ownerName: name,
|
|
584
|
+
ownerKind: ASTEngine.SymbolKind.Enum,
|
|
585
|
+
visibility: mname.startsWith("_") ? "private" : "public",
|
|
586
|
+
metadata: { isEnumMember: true }
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
this.extractClassHeritage(node, name, relationships, patterns);
|
|
593
|
+
scopeStack.push({ name, kind: "class" /* Class */ });
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case "type_alias": {
|
|
598
|
+
const nameNode = node.childForFieldName("name") || this.findChildByType(node, "type_identifier");
|
|
599
|
+
const name = nameNode?.text;
|
|
600
|
+
if (name && name !== "Function") {
|
|
601
|
+
const comments = this.collectComments(node);
|
|
602
|
+
symbols.push({
|
|
603
|
+
name,
|
|
604
|
+
kind: ASTEngine.SymbolKind.TypeAlias,
|
|
605
|
+
range: this.nodeToRange(node),
|
|
606
|
+
scope: scopeString(scopeStack),
|
|
607
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
608
|
+
comments,
|
|
609
|
+
metadata: { isTypeAlias: true }
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
case "declaration": {
|
|
615
|
+
const sig = this.findSignatureIn(node);
|
|
616
|
+
if (sig) {
|
|
617
|
+
if (this.isConstructorSignature(sig.type)) {
|
|
618
|
+
const owner = scopeOwnerName(scopeStack);
|
|
619
|
+
if (owner && this.isRealConstructor(sig, owner)) {
|
|
620
|
+
this.processConstructor(node, sig, owner, symbols, relationships, patterns);
|
|
621
|
+
} else if (owner) {
|
|
622
|
+
this.processFunctionOrMethod(node, sig, symbols, scopeStack, patterns, seenMembers, nameLines);
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
this.processFunctionOrMethod(node, sig, symbols, scopeStack, patterns, seenMembers, nameLines);
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
this.processFieldOrVariable(node, symbols, scopeStack, nameLines);
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case "method_signature":
|
|
633
|
+
case "function_signature":
|
|
634
|
+
case "getter_signature":
|
|
635
|
+
case "setter_signature":
|
|
636
|
+
case "function_declaration": {
|
|
637
|
+
if (node.parent?.type === "declaration") break;
|
|
638
|
+
if (node.parent?.type === "method_signature") break;
|
|
639
|
+
if (t === "function_signature" && node.parent?.type === "lambda_expression") break;
|
|
640
|
+
const owner = scopeOwnerName(scopeStack);
|
|
641
|
+
const inner = this.findInnerSignature(node);
|
|
642
|
+
if (owner && this.isConstructorSignature(inner.type) && this.isRealConstructor(inner, owner)) {
|
|
643
|
+
this.processConstructor(node, inner, owner, symbols, relationships, patterns);
|
|
644
|
+
} else if (owner || t === "function_declaration") {
|
|
645
|
+
this.processFunctionOrMethod(node, node, symbols, scopeStack, patterns, seenMembers, nameLines);
|
|
646
|
+
} else if (t === "function_signature" || t === "getter_signature" || t === "setter_signature") {
|
|
647
|
+
this.processTopLevelFunction(node, symbols, scopeStack, patterns, nameLines, seenMembers);
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case "constructor_signature":
|
|
652
|
+
case "factory_constructor_signature":
|
|
653
|
+
case "constant_constructor_signature":
|
|
654
|
+
case "redirecting_factory_constructor_signature": {
|
|
655
|
+
const ptype = node.parent?.type;
|
|
656
|
+
if (ptype === "declaration" || ptype === "method_signature") break;
|
|
657
|
+
const owner = scopeOwnerName(scopeStack);
|
|
658
|
+
if (owner && this.isRealConstructor(node, owner)) {
|
|
659
|
+
this.processConstructor(node, node, owner, symbols, relationships, patterns);
|
|
660
|
+
} else if (owner) {
|
|
661
|
+
this.processFunctionOrMethod(node, node, symbols, scopeStack, patterns, seenMembers, nameLines);
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "initialized_identifier_list":
|
|
666
|
+
case "static_final_declaration_list": {
|
|
667
|
+
const parent = node.parent;
|
|
668
|
+
if (parent && parent.type === "program") {
|
|
669
|
+
this.processTopLevelVariables(node, symbols, scopeStack, nameLines);
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case "import_specification":
|
|
674
|
+
case "library_export":
|
|
675
|
+
case "part_directive":
|
|
676
|
+
case "part_of_directive": {
|
|
677
|
+
this.processImportExport(node, imports, exports, frameworks, relationships, librarySite);
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
682
|
+
const child = node.namedChild(i);
|
|
683
|
+
if (child) {
|
|
684
|
+
stack.push({ node: child, scopes: scopeStack, functionDepth: nextFunctionDepth });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
this.addContainmentRelationships(symbols, relationships, nameLines);
|
|
689
|
+
this.extractOverrideRelationships(symbols, relationships, context?.overrideCompanion, nameLines, context?.projectIndex);
|
|
690
|
+
this.extractExpressionRelationships(rootNode, relationships, moduleName, symbols, context?.projectIndex);
|
|
691
|
+
{
|
|
692
|
+
const seenRel = /* @__PURE__ */ new Set();
|
|
693
|
+
const uniq = [];
|
|
694
|
+
for (const r of relationships) {
|
|
695
|
+
if (r.source === R7_PLACEHOLDER_NAME || r.target === R7_PLACEHOLDER_NAME || r.from === R7_PLACEHOLDER_NAME || r.to === R7_PLACEHOLDER_NAME) continue;
|
|
696
|
+
const key = `${r.type}|${r.source}|${r.target}|${r.confidence}`;
|
|
697
|
+
const scope = r.metadata?.localBindingScope;
|
|
698
|
+
const dedupKey = scope !== void 0 && scope !== null ? `${key}|${scope}` : key;
|
|
699
|
+
if (seenRel.has(dedupKey)) continue;
|
|
700
|
+
seenRel.add(dedupKey);
|
|
701
|
+
uniq.push(r);
|
|
702
|
+
}
|
|
703
|
+
relationships.splice(0, relationships.length, ...uniq);
|
|
704
|
+
}
|
|
705
|
+
this.classifyEvidenceTier(relationships);
|
|
706
|
+
{
|
|
707
|
+
const fieldTypeByClass = /* @__PURE__ */ new Map();
|
|
708
|
+
for (const s of symbols) {
|
|
709
|
+
if ((s.metadata ?? {}).isField && s.ownerName) {
|
|
710
|
+
const dt = s.metadata.dataType;
|
|
711
|
+
if (dt) fieldTypeByClass.set(`${s.ownerName}.${s.name}`, dt);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const s of symbols) {
|
|
715
|
+
if (s.kind === ASTEngine.SymbolKind.Constructor && s.ownerName) {
|
|
716
|
+
for (const p of s.parameters ?? []) {
|
|
717
|
+
if (p.type === IMPLICIT_DYNAMIC_TYPE) {
|
|
718
|
+
const ft = fieldTypeByClass.get(`${s.ownerName}.${p.name}`);
|
|
719
|
+
if (ft) p.type = ft;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const architecturalScore = Math.min(100, Math.max(0, 100 - errors.length * 5));
|
|
726
|
+
const detectedPatterns = Object.keys(patterns);
|
|
727
|
+
const detectedFrameworks = Object.keys(frameworks);
|
|
728
|
+
const frameworkPatterns = detectedPatterns.map((p) => ({
|
|
729
|
+
framework: "Dart",
|
|
730
|
+
patterns: [{ type: "usage", detected: true, description: p }]
|
|
731
|
+
}));
|
|
732
|
+
if (process.env.BB_DART_CITATION === "1") {
|
|
733
|
+
for (const r of relationships) {
|
|
734
|
+
const t = String(r.target ?? "");
|
|
735
|
+
if (!DART_CITATION_ROOTS.has(t)) continue;
|
|
736
|
+
const md = r.metadata ?? {};
|
|
737
|
+
const isAnnotation = md.isAnnotation === true;
|
|
738
|
+
const label = String(r.type);
|
|
739
|
+
const typeShaped = DART_CITATION_TYPE_SHAPED_LABELS.has(label) && isCapitalisedIdentifier(t);
|
|
740
|
+
if (!isAnnotation && !typeShaped) continue;
|
|
741
|
+
md.languageNamespace = [t];
|
|
742
|
+
md.languageNamespacePolicy = "strict-absent";
|
|
743
|
+
r.metadata = md;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (let i = relationships.length - 1; i >= 0; i--) {
|
|
747
|
+
const r = relationships[i];
|
|
748
|
+
const target = String(r.target ?? "");
|
|
749
|
+
const source = String(r.source ?? "");
|
|
750
|
+
if (target.length === 0 || source.length === 0) relationships.splice(i, 1);
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
language: "dart",
|
|
754
|
+
symbols,
|
|
755
|
+
relationships,
|
|
756
|
+
imports,
|
|
757
|
+
exports,
|
|
758
|
+
errors,
|
|
759
|
+
metadata: {
|
|
760
|
+
complexity: symbols.length + relationships.length,
|
|
761
|
+
linesOfCode: content.split("\n").length,
|
|
762
|
+
cyclomaticComplexity,
|
|
763
|
+
framework: detectedFrameworks.length > 1 ? detectedFrameworks[1] : detectedFrameworks[0],
|
|
764
|
+
patterns: detectedPatterns,
|
|
765
|
+
detectedFrameworks,
|
|
766
|
+
frameworkPatterns,
|
|
767
|
+
architecturalInsights: [],
|
|
768
|
+
architecturalScore,
|
|
769
|
+
codeQuality: errors.length === 0 ? "Excellent" : "Fair",
|
|
770
|
+
hasTypeInference: true,
|
|
771
|
+
hasFrameworkIntelligence: true,
|
|
772
|
+
analysisVersion: "4.0.0-m12.11-family-contract"
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
// ============================================================================
|
|
777
|
+
// SYMBOL PROCESSORS
|
|
778
|
+
// ============================================================================
|
|
779
|
+
processConstructor(node, sig, owner, symbols, relationships, patterns) {
|
|
780
|
+
const isFactory = sig.type === "factory_constructor_signature" || sig.type === "redirecting_factory_constructor_signature";
|
|
781
|
+
const isConst = sig.type === "constant_constructor_signature";
|
|
782
|
+
if (isFactory) patterns["Factory"] = true;
|
|
783
|
+
let isNamed = false;
|
|
784
|
+
let namedSegment;
|
|
785
|
+
const directIds = this.directIdentifiers(sig);
|
|
786
|
+
const named = directIds.find((id) => id !== owner);
|
|
787
|
+
if (named) {
|
|
788
|
+
isNamed = true;
|
|
789
|
+
namedSegment = named;
|
|
790
|
+
}
|
|
791
|
+
const params = this.extractParameters(sig);
|
|
792
|
+
const comments = this.collectComments(node);
|
|
793
|
+
const annotations = this.extractAnnotations(node);
|
|
794
|
+
symbols.push({
|
|
795
|
+
name: "constructor",
|
|
796
|
+
kind: ASTEngine.SymbolKind.Constructor,
|
|
797
|
+
range: this.nodeToRange(node),
|
|
798
|
+
scope: owner,
|
|
799
|
+
ownerName: owner,
|
|
800
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
801
|
+
visibility: "public",
|
|
802
|
+
parameters: params,
|
|
803
|
+
comments,
|
|
804
|
+
decorators: annotations,
|
|
805
|
+
metadata: { isFactory, isConst, isNamed, namedSegment, isConstructor: true }
|
|
806
|
+
});
|
|
807
|
+
if (isFactory && sig.type === "redirecting_factory_constructor_signature") {
|
|
808
|
+
const parts = [];
|
|
809
|
+
const partNodes = [];
|
|
810
|
+
let pastEquals = false;
|
|
811
|
+
for (let i = 0; i < sig.childCount; i++) {
|
|
812
|
+
const c = sig.child(i);
|
|
813
|
+
if (!c) continue;
|
|
814
|
+
if (!pastEquals) {
|
|
815
|
+
if (!c.isNamed && c.type === "=") pastEquals = true;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (c.isNamed && (c.type === "type_identifier" || c.type === "identifier")) {
|
|
819
|
+
parts.push(c.text);
|
|
820
|
+
partNodes.push(c);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const redirectTarget = parts.length <= 1 ? parts[0] : parts[parts.length - 2];
|
|
824
|
+
const redirectNode = partNodes.length <= 1 ? partNodes[0] : partNodes[partNodes.length - 2];
|
|
825
|
+
if (redirectTarget) {
|
|
826
|
+
relationships.push({
|
|
827
|
+
source: owner,
|
|
828
|
+
target: redirectTarget,
|
|
829
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
830
|
+
confidence: 0.95,
|
|
831
|
+
line_number: this.nodeLine(redirectNode),
|
|
832
|
+
metadata: { isFactoryRedirection: true }
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
processFunctionOrMethod(node, sig, symbols, scopeStack, patterns, seenMembers, nameLines) {
|
|
838
|
+
const inner = this.findInnerSignature(sig);
|
|
839
|
+
const nameNode = this.signatureNameNode(inner);
|
|
840
|
+
const name = this.signatureName(inner);
|
|
841
|
+
if (!name) return;
|
|
842
|
+
const owner = scopeOwnerName(scopeStack);
|
|
843
|
+
const isGetter0 = inner.type === "getter_signature";
|
|
844
|
+
const isSetter0 = inner.type === "setter_signature";
|
|
845
|
+
const dedupKey = `${owner ?? "module"}:${name}:${isGetter0 ? "get" : isSetter0 ? "set" : "fn"}`;
|
|
846
|
+
const isGetter = isGetter0;
|
|
847
|
+
const isSetter = isSetter0;
|
|
848
|
+
if (isGetter || isSetter) patterns["Getters and Setters"] = true;
|
|
849
|
+
const params = this.extractParameters(inner);
|
|
850
|
+
const returnType = this.extractReturnType(inner);
|
|
851
|
+
const isAsync = this.isAsyncSignature(node, inner);
|
|
852
|
+
const isStatic = this.hasModifier(node, "static") || this.hasModifier(inner, "static");
|
|
853
|
+
const isAbstract = !this.hasBody(node) && (this.hasModifier(node, "abstract") || owner !== void 0);
|
|
854
|
+
const comments = this.collectComments(node);
|
|
855
|
+
const annotations = this.extractAnnotations(node);
|
|
856
|
+
if (isAsync || returnType && returnType.startsWith("Future")) patterns["Async Programming"] = true;
|
|
857
|
+
const kind = isGetter || isSetter ? ASTEngine.SymbolKind.Property : owner ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function;
|
|
858
|
+
const symbol = {
|
|
859
|
+
name,
|
|
860
|
+
kind,
|
|
861
|
+
range: this.rangeIncludingBody(node, inner),
|
|
862
|
+
scope: scopeString(scopeStack),
|
|
863
|
+
ownerName: owner,
|
|
864
|
+
ownerKind: owner ? scopeOwnerKind(scopeStack) : void 0,
|
|
865
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
866
|
+
parameters: params,
|
|
867
|
+
returnType,
|
|
868
|
+
comments,
|
|
869
|
+
decorators: annotations,
|
|
870
|
+
isStatic,
|
|
871
|
+
isAsync,
|
|
872
|
+
isAbstract,
|
|
873
|
+
complexity: this.methodComplexity(node),
|
|
874
|
+
metadata: { isGetter, isSetter, isStatic, isAsync, isAbstract, hasBody: this.hasBody(node) }
|
|
875
|
+
};
|
|
876
|
+
if (!this.registerCallable(symbols, seenMembers, dedupKey, symbol, nameLines, nameNode)) return;
|
|
877
|
+
scopeStack.push({ name, kind: "function" /* Function */ });
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* One symbol per (scope,name,kind), LAST declaration wins (DARTD20 — the
|
|
881
|
+
* family dup-key ruling; #753). Both producers of callable symbols use this:
|
|
882
|
+
* a duplicate is a compile error in Dart, so the analyzer's answer for the
|
|
883
|
+
* shape is a deterministic contract, and the family's (settled by python
|
|
884
|
+
* #748) is that the LAST declaration owns the name. Returns false when the
|
|
885
|
+
* symbol REPLACED an earlier one, so the caller can skip its scope push.
|
|
886
|
+
*/
|
|
887
|
+
registerCallable(symbols, seenMembers, dedupKey, symbol, nameLines, nameNode) {
|
|
888
|
+
if (nameNode) nameLines.set(dartNameLineKey(symbol.name, symbol.range), this.nodeLine(nameNode));
|
|
889
|
+
const priorIdx = seenMembers.get(dedupKey);
|
|
890
|
+
if (priorIdx !== void 0) {
|
|
891
|
+
symbols[priorIdx] = symbol;
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
seenMembers.set(dedupKey, symbols.length);
|
|
895
|
+
symbols.push(symbol);
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
processTopLevelFunction(node, symbols, scopeStack, patterns, nameLines, seenMembers) {
|
|
899
|
+
const nameNode = node.childForFieldName("name") || this.findChildByType(node, "identifier");
|
|
900
|
+
if (!nameNode) return;
|
|
901
|
+
const name = nameNode.text;
|
|
902
|
+
const params = this.extractParameters(node);
|
|
903
|
+
const returnType = this.extractReturnType(node);
|
|
904
|
+
const isAsync = this.isAsyncSignature(node, node);
|
|
905
|
+
if (isAsync || returnType && returnType.startsWith("Future")) patterns["Async Programming"] = true;
|
|
906
|
+
const comments = this.collectComments(node);
|
|
907
|
+
const isAccessor = node.type === "getter_signature" || node.type === "setter_signature";
|
|
908
|
+
const range = this.rangeIncludingBody(node, node);
|
|
909
|
+
const symbol = {
|
|
910
|
+
name,
|
|
911
|
+
kind: isAccessor ? ASTEngine.SymbolKind.Property : ASTEngine.SymbolKind.Function,
|
|
912
|
+
range,
|
|
913
|
+
scope: scopeString(scopeStack),
|
|
914
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
915
|
+
parameters: params,
|
|
916
|
+
returnType,
|
|
917
|
+
comments,
|
|
918
|
+
isAsync,
|
|
919
|
+
complexity: this.methodComplexity(node),
|
|
920
|
+
metadata: {
|
|
921
|
+
isAsync,
|
|
922
|
+
isGetter: node.type === "getter_signature",
|
|
923
|
+
isSetter: node.type === "setter_signature",
|
|
924
|
+
isTopLevelFunction: !isAccessor,
|
|
925
|
+
isTopLevelAccessor: isAccessor
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const accessorKind = node.type === "getter_signature" ? "get" : node.type === "setter_signature" ? "set" : "fn";
|
|
929
|
+
if (!this.registerCallable(symbols, seenMembers, `module:${name}:${accessorKind}`, symbol, nameLines, nameNode)) return;
|
|
930
|
+
scopeStack.push({ name, kind: "function" /* Function */ });
|
|
931
|
+
}
|
|
932
|
+
processFieldOrVariable(node, symbols, scopeStack, nameLines) {
|
|
933
|
+
const declList = this.findChildByType(node, "initialized_identifier_list") || this.findChildByType(node, "static_final_declaration_list") || (node.type === "initialized_identifier_list" || node.type === "static_final_declaration_list" ? node : void 0);
|
|
934
|
+
if (!declList) return;
|
|
935
|
+
const owner = scopeOwnerName(scopeStack);
|
|
936
|
+
const isStatic = this.hasModifier(node, "static") || node.type === "static_final_declaration_list";
|
|
937
|
+
const isFinal = this.hasModifier(node, "final") || node.type === "static_final_declaration_list";
|
|
938
|
+
const isConst = this.hasModifier(node, "const");
|
|
939
|
+
const isLate = this.hasModifier(node, "late");
|
|
940
|
+
const dataType = this.extractFieldType(node, declList);
|
|
941
|
+
const declRange = this.nodeToRange(node);
|
|
942
|
+
for (let i = 0; i < declList.namedChildCount; i++) {
|
|
943
|
+
const child = declList.namedChild(i);
|
|
944
|
+
if (!child) continue;
|
|
945
|
+
let name;
|
|
946
|
+
let nameNode = null;
|
|
947
|
+
if (child.type === "initialized_identifier" || child.type === "static_final_declaration") {
|
|
948
|
+
nameNode = this.findChildByType(child, "identifier");
|
|
949
|
+
name = nameNode?.text;
|
|
950
|
+
} else if (child.type === "identifier") {
|
|
951
|
+
name = child.text;
|
|
952
|
+
nameNode = child;
|
|
953
|
+
}
|
|
954
|
+
if (!name) continue;
|
|
955
|
+
nameLines.set(dartNameLineKey(name, declRange), this.nodeLine(nameNode ?? child));
|
|
956
|
+
const kind = owner ? ASTEngine.SymbolKind.Property : isConst || isFinal ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable;
|
|
957
|
+
symbols.push({
|
|
958
|
+
name,
|
|
959
|
+
kind,
|
|
960
|
+
range: declRange,
|
|
961
|
+
scope: scopeString(scopeStack),
|
|
962
|
+
ownerName: owner,
|
|
963
|
+
ownerKind: owner ? scopeOwnerKind(scopeStack) : void 0,
|
|
964
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
965
|
+
isStatic,
|
|
966
|
+
metadata: { isStatic, isFinal, isConst, isLate, dataType, isField: owner !== void 0 }
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
processTopLevelVariables(node, symbols, scopeStack, nameLines) {
|
|
971
|
+
const isFinal = this.hasModifier(node, "final") || this.hasModifier(node.parent, "final");
|
|
972
|
+
const isConst = this.hasModifier(node, "const") || this.hasModifier(node.parent, "const");
|
|
973
|
+
const dataType = this.extractFieldType(node, node);
|
|
974
|
+
const declRange = this.nodeToRange(node);
|
|
975
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
976
|
+
const child = node.namedChild(i);
|
|
977
|
+
if (!child) continue;
|
|
978
|
+
let name;
|
|
979
|
+
let nameNode = null;
|
|
980
|
+
if (child.type === "initialized_identifier" || child.type === "static_final_declaration") {
|
|
981
|
+
nameNode = this.findChildByType(child, "identifier");
|
|
982
|
+
name = nameNode?.text;
|
|
983
|
+
} else if (child.type === "identifier") {
|
|
984
|
+
name = child.text;
|
|
985
|
+
nameNode = child;
|
|
986
|
+
}
|
|
987
|
+
if (!name) continue;
|
|
988
|
+
nameLines.set(dartNameLineKey(name, declRange), this.nodeLine(nameNode ?? child));
|
|
989
|
+
symbols.push({
|
|
990
|
+
name,
|
|
991
|
+
kind: isConst || isFinal ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable,
|
|
992
|
+
range: declRange,
|
|
993
|
+
scope: scopeString(scopeStack),
|
|
994
|
+
visibility: name.startsWith("_") ? "private" : "public",
|
|
995
|
+
metadata: { isStatic: false, isFinal, isConst, dataType, isField: false, isTopLevelVariable: true }
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
processImportExport(node, imports, exports, frameworks, relationships, site) {
|
|
1000
|
+
const targetOf = (uri2) => site.projectRoot === null ? null : dartImportTargetPath(site.projectRoot, site.filePath, uri2);
|
|
1001
|
+
const outsideProject = (uri2) => {
|
|
1002
|
+
if (site.projectRoot === null) return false;
|
|
1003
|
+
if (uri2.startsWith("package:")) {
|
|
1004
|
+
const rest = uri2.slice(8);
|
|
1005
|
+
const slash = rest.indexOf("/");
|
|
1006
|
+
if (slash <= 0) return false;
|
|
1007
|
+
const pkg = rest.slice(0, slash);
|
|
1008
|
+
if (!isDartLibraryName(pkg)) return false;
|
|
1009
|
+
return dartProjectFacts(site.projectRoot).packageRoots.get(pkg) === void 0;
|
|
1010
|
+
}
|
|
1011
|
+
if (uri2.startsWith("dart:")) {
|
|
1012
|
+
const lib = uri2.slice(5);
|
|
1013
|
+
if (!isDartLibraryName(lib)) return false;
|
|
1014
|
+
return !dartProjectFacts(site.projectRoot).sdkLib;
|
|
1015
|
+
}
|
|
1016
|
+
return uri2.indexOf(":") >= 0;
|
|
1017
|
+
};
|
|
1018
|
+
const claimMeta = (uri2, resolved) => resolved === null && outsideProject(uri2) ? { importOutsideProject: true } : {};
|
|
1019
|
+
if (node.type === "part_directive" || node.type === "part_of_directive") {
|
|
1020
|
+
const uriNode2 = this.findDescendantByType(node, "string_literal") || this.findDescendantByType(node, "uri");
|
|
1021
|
+
const uri2 = uriNode2 ? unquote(uriNode2.text) : "";
|
|
1022
|
+
if (!uri2 && node.type === "part_of_directive") {
|
|
1023
|
+
const named = this.findChildByType(node, "dotted_identifier_list");
|
|
1024
|
+
if (named) {
|
|
1025
|
+
relationships.push({
|
|
1026
|
+
source: site.moduleName,
|
|
1027
|
+
target: stripWhitespace(named.text),
|
|
1028
|
+
type: ASTEngine.RelationshipType.PartOf,
|
|
1029
|
+
confidence: 1,
|
|
1030
|
+
line_number: this.nodeLine(named),
|
|
1031
|
+
metadata: { partRole: "part", isNamedLibrary: true }
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (uri2) {
|
|
1037
|
+
const resolved = targetOf(uri2);
|
|
1038
|
+
imports.push({ module: uri2, source: uri2, imported: ["*"], isPart: true, range: this.nodeToRange(node) });
|
|
1039
|
+
if (resolved !== null) {
|
|
1040
|
+
relationships.push({
|
|
1041
|
+
source: site.moduleName,
|
|
1042
|
+
target: resolved,
|
|
1043
|
+
type: ASTEngine.RelationshipType.PartOf,
|
|
1044
|
+
confidence: 1,
|
|
1045
|
+
line_number: this.nodeLine(uriNode2),
|
|
1046
|
+
metadata: { partRole: node.type === "part_of_directive" ? "part" : "library" }
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const isExport = node.type === "library_export" || node.text.startsWith("export");
|
|
1053
|
+
const uriNode = this.findDescendantByType(node, "string_literal") || this.findDescendantByType(node, "uri");
|
|
1054
|
+
const uri = uriNode ? unquote(uriNode.text) : "";
|
|
1055
|
+
if (!uri) return;
|
|
1056
|
+
if (uri.startsWith("package:flutter/")) frameworks["Flutter"] = true;
|
|
1057
|
+
if (uri.startsWith("package:angular/")) frameworks["AngularDart"] = true;
|
|
1058
|
+
if (isExport) {
|
|
1059
|
+
const resolved = targetOf(uri);
|
|
1060
|
+
relationships.push({
|
|
1061
|
+
source: site.moduleName,
|
|
1062
|
+
target: resolved ?? uri,
|
|
1063
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
1064
|
+
confidence: 1,
|
|
1065
|
+
line_number: this.nodeLine(uriNode),
|
|
1066
|
+
metadata: { ...claimMeta(uri, resolved) }
|
|
1067
|
+
});
|
|
1068
|
+
} else {
|
|
1069
|
+
const resolved = targetOf(uri);
|
|
1070
|
+
imports.push({ module: uri, source: uri, imported: ["*"], isTypeOnly: false, range: this.nodeToRange(node) });
|
|
1071
|
+
relationships.push({
|
|
1072
|
+
source: site.moduleName,
|
|
1073
|
+
target: resolved ?? `${uri}:*`,
|
|
1074
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
1075
|
+
confidence: 1,
|
|
1076
|
+
line_number: this.nodeLine(uriNode),
|
|
1077
|
+
metadata: { ...claimMeta(uri, resolved) }
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// ============================================================================
|
|
1082
|
+
// RELATIONSHIP PASSES
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
addContainmentRelationships(symbols, relationships, nameLines) {
|
|
1085
|
+
for (const symbol of symbols) {
|
|
1086
|
+
if (symbol.ownerName) {
|
|
1087
|
+
const qualified = symbol.scope ? `${symbol.scope}.${symbol.name}` : symbol.name;
|
|
1088
|
+
relationships.push({
|
|
1089
|
+
source: symbol.ownerName,
|
|
1090
|
+
target: qualified,
|
|
1091
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1092
|
+
confidence: 1,
|
|
1093
|
+
// The member's OWN NAME line — the name node when the producers could
|
|
1094
|
+
// hand one over (a wrapped declaration moves it off the start line),
|
|
1095
|
+
// the declaration start otherwise (php stamps contains the same way).
|
|
1096
|
+
line_number: nameLines.get(dartNameLineKey(symbol.name, symbol.range)) ?? symbol.range?.startLine,
|
|
1097
|
+
metadata: {}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
extractOverrideRelationships(symbols, relationships, overrideCompanion, nameLines, projectIndex) {
|
|
1103
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1104
|
+
for (const s of symbols) {
|
|
1105
|
+
if (s.kind === ASTEngine.SymbolKind.Constructor) continue;
|
|
1106
|
+
const list = byName.get(s.name) ?? [];
|
|
1107
|
+
list.push(s);
|
|
1108
|
+
byName.set(s.name, list);
|
|
1109
|
+
}
|
|
1110
|
+
const companionForPass2 = overrideCompanion ?? dartCompanionFor(projectIndex);
|
|
1111
|
+
const seesProject = companionForPass2 !== void 0;
|
|
1112
|
+
const heritageByClass = /* @__PURE__ */ new Map();
|
|
1113
|
+
const firstHeritage = /* @__PURE__ */ new Map();
|
|
1114
|
+
for (const r of relationships) {
|
|
1115
|
+
if (!r.source || !r.target) continue;
|
|
1116
|
+
const type = r.type ?? r.relationshipType ?? "";
|
|
1117
|
+
if (type === "extends" || type === "implements" || type === "uses" || type === "mixes_in") {
|
|
1118
|
+
const list = heritageByClass.get(r.source) ?? [];
|
|
1119
|
+
if (!list.includes(r.target)) list.push(r.target);
|
|
1120
|
+
heritageByClass.set(r.source, list);
|
|
1121
|
+
if (!firstHeritage.has(r.source)) firstHeritage.set(r.source, r.target);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const basesOf = /* @__PURE__ */ new Map();
|
|
1125
|
+
for (const r of relationships) {
|
|
1126
|
+
const type = r.type ?? r.relationshipType ?? "";
|
|
1127
|
+
if (type !== "extends" && type !== "implements") continue;
|
|
1128
|
+
if (!r.source || !r.target) continue;
|
|
1129
|
+
const list = basesOf.get(r.source) ?? [];
|
|
1130
|
+
if (!list.includes(r.target)) list.push(r.target);
|
|
1131
|
+
basesOf.set(r.source, list);
|
|
1132
|
+
}
|
|
1133
|
+
const baseOrder = /* @__PURE__ */ new Map();
|
|
1134
|
+
for (const r of relationships) {
|
|
1135
|
+
const type = r.type ?? r.relationshipType ?? "";
|
|
1136
|
+
const isBase = type === "extends" || type === "implements" || type === "mixes_in";
|
|
1137
|
+
if (!isBase || !r.source || !r.target) continue;
|
|
1138
|
+
const list = baseOrder.get(r.source) ?? [];
|
|
1139
|
+
if (!list.includes(r.target)) list.push(r.target);
|
|
1140
|
+
baseOrder.set(r.source, list);
|
|
1141
|
+
}
|
|
1142
|
+
const exposedCache = /* @__PURE__ */ new Map();
|
|
1143
|
+
const exposing = /* @__PURE__ */ new Set();
|
|
1144
|
+
const exposed = (cls) => {
|
|
1145
|
+
const cached = exposedCache.get(cls);
|
|
1146
|
+
if (cached) return cached;
|
|
1147
|
+
if (exposing.has(cls)) return /* @__PURE__ */ new Set();
|
|
1148
|
+
exposing.add(cls);
|
|
1149
|
+
const out = /* @__PURE__ */ new Set();
|
|
1150
|
+
for (const [name, syms] of byName) if (syms.some((bm) => bm.ownerName === cls)) out.add(name);
|
|
1151
|
+
for (const b of basesOf.get(cls) ?? []) for (const m of exposed(b)) out.add(m);
|
|
1152
|
+
exposing.delete(cls);
|
|
1153
|
+
exposedCache.set(cls, out);
|
|
1154
|
+
return out;
|
|
1155
|
+
};
|
|
1156
|
+
const declares = (base, method) => {
|
|
1157
|
+
if (exposed(base).has(method)) return true;
|
|
1158
|
+
const companion = companionForPass2?.get(base);
|
|
1159
|
+
return !!companion && companion.has(method);
|
|
1160
|
+
};
|
|
1161
|
+
const TYPE_KINDS = /* @__PURE__ */ new Set([
|
|
1162
|
+
ASTEngine.SymbolKind.Class,
|
|
1163
|
+
ASTEngine.SymbolKind.Interface,
|
|
1164
|
+
ASTEngine.SymbolKind.Enum,
|
|
1165
|
+
ASTEngine.SymbolKind.Type
|
|
1166
|
+
]);
|
|
1167
|
+
const inFileTypes = /* @__PURE__ */ new Set();
|
|
1168
|
+
for (const s of symbols) {
|
|
1169
|
+
if (typeof s.name === "string" && s.name && TYPE_KINDS.has(s.kind)) inFileTypes.add(s.name);
|
|
1170
|
+
}
|
|
1171
|
+
const seenOverride = /* @__PURE__ */ new Set();
|
|
1172
|
+
for (const s of symbols) {
|
|
1173
|
+
if ((s.kind === ASTEngine.SymbolKind.Method || s.kind === ASTEngine.SymbolKind.Property) && s.ownerName) {
|
|
1174
|
+
if (s.isStatic) continue;
|
|
1175
|
+
const heritage = heritageByClass.get(s.ownerName);
|
|
1176
|
+
if (!heritage || heritage.length === 0) continue;
|
|
1177
|
+
let target = null;
|
|
1178
|
+
const seenCls = /* @__PURE__ */ new Set();
|
|
1179
|
+
const queue = [...heritage];
|
|
1180
|
+
while (queue.length > 0 && !target) {
|
|
1181
|
+
const base = queue.shift();
|
|
1182
|
+
if (seenCls.has(base)) continue;
|
|
1183
|
+
seenCls.add(base);
|
|
1184
|
+
if (declares(base, s.name)) {
|
|
1185
|
+
target = base;
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
for (const h of heritageByClass.get(base) ?? []) queue.push(h);
|
|
1189
|
+
}
|
|
1190
|
+
let confidence = 0.95;
|
|
1191
|
+
let metadata = { isOverride: true, relationshipType: "method_override" };
|
|
1192
|
+
if (target) {
|
|
1193
|
+
} else if (companionForPass2 && heritage.length > 0) {
|
|
1194
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
1195
|
+
const q2 = [...heritage];
|
|
1196
|
+
while (q2.length > 0 && !target) {
|
|
1197
|
+
const base = q2.shift();
|
|
1198
|
+
if (seen2.has(base)) continue;
|
|
1199
|
+
seen2.add(base);
|
|
1200
|
+
if (this.companionHas(companionForPass2, base, s.name)) {
|
|
1201
|
+
target = base;
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
for (const h of heritageByClass.get(base) ?? []) q2.push(h);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (!target) {
|
|
1208
|
+
const hasOverrideAnn = (s.decorators ?? []).some((d) => d.includes("@override"));
|
|
1209
|
+
if (hasOverrideAnn) {
|
|
1210
|
+
const rawFirst = firstHeritage.get(s.ownerName) ?? null;
|
|
1211
|
+
const first = rawFirst === DART_OBJECT_TYPE && !DART_OBJECT_INTERFACE_MEMBERS.has(s.name) ? (baseOrder.get(s.ownerName) ?? []).find((b) => b !== DART_OBJECT_TYPE) ?? null : rawFirst;
|
|
1212
|
+
const indexKnowsBase = first !== null && companionForPass2?.has(first) === true;
|
|
1213
|
+
const refuted = first !== null && !declares(first, s.name) && (byName.get(s.name)?.some((bm) => bm.ownerName === first) ?? false) === false && (heritageByClass.has(first) || inFileTypes.has(first) || indexKnowsBase);
|
|
1214
|
+
if (!refuted) {
|
|
1215
|
+
target = first;
|
|
1216
|
+
confidence = 0.8;
|
|
1217
|
+
metadata = seesProject || first === DART_OBJECT_TYPE ? { isOverride: true, relationshipType: "method_override", isExternalBase: true } : { isOverride: true, relationshipType: "method_override", baseBeyondRunScope: String(first) };
|
|
1218
|
+
if (first !== null && first !== DART_OBJECT_TYPE && DART_OBJECT_INTERFACE_MEMBERS.has(s.name)) {
|
|
1219
|
+
target = DART_OBJECT_TYPE;
|
|
1220
|
+
const projectPublishesObject = companionForPass2 !== void 0 && this.companionHas(companionForPass2, DART_OBJECT_TYPE, s.name);
|
|
1221
|
+
metadata = {
|
|
1222
|
+
isOverride: true,
|
|
1223
|
+
relationshipType: "method_override",
|
|
1224
|
+
...projectPublishesObject ? {} : { isExternalBase: true },
|
|
1225
|
+
isObjectInterfaceBase: true,
|
|
1226
|
+
firstHeritageParent: first
|
|
1227
|
+
};
|
|
1228
|
+
if (projectPublishesObject) confidence = 0.95;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (!target) continue;
|
|
1234
|
+
const key = `${s.ownerName}.${s.name}->${target}.${s.name}`;
|
|
1235
|
+
if (seenOverride.has(key)) continue;
|
|
1236
|
+
seenOverride.add(key);
|
|
1237
|
+
relationships.push({
|
|
1238
|
+
source: `${s.ownerName}.${s.name}`,
|
|
1239
|
+
target: `${target}.${s.name}`,
|
|
1240
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
1241
|
+
confidence,
|
|
1242
|
+
// The overriding METHOD's own name line — the node that names the
|
|
1243
|
+
// edge, not the base it shadows (and not a wrapped start line).
|
|
1244
|
+
line_number: nameLines?.get(dartNameLineKey(s.name, s.range)) ?? s.range?.startLine,
|
|
1245
|
+
metadata
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/** Companion lookup with leaf-name fallback (bases may arrive qualified). */
|
|
1251
|
+
companionHas(companion, base, method) {
|
|
1252
|
+
if (companion.get(base)?.has(method)) return true;
|
|
1253
|
+
const leaf = base.includes(".") ? base.split(".").pop() : base;
|
|
1254
|
+
return leaf !== base && !!companion.get(leaf)?.has(method);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The whole-tree EXPRESSION pass: `calls`, `uses`, `instantiates` and
|
|
1258
|
+
* `references`.
|
|
1259
|
+
*
|
|
1260
|
+
* Named for what it reads rather than for when it was written. It began as the
|
|
1261
|
+
* call pass, but it is also the only traversal that already knows, at every
|
|
1262
|
+
* node, which declaration encloses it (`current`/`owner`, the family attribution
|
|
1263
|
+
* rule), and a type mention's SOURCE is the same question a call's source is.
|
|
1264
|
+
* A second traversal would have had to re-derive that attribution, and two
|
|
1265
|
+
* copies of one rule are two rules that drift.
|
|
1266
|
+
*
|
|
1267
|
+
* `projectIndex` is LAST and it must stay last — the same hazard
|
|
1268
|
+
* `extractOverrideRelationships` documents: an optional parameter inserted
|
|
1269
|
+
* earlier silently swapped the ones after it.
|
|
1270
|
+
*/
|
|
1271
|
+
extractExpressionRelationships(node, relationships, moduleName, symbols, projectIndex) {
|
|
1272
|
+
const importAliases = this.collectImportAliases(node);
|
|
1273
|
+
const visibleTypes = /* @__PURE__ */ new Set();
|
|
1274
|
+
for (const s of symbols) {
|
|
1275
|
+
const kind = s.kind;
|
|
1276
|
+
const isType = kind === ASTEngine.SymbolKind.Class || kind === ASTEngine.SymbolKind.Mixin || kind === ASTEngine.SymbolKind.Extension || kind === ASTEngine.SymbolKind.Enum || kind === ASTEngine.SymbolKind.TypeAlias;
|
|
1277
|
+
if (!isType) continue;
|
|
1278
|
+
if (kind === ASTEngine.SymbolKind.Extension && s.metadata?.isUnnamedExtension) continue;
|
|
1279
|
+
if (s.name) visibleTypes.add(s.name);
|
|
1280
|
+
}
|
|
1281
|
+
const fileTypes = new Set(visibleTypes);
|
|
1282
|
+
const libraryScope = /* @__PURE__ */ new Set();
|
|
1283
|
+
for (const s of symbols) {
|
|
1284
|
+
if (s.ownerName) continue;
|
|
1285
|
+
if (fileTypes.has(s.name)) continue;
|
|
1286
|
+
if (s.kind === ASTEngine.SymbolKind.EnumMember) continue;
|
|
1287
|
+
if (s.name) libraryScope.add(s.name);
|
|
1288
|
+
}
|
|
1289
|
+
for (const t of projectIndex?.typeKinds?.keys?.() ?? []) visibleTypes.add(String(t));
|
|
1290
|
+
const namedCtors = /* @__PURE__ */ new Set();
|
|
1291
|
+
for (const s of symbols) {
|
|
1292
|
+
if (s.kind === ASTEngine.SymbolKind.Constructor && s.ownerName) {
|
|
1293
|
+
const seg = s.metadata?.namedSegment;
|
|
1294
|
+
if (seg) namedCtors.add(`${s.ownerName}.${seg}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const membersByOwner = /* @__PURE__ */ new Map();
|
|
1298
|
+
for (const s of symbols) {
|
|
1299
|
+
if (!s.ownerName) continue;
|
|
1300
|
+
let set = membersByOwner.get(s.ownerName);
|
|
1301
|
+
if (!set) {
|
|
1302
|
+
set = /* @__PURE__ */ new Set();
|
|
1303
|
+
membersByOwner.set(s.ownerName, set);
|
|
1304
|
+
}
|
|
1305
|
+
set.add(s.name);
|
|
1306
|
+
}
|
|
1307
|
+
const topLevelValues = /* @__PURE__ */ new Set();
|
|
1308
|
+
for (const s of symbols) {
|
|
1309
|
+
if (s.ownerName) continue;
|
|
1310
|
+
if (s.kind === ASTEngine.SymbolKind.Variable || s.kind === ASTEngine.SymbolKind.Constant || s.kind === ASTEngine.SymbolKind.Function || s.kind === ASTEngine.SymbolKind.Property) {
|
|
1311
|
+
topLevelValues.add(s.name);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const superByClass = /* @__PURE__ */ new Map();
|
|
1315
|
+
{
|
|
1316
|
+
const pre = [node];
|
|
1317
|
+
while (pre.length > 0) {
|
|
1318
|
+
const cn = pre.pop();
|
|
1319
|
+
if (!cn) continue;
|
|
1320
|
+
if (cn.type === "class_definition") {
|
|
1321
|
+
const nm = cn.childForFieldName?.("name")?.text;
|
|
1322
|
+
const sup = this.findChildByType(cn, "superclass");
|
|
1323
|
+
if (nm && sup) {
|
|
1324
|
+
const base = this.resolvedHeritageNames(sup)[0];
|
|
1325
|
+
if (base) superByClass.set(nm, base);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
for (let i = cn.namedChildCount - 1; i >= 0; i--) {
|
|
1329
|
+
const ch = cn.namedChild(i);
|
|
1330
|
+
if (ch) pre.push(ch);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const fileQualified = /* @__PURE__ */ new Set();
|
|
1335
|
+
for (const s of symbols) {
|
|
1336
|
+
if (s.ownerName) fileQualified.add(`${s.ownerName}.${s.name}`);
|
|
1337
|
+
}
|
|
1338
|
+
const indexDeclares = (typeName, member) => {
|
|
1339
|
+
const set = projectIndex?.typeMembers?.get?.(typeName);
|
|
1340
|
+
if (set instanceof Set) return set.has(member);
|
|
1341
|
+
if (Array.isArray(set)) return set.includes(member);
|
|
1342
|
+
return false;
|
|
1343
|
+
};
|
|
1344
|
+
const declaringOwner = (member, from, memberNodesByOwner) => {
|
|
1345
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1346
|
+
let cur = from;
|
|
1347
|
+
while (cur && !seen.has(cur)) {
|
|
1348
|
+
seen.add(cur);
|
|
1349
|
+
if (memberNodesByOwner.get(cur)?.has(member)) return cur;
|
|
1350
|
+
cur = superByClass.get(cur) ?? null;
|
|
1351
|
+
}
|
|
1352
|
+
return null;
|
|
1353
|
+
};
|
|
1354
|
+
const typeKindByName = /* @__PURE__ */ new Map();
|
|
1355
|
+
const fieldTypeByField = /* @__PURE__ */ new Map();
|
|
1356
|
+
for (const s of symbols) {
|
|
1357
|
+
if (!s.ownerName) continue;
|
|
1358
|
+
const dt = String(s.metadata?.dataType ?? "");
|
|
1359
|
+
if (isPlainIdentifier(dt)) fieldTypeByField.set(`${s.ownerName}.${s.name}`, dt);
|
|
1360
|
+
}
|
|
1361
|
+
const qualifiedTarget = (ownerText, member) => {
|
|
1362
|
+
if (!ownerText || !member) return { target: null, cause: "receiverUnnamed" };
|
|
1363
|
+
if (!visibleTypes.has(ownerText)) {
|
|
1364
|
+
if (!isCapitalized(ownerText)) return { target: null, cause: "receiverTypeUnknown" };
|
|
1365
|
+
return { target: `${ownerText}.${member}`, cause: null, typeName: ownerText, claim: ownerText };
|
|
1366
|
+
}
|
|
1367
|
+
const declared = declaringOwner(member, ownerText, membersByOwner);
|
|
1368
|
+
if (declared) {
|
|
1369
|
+
const spelling = `${declared}.${member}`;
|
|
1370
|
+
if (fileQualified.has(spelling)) return { target: spelling, cause: null };
|
|
1371
|
+
return { target: null, cause: "receiverInFileNotSpelled" };
|
|
1372
|
+
}
|
|
1373
|
+
if (indexDeclares(ownerText, member)) return { target: `${ownerText}.${member}`, cause: null };
|
|
1374
|
+
return { target: null, cause: "receiverMemberOutsideRun" };
|
|
1375
|
+
};
|
|
1376
|
+
const receiverCut = (q) => {
|
|
1377
|
+
if (q.claim !== void 0) return { baseBeyondRunScope: q.claim };
|
|
1378
|
+
return q.cause ? { receiverCause: q.cause, ...q.typeName ? { receiverType: q.typeName } : {} } : {};
|
|
1379
|
+
};
|
|
1380
|
+
const plainTypeName = (text) => {
|
|
1381
|
+
const leaf = stripNullableSuffix(String(text ?? ""));
|
|
1382
|
+
return isPlainIdentifier(leaf) ? leaf : void 0;
|
|
1383
|
+
};
|
|
1384
|
+
const headOwner = (head, enclosing, localTypes) => {
|
|
1385
|
+
const t = head?.type;
|
|
1386
|
+
if (t === "this") return enclosing;
|
|
1387
|
+
if (t === "super") return superByClass.get(enclosing ?? "") ?? null;
|
|
1388
|
+
const text = this.headNameText(head);
|
|
1389
|
+
if (text === null) return null;
|
|
1390
|
+
const localType = localTypes.get(text);
|
|
1391
|
+
if (localType) return localType;
|
|
1392
|
+
const fieldType = fieldTypeByField.get(`${enclosing ?? ""}.${text}`);
|
|
1393
|
+
if (fieldType) return fieldType;
|
|
1394
|
+
return text;
|
|
1395
|
+
};
|
|
1396
|
+
const receiverOwnerMeta = (head, enclosing, localTypes, localEvidence) => {
|
|
1397
|
+
const t = head?.type;
|
|
1398
|
+
if (t === "this" || t === "super") return null;
|
|
1399
|
+
const text = this.headNameText(head);
|
|
1400
|
+
if (text === null) return null;
|
|
1401
|
+
const classLikeType = (name) => {
|
|
1402
|
+
if (name === "dynamic" || name === "void" || name === "Never") return null;
|
|
1403
|
+
const kind = typeKindByName.get(name);
|
|
1404
|
+
if (kind === void 0) return name;
|
|
1405
|
+
if (kind === "class" || kind === "enum") return name;
|
|
1406
|
+
return null;
|
|
1407
|
+
};
|
|
1408
|
+
const localType = localTypes.get(text);
|
|
1409
|
+
if (localType) {
|
|
1410
|
+
const evidence = localEvidence.get(text);
|
|
1411
|
+
const owner2 = classLikeType(localType);
|
|
1412
|
+
return evidence && owner2 ? { owner: owner2, ownerEvidence: evidence } : null;
|
|
1413
|
+
}
|
|
1414
|
+
const fieldType = fieldTypeByField.get(`${enclosing ?? ""}.${text}`);
|
|
1415
|
+
const owner = fieldType ? classLikeType(fieldType) : null;
|
|
1416
|
+
if (owner) return { owner, ownerEvidence: "field" };
|
|
1417
|
+
return null;
|
|
1418
|
+
};
|
|
1419
|
+
for (const s of symbols) {
|
|
1420
|
+
if (s.ownerName) continue;
|
|
1421
|
+
const kind = String(s.kind ?? "");
|
|
1422
|
+
if (kind === "class" || kind === "enum" || kind === "mixin" || kind === "extension" || kind === "extension_type" || kind === "type_alias" || kind === "type") {
|
|
1423
|
+
if (s.name) typeKindByName.set(s.name, kind);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const resolveCurrent = (n, current) => {
|
|
1427
|
+
if (n.type === "function_signature" && n.parent?.type === "lambda_expression") {
|
|
1428
|
+
return current;
|
|
1429
|
+
}
|
|
1430
|
+
let sig = null;
|
|
1431
|
+
if (this.isSignatureNode(n.type)) {
|
|
1432
|
+
sig = n;
|
|
1433
|
+
} else if (n.type === "declaration") {
|
|
1434
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1435
|
+
const c = n.namedChild(i);
|
|
1436
|
+
if (c && this.isSignatureNode(c.type)) {
|
|
1437
|
+
sig = c;
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (sig) {
|
|
1443
|
+
const inner = this.findInnerSignature(sig);
|
|
1444
|
+
if (this.isConstructorSignature(sig.type) || this.isConstructorSignature(inner.type)) {
|
|
1445
|
+
if (this.isRealConstructor(inner, this.enclosingTypeName(inner))) return "constructor";
|
|
1446
|
+
}
|
|
1447
|
+
const nm = this.signatureName(inner);
|
|
1448
|
+
if (nm) return nm;
|
|
1449
|
+
}
|
|
1450
|
+
return current;
|
|
1451
|
+
};
|
|
1452
|
+
const resolveOwner = (n, owner) => {
|
|
1453
|
+
const t = n.type;
|
|
1454
|
+
if (t === "class_definition" || t === "enum_declaration") {
|
|
1455
|
+
const nm = n.childForFieldName?.("name")?.text;
|
|
1456
|
+
return nm || owner;
|
|
1457
|
+
}
|
|
1458
|
+
if (t === "extension_declaration" || t === "extension_type_declaration") {
|
|
1459
|
+
return this.extensionPublishedName(n) || owner;
|
|
1460
|
+
}
|
|
1461
|
+
if (t === "mixin_declaration") {
|
|
1462
|
+
const id = this.findChildByType(n, "identifier");
|
|
1463
|
+
return id ? id.text : owner;
|
|
1464
|
+
}
|
|
1465
|
+
if (t === "type_alias") {
|
|
1466
|
+
const nm = n.childForFieldName?.("name")?.text || this.findChildByType(n, "type_identifier")?.text;
|
|
1467
|
+
return nm || owner;
|
|
1468
|
+
}
|
|
1469
|
+
return owner;
|
|
1470
|
+
};
|
|
1471
|
+
const visitNode = (n, current, owner, locals, localTypes, localEvidence, localElements, insideCollected) => {
|
|
1472
|
+
const prev = current;
|
|
1473
|
+
const prevOwner = owner;
|
|
1474
|
+
const t = n.type;
|
|
1475
|
+
current = resolveCurrent(n, current);
|
|
1476
|
+
owner = resolveOwner(n, owner);
|
|
1477
|
+
let childLocals = locals;
|
|
1478
|
+
let childLocalTypes = localTypes;
|
|
1479
|
+
let childLocalEvidence = localEvidence;
|
|
1480
|
+
let childLocalElements = localElements;
|
|
1481
|
+
let childInsideCollected = insideCollected;
|
|
1482
|
+
if (t === "function_body" || t === "function_expression_body" || t === "block") {
|
|
1483
|
+
let bound = null;
|
|
1484
|
+
let boundTypes = null;
|
|
1485
|
+
if (!insideCollected) {
|
|
1486
|
+
const own = this.collectDartLocals(n);
|
|
1487
|
+
if (own.size > 0) bound = own;
|
|
1488
|
+
const ownTypes = this.collectDartLocalTypes(n);
|
|
1489
|
+
if (ownTypes.size > 0) boundTypes = ownTypes;
|
|
1490
|
+
}
|
|
1491
|
+
const sig = n.previousNamedSibling;
|
|
1492
|
+
if (sig) {
|
|
1493
|
+
const sigNames = this.collectDartLocals(sig);
|
|
1494
|
+
if (sigNames.size > 0) {
|
|
1495
|
+
if (!bound) bound = /* @__PURE__ */ new Set();
|
|
1496
|
+
for (const nm of sigNames) bound.add(nm);
|
|
1497
|
+
}
|
|
1498
|
+
const sigTypes = this.collectDartParameterTypes(sig);
|
|
1499
|
+
if (sigTypes.size > 0) {
|
|
1500
|
+
if (!boundTypes) boundTypes = /* @__PURE__ */ new Map();
|
|
1501
|
+
for (const [nm, ty] of sigTypes) boundTypes.set(nm, ty);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (bound) {
|
|
1505
|
+
childLocals = new Set(locals);
|
|
1506
|
+
for (const nm of bound) childLocals.add(nm);
|
|
1507
|
+
}
|
|
1508
|
+
if (boundTypes) {
|
|
1509
|
+
childLocalTypes = new Map(localTypes);
|
|
1510
|
+
childLocalEvidence = new Map(localEvidence);
|
|
1511
|
+
childLocalElements = new Map(localElements);
|
|
1512
|
+
for (const [nm, ty] of boundTypes) {
|
|
1513
|
+
childLocalTypes.set(nm, ty.type);
|
|
1514
|
+
childLocalEvidence.set(nm, ty.evidence);
|
|
1515
|
+
if (ty.element) childLocalElements.set(nm, ty.element);
|
|
1516
|
+
if (ty.keyElement) childLocalElements.set(`${nm}\0key`, ty.keyElement);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const awaitTypes = this.collectAwaitLocalTypes(n, childLocalElements, childLocalTypes);
|
|
1520
|
+
if (awaitTypes.size > 0) {
|
|
1521
|
+
childLocalTypes = new Map(childLocalTypes);
|
|
1522
|
+
childLocalEvidence = new Map(childLocalEvidence);
|
|
1523
|
+
childLocalElements = new Map(childLocalElements);
|
|
1524
|
+
for (const [nm, ty] of awaitTypes) {
|
|
1525
|
+
childLocalTypes.set(nm, ty);
|
|
1526
|
+
childLocalEvidence.set(nm, "annotation");
|
|
1527
|
+
childLocalElements.set(nm, ty);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if (!insideCollected) childInsideCollected = true;
|
|
1531
|
+
}
|
|
1532
|
+
if (t === "for_statement") {
|
|
1533
|
+
const parts = this.findChildByType(n, "for_loop_parts");
|
|
1534
|
+
if (parts) {
|
|
1535
|
+
const hasWrittenType = (parts.namedChildren ?? []).some((c) => c?.type === "type_identifier");
|
|
1536
|
+
const nameNode = parts.childForFieldName?.("name");
|
|
1537
|
+
const valueNode = parts.childForFieldName?.("value");
|
|
1538
|
+
if (!hasWrittenType && nameNode && valueNode && !childLocalTypes.has(nameNode.text)) {
|
|
1539
|
+
let element;
|
|
1540
|
+
let link;
|
|
1541
|
+
const sel = valueNode.nextNamedSibling;
|
|
1542
|
+
if (sel && sel.type === "selector") {
|
|
1543
|
+
const ids = [];
|
|
1544
|
+
const linkStack = [sel];
|
|
1545
|
+
while (linkStack.length > 0) {
|
|
1546
|
+
const x = linkStack.pop();
|
|
1547
|
+
if (x.type === "identifier") {
|
|
1548
|
+
ids.push(x.text);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
for (const c of x.namedChildren ?? []) if (c) linkStack.push(c);
|
|
1552
|
+
}
|
|
1553
|
+
link = ids.find((t2) => t2 === "values" || t2 === "keys");
|
|
1554
|
+
}
|
|
1555
|
+
if (link === "values") element = localElements.get(valueNode.text);
|
|
1556
|
+
else if (link === "keys") element = localElements.get(`${valueNode.text}\0key`);
|
|
1557
|
+
else element = localElements.get(valueNode.text);
|
|
1558
|
+
if (element) {
|
|
1559
|
+
childLocalTypes = new Map(childLocalTypes);
|
|
1560
|
+
childLocalTypes.set(nameNode.text, element);
|
|
1561
|
+
childLocalEvidence = new Map(childLocalEvidence);
|
|
1562
|
+
childLocalEvidence.set(nameNode.text, "annotation");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (t === "initializers") {
|
|
1568
|
+
const par = n.parent;
|
|
1569
|
+
if (par) {
|
|
1570
|
+
for (let k = 0; k < par.namedChildCount; k++) {
|
|
1571
|
+
const sib = par.namedChild(k);
|
|
1572
|
+
if (sib && (sib.type === "constructor_signature" || sib.type === "method_signature" || sib.type === "function_signature")) {
|
|
1573
|
+
const bound = this.collectDartLocals(sib);
|
|
1574
|
+
if (bound.size > 0) {
|
|
1575
|
+
childLocals = new Set(locals);
|
|
1576
|
+
for (const nm of bound) childLocals.add(nm);
|
|
1577
|
+
}
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if ((t === "function_body" || t === "block" || t === "function_expression_body") && current === prev) {
|
|
1584
|
+
const prevSib = n.previousNamedSibling;
|
|
1585
|
+
if (prevSib) {
|
|
1586
|
+
const sigSource = resolveCurrent(prevSib, current);
|
|
1587
|
+
if (sigSource !== current) current = sigSource;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const callSource = current || owner || moduleName;
|
|
1591
|
+
if (t === "constructor_invocation" || t === "new_expression" || t === "const_object_expression") {
|
|
1592
|
+
const typeNode = this.findChildByType(n, "type_identifier");
|
|
1593
|
+
if (typeNode && isCapitalized(typeNode.text)) {
|
|
1594
|
+
relationships.push({
|
|
1595
|
+
source: callSource,
|
|
1596
|
+
target: typeNode.text,
|
|
1597
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1598
|
+
confidence: 0.95,
|
|
1599
|
+
line_number: this.nodeLine(typeNode),
|
|
1600
|
+
metadata: { isConstructorInvocation: true }
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (t === "selector" && this.findChildByType(n, "argument_part")) {
|
|
1605
|
+
const { head, links } = this.invocationChain(n);
|
|
1606
|
+
if (links.length === 0) {
|
|
1607
|
+
const leafNode = this.resolveSelectorCalleeNode(n);
|
|
1608
|
+
const leaf = leafNode?.text ?? null;
|
|
1609
|
+
if (leaf) {
|
|
1610
|
+
if (this.isDartGlobalBuiltin(leaf)) {
|
|
1611
|
+
} else if (isCapitalized(leaf) && this.selectorIsBareCall(n)) {
|
|
1612
|
+
relationships.push({
|
|
1613
|
+
source: callSource,
|
|
1614
|
+
target: leaf,
|
|
1615
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1616
|
+
confidence: 0.95,
|
|
1617
|
+
line_number: this.nodeLine(leafNode),
|
|
1618
|
+
metadata: {}
|
|
1619
|
+
});
|
|
1620
|
+
} else {
|
|
1621
|
+
const isBinding = locals.has(leaf);
|
|
1622
|
+
const q0 = isBinding ? { target: null, cause: null } : qualifiedTarget(owner, leaf);
|
|
1623
|
+
const q = q0.cause === "receiverMemberOutsideRun" && libraryScope.has(leaf) ? { target: null, cause: "libraryScopeCallee" } : q0;
|
|
1624
|
+
relationships.push({
|
|
1625
|
+
source: callSource,
|
|
1626
|
+
target: q.target ?? (isBinding ? `${callSource}${BINDING_SEP}${leaf}` : leaf),
|
|
1627
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1628
|
+
confidence: 0.9,
|
|
1629
|
+
line_number: this.nodeLine(leafNode),
|
|
1630
|
+
// Metadata only, for the marker half. No row is added or removed and no
|
|
1631
|
+
// confidence moves, so this cannot change WHICH edges exist; it makes an
|
|
1632
|
+
// existing population queryable, which is what M14 §4 asks of a heuristic
|
|
1633
|
+
// edge.
|
|
1634
|
+
metadata: {
|
|
1635
|
+
...isBinding ? { isLocalRead: true, localBinding: true } : {},
|
|
1636
|
+
...q.target ? { qualifier: "enclosing" } : {},
|
|
1637
|
+
// THE RECEIVER CUT, on the rows that stayed bare and only there — a row that
|
|
1638
|
+
// resolved already says how it resolved (`qualifier`), and a binding has no
|
|
1639
|
+
// receiver question to answer.
|
|
1640
|
+
...isBinding ? {} : receiverCut(q)
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
} else {
|
|
1646
|
+
const method = links[links.length - 1];
|
|
1647
|
+
const before = links.length > 1 ? links[links.length - 2] : this.headNameText(head);
|
|
1648
|
+
const headText = this.headNameText(head);
|
|
1649
|
+
const calleeNode = n.previousNamedSibling ? this.lastIdentifierNode(n.previousNamedSibling) : null;
|
|
1650
|
+
if (this.isDartGlobalBuiltin(method)) {
|
|
1651
|
+
} else if (links.length === 1 && isCapitalized(method) && headText !== null && !isCapitalized(headText) && head !== null && (head.type === "identifier" || head.type === "type_identifier")) {
|
|
1652
|
+
relationships.push({
|
|
1653
|
+
source: callSource,
|
|
1654
|
+
target: method,
|
|
1655
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1656
|
+
confidence: 0.95,
|
|
1657
|
+
line_number: this.nodeLine(calleeNode),
|
|
1658
|
+
metadata: { isNamedConstructor: true, isPrefixQualified: true }
|
|
1659
|
+
});
|
|
1660
|
+
} else if (before !== null && isCapitalized(before) && namedCtors.has(`${before}.${method}`)) {
|
|
1661
|
+
relationships.push({
|
|
1662
|
+
source: callSource,
|
|
1663
|
+
target: before,
|
|
1664
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1665
|
+
confidence: 0.95,
|
|
1666
|
+
line_number: this.nodeLine(calleeNode),
|
|
1667
|
+
metadata: { isNamedConstructor: true }
|
|
1668
|
+
});
|
|
1669
|
+
} else {
|
|
1670
|
+
const ownerCand = links.length > 1 ? before : headOwner(head, owner, localTypes);
|
|
1671
|
+
const ownMeta = links.length > 1 ? null : receiverOwnerMeta(head, owner, localTypes, localEvidence);
|
|
1672
|
+
const q = qualifiedTarget(ownerCand, method);
|
|
1673
|
+
relationships.push({
|
|
1674
|
+
source: callSource,
|
|
1675
|
+
target: q.target ?? method,
|
|
1676
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1677
|
+
confidence: 0.9,
|
|
1678
|
+
line_number: this.nodeLine(calleeNode),
|
|
1679
|
+
// Same reason as the bare-call arm above: a leaf that the enclosing
|
|
1680
|
+
// function binds is a binding, and the row is labelled so a consumer
|
|
1681
|
+
// can tell it from a call to a declared member.
|
|
1682
|
+
//
|
|
1683
|
+
// 2026-10-08 — DELIBERATELY NOT re-spelled as a binding, and the reason is a
|
|
1684
|
+
// finding rather than an omission. This arm runs for a chain of TWO or more
|
|
1685
|
+
// links (`a.b.c()`), where the leaf is resolved against the RECEIVER, not
|
|
1686
|
+
// against the enclosing function: the `locals` set consulted here is the
|
|
1687
|
+
// enclosing function's, so `isLocalRead` on this arm answers a question about
|
|
1688
|
+
// the WRONG SCOPE whenever `links.length > 1`. The target cannot be re-spelled
|
|
1689
|
+
// to `#` for the same reason — for a chain the leaf may genuinely name a
|
|
1690
|
+
// declaration reachable through the receiver, and `qualifiedTarget` is the rule
|
|
1691
|
+
// that decides it. Named, not fixed: a one-link chain reaches the bare-call arm
|
|
1692
|
+
// above and IS re-spelled; the multi-link case needs its own measurement of how
|
|
1693
|
+
// often `locals.has(method)` is true where no receiver member exists.
|
|
1694
|
+
metadata: {
|
|
1695
|
+
...locals.has(method) ? { isLocalRead: true } : {},
|
|
1696
|
+
...q.target ? { qualifier: "chain" } : {},
|
|
1697
|
+
...receiverCut(q),
|
|
1698
|
+
// THE STORE'S CHANNEL (see `receiverOwnerMeta`): the exactly-known receiver
|
|
1699
|
+
// type plus the evidence class. Additive — the target spelling above is
|
|
1700
|
+
// unchanged, so nothing here can move an edge.
|
|
1701
|
+
...ownMeta ?? {}
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (t === "annotation") {
|
|
1708
|
+
this.emitAnnotationReferences(n, relationships, current, owner, moduleName, importAliases);
|
|
1709
|
+
}
|
|
1710
|
+
if (t === "selector" && !this.findChildByType(n, "argument_part")) {
|
|
1711
|
+
const memberNode = this.linkMemberNode(n);
|
|
1712
|
+
const member = memberNode?.text ?? null;
|
|
1713
|
+
if (member && !this.isDartGlobalBuiltin(member)) {
|
|
1714
|
+
const next = n.nextNamedSibling;
|
|
1715
|
+
const nextIsArgs = !!next && next.type === "selector" && !!this.findChildByType(next, "argument_part");
|
|
1716
|
+
if (!nextIsArgs && !this.selectorIsLhsWrite(n)) {
|
|
1717
|
+
const head = n.previousNamedSibling;
|
|
1718
|
+
const headText = this.headNameText(head);
|
|
1719
|
+
if (headText !== null && isCapitalized(headText) && namedCtors.has(`${headText}.${member}`)) {
|
|
1720
|
+
relationships.push({
|
|
1721
|
+
source: callSource,
|
|
1722
|
+
target: headText,
|
|
1723
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1724
|
+
confidence: 0.95,
|
|
1725
|
+
line_number: this.nodeLine(memberNode),
|
|
1726
|
+
metadata: { isNamedConstructor: true, isTearOff: true }
|
|
1727
|
+
});
|
|
1728
|
+
} else {
|
|
1729
|
+
const ownMeta = receiverOwnerMeta(head, owner, localTypes, localEvidence);
|
|
1730
|
+
const q = qualifiedTarget(headOwner(head, owner, localTypes), member);
|
|
1731
|
+
relationships.push({
|
|
1732
|
+
source: callSource,
|
|
1733
|
+
target: q.target ?? member,
|
|
1734
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1735
|
+
confidence: 0.9,
|
|
1736
|
+
line_number: this.nodeLine(memberNode),
|
|
1737
|
+
metadata: q.target ? { isMemberRead: true, qualifier: "head", ...receiverCut(q), ...ownMeta ?? {} } : { isMemberRead: true, ...receiverCut(q), ...ownMeta ?? {} }
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
if (t === "qualified" && this.isPatternTestConstant(n)) {
|
|
1744
|
+
const segs = n.text.split(".");
|
|
1745
|
+
const member = segs[segs.length - 1];
|
|
1746
|
+
const head = segs.length > 1 ? typeNameWithoutArguments(segs[segs.length - 2]) : null;
|
|
1747
|
+
if (member && !this.isDartGlobalBuiltin(member) && member !== head) {
|
|
1748
|
+
if (head !== null && isCapitalized(head) && (namedCtors.has(`${head}.${member}`) || member === "new")) {
|
|
1749
|
+
relationships.push({
|
|
1750
|
+
source: callSource,
|
|
1751
|
+
target: head,
|
|
1752
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1753
|
+
confidence: 0.95,
|
|
1754
|
+
line_number: this.nodeLine(n),
|
|
1755
|
+
metadata: member === "new" ? { isConstructorReference: true, isConstantPattern: true } : { isNamedConstructor: true, isConstantPattern: true }
|
|
1756
|
+
});
|
|
1757
|
+
} else {
|
|
1758
|
+
const q = qualifiedTarget(head, member);
|
|
1759
|
+
relationships.push({
|
|
1760
|
+
source: callSource,
|
|
1761
|
+
target: q.target ?? member,
|
|
1762
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1763
|
+
confidence: 0.9,
|
|
1764
|
+
line_number: this.nodeLine(n),
|
|
1765
|
+
metadata: q.target ? { isMemberRead: true, isConstantPattern: true, qualifier: "pattern", ...receiverCut(q) } : { isMemberRead: true, isConstantPattern: true, ...receiverCut(q) }
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (t === "identifier") {
|
|
1771
|
+
const txt = n.text;
|
|
1772
|
+
const ptype = n.parent?.type ?? "";
|
|
1773
|
+
const memberLink = ptype === "unconditional_assignable_selector" || ptype === "conditional_assignable_selector" || ptype === "cascade_selector" || ptype === "selector";
|
|
1774
|
+
const next = n.nextNamedSibling;
|
|
1775
|
+
const nextIsCall = !!next && next.type === "selector" && !!this.findChildByType(next, "argument_part");
|
|
1776
|
+
const declName = this.isDartDeclNameParent(ptype) || this.isDartDeclaredName(n);
|
|
1777
|
+
const isWrite = this.selectorIsLhsWrite(n);
|
|
1778
|
+
const isLabel = ptype === "labeled_statement" || ptype === "break_statement" || ptype === "continue_statement";
|
|
1779
|
+
const isFieldInitTarget = ptype === "field_initializer" && (() => {
|
|
1780
|
+
const first = n.parent?.namedChild(0);
|
|
1781
|
+
return !!first && first.startIndex === n.startIndex && first.endIndex === n.endIndex;
|
|
1782
|
+
})();
|
|
1783
|
+
const isAnnotationName = ptype === "annotation" || ptype === "scoped_identifier" && n.parent?.parent?.type === "annotation";
|
|
1784
|
+
if (txt === "this" || txt === "super") {
|
|
1785
|
+
} else if (!memberLink && !nextIsCall && !declName && !isWrite && !isLabel && !isFieldInitTarget && !isAnnotationName) {
|
|
1786
|
+
const isDeclaredMember = !!owner && (membersByOwner.get(owner)?.has(txt) ?? false);
|
|
1787
|
+
if (!locals.has(txt) && isDeclaredMember) {
|
|
1788
|
+
const q = qualifiedTarget(owner, txt);
|
|
1789
|
+
relationships.push({
|
|
1790
|
+
source: callSource,
|
|
1791
|
+
target: q.target ?? txt,
|
|
1792
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1793
|
+
confidence: 0.6,
|
|
1794
|
+
line_number: this.nodeLine(n),
|
|
1795
|
+
metadata: q.target ? { isBareRead: true, qualifier: "enclosing", ...receiverCut(q) } : { isBareRead: true, ...receiverCut(q) }
|
|
1796
|
+
});
|
|
1797
|
+
} else if (locals.has(txt)) {
|
|
1798
|
+
relationships.push({
|
|
1799
|
+
source: callSource,
|
|
1800
|
+
target: `${callSource}${BINDING_SEP}${txt}`,
|
|
1801
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1802
|
+
confidence: 0.9,
|
|
1803
|
+
line_number: this.nodeLine(n),
|
|
1804
|
+
// A read of a PARAMETER is a binding too: `locals` holds the function's bound
|
|
1805
|
+
// names (parameters and `var`s alike), which is why both land here.
|
|
1806
|
+
metadata: { isLocalRead: true, localBinding: true, ...this.localBindingScope(n, txt) ?? {} }
|
|
1807
|
+
});
|
|
1808
|
+
} else if (this.isPatternTestConstant(n)) {
|
|
1809
|
+
relationships.push({
|
|
1810
|
+
source: callSource,
|
|
1811
|
+
target: txt,
|
|
1812
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1813
|
+
confidence: 0.9,
|
|
1814
|
+
line_number: this.nodeLine(n),
|
|
1815
|
+
metadata: { isConstantPattern: true, isPatternTest: true }
|
|
1816
|
+
});
|
|
1817
|
+
} else if (!isDeclaredMember && topLevelValues.has(txt)) {
|
|
1818
|
+
relationships.push({
|
|
1819
|
+
source: callSource,
|
|
1820
|
+
target: txt,
|
|
1821
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1822
|
+
confidence: 0.9,
|
|
1823
|
+
line_number: this.nodeLine(n),
|
|
1824
|
+
metadata: { isGlobalRead: true }
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (t === "initializer_list_entry") {
|
|
1830
|
+
let headIsSuper = false;
|
|
1831
|
+
let headIsThis = false;
|
|
1832
|
+
let headNode = null;
|
|
1833
|
+
const firstRaw = n.child(0);
|
|
1834
|
+
if (firstRaw && !firstRaw.isNamed && (firstRaw.text === "super" || firstRaw.text === "this")) {
|
|
1835
|
+
headIsSuper = firstRaw.text === "super";
|
|
1836
|
+
headIsThis = firstRaw.text === "this";
|
|
1837
|
+
headNode = firstRaw;
|
|
1838
|
+
} else {
|
|
1839
|
+
const firstNamed = n.namedChild(0);
|
|
1840
|
+
if (firstNamed && (firstNamed.type === "super" || firstNamed.type === "this")) {
|
|
1841
|
+
headIsSuper = firstNamed.type === "super";
|
|
1842
|
+
headIsThis = firstNamed.type === "this";
|
|
1843
|
+
headNode = firstNamed;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (headIsSuper && owner) {
|
|
1847
|
+
const base = superByClass.get(owner);
|
|
1848
|
+
if (base) {
|
|
1849
|
+
relationships.push({
|
|
1850
|
+
source: callSource,
|
|
1851
|
+
target: base,
|
|
1852
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1853
|
+
confidence: 0.95,
|
|
1854
|
+
line_number: this.nodeLine(headNode),
|
|
1855
|
+
metadata: { isSuperConstructor: true }
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
} else if (headIsThis && owner) {
|
|
1859
|
+
relationships.push({
|
|
1860
|
+
source: callSource,
|
|
1861
|
+
target: owner,
|
|
1862
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1863
|
+
confidence: 0.95,
|
|
1864
|
+
line_number: this.nodeLine(headNode),
|
|
1865
|
+
metadata: { isRedirectingConstructor: true }
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (t === "redirection" && owner) {
|
|
1870
|
+
let rSuper = false;
|
|
1871
|
+
let rThis = false;
|
|
1872
|
+
let headTok = null;
|
|
1873
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1874
|
+
const c = n.namedChild(i);
|
|
1875
|
+
if (!c) continue;
|
|
1876
|
+
if (c.type === "this") {
|
|
1877
|
+
rThis = true;
|
|
1878
|
+
headTok = c;
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
if (c.type === "super") {
|
|
1882
|
+
rSuper = true;
|
|
1883
|
+
headTok = c;
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
if (c.type === "identifier" || c.type === "type_identifier" || c.type === "arguments") continue;
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
if (rSuper) {
|
|
1890
|
+
const base = superByClass.get(owner);
|
|
1891
|
+
if (base) {
|
|
1892
|
+
relationships.push({
|
|
1893
|
+
source: callSource,
|
|
1894
|
+
target: base,
|
|
1895
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1896
|
+
confidence: 0.95,
|
|
1897
|
+
line_number: this.nodeLine(headTok),
|
|
1898
|
+
metadata: { isSuperConstructor: true }
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
} else if (rThis) {
|
|
1902
|
+
relationships.push({
|
|
1903
|
+
source: callSource,
|
|
1904
|
+
target: owner,
|
|
1905
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1906
|
+
confidence: 0.95,
|
|
1907
|
+
line_number: this.nodeLine(headTok),
|
|
1908
|
+
metadata: { isRedirectingConstructor: true }
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (t === "identifier") {
|
|
1913
|
+
const parent = n.parent;
|
|
1914
|
+
const ptype = parent?.type ?? "";
|
|
1915
|
+
if (!this.isDartDeclNameParent(ptype) && ptype !== "label" && !this.isDartDeclaredName(n)) {
|
|
1916
|
+
const nx = n.nextNamedSibling;
|
|
1917
|
+
if (nx && (nx.type === "selector" && this.isIndexSelector(nx) || this.isIndexAssignable(nx))) {
|
|
1918
|
+
relationships.push({
|
|
1919
|
+
source: callSource,
|
|
1920
|
+
target: n.text,
|
|
1921
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1922
|
+
confidence: 0.9,
|
|
1923
|
+
line_number: this.nodeLine(n),
|
|
1924
|
+
metadata: { isSubscriptBase: true }
|
|
1925
|
+
});
|
|
1926
|
+
} else if (this.isInsideTemplateSubstitution(n)) {
|
|
1927
|
+
const isLocal = locals.has(n.text);
|
|
1928
|
+
relationships.push({
|
|
1929
|
+
source: callSource,
|
|
1930
|
+
target: isLocal ? `${callSource}${BINDING_SEP}${n.text}` : n.text,
|
|
1931
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1932
|
+
confidence: 0.9,
|
|
1933
|
+
line_number: this.nodeLine(n),
|
|
1934
|
+
metadata: isLocal ? { isInterpolationRead: true, isLocalRead: true, localBinding: true, ...this.localBindingScope(n, n.text) ?? {} } : { isInterpolationRead: true }
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (t === "identifier_dollar_escaped") {
|
|
1940
|
+
const raw = n.text;
|
|
1941
|
+
const name = raw.startsWith("$") ? raw.slice(1) : raw;
|
|
1942
|
+
if (name) {
|
|
1943
|
+
const isLocal = locals.has(name);
|
|
1944
|
+
relationships.push({
|
|
1945
|
+
source: callSource,
|
|
1946
|
+
target: isLocal ? `${callSource}${BINDING_SEP}${name}` : name,
|
|
1947
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1948
|
+
confidence: 0.9,
|
|
1949
|
+
line_number: this.nodeLine(n),
|
|
1950
|
+
metadata: isLocal ? { isInterpolationRead: true, isLocalRead: true, localBinding: true, ...this.localBindingScope(n, name) ?? {} } : { isInterpolationRead: true }
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
if (t === "cascade_section") {
|
|
1955
|
+
let sectionAssigns = false;
|
|
1956
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1957
|
+
const c = n.child(i);
|
|
1958
|
+
if (c && !c.isNamed && c.text === "=") {
|
|
1959
|
+
sectionAssigns = true;
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1964
|
+
const c = n.namedChild(i);
|
|
1965
|
+
if (!c) continue;
|
|
1966
|
+
if (c.type !== "cascade_selector" && c.type !== "unconditional_assignable_selector" && c.type !== "conditional_assignable_selector" && c.type !== "selector") continue;
|
|
1967
|
+
const next = n.namedChild(i + 1);
|
|
1968
|
+
if (next && next.type === "argument_part") {
|
|
1969
|
+
const idNode = this.lastIdentifierNode(c);
|
|
1970
|
+
const id = idNode?.text ?? null;
|
|
1971
|
+
if (id && !this.isDartGlobalBuiltin(id)) {
|
|
1972
|
+
relationships.push({
|
|
1973
|
+
source: callSource,
|
|
1974
|
+
target: id,
|
|
1975
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1976
|
+
confidence: 0.9,
|
|
1977
|
+
line_number: this.nodeLine(idNode),
|
|
1978
|
+
metadata: { isCascade: true }
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
} else if (!sectionAssigns) {
|
|
1982
|
+
const idNode = this.lastIdentifierNode(c);
|
|
1983
|
+
const id = idNode?.text ?? null;
|
|
1984
|
+
if (id && !this.isDartGlobalBuiltin(id)) {
|
|
1985
|
+
relationships.push({
|
|
1986
|
+
source: callSource,
|
|
1987
|
+
target: id,
|
|
1988
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1989
|
+
confidence: 0.9,
|
|
1990
|
+
line_number: this.nodeLine(idNode),
|
|
1991
|
+
metadata: { isMemberRead: true, isCascade: true }
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (t === "type_identifier") {
|
|
1998
|
+
this.collectTypeMention(n, callSource, relationships, visibleTypes, fileTypes);
|
|
1999
|
+
}
|
|
2000
|
+
return [current, owner, childLocals, childLocalTypes, childLocalEvidence, childLocalElements, childInsideCollected];
|
|
2001
|
+
};
|
|
2002
|
+
const processNode = (root, current, owner, locals, localTypes, localEvidence, localElements, insideCollected) => {
|
|
2003
|
+
const work = [
|
|
2004
|
+
[root, current, owner, locals, localTypes, localEvidence, localElements, insideCollected]
|
|
2005
|
+
];
|
|
2006
|
+
while (work.length > 0) {
|
|
2007
|
+
const [n2, cur, own, loc, types, evid, elems, inside] = work.pop();
|
|
2008
|
+
const [cur2, own2, loc2, types2, evid2, elems2, inside2] = visitNode(n2, cur, own, loc, types, evid, elems, inside);
|
|
2009
|
+
const kids = n2.namedChildren ?? [];
|
|
2010
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
2011
|
+
const child = kids[i];
|
|
2012
|
+
if (child) work.push([child, cur2, own2, loc2, types2, evid2, elems2, inside2]);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
processNode(node, null, null, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), false);
|
|
2017
|
+
}
|
|
2018
|
+
// ============================================================================
|
|
2019
|
+
// `references` — A DECLARATION MENTIONS A TYPE
|
|
2020
|
+
// ============================================================================
|
|
2021
|
+
/**
|
|
2022
|
+
* One type mention, read off ONE node. No recursion, no state: everything it
|
|
2023
|
+
* needs (`source`, the type set, the row sink) arrives as an argument, so the
|
|
2024
|
+
* rule below is the whole rule and a reader does not have to reconstruct a walk
|
|
2025
|
+
* to know what a `references` edge claims.
|
|
2026
|
+
*
|
|
2027
|
+
* ## WHAT IS CLAIMED
|
|
2028
|
+
*
|
|
2029
|
+
* `X references T` means: the declaration `X` names the type `T` in a TYPE
|
|
2030
|
+
* POSITION, and `T` binds to a type declaration this run can see.
|
|
2031
|
+
*
|
|
2032
|
+
* Dart is statically typed, and a type position is a grammatical position, not
|
|
2033
|
+
* a convention — the grammar has a dedicated node (`type_identifier`) that never
|
|
2034
|
+
* appears where a value identifier appears. So the mention is a PARSE FACT, not
|
|
2035
|
+
* a name-shape guess. What is not a parse fact is the BINDING of that name to a
|
|
2036
|
+
* declaration, which is why the edge is published only when the name resolves
|
|
2037
|
+
* and carries 0.95 rather than 1.0 — the same derivation `instantiates` is
|
|
2038
|
+
* published at, for the same reason (a syntactic role plus a resolved name).
|
|
2039
|
+
*
|
|
2040
|
+
* ## WHY RESOLUTION IS A GATE AND NOT A REPORT
|
|
2041
|
+
*
|
|
2042
|
+
* Measured over 1,000 corpus files across 5 repositories: 75,769 type mentions
|
|
2043
|
+
* outside heritage clauses, of which 3,460 (4.57%) name a type the same file
|
|
2044
|
+
* declares. The other 95.43% name `String`, `Widget`, `Future` — real types, in
|
|
2045
|
+
* `dart:core` and the Flutter SDK, declared in files this per-file run has not
|
|
2046
|
+
* read. Publishing those would make the label mostly a list of SDK types, at
|
|
2047
|
+
* ~22x the edge count, and every one of them an endpoint no symbol in the index
|
|
2048
|
+
* can bind. A narrow graph that is true beats a wide graph that is not, so the
|
|
2049
|
+
* unresolvable majority is deliberately unread, and the number is stated rather
|
|
2050
|
+
* than left to be discovered downstream.
|
|
2051
|
+
*
|
|
2052
|
+
* A `projectIndex` widens the gate to every type the run can see
|
|
2053
|
+
* (`src/ast/project-index.ts` `typeKinds`), which is how the differential's
|
|
2054
|
+
* `--dart-deps=direct` runs measure more than production does. Production does
|
|
2055
|
+
* not supply one for `.dart`: `parallel-indexer.ts` `PROJECT_INDEX_EXTENSIONS`
|
|
2056
|
+
* names only `.sol`. So the published set is file-local in production and grows
|
|
2057
|
+
* with an index — recorded per row in `isIndexResolved` so the two answers are
|
|
2058
|
+
* distinguishable in the store rather than conflated.
|
|
2059
|
+
*
|
|
2060
|
+
* ## WHAT IS NOT A REFERENCE
|
|
2061
|
+
*
|
|
2062
|
+
* Measured parent-position inventory (the probe that produced it is
|
|
2063
|
+
* `validation/m14/fixtures/dart/type-position-probe.ts`):
|
|
2064
|
+
*
|
|
2065
|
+
* published — formal_parameter, normal_parameter_type, super_formal_parameter,
|
|
2066
|
+
* constructor_param (a `this.x` parameter's type), function_signature /
|
|
2067
|
+
* getter_signature / setter_signature / operator_signature (return types),
|
|
2068
|
+
* type_arguments (`List<T>`, `Map<K,V>`), function_type, record_type_field,
|
|
2069
|
+
* type_cast (`as T`), type_test (`is T`), try_statement (`on T catch`),
|
|
2070
|
+
* initialized_variable_definition, typed_identifier, for_loop_parts,
|
|
2071
|
+
* declaration (a field's type), program (a top-level `const String x`),
|
|
2072
|
+
* qualified (the LEAF of `prefix.Type`), constructor_tearoff (`http.Client`
|
|
2073
|
+
* used as a value), extension_declaration (`on T`), representation_declaration
|
|
2074
|
+
* (an extension type's representation type), type_alias (`typedef A = B`),
|
|
2075
|
+
* type_bound (`<T extends B>`), object_pattern / variable_pattern / cast_pattern
|
|
2076
|
+
* (a Dart 3 pattern's type), record_type.
|
|
2077
|
+
*
|
|
2078
|
+
* NOT published, each for a stated reason:
|
|
2079
|
+
* superclass / interfaces / mixins / mixin_declaration / mixin_application —
|
|
2080
|
+
* heritage already publishes `extends` / `implements` / `uses`, and a second
|
|
2081
|
+
* label for the same fact double-counts it.
|
|
2082
|
+
* new_expression / const_object_expression / constructor_invocation /
|
|
2083
|
+
* redirecting_factory_constructor_signature — construction already publishes
|
|
2084
|
+
* `instantiates`.
|
|
2085
|
+
* type_parameter — `class Box<T>` DECLARES a type variable; it does not
|
|
2086
|
+
* mention a type. A mention of `T` inside that class resolves to the type
|
|
2087
|
+
* VARIABLE, which is what `isTypeParameterShadowed` refuses.
|
|
2088
|
+
* ERROR — nothing inside a parse-error region is a fact about a well-formed
|
|
2089
|
+
* program, and this analyzer's error policy is to publish what survived the
|
|
2090
|
+
* parse, not what the recovery invented.
|
|
2091
|
+
*
|
|
2092
|
+
* The last one is the reason this method checks the PARENT at all: in this
|
|
2093
|
+
* grammar `type_identifier` only ever appears in a type position, so the node
|
|
2094
|
+
* type alone would publish the double-counted positions above.
|
|
2095
|
+
*/
|
|
2096
|
+
collectTypeMention(n, source, relationships, visibleTypes, fileTypes) {
|
|
2097
|
+
const parent = n.parent;
|
|
2098
|
+
const position = parent ? parent.type : "";
|
|
2099
|
+
if (DART_TYPE_MENTION_DOUBLE_COUNTED.has(position)) return;
|
|
2100
|
+
const name = n.text;
|
|
2101
|
+
if (!name || name === IMPLICIT_DYNAMIC_TYPE) return;
|
|
2102
|
+
if (name === source) return;
|
|
2103
|
+
if (this.isUnnamedExtensionSubject(n)) return;
|
|
2104
|
+
if (!visibleTypes.has(name)) return;
|
|
2105
|
+
if (this.isTypeParameterShadowed(n, name)) return;
|
|
2106
|
+
relationships.push({
|
|
2107
|
+
source,
|
|
2108
|
+
target: name,
|
|
2109
|
+
type: ASTEngine.RelationshipType.References,
|
|
2110
|
+
confidence: 0.95,
|
|
2111
|
+
line_number: this.nodeLine(n),
|
|
2112
|
+
metadata: {
|
|
2113
|
+
isTypeMention: true,
|
|
2114
|
+
typePosition: position,
|
|
2115
|
+
isIndexResolved: !fileTypes.has(name)
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Is `name` bound by a TYPE PARAMETER of an enclosing declaration?
|
|
2121
|
+
* `class Box<T> { T value; }` — `T` here is the type variable the class
|
|
2122
|
+
* introduced, so the mention is not a reference to a declared type even when
|
|
2123
|
+
* something in the project is also called `T`.
|
|
2124
|
+
*/
|
|
2125
|
+
isTypeParameterShadowed(n, name) {
|
|
2126
|
+
let cur = n.parent;
|
|
2127
|
+
let guard = 0;
|
|
2128
|
+
while (cur && guard++ < 32) {
|
|
2129
|
+
const tp = this.findChildByType(cur, "type_parameters");
|
|
2130
|
+
if (tp) {
|
|
2131
|
+
for (let i = 0; i < tp.namedChildCount; i++) {
|
|
2132
|
+
const c = tp.namedChild(i);
|
|
2133
|
+
if (!c || c.type !== "type_parameter") continue;
|
|
2134
|
+
const declared = c.childForFieldName?.("name")?.text ?? this.findChildByType(c, "type_identifier")?.text;
|
|
2135
|
+
if (declared === name) return true;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
cur = cur.parent;
|
|
2139
|
+
}
|
|
2140
|
+
return false;
|
|
2141
|
+
}
|
|
2142
|
+
// ============================================================================
|
|
2143
|
+
// CLASS HERITAGE
|
|
2144
|
+
// ============================================================================
|
|
2145
|
+
extractClassHeritage(node, className, relationships, patterns) {
|
|
2146
|
+
const mixinAppCls = this.findChildByType(node, "mixin_application_class");
|
|
2147
|
+
if (mixinAppCls) {
|
|
2148
|
+
const app = this.findChildByType(mixinAppCls, "mixin_application");
|
|
2149
|
+
if (app) {
|
|
2150
|
+
const baseNode = this.resolvedHeritageNodes(app)[0] ?? null;
|
|
2151
|
+
if (baseNode) {
|
|
2152
|
+
relationships.push({
|
|
2153
|
+
source: className,
|
|
2154
|
+
target: baseNode.text,
|
|
2155
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
2156
|
+
confidence: 1,
|
|
2157
|
+
line_number: this.nodeLine(baseNode),
|
|
2158
|
+
metadata: {}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
const mx = this.findChildByType(app, "mixins");
|
|
2162
|
+
if (mx) {
|
|
2163
|
+
patterns["Mixins"] = true;
|
|
2164
|
+
for (const ti of this.resolvedHeritageNodes(mx)) {
|
|
2165
|
+
relationships.push({
|
|
2166
|
+
source: className,
|
|
2167
|
+
target: ti.text,
|
|
2168
|
+
type: ASTEngine.RelationshipType.MixesIn,
|
|
2169
|
+
confidence: 1,
|
|
2170
|
+
line_number: this.nodeLine(ti),
|
|
2171
|
+
metadata: { isMixin: true }
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (node.type === "mixin_declaration") {
|
|
2178
|
+
const onNodes = this.resolvedHeritageNodes(node).filter((n) => n.text !== className);
|
|
2179
|
+
for (const baseNode of onNodes) {
|
|
2180
|
+
relationships.push({
|
|
2181
|
+
source: className,
|
|
2182
|
+
target: baseNode.text,
|
|
2183
|
+
type: ASTEngine.RelationshipType.References,
|
|
2184
|
+
confidence: 1,
|
|
2185
|
+
line_number: this.nodeLine(baseNode),
|
|
2186
|
+
metadata: { isMixinOn: true, isTypeMention: true, typePosition: "mixin_on_clause" }
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const superclass = this.findChildByType(node, "superclass");
|
|
2191
|
+
if (superclass) {
|
|
2192
|
+
const baseNode = this.resolvedHeritageNodes(superclass)[0] ?? null;
|
|
2193
|
+
if (baseNode) {
|
|
2194
|
+
relationships.push({
|
|
2195
|
+
source: className,
|
|
2196
|
+
target: baseNode.text,
|
|
2197
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
2198
|
+
confidence: 1,
|
|
2199
|
+
line_number: this.nodeLine(baseNode),
|
|
2200
|
+
metadata: {}
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
const interfaces = this.findChildByType(node, "interfaces");
|
|
2205
|
+
if (interfaces) {
|
|
2206
|
+
for (const ti of this.resolvedHeritageNodes(interfaces)) {
|
|
2207
|
+
relationships.push({
|
|
2208
|
+
source: className,
|
|
2209
|
+
target: ti.text,
|
|
2210
|
+
type: ASTEngine.RelationshipType.Implements,
|
|
2211
|
+
confidence: 1,
|
|
2212
|
+
line_number: this.nodeLine(ti),
|
|
2213
|
+
metadata: { isImplements: true }
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
let mixinsNode = superclass ? this.findChildByType(superclass, "mixins") : void 0;
|
|
2218
|
+
if (!mixinsNode) mixinsNode = this.findChildByType(node, "mixins");
|
|
2219
|
+
if (mixinsNode) {
|
|
2220
|
+
patterns["Mixins"] = true;
|
|
2221
|
+
for (const ti of this.resolvedHeritageNodes(mixinsNode)) {
|
|
2222
|
+
relationships.push({
|
|
2223
|
+
source: className,
|
|
2224
|
+
target: ti.text,
|
|
2225
|
+
type: ASTEngine.RelationshipType.MixesIn,
|
|
2226
|
+
confidence: 1,
|
|
2227
|
+
line_number: this.nodeLine(ti),
|
|
2228
|
+
metadata: { isMixin: true }
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
// ============================================================================
|
|
2234
|
+
// `renders` — A COMPONENT RENDERS ANOTHER COMPONENT
|
|
2235
|
+
// ============================================================================
|
|
2236
|
+
//
|
|
2237
|
+
// THE CONTRACT (`src/ast/types.ts:131-140`): *"A component RENDERS another component: source = the
|
|
2238
|
+
// rendering symbol, target = the rendered component."* The fleet's OTHER lanes publish it in
|
|
2239
|
+
// three different languages and ALL THREE use this shape — the RENDERING SYMBOL as the source and
|
|
2240
|
+
// the RENDERED COMPONENT'S NAME as the target:
|
|
2241
|
+
//
|
|
2242
|
+
// · typescript/js (`real-typescript-analyzer.ts:6758`, `real-javascript-analyzer.ts:2667`):
|
|
2243
|
+
// source = the enclosing component function, target = the JSX tag name, and a LOWERCASE tag is
|
|
2244
|
+
// NOT a component (the language's own capitalization convention).
|
|
2245
|
+
// · clojure (`real-clojure-analyzer.ts:1584`): source = the enclosing fn, target = the component
|
|
2246
|
+
// symbol inside a hiccup vector — a VECTOR LITERAL, an EXISTING node type, no grammar change.
|
|
2247
|
+
// · csharp (`real-csharp-analyzer.ts:2285`): source = the rendering context, target = the
|
|
2248
|
+
// component named by a TYPE ARGUMENT — an EXISTING node type, no grammar change.
|
|
2249
|
+
//
|
|
2250
|
+
// ⇒⇒ THE VALUE IS NOT JSX-SPECIFIC, AND FOR DART THE CONSTRUCT IS FLUTTER'S `build` METHOD:
|
|
2251
|
+
// `class Home extends StatelessWidget { Widget build(BuildContext context) => Scaffold(); }`
|
|
2252
|
+
// `Home` IS the component and `Scaffold` IS the component it renders — the structural analogue of
|
|
2253
|
+
// a React component returning `<Scaffold/>`. Dart has no JSX, so the syntax that carries the
|
|
2254
|
+
// construct is the rendering ENTRY POINT, which is what the `build` method is.
|
|
2255
|
+
//
|
|
2256
|
+
// THE TRIGGER, AND WHY IT IS NOT THE METHOD NAME ALONE:
|
|
2257
|
+
// · the method is named `build` (Flutter's rendering entry point), AND
|
|
2258
|
+
// · its DECLARED RETURN TYPE is `Widget` — the framework's own marker, the same kind of
|
|
2259
|
+
// language-level convention the JS lane reads off capitalization. Without it, any class with
|
|
2260
|
+
// a `build()` method would qualify, including the protobuf-style message builders whose
|
|
2261
|
+
// `build()` returns a message and not a component.
|
|
2262
|
+
// The measured populations behind both halves are in
|
|
2263
|
+
// `validation/m14/measurements/dart-extended-constructs-2026-10-21.json`.
|
|
2264
|
+
//
|
|
2265
|
+
// WHAT DOES NOT FIRE, each for its own reason (and each pinned by the fixture):
|
|
2266
|
+
// · a `build` method whose body returns a VARIABLE or a CALL — no component is NAMED, so there
|
|
2267
|
+
// is nothing to render (the nearest construct, a call, stays a `calls` row);
|
|
2268
|
+
// · a `build` method whose declared return type is not `Widget` — not a rendering host;
|
|
2269
|
+
// · a construction that is NOT the returned expression — it is a `calls`/`instantiates` row,
|
|
2270
|
+
// because the widget tree is not the return value;
|
|
2271
|
+
// · a LOWERCASE name — Dart's class-naming convention, the same test the JS lane applies to a
|
|
2272
|
+
// JSX tag (`<div>` is an intrinsic, not a component).
|
|
2273
|
+
extractRenderRelationships(node, className, relationships) {
|
|
2274
|
+
const classBody = node.childForFieldName("body");
|
|
2275
|
+
if (!classBody) return;
|
|
2276
|
+
for (const member of classBody.namedChildren) {
|
|
2277
|
+
if (!member || member.type !== "method_signature") continue;
|
|
2278
|
+
const fsig = member.namedChildren.find((k) => k && k.type === "function_signature");
|
|
2279
|
+
if (!fsig) continue;
|
|
2280
|
+
if (fsig.childForFieldName("name")?.text !== "build") continue;
|
|
2281
|
+
const first = fsig.namedChild(0);
|
|
2282
|
+
const declared = first && (first.type === "type_identifier" || first.type === "type") ? first.text : "";
|
|
2283
|
+
if (declared !== "Widget") continue;
|
|
2284
|
+
let body = member.nextNamedSibling;
|
|
2285
|
+
while (body && body.type !== "function_body" && body.type !== "method_signature") {
|
|
2286
|
+
body = body.nextNamedSibling;
|
|
2287
|
+
}
|
|
2288
|
+
if (!body || body.type !== "function_body") continue;
|
|
2289
|
+
const block = body.namedChildren.find((k) => k && k.type === "block");
|
|
2290
|
+
const ret = (block ?? body).namedChildren.find((k) => k && k.type === "return_statement");
|
|
2291
|
+
const expr = ret ? ret.namedChild(0) : body.namedChildren.find((k) => k && k.type !== "comment") ?? null;
|
|
2292
|
+
const target = this.renderedComponentName(expr);
|
|
2293
|
+
if (!target) continue;
|
|
2294
|
+
relationships.push({
|
|
2295
|
+
source: className,
|
|
2296
|
+
target,
|
|
2297
|
+
type: ASTEngine.RelationshipType.Renders,
|
|
2298
|
+
confidence: 0.9,
|
|
2299
|
+
line_number: this.nodeLine(expr),
|
|
2300
|
+
metadata: { isRenderHost: true, renderHost: "build", renderTarget: target }
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* THE NAME OF THE COMPONENT A CONSTRUCTION EXPRESSION NAMES — dart spells a construction three
|
|
2306
|
+
* ways and all three name the type: `const T()` is a `const_object_expression`, `new T()` is a
|
|
2307
|
+
* `new_expression`, and the modern `T()` is a BARE `identifier` FOLLOWED BY A `selector` (the
|
|
2308
|
+
* call). A bare identifier with NO selector is a VARIABLE, not a construction — that distinction
|
|
2309
|
+
* is the refusal, and it is why the selector is checked rather than the identifier alone.
|
|
2310
|
+
*/
|
|
2311
|
+
renderedComponentName(expr) {
|
|
2312
|
+
if (!expr) return null;
|
|
2313
|
+
let name = null;
|
|
2314
|
+
if (expr.type === "const_object_expression" || expr.type === "new_expression") {
|
|
2315
|
+
const t = expr.childForFieldName("type") ?? expr.namedChildren.find((k) => k && (k.type === "type_identifier" || k.type === "identifier"));
|
|
2316
|
+
name = t ? t.text : null;
|
|
2317
|
+
} else if (expr.type === "identifier") {
|
|
2318
|
+
const sib = expr.nextNamedSibling;
|
|
2319
|
+
name = sib && sib.type === "selector" ? expr.text : null;
|
|
2320
|
+
}
|
|
2321
|
+
if (!name) return null;
|
|
2322
|
+
const first = name[0];
|
|
2323
|
+
if (!first || first === first.toLowerCase()) return null;
|
|
2324
|
+
return name;
|
|
2325
|
+
}
|
|
2326
|
+
// ============================================================================
|
|
2327
|
+
// EVIDENCE TIERS
|
|
2328
|
+
// ============================================================================
|
|
2329
|
+
/**
|
|
2330
|
+
* Evidence-tier classification (family contract — mirrors csharp/java/rust;
|
|
2331
|
+
* M14 section D requires >=2 distinct evidenceClass values). Same rules as
|
|
2332
|
+
* the managed-OO family: structural (grammar clauses, conf 1.0) |
|
|
2333
|
+
* usage_contextual (calls, mixin uses) | import_based (import/export/part
|
|
2334
|
+
* directives) | inferred (confidence < 1 edges).
|
|
2335
|
+
*/
|
|
2336
|
+
classifyEvidenceTier(relationships) {
|
|
2337
|
+
for (const r of relationships) {
|
|
2338
|
+
const type = r.type ?? r.relationshipType ?? "";
|
|
2339
|
+
const md = r.metadata ?? {};
|
|
2340
|
+
if (type === "contains") {
|
|
2341
|
+
md.evidenceClass = "structural";
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
if (type === "imports" || type === "exports") {
|
|
2345
|
+
md.evidenceClass = "import_based";
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
if (type === "part_of") {
|
|
2349
|
+
md.evidenceClass = "import_based";
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
if (type === "mixes_in") {
|
|
2353
|
+
md.evidenceClass = "structural";
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (type === "calls" || type === "uses") {
|
|
2357
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.85 ? "usage_contextual" : "inferred";
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
if (type === "instantiates" || type === "extends" || type === "implements") {
|
|
2361
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
if (type === "overrides") {
|
|
2365
|
+
md.evidenceClass = "structural";
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
if (type === "renders") {
|
|
2369
|
+
md.evidenceClass = "usage_contextual";
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (type === "references") {
|
|
2373
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.85 ? "usage_contextual" : "inferred";
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
md.evidenceClass = "usage_contextual";
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
// ============================================================================
|
|
2380
|
+
// HELPERS
|
|
2381
|
+
// ============================================================================
|
|
2382
|
+
/**
|
|
2383
|
+
* The library this file declares: its name, the node that declared it, and which of
|
|
2384
|
+
* the three directive forms it was.
|
|
2385
|
+
*
|
|
2386
|
+
* `library X;` names the library X. No `library` directive means the file is an
|
|
2387
|
+
* implicit library named by its stem (Dart spec). `library;` — legal, and 101 of a
|
|
2388
|
+
* 3,000-file sample — declares a library with NO name, so the stem there is this
|
|
2389
|
+
* analyzer's convention and the caller records it as `kind: "unnamed"` rather than
|
|
2390
|
+
* letting a consumer read it as a declared fact.
|
|
2391
|
+
*
|
|
2392
|
+
* The declared name is read from the parse tree, never from a regex over the text,
|
|
2393
|
+
* because it is frequently DOTTED: `_fe_analyzer_shared.scanner.characters`, which a
|
|
2394
|
+
* first-token match truncates to `_fe_analyzer_shared` — a name no other file imports.
|
|
2395
|
+
* The corpus measurement that established the three frequencies and the `annotation`
|
|
2396
|
+
* case is `validation/m14/fixtures/dart/library-directive-census.ts`; the
|
|
2397
|
+
* `annotation` case is why the child must pass an IDENTIFIER shape test rather than
|
|
2398
|
+
* being taken as text.
|
|
2399
|
+
*
|
|
2400
|
+
* Only a DIRECT child of `program` is considered: a `library_name` anywhere else in
|
|
2401
|
+
* the tree is a declaration inside a part or a raw string (52 of 3,000 sampled files
|
|
2402
|
+
* carry `library X;` inside a `'''...'''` test fixture, which is not a directive at
|
|
2403
|
+
* all), not this file's library. Measured: no sampled file has more than one
|
|
2404
|
+
* top-level `library_name`, so "first valid" is not a choice between two candidates.
|
|
2405
|
+
*/
|
|
2406
|
+
libraryName(rootNode, filePath) {
|
|
2407
|
+
const stem = path.basename(filePath, path.extname(filePath));
|
|
2408
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
2409
|
+
const child = rootNode.namedChild(i);
|
|
2410
|
+
if (!child || child.type !== "library_name") continue;
|
|
2411
|
+
if (child.namedChildCount === 0) return { name: stem, directive: child, kind: "unnamed" };
|
|
2412
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
2413
|
+
const nameNode = child.namedChild(j);
|
|
2414
|
+
if (!nameNode) continue;
|
|
2415
|
+
const declared = stripWhitespace(nameNode.text);
|
|
2416
|
+
if (declared.length > 0 && isDottedIdentifier(declared)) {
|
|
2417
|
+
return { name: declared, directive: child, kind: "named" };
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return { name: stem, directive: null, kind: "implicit" };
|
|
2422
|
+
}
|
|
2423
|
+
isDartGlobalBuiltin(name) {
|
|
2424
|
+
if (name === "print" || name === "assert" || name === "identical") return true;
|
|
2425
|
+
if (name === "toString" || name === "hashCode" || name === "runtimeType" || name === "noSuchMethod") return true;
|
|
2426
|
+
return false;
|
|
2427
|
+
}
|
|
2428
|
+
selectorIsBareCall(selector) {
|
|
2429
|
+
const prev = selector.previousNamedSibling;
|
|
2430
|
+
if (!prev) return false;
|
|
2431
|
+
return prev.type === "identifier" || prev.type === "type_identifier";
|
|
2432
|
+
}
|
|
2433
|
+
resolveSelectorCallee(selector) {
|
|
2434
|
+
return this.resolveSelectorCalleeNode(selector)?.text ?? null;
|
|
2435
|
+
}
|
|
2436
|
+
/** Node form of `resolveSelectorCallee` — the CALLEE identifier is the node
|
|
2437
|
+
* that names a call/instantiates edge (M60 line contract). */
|
|
2438
|
+
resolveSelectorCalleeNode(selector) {
|
|
2439
|
+
const prev = selector.previousNamedSibling;
|
|
2440
|
+
if (!prev) return null;
|
|
2441
|
+
if (prev.type === "unconditional_assignable_selector" || prev.type === "conditional_assignable_selector" || prev.type === "selector") {
|
|
2442
|
+
const inner = this.findChildByType(prev, "identifier");
|
|
2443
|
+
if (inner) return inner;
|
|
2444
|
+
return this.lastIdentifierNode(prev);
|
|
2445
|
+
}
|
|
2446
|
+
if (prev.type === "identifier" || prev.type === "type_identifier") {
|
|
2447
|
+
return prev;
|
|
2448
|
+
}
|
|
2449
|
+
return null;
|
|
2450
|
+
}
|
|
2451
|
+
lastIdentifier(node) {
|
|
2452
|
+
return this.lastIdentifierNode(node)?.text ?? null;
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Node form of `lastIdentifier`: the identifier that NAMES a cascade link's
|
|
2456
|
+
* edge is also the node whose line the edge carries (M60 line contract), so
|
|
2457
|
+
* both views come from ONE traversal and cannot disagree.
|
|
2458
|
+
*/
|
|
2459
|
+
lastIdentifierNode(node) {
|
|
2460
|
+
let result = null;
|
|
2461
|
+
const stack = [node];
|
|
2462
|
+
while (stack.length > 0) {
|
|
2463
|
+
const n = stack.pop();
|
|
2464
|
+
if (n.type === "identifier" || n.type === "type_identifier") result = n;
|
|
2465
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2466
|
+
const c = n.namedChild(i);
|
|
2467
|
+
if (c) stack.push(c);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return result;
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* DT-A/DT-F: the member identifier of a link selector
|
|
2474
|
+
* (`unconditional_assignable_selector` / `conditional_assignable_selector`
|
|
2475
|
+
* child holding `method` in `obj.method`, `?.value`, `..length`).
|
|
2476
|
+
*/
|
|
2477
|
+
linkMember(sel) {
|
|
2478
|
+
return this.linkMemberNode(sel)?.text ?? null;
|
|
2479
|
+
}
|
|
2480
|
+
/** Node form of `linkMember`: the member identifier itself is the node that
|
|
2481
|
+
* NAMES a member-read/tear-off edge (M60 line contract). */
|
|
2482
|
+
linkMemberNode(sel) {
|
|
2483
|
+
const probe = sel.type === "selector" ? this.namedChildrenOf(sel) : [sel];
|
|
2484
|
+
for (const c of probe) {
|
|
2485
|
+
if (c.type === "unconditional_assignable_selector" || c.type === "conditional_assignable_selector" || c.type === "cascade_selector") {
|
|
2486
|
+
const id = this.findChildByType(c, "identifier") ?? this.findChildByType(c, "type_identifier");
|
|
2487
|
+
if (id) return id;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
namedChildrenOf(n) {
|
|
2493
|
+
const out = [];
|
|
2494
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2495
|
+
const c = n.namedChild(i);
|
|
2496
|
+
if (c) out.push(c);
|
|
2497
|
+
}
|
|
2498
|
+
return out;
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* DT-A: receiver chain of an invocation, walking back through
|
|
2502
|
+
* link-selector siblings from the argument-carrying selector.
|
|
2503
|
+
* `P.Foo.bar()` -> head `P`, links [`Foo`, `bar`]; bare `foo()` ->
|
|
2504
|
+
* head `foo`, no links.
|
|
2505
|
+
*/
|
|
2506
|
+
invocationChain(argsSelector) {
|
|
2507
|
+
const links = [];
|
|
2508
|
+
let c = argsSelector.previousNamedSibling;
|
|
2509
|
+
while (c) {
|
|
2510
|
+
if (c.type === "selector") {
|
|
2511
|
+
const m = this.linkMember(c);
|
|
2512
|
+
if (!m) break;
|
|
2513
|
+
links.unshift(m);
|
|
2514
|
+
c = c.previousNamedSibling;
|
|
2515
|
+
} else if (c.type === "unconditional_assignable_selector" || c.type === "conditional_assignable_selector") {
|
|
2516
|
+
const id = this.findChildByType(c, "identifier") ?? this.findChildByType(c, "type_identifier");
|
|
2517
|
+
if (!id) break;
|
|
2518
|
+
links.unshift(id.text);
|
|
2519
|
+
c = c.previousNamedSibling;
|
|
2520
|
+
} else break;
|
|
2521
|
+
}
|
|
2522
|
+
return { head: c, links };
|
|
2523
|
+
}
|
|
2524
|
+
headNameText(head) {
|
|
2525
|
+
if (head && (head.type === "identifier" || head.type === "type_identifier")) return head.text;
|
|
2526
|
+
return null;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* DT-F: true when the selector sits in an assignment LHS write position
|
|
2530
|
+
* with a plain `=` operator (`a.b = c` writes b; `a.b += c` and `a.b ??= c`
|
|
2531
|
+
* read it). Span-matched like the C analyzer's LHS gate.
|
|
2532
|
+
*/
|
|
2533
|
+
selectorIsLhsWrite(sel) {
|
|
2534
|
+
let p = sel.parent;
|
|
2535
|
+
let guard = 0;
|
|
2536
|
+
while (p && guard++ < 16) {
|
|
2537
|
+
if (p.type === "assignment_expression") {
|
|
2538
|
+
let opText = null;
|
|
2539
|
+
for (let i = 0; i < p.childCount; i++) {
|
|
2540
|
+
const c = p.child(i);
|
|
2541
|
+
if (c && !c.isNamed && c.text.includes("=")) {
|
|
2542
|
+
opText = c.text;
|
|
2543
|
+
break;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (opText !== "=") return false;
|
|
2547
|
+
const lhs = p.namedChild(0);
|
|
2548
|
+
if (lhs && lhs.startIndex <= sel.startIndex && lhs.endIndex >= sel.endIndex) return true;
|
|
2549
|
+
return false;
|
|
2550
|
+
}
|
|
2551
|
+
if (p.type === "function_body" || p.type === "block" || p.type === "class_body" || p.type === "program" || p.type === "declaration") return false;
|
|
2552
|
+
p = p.parent;
|
|
2553
|
+
}
|
|
2554
|
+
return false;
|
|
2555
|
+
}
|
|
2556
|
+
/** DT-F: declaration-name positions whose identifiers are binds, never reads. */
|
|
2557
|
+
isDartDeclNameParent(ptype) {
|
|
2558
|
+
return ptype === "formal_parameter" || ptype === "constructor_param" || ptype === "function_signature" || ptype === "method_signature" || ptype === "getter_signature" || ptype === "setter_signature" || ptype === "type_alias" || ptype === "label" || // #752: field / static-field declaration names (`int count = 0;`,
|
|
2559
|
+
// `static final int x = 1;`) — the declaration is a BIND, not a read.
|
|
2560
|
+
ptype === "initialized_identifier" || ptype === "static_final_declaration" || ptype === "class_definition" || ptype === "enum_declaration" || ptype === "mixin_declaration" || ptype === "extension_declaration" || // M60 (#933 triage): `enum_declaration` covered the enum's OWN name but
|
|
2561
|
+
// not each VALUE's node, so every enum constant was published as a bare
|
|
2562
|
+
// read of its own enum (`uses Shape -> circle` at the declaration line,
|
|
2563
|
+
// confidence 0.6). The M12 harness files sub-threshold facts as
|
|
2564
|
+
// `unknown`, never as false positives, which is how it survived the
|
|
2565
|
+
// lane's whole fixture and mutant program — probe-verified node shape:
|
|
2566
|
+
// `(enum_body (enum_constant (identifier)))`. The constant's ARGUMENTS
|
|
2567
|
+
// are outside this gate and stay ordinary reads.
|
|
2568
|
+
ptype === "enum_constant" || // DARTD21: an extension type's container name AND its representation
|
|
2569
|
+
// field name (`extension type X._(T object$)`) are declarations.
|
|
2570
|
+
ptype === "extension_type_declaration" || ptype === "representation_declaration" || // DARTD22: a signature's OWN name binds, and the constructor-shaped
|
|
2571
|
+
// signatures were the one family missing from this set. A constructor
|
|
2572
|
+
// `Real()` and an UNTYPED METHOD `untypedMethod()` parse into the same
|
|
2573
|
+
// node, and in both the leading `identifier` is the declared name — so
|
|
2574
|
+
// without this row every one of them published a bare read of ITSELF
|
|
2575
|
+
// (`uses untypedMethod -> untypedMethod` at 0.6). Before DARTD22 the same
|
|
2576
|
+
// node was published as a member called `constructor`, so the read was
|
|
2577
|
+
// invisible: the source and the target were two different wrong names and
|
|
2578
|
+
// cancelled out. Fixing only the symbol left the self-loop visible, which
|
|
2579
|
+
// is why the row belongs here and not in the constructor dispatch.
|
|
2580
|
+
ptype === "constructor_signature" || ptype === "factory_constructor_signature" || ptype === "constant_constructor_signature" || ptype === "redirecting_factory_constructor_signature";
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* DT-F+ (#752): the names a function scope binds locally — its own
|
|
2584
|
+
* declarators plus formal / constructor / catch parameters. Used by the
|
|
2585
|
+
* bare-member-read gate so a name the CURRENT FUNCTION binds suppresses the
|
|
2586
|
+
* member read (CGD15 parity: a local is not a member, #299 — a false edge is
|
|
2587
|
+
* worse than a missing one). O(subtree), no regex.
|
|
2588
|
+
*/
|
|
2589
|
+
/**
|
|
2590
|
+
* THE DECLARED TYPE OF EVERY LOCAL A SUBTREE BINDS (`collectDartLocals`' twin).
|
|
2591
|
+
*
|
|
2592
|
+
* The NAME set answers "is this a local", which is what suppresses a false member read.
|
|
2593
|
+
* It cannot answer "what IS this local", which is what makes a call target determinate:
|
|
2594
|
+
* `r.getOrders()` with `void run(OrderRepository r)` names `OrderRepository.getOrders`, and
|
|
2595
|
+
* without the type the edge can only say `getOrders` — the bare-name population the
|
|
2596
|
+
* incoming gate counts as ambiguous whenever any other declaration in the PROJECT shares
|
|
2597
|
+
* the name.
|
|
2598
|
+
*
|
|
2599
|
+
* Two shapes, and both are read off the GRAMMAR rather than guessed from text:
|
|
2600
|
+
*
|
|
2601
|
+
* local_variable_declaration the declared type is a direct child (`extractFieldType`,
|
|
2602
|
+
* the same reader fields use — `Baz y = Baz();`). It is
|
|
2603
|
+
* `undefined` for `var x = Widget();`, so the fallback below
|
|
2604
|
+
* is not a convenience: it is the only reading available.
|
|
2605
|
+
* initialized_variable_definition
|
|
2606
|
+
* no declared type, so the INITIALIZER names it when it is a
|
|
2607
|
+
* construction (`Widget()`), which is a fact about the source
|
|
2608
|
+
* and not an inference about the program.
|
|
2609
|
+
*
|
|
2610
|
+
* `Widget? w` is deliberately NOT reduced (`extractFieldType` does not know the nullable
|
|
2611
|
+
* wrapper) and a generic type is refused by `plainTypeName` — a receiver typed `List<int>`
|
|
2612
|
+
* resolves through a type argument this pass has not read, and naming the wrong declaration
|
|
2613
|
+
* is worse than naming none.
|
|
2614
|
+
*/
|
|
2615
|
+
collectDartLocalTypes(root) {
|
|
2616
|
+
const out = /* @__PURE__ */ new Map();
|
|
2617
|
+
const stack = [root];
|
|
2618
|
+
while (stack.length > 0) {
|
|
2619
|
+
const n = stack.pop();
|
|
2620
|
+
const t = n.type;
|
|
2621
|
+
if (t === "for_loop_parts") {
|
|
2622
|
+
const nameNode = n.childForFieldName?.("name");
|
|
2623
|
+
const iterable = n.childForFieldName?.("value");
|
|
2624
|
+
const inferred = (n.namedChildren ?? []).some((c) => c?.type === "inferred_type");
|
|
2625
|
+
if (nameNode && iterable && !inferred) {
|
|
2626
|
+
const nameIdx = (n.namedChildren ?? []).findIndex(
|
|
2627
|
+
(c) => c && c.startIndex === nameNode.startIndex && c.endIndex === nameNode.endIndex
|
|
2628
|
+
);
|
|
2629
|
+
for (let i = 0; i < nameIdx; i++) {
|
|
2630
|
+
const c = n.namedChild(i);
|
|
2631
|
+
if (c && c.type === "type_identifier") {
|
|
2632
|
+
const plain = stripNullableSuffix(c.text);
|
|
2633
|
+
if (isPlainIdentifier(plain)) out.set(nameNode.text, { type: plain, evidence: "annotation" });
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
if (t === "catch_clause") {
|
|
2640
|
+
const prev = n.previousNamedSibling;
|
|
2641
|
+
const typeNode = prev && (prev.type === "type_identifier" || prev.type === "nullable_type") ? prev : void 0;
|
|
2642
|
+
const typeLeaf = typeNode ? typeNode.type === "type_identifier" ? typeNode.text : this.findChildByType(typeNode, "type_identifier")?.text : void 0;
|
|
2643
|
+
const typeName = typeLeaf ? stripNullableSuffix(String(typeLeaf)) : void 0;
|
|
2644
|
+
const params = this.findChildByType(n, "catch_parameters");
|
|
2645
|
+
if (typeName && isPlainIdentifier(typeName) && params) {
|
|
2646
|
+
for (const c of params.namedChildren ?? []) {
|
|
2647
|
+
if (c && c.type === "identifier") out.set(c.text, { type: typeName, evidence: "annotation" });
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (t === "local_variable_declaration" || t === "declaration" || t === "initialized_variable_definition") {
|
|
2652
|
+
const name = n.childForFieldName?.("name")?.text ?? this.findChildByType(n, "initialized_variable_definition")?.childForFieldName?.("name")?.text;
|
|
2653
|
+
if (name && !out.has(name)) {
|
|
2654
|
+
const explicit = this.extractFieldType(n, n);
|
|
2655
|
+
const typeName = explicit ? stripNullableSuffix(String(explicit)) : this.constructedTypeName(n);
|
|
2656
|
+
if (typeName && isPlainIdentifier(typeName)) {
|
|
2657
|
+
let element;
|
|
2658
|
+
const args = this.findChildByType(n, "type_arguments");
|
|
2659
|
+
let keyElement;
|
|
2660
|
+
if (args) {
|
|
2661
|
+
const argsIds = [];
|
|
2662
|
+
const argStack = [args];
|
|
2663
|
+
while (argStack.length > 0) {
|
|
2664
|
+
const a = argStack.pop();
|
|
2665
|
+
if (a.type === "type_identifier") {
|
|
2666
|
+
argsIds.push(a.text);
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
for (const c of a.namedChildren ?? []) if (c) argStack.push(c);
|
|
2670
|
+
}
|
|
2671
|
+
if (_RealDartAnalyzer.DART_ELEMENT_CONTAINERS.has(typeName)) element = argsIds[0];
|
|
2672
|
+
else if (_RealDartAnalyzer.DART_MAP_CONTAINERS.has(typeName)) {
|
|
2673
|
+
keyElement = argsIds[0];
|
|
2674
|
+
element = argsIds[1];
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
out.set(name, { type: typeName, evidence: explicit ? "annotation" : "assignment", ...element ? { element } : {}, ...keyElement ? { keyElement } : {} });
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
for (const c of n.namedChildren ?? []) if (c) stack.push(c);
|
|
2682
|
+
}
|
|
2683
|
+
const awaits = [];
|
|
2684
|
+
const awaitStack = [root];
|
|
2685
|
+
while (awaitStack.length > 0) {
|
|
2686
|
+
const n = awaitStack.pop();
|
|
2687
|
+
if (n.type === "initialized_variable_definition") {
|
|
2688
|
+
const nameNode = n.childForFieldName?.("name");
|
|
2689
|
+
const value = n.childForFieldName?.("value");
|
|
2690
|
+
if (nameNode && value && value.type === "await_expression") {
|
|
2691
|
+
const operand = this.findChildByType(value, "identifier");
|
|
2692
|
+
if (operand) awaits.push({ name: nameNode.text, operand: operand.text });
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
for (const c of n.namedChildren ?? []) if (c) awaitStack.push(c);
|
|
2696
|
+
}
|
|
2697
|
+
for (let round = 0; round < 3 && awaits.length > 0; round++) {
|
|
2698
|
+
let changed = false;
|
|
2699
|
+
for (const a of awaits) {
|
|
2700
|
+
if (out.has(a.name)) continue;
|
|
2701
|
+
const fut = out.get(a.operand);
|
|
2702
|
+
if (!fut || !fut.element) continue;
|
|
2703
|
+
if (!_RealDartAnalyzer.DART_ELEMENT_CONTAINERS.has(fut.type)) continue;
|
|
2704
|
+
out.set(a.name, { type: fut.element, evidence: "annotation" });
|
|
2705
|
+
changed = true;
|
|
2706
|
+
}
|
|
2707
|
+
if (!changed) break;
|
|
2708
|
+
}
|
|
2709
|
+
return out;
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* AWAITED LOCALS: `final repo = await pending;` where the awaited variable's type is written as
|
|
2713
|
+
* `Future<Repo>` — the awaited VALUE's type is the future's type argument (written evidence, the
|
|
2714
|
+
* `annotation` class). Returns the declarations to type; the caller applies them to the body scope
|
|
2715
|
+
* so SIBLING statements see them. Grammar note: `await pending` parses as
|
|
2716
|
+
* `unary_expression > await_expression > identifier` (probed), not as a bare await_expression.
|
|
2717
|
+
*/
|
|
2718
|
+
collectAwaitLocalTypes(root, localElements, localTypes) {
|
|
2719
|
+
const out = /* @__PURE__ */ new Map();
|
|
2720
|
+
const decls = [];
|
|
2721
|
+
const stack = [root];
|
|
2722
|
+
while (stack.length > 0) {
|
|
2723
|
+
const n = stack.pop();
|
|
2724
|
+
if (n.type === "initialized_variable_definition") {
|
|
2725
|
+
const nameNode = n.childForFieldName?.("name");
|
|
2726
|
+
const valueRaw = n.childForFieldName?.("value");
|
|
2727
|
+
const awaitNode = valueRaw && valueRaw.type === "unary_expression" ? this.findChildByType(valueRaw, "await_expression") : valueRaw;
|
|
2728
|
+
if (nameNode && awaitNode && awaitNode.type === "await_expression") {
|
|
2729
|
+
const operand = this.findChildByType(awaitNode, "identifier");
|
|
2730
|
+
if (operand) decls.push({ name: nameNode.text, operand: operand.text });
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
for (const c of n.namedChildren ?? []) if (c) stack.push(c);
|
|
2734
|
+
}
|
|
2735
|
+
for (let round = 0; round < 3 && decls.length > 0; round++) {
|
|
2736
|
+
let changed = false;
|
|
2737
|
+
for (const d of decls) {
|
|
2738
|
+
if (out.has(d.name) || localTypes.has(d.name)) continue;
|
|
2739
|
+
const fut = localTypes.get(d.operand) ?? "";
|
|
2740
|
+
const element = localElements.get(d.operand);
|
|
2741
|
+
if (!element) continue;
|
|
2742
|
+
if (fut !== "Future" && fut !== "FutureOr") continue;
|
|
2743
|
+
out.set(d.name, element);
|
|
2744
|
+
changed = true;
|
|
2745
|
+
}
|
|
2746
|
+
if (!changed) break;
|
|
2747
|
+
}
|
|
2748
|
+
return out;
|
|
2749
|
+
}
|
|
2750
|
+
/** The type a variable's INITIALIZER constructs: `var x = Widget()` -> `Widget`.
|
|
2751
|
+
* `new`/`const` expressions and a bare capitalized callee are all constructions in
|
|
2752
|
+
* Dart, and a lowercase callee is a function call whose RESULT type this pass has not
|
|
2753
|
+
* read — so it returns undefined rather than the callee's name. */
|
|
2754
|
+
constructedTypeName(n) {
|
|
2755
|
+
const value = n.childForFieldName?.("value");
|
|
2756
|
+
if (!value) return void 0;
|
|
2757
|
+
const head = value.type === "constructor_invocation" || value.type === "new_expression" || value.type === "const_object_expression" ? this.findChildByType(value, "type_identifier") : value.type === "identifier" || value.type === "type_identifier" ? value : null;
|
|
2758
|
+
const text = head?.text;
|
|
2759
|
+
if (!text || !isCapitalized(text)) return void 0;
|
|
2760
|
+
return text;
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* PARAMETER DECLARED TYPES, from the signature node the body follows.
|
|
2764
|
+
*
|
|
2765
|
+
* `extractParameters` is the reader the symbol payload already uses (and its fixture
|
|
2766
|
+
* asserts), so a parameter is typed here exactly as it is typed on the symbol — one
|
|
2767
|
+
* reader, two consumers. A parameter whose type is not a plain identifier contributes
|
|
2768
|
+
* nothing, which is the conservative half of the rule.
|
|
2769
|
+
*/
|
|
2770
|
+
/** Container types whose ELEMENT type this lane can read from a written type argument. */
|
|
2771
|
+
static DART_ELEMENT_CONTAINERS = /* @__PURE__ */ new Set([
|
|
2772
|
+
"List",
|
|
2773
|
+
"Iterable",
|
|
2774
|
+
"Set",
|
|
2775
|
+
"Queue",
|
|
2776
|
+
"Stream",
|
|
2777
|
+
"Iterator",
|
|
2778
|
+
"ListQueue",
|
|
2779
|
+
"DoubleLinkedQueue",
|
|
2780
|
+
// `Future<T>`: the AWAITED value is T, and `await f` is written evidence of exactly the same
|
|
2781
|
+
// class as a declared local — it types the receiver of the statements after the await.
|
|
2782
|
+
"Future",
|
|
2783
|
+
"FutureOr"
|
|
2784
|
+
]);
|
|
2785
|
+
/** Map types whose KEY/VALUE type arguments this lane can read (value = `element`, key = `keyElement`). */
|
|
2786
|
+
static DART_MAP_CONTAINERS = /* @__PURE__ */ new Set([
|
|
2787
|
+
"Map",
|
|
2788
|
+
"HashMap",
|
|
2789
|
+
"LinkedHashMap",
|
|
2790
|
+
"SplayTreeMap",
|
|
2791
|
+
"UnmodifiableMapView"
|
|
2792
|
+
]);
|
|
2793
|
+
collectDartParameterTypes(sig) {
|
|
2794
|
+
const out = /* @__PURE__ */ new Map();
|
|
2795
|
+
let params = [];
|
|
2796
|
+
try {
|
|
2797
|
+
params = this.extractParameters(sig) ?? [];
|
|
2798
|
+
} catch {
|
|
2799
|
+
return out;
|
|
2800
|
+
}
|
|
2801
|
+
for (const p of params) {
|
|
2802
|
+
const name = String(p?.name ?? "");
|
|
2803
|
+
const typeName = stripNullableSuffix(String(p?.type ?? ""));
|
|
2804
|
+
if (name && isPlainIdentifier(typeName)) out.set(name, { type: typeName, evidence: "param" });
|
|
2805
|
+
}
|
|
2806
|
+
const stack = [sig];
|
|
2807
|
+
while (stack.length > 0) {
|
|
2808
|
+
const n = stack.pop();
|
|
2809
|
+
if (n.type === "formal_parameter" || n.type === "constructor_param" || n.type === "super_formal_parameter") {
|
|
2810
|
+
const nameNode = n.childForFieldName?.("name") ?? this.findChildByType(n, "identifier");
|
|
2811
|
+
const typeNode = this.findChildByType(n, "type_identifier");
|
|
2812
|
+
const args = this.findChildByType(n, "type_arguments");
|
|
2813
|
+
const argsIds = args ? (args.namedChildren ?? []).filter((c) => c && c.type === "type_identifier") : [];
|
|
2814
|
+
const base = typeNode ? stripNullableSuffix(typeNode.text) : "";
|
|
2815
|
+
if (nameNode && argsIds.length > 0 && _RealDartAnalyzer.DART_ELEMENT_CONTAINERS.has(base)) {
|
|
2816
|
+
const prev = out.get(nameNode.text);
|
|
2817
|
+
if (prev) prev.element = argsIds[0].text;
|
|
2818
|
+
else out.set(nameNode.text, { type: base, evidence: "param", element: argsIds[0].text });
|
|
2819
|
+
} else if (nameNode && argsIds.length >= 2 && _RealDartAnalyzer.DART_MAP_CONTAINERS.has(base)) {
|
|
2820
|
+
const prev = out.get(nameNode.text);
|
|
2821
|
+
if (prev) {
|
|
2822
|
+
prev.keyElement = argsIds[0].text;
|
|
2823
|
+
prev.element = argsIds[1].text;
|
|
2824
|
+
} else out.set(nameNode.text, { type: base, evidence: "param", keyElement: argsIds[0].text, element: argsIds[1].text });
|
|
2825
|
+
}
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
for (const c of n.namedChildren ?? []) if (c) stack.push(c);
|
|
2829
|
+
}
|
|
2830
|
+
return out;
|
|
2831
|
+
}
|
|
2832
|
+
/** The import aliases this file declares (`import 'x.dart' as _i1;`). Only the identifier that
|
|
2833
|
+
* is a DIRECT child of `import_specification` is an alias — the specifier's own identifiers sit
|
|
2834
|
+
* inside `configurable_uri`/`uri`, which is why the walk stops at those nodes. */
|
|
2835
|
+
collectImportAliases(root) {
|
|
2836
|
+
const out = /* @__PURE__ */ new Set();
|
|
2837
|
+
const stack = [root];
|
|
2838
|
+
while (stack.length > 0) {
|
|
2839
|
+
const n = stack.pop();
|
|
2840
|
+
if (n.type === "import_specification") {
|
|
2841
|
+
for (const c of n.namedChildren ?? []) {
|
|
2842
|
+
if (c && c.type === "identifier") out.add(c.text);
|
|
2843
|
+
}
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
if (n.type === "uri" || n.type === "configurable_uri" || n.type === "string_literal") continue;
|
|
2847
|
+
for (const c of n.namedChildren ?? []) if (c) stack.push(c);
|
|
2848
|
+
}
|
|
2849
|
+
return out;
|
|
2850
|
+
}
|
|
2851
|
+
collectDartLocals(root) {
|
|
2852
|
+
const out = /* @__PURE__ */ new Set();
|
|
2853
|
+
const stack = [root];
|
|
2854
|
+
while (stack.length > 0) {
|
|
2855
|
+
const n = stack.pop();
|
|
2856
|
+
if (n.type === "identifier" && this.isDartDeclaredName(n)) out.add(n.text);
|
|
2857
|
+
if (n.type === "formal_parameter" || n.type === "constructor_param" || n.type === "super_formal_parameter" || n.type === "catch_parameters") {
|
|
2858
|
+
const id = this.findChildByType(n, "identifier");
|
|
2859
|
+
if (id) out.add(id.text);
|
|
2860
|
+
}
|
|
2861
|
+
if (n.type === "for_loop_parts") {
|
|
2862
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
2863
|
+
const c = n.child(i);
|
|
2864
|
+
if (c && !c.isNamed && c.text === "in") {
|
|
2865
|
+
for (let j = 0; j < i; j++) {
|
|
2866
|
+
const pc = n.child(j);
|
|
2867
|
+
if (pc && pc.isNamed && pc.type === "identifier") out.add(pc.text);
|
|
2868
|
+
}
|
|
2869
|
+
break;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
if (n.type === "variable_pattern") {
|
|
2874
|
+
const id = this.deepIdentifier(n);
|
|
2875
|
+
if (id) out.add(id.text);
|
|
2876
|
+
}
|
|
2877
|
+
if (n.type === "rest_pattern") {
|
|
2878
|
+
const id = this.deepIdentifier(n);
|
|
2879
|
+
if (id) out.add(id.text);
|
|
2880
|
+
}
|
|
2881
|
+
if (n.type === "constant_pattern" && this.isBindingPatternContext(n)) {
|
|
2882
|
+
const id = this.deepIdentifier(n);
|
|
2883
|
+
if (id) out.add(id.text);
|
|
2884
|
+
}
|
|
2885
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2886
|
+
const c = n.namedChild(i);
|
|
2887
|
+
if (c) stack.push(c);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return out;
|
|
2891
|
+
}
|
|
2892
|
+
/**
|
|
2893
|
+
* DT2: first document-order `identifier` in a subtree (pattern contents
|
|
2894
|
+
* nest — direct-child lookup misses `var (a, b)`-style bindings).
|
|
2895
|
+
*/
|
|
2896
|
+
deepIdentifier(n) {
|
|
2897
|
+
const stack = [n];
|
|
2898
|
+
while (stack.length > 0) {
|
|
2899
|
+
const c = stack.pop();
|
|
2900
|
+
if (c.type === "identifier") return c;
|
|
2901
|
+
for (let i = c.namedChildCount - 1; i >= 0; i--) {
|
|
2902
|
+
const ch = c.namedChild(i);
|
|
2903
|
+
if (ch) stack.push(ch);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return null;
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* DARTGAP2: is this node a CONSTANT being TESTED by a pattern — i.e. a position whose
|
|
2910
|
+
* owed relationship is a `uses` read of the constant, not a declaration and not a binding?
|
|
2911
|
+
*
|
|
2912
|
+
* The qualified form (`case M.off:`) is already read by the `qualified`-under-`constant_pattern`
|
|
2913
|
+
* branch (3d4d7fa4). This predicate covers the BARE identifier and the RELATIONAL operand.
|
|
2914
|
+
* The shapes are not inferred from the Dart spec — they were probed against the PINNED grammar
|
|
2915
|
+
* (`validation/m14/fixtures/dart/pattern-forms-probe.ts`):
|
|
2916
|
+
*
|
|
2917
|
+
* case kOne: (switch_statement_case (case_builtin) (constant_pattern (identifier)))
|
|
2918
|
+
* if (x case kOne) (if_statement (identifier) (constant_pattern (identifier)))
|
|
2919
|
+
* if (x case kNested?) (if_statement (identifier) (null_check_pattern (constant_pattern (identifier))))
|
|
2920
|
+
* if (x case kOne!) (if_statement (identifier) (null_assert_pattern (constant_pattern (identifier))))
|
|
2921
|
+
* if (x case >= kOne) (if_statement (identifier) (relational_operator) (identifier))
|
|
2922
|
+
* — tree-sitter-dart has NO `relational_pattern` wrapper, so the
|
|
2923
|
+
* operand is a direct child of the `if_statement` whose previous
|
|
2924
|
+
* NAMED sibling is the operator; the same shape appears under
|
|
2925
|
+
* `switch_statement_case` (probed) and `switch_expression_case`.
|
|
2926
|
+
* case [kOne, 2] (list_pattern (constant_pattern (identifier)) (constant_pattern (decimal_integer_literal)))
|
|
2927
|
+
* case {'a': kOne} (map_pattern (string_literal) (constant_pattern (identifier)))
|
|
2928
|
+
* case (kOne, 2) (record_pattern (constant_pattern (identifier)) (constant_pattern (decimal_integer_literal)))
|
|
2929
|
+
*
|
|
2930
|
+
* THREE EXCLUSIONS, each measured rather than assumed:
|
|
2931
|
+
* * BINDING context. `var (int a, int b) = ...` parses its names as `variable_pattern`, so
|
|
2932
|
+
* they never reach this predicate — but a destructuring declaration can still hold a
|
|
2933
|
+
* `constant_pattern`, and `isBindingPatternContext` is the ONE rule that already decides
|
|
2934
|
+
* that question for the binding collector. Reused rather than re-spelled.
|
|
2935
|
+
* * `...rest` in `final [int f, ...rest] = ...` parses as
|
|
2936
|
+
* `(rest_pattern (constant_pattern (identifier)))` — probed. That identifier is the BINDING
|
|
2937
|
+
* being declared, not a constant being tested, so reading it would fabricate a use of the
|
|
2938
|
+
* very name being bound.
|
|
2939
|
+
* * the `_` wildcard parses as `constant_pattern (identifier "_")` in map patterns (probed:
|
|
2940
|
+
* `{'b': _}`). `_` is not a constant and no declaration is named by it.
|
|
2941
|
+
*/
|
|
2942
|
+
isPatternTestConstant(n) {
|
|
2943
|
+
const id = n.type === "identifier" ? n : null;
|
|
2944
|
+
const p = n.parent;
|
|
2945
|
+
if (!p) return false;
|
|
2946
|
+
if (p.type === "constant_pattern") {
|
|
2947
|
+
if (id && id.text === "_") return false;
|
|
2948
|
+
if (p.parent?.type === "rest_pattern") return false;
|
|
2949
|
+
return !this.isBindingPatternContext(p);
|
|
2950
|
+
}
|
|
2951
|
+
if (p.type === "if_statement" || p.type === "switch_statement_case" || p.type === "switch_expression_case") {
|
|
2952
|
+
return n.previousNamedSibling?.type === "relational_operator";
|
|
2953
|
+
}
|
|
2954
|
+
return false;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* DT2: is this pattern node inside a BINDING context? Only structural
|
|
2958
|
+
* descent counts: a `var (a, b)` destructuring binds through record /
|
|
2959
|
+
* list / map wrappers to the declaration (or for-in head). A case TEST
|
|
2960
|
+
* (`InvitationStatus.declined => ...` inside `var x = switch ...`)
|
|
2961
|
+
* crosses a switch/if boundary — it tests, never binds, even when the
|
|
2962
|
+
* whole switch initializes a variable.
|
|
2963
|
+
*/
|
|
2964
|
+
isBindingPatternContext(n) {
|
|
2965
|
+
let p = n.parent;
|
|
2966
|
+
let guard = 0;
|
|
2967
|
+
while (p && guard++ < 8) {
|
|
2968
|
+
const t = p.type;
|
|
2969
|
+
if (t === "pattern_variable_declaration" || t === "for_loop_parts") return true;
|
|
2970
|
+
if (t !== "record_pattern" && t !== "list_pattern" && t !== "map_pattern" && t !== "constant_pattern" && t !== "variable_pattern") return false;
|
|
2971
|
+
p = p.parent;
|
|
2972
|
+
}
|
|
2973
|
+
return false;
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* THE DECLARATION A LOCAL READ BINDS TO — the innermost enclosing scope that declares the
|
|
2977
|
+
* name, and the identity that makes two reads of the same name DIFFERENT facts when they
|
|
2978
|
+
* read DIFFERENT declarations.
|
|
2979
|
+
*
|
|
2980
|
+
* WHY IT EXISTS. A local read's spelling is `<callable>\u0000<name>`, and the CALLABLE is the
|
|
2981
|
+
* enclosing FUNCTION — so two reads of two different declarations that share a name inside one
|
|
2982
|
+
* function spell IDENTICALLY. The relationship dedup key (`type|source|target|confidence`)
|
|
2983
|
+
* carried no line, so the two collapsed into one row and the second was LOST: a resolution
|
|
2984
|
+
* loss wearing a dedup's clothes.
|
|
2985
|
+
*
|
|
2986
|
+
* THE SCOPE RULE, measured on the dart lane's own `nested-block-shadows-member` fixture (its
|
|
2987
|
+
* line numbers are the fixture's):
|
|
2988
|
+
*
|
|
2989
|
+
* 44: int value = 0; // the FIELD
|
|
2990
|
+
* 47: var value = 1; // the BODY local
|
|
2991
|
+
* 48: print(value); // -> the body local
|
|
2992
|
+
* 50: var value = 2; // the NESTED declaration, shadowing the body local
|
|
2993
|
+
* 51: print(value); // -> the NESTED local
|
|
2994
|
+
* 53: print(value); // -> the BODY local again
|
|
2995
|
+
*
|
|
2996
|
+
* Dart's block scope: an inner declaration shadows an outer binding of the same name for the
|
|
2997
|
+
* whole of the inner block. So the read at 51 reads a DIFFERENT declaration from the reads at
|
|
2998
|
+
* 48 and 53 — and the read at 53 reads the SAME declaration as the read at 48, so those two
|
|
2999
|
+
* are ONE relationship and must still collapse.
|
|
3000
|
+
*
|
|
3001
|
+
* WHY A BLOCK'S OWN DECLARATIONS, not its whole subtree. The scope of a declaration is the
|
|
3002
|
+
* block that holds it, so the nested block's `value` is NOT a declaration of the outer block.
|
|
3003
|
+
* A subtree scan would find it and attribute the read at 53 to the nested declaration — which
|
|
3004
|
+
* is the same class of mistake as the analyzer's own `heritageByClass` comment records
|
|
3005
|
+
* ("a `uses` edge is evidence that a type is relevant to this file — but it is not a BASE").
|
|
3006
|
+
*
|
|
3007
|
+
* THE RESULT IS A SCOPE IDENTITY, not a line: the enclosing scope's own start position, plus
|
|
3008
|
+
* the declaration's own line. Two reads of the SAME declaration share it; two reads of
|
|
3009
|
+
* DIFFERENT declarations do not. It rides the row as `localBindingScope` and joins the dedup
|
|
3010
|
+
* key ONLY for a `localBinding` row — every other row keeps its key byte for byte, so the
|
|
3011
|
+
* 192,761-row payload-pollution dedup is untouched.
|
|
3012
|
+
*/
|
|
3013
|
+
localBindingScope(n, name) {
|
|
3014
|
+
let p = n.parent;
|
|
3015
|
+
let guard = 0;
|
|
3016
|
+
while (p && guard++ < 64) {
|
|
3017
|
+
if (p.type === "block" || p.type === "function_body") {
|
|
3018
|
+
const decl = this.scopeOwnDeclaration(p, name, n.startIndex);
|
|
3019
|
+
if (decl !== null) {
|
|
3020
|
+
return { localBindingScope: `${p.startIndex}:${decl}` };
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
p = p.parent;
|
|
3024
|
+
}
|
|
3025
|
+
return null;
|
|
3026
|
+
}
|
|
3027
|
+
// ══ THE SCOPE-DECLARATION INDEX — ONE WALK PER SCOPE, NOT ONE WALK PER READ.
|
|
3028
|
+
//
|
|
3029
|
+
// `scopeOwnDeclaration` used to walk the ENTIRE subtree of the enclosing scope for EVERY local
|
|
3030
|
+
// read. On ordinary files that is invisible; on a file of deeply nested statements it is fatal,
|
|
3031
|
+
// because each of the N reads walks a subtree that contains most of the file, so the pass is
|
|
3032
|
+
// O(N × subtree) — quadratic in DEPTH. Measured on `deep_nesting_statement_test.dart`
|
|
3033
|
+
// (350,435 B, 130,042 nodes): the raw tree walk costs 187 ms (0.0014 ms/node) while the analyzer
|
|
3034
|
+
// spent ~3.8 ms per node-visit, and this function is where that went.
|
|
3035
|
+
//
|
|
3036
|
+
// The fix is the same shape the gate's `addKey` fix had: PAY ONCE. A scope's own declarations are
|
|
3037
|
+
// a property of the SCOPE, so they are indexed on first use and every later read is a lookup.
|
|
3038
|
+
// The key is `${startIndex}:${endIndex}:${type}` and NOT the node object, because web-tree-sitter
|
|
3039
|
+
// hands out FRESH wrappers on every traversal — a memo keyed on the object would never hit and
|
|
3040
|
+
// would grow without bound. A memo that never hits is a leak with extra steps.
|
|
3041
|
+
//
|
|
3042
|
+
// The index is cleared at the top of `analyze()`: an analyzer instance outlives one file, and a
|
|
3043
|
+
// cache that outlived it would serve one file's declarations to the next file's reads.
|
|
3044
|
+
scopeDeclIndex = /* @__PURE__ */ new Map();
|
|
3045
|
+
/**
|
|
3046
|
+
* THE DECLARATION OF `name` THAT BELONGS TO THIS SCOPE — the block's OWN declarations, not
|
|
3047
|
+
* the ones in a nested block (a nested block is a DIFFERENT scope, and a declaration there
|
|
3048
|
+
* does not belong to this one). Returns the declaration's START INDEX, which is all the caller
|
|
3049
|
+
* needs (`<scopeStart>:<declStart>`), so no node is retained.
|
|
3050
|
+
*/
|
|
3051
|
+
scopeOwnDeclaration(scope, name, before) {
|
|
3052
|
+
const key = `${scope.startIndex}:${scope.endIndex}:${scope.type}`;
|
|
3053
|
+
let index = this.scopeDeclIndex.get(key);
|
|
3054
|
+
if (!index) {
|
|
3055
|
+
index = /* @__PURE__ */ new Map();
|
|
3056
|
+
const walk = (n, depth) => {
|
|
3057
|
+
if (depth > 64) return;
|
|
3058
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3059
|
+
const c = n.namedChild(i);
|
|
3060
|
+
if (!c) continue;
|
|
3061
|
+
if (c.type === "block" || c.type === "function_body" || c.type === "class_body") continue;
|
|
3062
|
+
if (c.type === "identifier" && this.isDartDeclaredName(c)) {
|
|
3063
|
+
const arr = index.get(c.text);
|
|
3064
|
+
if (arr) arr.push(c.startIndex);
|
|
3065
|
+
else index.set(c.text, [c.startIndex]);
|
|
3066
|
+
}
|
|
3067
|
+
walk(c, depth + 1);
|
|
3068
|
+
}
|
|
3069
|
+
};
|
|
3070
|
+
walk(scope, 0);
|
|
3071
|
+
this.scopeDeclIndex.set(key, index);
|
|
3072
|
+
}
|
|
3073
|
+
const starts = index.get(name);
|
|
3074
|
+
if (!starts || starts.length === 0) return null;
|
|
3075
|
+
let best = -1;
|
|
3076
|
+
for (const s of starts) if (s < before && s > best) best = s;
|
|
3077
|
+
return best < 0 ? null : best;
|
|
3078
|
+
}
|
|
3079
|
+
/**
|
|
3080
|
+
* DT-F: is this identifier the DECLARED name of its definition
|
|
3081
|
+
* (`var q = ...`, `int xs[3]`, `int xs[3] = ...`)? Names precede the
|
|
3082
|
+
* first `=` child; values follow it. Definitions without `=` bind every
|
|
3083
|
+
* identifier they directly hold.
|
|
3084
|
+
*/
|
|
3085
|
+
isDartDeclaredName(n) {
|
|
3086
|
+
const parent = n.parent;
|
|
3087
|
+
if (!parent) return false;
|
|
3088
|
+
const ptype = parent.type;
|
|
3089
|
+
if (ptype !== "initialized_variable_definition" && ptype !== "static_final_declaration") return false;
|
|
3090
|
+
let eqIdx = -1;
|
|
3091
|
+
let myIdx = -1;
|
|
3092
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
3093
|
+
const c = parent.child(i);
|
|
3094
|
+
if (!c) continue;
|
|
3095
|
+
if (!c.isNamed && c.text === "=" && eqIdx === -1) eqIdx = i;
|
|
3096
|
+
if (c.isNamed && c.startIndex === n.startIndex && c.endIndex === n.endIndex) myIdx = i;
|
|
3097
|
+
}
|
|
3098
|
+
if (myIdx === -1) return false;
|
|
3099
|
+
if (eqIdx === -1) return true;
|
|
3100
|
+
return myIdx < eqIdx;
|
|
3101
|
+
}
|
|
3102
|
+
/** DT-F: is this selector an index operation (`xs[0]` parses the index
|
|
3103
|
+
* as a member-less selector holding brackets)? Member selectors name a
|
|
3104
|
+
* member; argument selectors invoke. Bounded shape scan, never regex. */
|
|
3105
|
+
isIndexSelector(sel) {
|
|
3106
|
+
if (this.linkMember(sel) !== null) return false;
|
|
3107
|
+
if (this.findChildByType(sel, "argument_part")) return false;
|
|
3108
|
+
return this.subtreeHasBracket(sel);
|
|
3109
|
+
}
|
|
3110
|
+
/** DT-F: is this assignable an index target (`xs[3] = 4` wraps the base
|
|
3111
|
+
* and index in assignable_expression)? Same member-less + bracket test
|
|
3112
|
+
* applied to the node itself. */
|
|
3113
|
+
isIndexAssignable(sel) {
|
|
3114
|
+
if (sel.type !== "unconditional_assignable_selector" && sel.type !== "conditional_assignable_selector") return false;
|
|
3115
|
+
if (this.findChildByType(sel, "identifier") ?? this.findChildByType(sel, "type_identifier")) return false;
|
|
3116
|
+
return this.subtreeHasBracket(sel);
|
|
3117
|
+
}
|
|
3118
|
+
subtreeHasBracket(sel) {
|
|
3119
|
+
const stack = [sel];
|
|
3120
|
+
let guard = 0;
|
|
3121
|
+
while (stack.length > 0 && guard++ < 32) {
|
|
3122
|
+
const c = stack.pop();
|
|
3123
|
+
if (!c.isNamed && (c.text === "[" || c.text === "?[")) return true;
|
|
3124
|
+
for (let i = 0; i < c.childCount; i++) {
|
|
3125
|
+
const ch = c.child(i);
|
|
3126
|
+
if (ch) stack.push(ch);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return false;
|
|
3130
|
+
}
|
|
3131
|
+
/** DT-F: bounded ancestor climb — is this node inside string interpolation? */
|
|
3132
|
+
isInsideTemplateSubstitution(n) {
|
|
3133
|
+
let p = n.parent;
|
|
3134
|
+
let guard = 0;
|
|
3135
|
+
while (p && guard++ < 8) {
|
|
3136
|
+
if (p.type === "template_substitution") return true;
|
|
3137
|
+
if (p.type === "function_body" || p.type === "block" || p.type === "class_body" || p.type === "program" || p.type === "declaration") return false;
|
|
3138
|
+
p = p.parent;
|
|
3139
|
+
}
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
isSignatureNode(type) {
|
|
3143
|
+
return type === "function_signature" || type === "method_signature" || type === "getter_signature" || type === "setter_signature" || type === "constructor_signature" || type === "factory_constructor_signature" || type === "constant_constructor_signature" || type === "redirecting_factory_constructor_signature" || type === "operator_signature";
|
|
3144
|
+
}
|
|
3145
|
+
isConstructorSignature(type) {
|
|
3146
|
+
return type === "constructor_signature" || type === "factory_constructor_signature" || type === "constant_constructor_signature" || type === "redirecting_factory_constructor_signature";
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Is this constructor-shaped signature actually a CONSTRUCTOR, or is it a
|
|
3150
|
+
* METHOD the grammar has no node for?
|
|
3151
|
+
*
|
|
3152
|
+
* ## The shape the grammar cannot distinguish
|
|
3153
|
+
*
|
|
3154
|
+
* `tree-sitter-dart` has no node for a method declared WITHOUT a return
|
|
3155
|
+
* type. `class A { noRetType() { } }` parses as
|
|
3156
|
+
*
|
|
3157
|
+
* method_signature
|
|
3158
|
+
* constructor_signature <- an UNTYPED METHOD
|
|
3159
|
+
* identifier noRetType
|
|
3160
|
+
* formal_parameter_list
|
|
3161
|
+
*
|
|
3162
|
+
* which is structurally identical to a real constructor `A()`. Reading the
|
|
3163
|
+
* node TYPE alone therefore publishes every untyped method in the corpus as a
|
|
3164
|
+
* `constructor` symbol named `constructor`, and the method's own name is never
|
|
3165
|
+
* published at all. MEASURED over a 600-file stride sample of the corpus at
|
|
3166
|
+
* 35,090 `.dart` files: 1,197 constructor-shaped signatures, of which 521
|
|
3167
|
+
* (43.5%) are untyped methods, spread over 56 files — and one file,
|
|
3168
|
+
* `dart-sdk/pkg/analyzer/test/src/summary/elements/type_inference_test.dart`,
|
|
3169
|
+
* holds 91 of them, all collapsed onto the single name
|
|
3170
|
+
* `TypeInferenceElementTest.constructor`. `validation/m14/fixtures/dart/
|
|
3171
|
+
* untyped-method-census.ts` is the census and the discriminator cross-tab.
|
|
3172
|
+
*
|
|
3173
|
+
* ## Why the PARENT is not the test
|
|
3174
|
+
*
|
|
3175
|
+
* "Is the inner signature's parent a `method_signature`?" looks like the
|
|
3176
|
+
* discriminator and is not: the grammar ALSO wraps real constructors that
|
|
3177
|
+
* way (`A.named(int a)`, `factory A.f() = A.named`), so it would misclassify
|
|
3178
|
+
* 559 real constructors as methods. The cross-tab in the census prints both.
|
|
3179
|
+
*
|
|
3180
|
+
* ## The test that IS exact — the language's own naming rule
|
|
3181
|
+
*
|
|
3182
|
+
* A constructor is named after its class, and the unnamed one is spelled
|
|
3183
|
+
* `.new`. A MEMBER may not share its class's name — that is a compile error —
|
|
3184
|
+
* so
|
|
3185
|
+
*
|
|
3186
|
+
* lead === ownerName || lead === "new"
|
|
3187
|
+
*
|
|
3188
|
+
* holds for every constructor and for no method. It is a property of the
|
|
3189
|
+
* declaration's own text, not of the grammar's node choice, so it survives a
|
|
3190
|
+
* grammar upgrade that re-shapes the wrapper.
|
|
3191
|
+
*
|
|
3192
|
+
* The census measures it as exact on both classes: 662 constructors and 535
|
|
3193
|
+
* untyped methods, with no cell of the contingency table requiring a choice.
|
|
3194
|
+
*
|
|
3195
|
+
* ## An ownerless signature is NOT decided here
|
|
3196
|
+
*
|
|
3197
|
+
* An `extension type` declares constructors too (`Ext1.name1(this i);`), and
|
|
3198
|
+
* `scopeOwnerName` resolves no enclosing type for one. Returning true there
|
|
3199
|
+
* would keep the current behaviour — a constructor published without an owner
|
|
3200
|
+
* — which is right, and is not this method's business to change. The 14
|
|
3201
|
+
* ownerless rows in the census are counted, not silently folded into either
|
|
3202
|
+
* class.
|
|
3203
|
+
*/
|
|
3204
|
+
isRealConstructor(sig, owner) {
|
|
3205
|
+
if (!owner) return true;
|
|
3206
|
+
const lead = this.signatureName(sig);
|
|
3207
|
+
if (lead === null) return true;
|
|
3208
|
+
return lead === "new" || lead === owner;
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* The name of the type a signature is declared in, read from the PARENT chain.
|
|
3212
|
+
*
|
|
3213
|
+
* The symbol pass has a `scopeStack` already carrying this, so it passes the
|
|
3214
|
+
* owner in. The relationship passes have no scope stack — they walk with
|
|
3215
|
+
* their own `current`/`owner` cursors — so they need it derived from the tree.
|
|
3216
|
+
* The two must agree or an edge's source and the symbol it names are decided
|
|
3217
|
+
* by different rules, which is how `resolveCurrent` came to publish the
|
|
3218
|
+
* literal `constructor` for every untyped method's body while the symbol pass
|
|
3219
|
+
* published the method under its own name.
|
|
3220
|
+
*
|
|
3221
|
+
* The owner resolution is the analyzer's, not the grammar's: `class_definition`
|
|
3222
|
+
* and `enum_declaration` carry a `name` FIELD, while `mixin_declaration` and
|
|
3223
|
+
* `extension_declaration` carry a direct `identifier` child. A lookup that read
|
|
3224
|
+
* only the name field would resolve no owner for a mixin and send every
|
|
3225
|
+
* untyped mixin method down the ownerless path.
|
|
3226
|
+
*/
|
|
3227
|
+
enclosingTypeName(node) {
|
|
3228
|
+
let n = node;
|
|
3229
|
+
let guard = 0;
|
|
3230
|
+
while (n && guard++ < 64) {
|
|
3231
|
+
const t = n.type;
|
|
3232
|
+
if (t === "class_definition" || t === "enum_declaration" || t === "mixin_declaration" || t === "extension_declaration" || t === "extension_type_declaration") {
|
|
3233
|
+
const named = n.childForFieldName("name");
|
|
3234
|
+
if (named) return named.text;
|
|
3235
|
+
const id = this.findChildByType(n, "identifier");
|
|
3236
|
+
if (id) return id.text;
|
|
3237
|
+
return void 0;
|
|
3238
|
+
}
|
|
3239
|
+
n = n.parent;
|
|
3240
|
+
}
|
|
3241
|
+
return void 0;
|
|
3242
|
+
}
|
|
3243
|
+
signatureName(inner) {
|
|
3244
|
+
const nameNode = inner.childForFieldName("name");
|
|
3245
|
+
if (nameNode) return nameNode.text;
|
|
3246
|
+
if (inner.type === "operator_signature") {
|
|
3247
|
+
const opTok = this.operatorTokenNode(inner);
|
|
3248
|
+
return opTok ? `operator ${opTok.text}` : null;
|
|
3249
|
+
}
|
|
3250
|
+
const id = this.findChildByType(inner, "identifier");
|
|
3251
|
+
return id ? id.text : null;
|
|
3252
|
+
}
|
|
3253
|
+
/** The operator symbol token of an `operator_signature`.
|
|
3254
|
+
*
|
|
3255
|
+
* This locates the `operator` KEYWORD and takes the token after it. It must NOT
|
|
3256
|
+
* hardcode a child index: the return type in front of the keyword is not one
|
|
3257
|
+
* node but a variable number of them. A plain `bool` is a single
|
|
3258
|
+
* `type_identifier`, so `child(2)` is the operator token — but a SCOPED return
|
|
3259
|
+
* type (`core$_.bool`, `ns.bool`) is a `scoped_identifier` the grammar splits,
|
|
3260
|
+
* which shifts every later child by one.
|
|
3261
|
+
*
|
|
3262
|
+
* Measured: `extension ByteString$$Methods on ByteString { core$_.bool
|
|
3263
|
+
* operator >(ByteString byteString) { ... } }` in ok_http's JNI bindings, where
|
|
3264
|
+
* `child(2)` then names the member after the return type's last segment and
|
|
3265
|
+
* publishes `operator bool` — a member name that names a TYPE, on a class that
|
|
3266
|
+
* has no such member, while the real `operator >` went missing. Scanning for
|
|
3267
|
+
* the keyword is invariant to how the return type is spelled. */
|
|
3268
|
+
operatorTokenNode(inner) {
|
|
3269
|
+
for (let i = 0; i < inner.childCount; i++) {
|
|
3270
|
+
const c = inner.child(i);
|
|
3271
|
+
if (c && !c.isNamed && c.text === "operator") return inner.child(i + 1);
|
|
3272
|
+
}
|
|
3273
|
+
return null;
|
|
3274
|
+
}
|
|
3275
|
+
/** Node form of `signatureName`: the node that names a member edge is also
|
|
3276
|
+
* the node whose LINE the edge carries (M60 line contract). For an operator
|
|
3277
|
+
* overload the naming node is the operator TOKEN — its line, not its text,
|
|
3278
|
+
* is what the synthesised spelling contributes. */
|
|
3279
|
+
signatureNameNode(inner) {
|
|
3280
|
+
const nameNode = inner.childForFieldName("name");
|
|
3281
|
+
if (nameNode) return nameNode;
|
|
3282
|
+
if (inner.type === "operator_signature") {
|
|
3283
|
+
return this.operatorTokenNode(inner);
|
|
3284
|
+
}
|
|
3285
|
+
return this.findChildByType(inner, "identifier");
|
|
3286
|
+
}
|
|
3287
|
+
findSignatureIn(node) {
|
|
3288
|
+
const stack = [node];
|
|
3289
|
+
let guard = 0;
|
|
3290
|
+
while (stack.length > 0 && guard++ < 64) {
|
|
3291
|
+
const n = stack.pop();
|
|
3292
|
+
if (this.isSignatureNode(n.type)) return n;
|
|
3293
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3294
|
+
const c = n.namedChild(i);
|
|
3295
|
+
if (c) stack.push(c);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return null;
|
|
3299
|
+
}
|
|
3300
|
+
findInnerSignature(node) {
|
|
3301
|
+
if (node.type !== "method_signature") return node;
|
|
3302
|
+
const stack = [node];
|
|
3303
|
+
while (stack.length > 0) {
|
|
3304
|
+
const n = stack.pop();
|
|
3305
|
+
if (this.isSignatureNode(n.type) && n.type !== "method_signature") {
|
|
3306
|
+
return n;
|
|
3307
|
+
}
|
|
3308
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3309
|
+
const c = n.namedChild(i);
|
|
3310
|
+
if (c) stack.push(c);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return node;
|
|
3314
|
+
}
|
|
3315
|
+
hasBody(node) {
|
|
3316
|
+
const sib = node.nextNamedSibling;
|
|
3317
|
+
if (sib && (sib.type === "function_body" || sib.type === "block")) return true;
|
|
3318
|
+
return this.findChildByType(node, "function_body") !== null;
|
|
3319
|
+
}
|
|
3320
|
+
rangeIncludingBody(node, sig) {
|
|
3321
|
+
const range = this.nodeToRange(node);
|
|
3322
|
+
let sib = node.nextSibling;
|
|
3323
|
+
let guard = 0;
|
|
3324
|
+
while (sib && guard++ < 8) {
|
|
3325
|
+
if (sib.type === "function_body" || sib.type === "block") {
|
|
3326
|
+
range.endLine = sib.endPosition.row + 1;
|
|
3327
|
+
range.endColumn = sib.endPosition.column + 1;
|
|
3328
|
+
range.endIndex = sib.endIndex;
|
|
3329
|
+
break;
|
|
3330
|
+
}
|
|
3331
|
+
if (sib.type === "}" || sib.type === "class_body" || sib.type === "extension_body") break;
|
|
3332
|
+
sib = sib.nextSibling;
|
|
3333
|
+
}
|
|
3334
|
+
return range;
|
|
3335
|
+
}
|
|
3336
|
+
isAsyncSignature(node, sig) {
|
|
3337
|
+
const body = this.findChildByType(node, "function_body");
|
|
3338
|
+
if (body) {
|
|
3339
|
+
if (this.hasToken(body, "async")) return true;
|
|
3340
|
+
} else {
|
|
3341
|
+
const sib = node.nextSibling;
|
|
3342
|
+
if (sib && sib.type === "function_body" && this.hasToken(sib, "async")) return true;
|
|
3343
|
+
}
|
|
3344
|
+
return false;
|
|
3345
|
+
}
|
|
3346
|
+
hasToken(node, token) {
|
|
3347
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3348
|
+
const c = node.child(i);
|
|
3349
|
+
if (c && (c.type === token || c.text === token)) return true;
|
|
3350
|
+
}
|
|
3351
|
+
return false;
|
|
3352
|
+
}
|
|
3353
|
+
hasModifier(node, modifier) {
|
|
3354
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3355
|
+
const c = node.child(i);
|
|
3356
|
+
if (c && (c.type === modifier || c.text === modifier)) return true;
|
|
3357
|
+
}
|
|
3358
|
+
if (node.parent && node.parent.type === "declaration") {
|
|
3359
|
+
for (let i = 0; i < node.parent.childCount; i++) {
|
|
3360
|
+
const c = node.parent.child(i);
|
|
3361
|
+
if (c && (c.type === modifier || c.text === modifier)) return true;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
extractParameters(node) {
|
|
3367
|
+
const out = [];
|
|
3368
|
+
const paramsNode = node.type === "formal_parameter_list" ? node : this.findChildByType(node, "formal_parameter_list");
|
|
3369
|
+
if (!paramsNode) return out;
|
|
3370
|
+
const stack = [paramsNode];
|
|
3371
|
+
while (stack.length > 0) {
|
|
3372
|
+
const n = stack.pop();
|
|
3373
|
+
if (n.type === "formal_parameter" || n.type === "constructor_param" || n.type === "typed_identifier") {
|
|
3374
|
+
const nameNode = n.childForFieldName("name") || this.findChildByType(n, "identifier");
|
|
3375
|
+
if (nameNode && nameNode.text !== "this" && nameNode.text !== "super") {
|
|
3376
|
+
let typeText;
|
|
3377
|
+
if (n.type === "typed_identifier") {
|
|
3378
|
+
typeText = this.firstTypeIdentifier(n, false)?.text;
|
|
3379
|
+
} else {
|
|
3380
|
+
const kids = [];
|
|
3381
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3382
|
+
const c = n.namedChild(i);
|
|
3383
|
+
if (c) kids.push(c);
|
|
3384
|
+
}
|
|
3385
|
+
const nameIdx = kids.findIndex(
|
|
3386
|
+
(c) => (
|
|
3387
|
+
// web-tree-sitter creates a NEW wrapper object per access, so
|
|
3388
|
+
// reference equality always fails (same trap extractReturnType's
|
|
3389
|
+
// KAD2 sweep fixed) — compare byte spans.
|
|
3390
|
+
nameNode && c.startIndex === nameNode.startIndex && c.endIndex === nameNode.endIndex
|
|
3391
|
+
)
|
|
3392
|
+
);
|
|
3393
|
+
for (let i = 0; i < nameIdx; i++) {
|
|
3394
|
+
const c = kids[i];
|
|
3395
|
+
if (c.type === "type_identifier" || c.type === "function_type" || c.type === "nullable_type" || c.type === "void_type" || c.type === "record_type") {
|
|
3396
|
+
typeText = c.text;
|
|
3397
|
+
break;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
let group;
|
|
3402
|
+
let p = n.parent;
|
|
3403
|
+
while (p && p.type !== "optional_formal_parameters") p = p.parent;
|
|
3404
|
+
if (p && p.type === "optional_formal_parameters") group = p;
|
|
3405
|
+
const isNamed = group ? group.text.startsWith("{") : false;
|
|
3406
|
+
const isOptional = group !== void 0;
|
|
3407
|
+
const hasDefault = n.text.includes("=");
|
|
3408
|
+
const already = out.some((p2) => p2.name === nameNode.text);
|
|
3409
|
+
if (!already) out.push({ name: nameNode.text, type: typeText ?? IMPLICIT_DYNAMIC_TYPE, isOptional, isNamed });
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3413
|
+
const c = n.namedChild(i);
|
|
3414
|
+
if (c) stack.push(c);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
return out;
|
|
3418
|
+
}
|
|
3419
|
+
extractReturnType(node) {
|
|
3420
|
+
const voidType = this.findChildByType(node, "void_type");
|
|
3421
|
+
if (voidType) return "void";
|
|
3422
|
+
const nameNode = node.childForFieldName("name");
|
|
3423
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3424
|
+
const c = node.namedChild(i);
|
|
3425
|
+
if (!c) continue;
|
|
3426
|
+
if (nameNode && c.startIndex === nameNode.startIndex && c.endIndex === nameNode.endIndex) break;
|
|
3427
|
+
if (c.type === "type_identifier" || c.type === "void_type" || c.type === "record_type") return c.text;
|
|
3428
|
+
}
|
|
3429
|
+
return void 0;
|
|
3430
|
+
}
|
|
3431
|
+
extractFieldType(node, declList) {
|
|
3432
|
+
const candidates = [node, declList];
|
|
3433
|
+
for (const cand of candidates) {
|
|
3434
|
+
for (let i = 0; i < cand.namedChildCount; i++) {
|
|
3435
|
+
const c = cand.namedChild(i);
|
|
3436
|
+
if (!c) continue;
|
|
3437
|
+
if (c.type === "type_identifier" || c.type === "void_type" || c.type === "record_type") {
|
|
3438
|
+
return c.text;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return void 0;
|
|
3443
|
+
}
|
|
3444
|
+
/** The representation type's leaf name (`extension type X(objc.ObjCObject o)`
|
|
3445
|
+
* -> `ObjCObject`): alias-free like every other type name this lane emits
|
|
3446
|
+
* (DARTD1/DARTD2), since the first identifier is the IMPORT PREFIX. */
|
|
3447
|
+
representationTypeName(rep) {
|
|
3448
|
+
if (!rep) return void 0;
|
|
3449
|
+
const nameNode = rep.childForFieldName("name");
|
|
3450
|
+
let leaf;
|
|
3451
|
+
for (let i = 0; i < rep.namedChildCount; i++) {
|
|
3452
|
+
const c = rep.namedChild(i);
|
|
3453
|
+
if (!c) continue;
|
|
3454
|
+
if (nameNode && c.startIndex === nameNode.startIndex && c.endIndex === nameNode.endIndex) break;
|
|
3455
|
+
if (c.type === "type_identifier" || c.type === "void_type") leaf = c.text;
|
|
3456
|
+
}
|
|
3457
|
+
return leaf;
|
|
3458
|
+
}
|
|
3459
|
+
extractTypeParameters(node) {
|
|
3460
|
+
const tp = this.findChildByType(node, "type_parameters");
|
|
3461
|
+
if (!tp) return void 0;
|
|
3462
|
+
const out = [];
|
|
3463
|
+
for (let i = 0; i < tp.namedChildCount; i++) {
|
|
3464
|
+
const c = tp.namedChild(i);
|
|
3465
|
+
if (c && c.type === "type_parameter") out.push(c.text);
|
|
3466
|
+
}
|
|
3467
|
+
return out.length > 0 ? out : void 0;
|
|
3468
|
+
}
|
|
3469
|
+
extensionTargetType(node) {
|
|
3470
|
+
const name = node.childForFieldName("name")?.text;
|
|
3471
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3472
|
+
const c = node.namedChild(i);
|
|
3473
|
+
if (!c || c.type === "extension_body" || c.type === "identifier") continue;
|
|
3474
|
+
if (c.type === "type_identifier" || c.type === "void_type") {
|
|
3475
|
+
if (c.text === name) continue;
|
|
3476
|
+
return c.text;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return void 0;
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* THE NAME AN EXTENSION IS PUBLISHED UNDER — ONE rule at BOTH sites that need it (the symbol
|
|
3483
|
+
* emitter and `resolveOwner`), because a member whose published scope and whose container's name
|
|
3484
|
+
* disagree is a `contains` edge that names nothing.
|
|
3485
|
+
*
|
|
3486
|
+
* A named extension is published under its own name. An unnamed one has none, so it takes the
|
|
3487
|
+
* family's synthesized spelling — see `DART_UNNAMED_EXTENSION` for why borrowing the target
|
|
3488
|
+
* type's name was a defect rather than untidiness, and what it cost. The `undefined` fallback is
|
|
3489
|
+
* `DART_UNNAMED_EXTENSION` itself, mirroring java's `anonymous_class` (real-java-analyzer.ts):
|
|
3490
|
+
* a declaration that HAS members and no name still needs an owner string, and the alternative is
|
|
3491
|
+
* leaking those members into whatever scope surrounded them.
|
|
3492
|
+
*/
|
|
3493
|
+
extensionPublishedName(node) {
|
|
3494
|
+
const declared = node.childForFieldName?.("name")?.text;
|
|
3495
|
+
if (declared) return declared;
|
|
3496
|
+
const target = this.extensionTargetType(node);
|
|
3497
|
+
return target ? `${DART_UNNAMED_EXTENSION}_${target}` : DART_UNNAMED_EXTENSION;
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Is this `type_identifier` the `on` SUBJECT of the UNNAMED extension that encloses it?
|
|
3501
|
+
*
|
|
3502
|
+
* The subject is part of the extension's own identity — `extensionPublishedName` derives the
|
|
3503
|
+
* published name FROM it — so it is not a mention the body makes. Only an UNNAMED extension is
|
|
3504
|
+
* covered: `extension M on M2 {}` publishes `M -> M2` and must keep doing so (pinned by
|
|
3505
|
+
* `verify-dart-references.ts`).
|
|
3506
|
+
*/
|
|
3507
|
+
isUnnamedExtensionSubject(n) {
|
|
3508
|
+
const parent = n.parent;
|
|
3509
|
+
if (!parent || parent.type !== "extension_declaration") return false;
|
|
3510
|
+
return parent.childForFieldName?.("name")?.text === void 0;
|
|
3511
|
+
}
|
|
3512
|
+
extractAnnotations(node) {
|
|
3513
|
+
const annotations = [];
|
|
3514
|
+
let prev = node.previousNamedSibling;
|
|
3515
|
+
let guard = 0;
|
|
3516
|
+
while (prev && guard++ < 8) {
|
|
3517
|
+
if (prev.type === "annotation") {
|
|
3518
|
+
annotations.unshift(prev.text);
|
|
3519
|
+
prev = prev.previousNamedSibling;
|
|
3520
|
+
} else {
|
|
3521
|
+
break;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
return annotations.length > 0 ? annotations : void 0;
|
|
3525
|
+
}
|
|
3526
|
+
// ============================================================================
|
|
3527
|
+
// ANNOTATION REFERENCES — the type a `@...` names is a `references` edge
|
|
3528
|
+
// ============================================================================
|
|
3529
|
+
/**
|
|
3530
|
+
* Emit one `references` row per (annotation, decorated declaration) pair.
|
|
3531
|
+
*
|
|
3532
|
+
* THE RULES, each from the pinned grammar (see `annotation-shapes-probe.ts`):
|
|
3533
|
+
*
|
|
3534
|
+
* TARGET — the annotation's `name` field, read as written and WITHOUT its `@`, arguments
|
|
3535
|
+
* or type arguments. For `@prefix.Qualified` the target is the LEAF the `name` field names,
|
|
3536
|
+
* not the import alias: this lane strips alias prefixes everywhere heritage and type
|
|
3537
|
+
* mentions are spelled (DT-B/DAD1/DAD2 — `_i2.Mixin` is `Mixin`), and an annotation alias
|
|
3538
|
+
* is the same alias. (Java keeps the full path because Java's qualifier is a package, not
|
|
3539
|
+
* an import alias; the two lanes' conventions differ for a reason.)
|
|
3540
|
+
*
|
|
3541
|
+
* SOURCE — the declaration the annotation DECORATES, because an annotation node has no
|
|
3542
|
+
* name of its own. Three carrier shapes, all probed:
|
|
3543
|
+
* child form `@deprecated class C` -> annotation is a CHILD of `class_definition`;
|
|
3544
|
+
* `enum Kind { @deprecated old }` -> child of `enum_constant`;
|
|
3545
|
+
* `@deprecated final x = 1;` -> child of `local_variable_declaration`.
|
|
3546
|
+
* sibling form inside `class_body` and `program` the annotation is a SIBLING of the
|
|
3547
|
+
* declaration (`class_body (annotation) (method_signature)`), so the
|
|
3548
|
+
* carrier is the next non-annotation sibling.
|
|
3549
|
+
* signature-param form `void f(@required int x)` -> annotation is a CHILD of
|
|
3550
|
+
* `formal_parameter`; the carrier is the enclosing callable, because a
|
|
3551
|
+
* parameter publishes no symbol (DARTD8) and the annotation is a fact
|
|
3552
|
+
* about the declaration that carries it.
|
|
3553
|
+
* Multi-declarator fields (`@JsonKey(...) int a = 1, b = 2;`) publish ONE row per
|
|
3554
|
+
* declarator, matching the symbol model (each declarator is its own property).
|
|
3555
|
+
*
|
|
3556
|
+
* CONFIDENCE 0.95, `evidenceClass: structural`: the annotation's type name is WRITTEN at
|
|
3557
|
+
* the declaration, not inferred. `annotatedKind` is the carrier's grammar node type so a
|
|
3558
|
+
* consumer can tell a class annotation from a parameter annotation — the node type alone
|
|
3559
|
+
* cannot.
|
|
3560
|
+
*
|
|
3561
|
+
* NOT A CLAIM, and not an unresolvable-target exemption: the row is an ordinary `references`
|
|
3562
|
+
* edge and the project's resolver decides whether the annotation type has exactly one
|
|
3563
|
+
* in-project declaration (`@override` is dart:core's, so it will read unresolved in a
|
|
3564
|
+
* per-file run and resolve under an index — the same treatment every out-of-file type
|
|
3565
|
+
* mention gets on this label).
|
|
3566
|
+
*/
|
|
3567
|
+
emitAnnotationReferences(ann, relationships, current, owner, moduleName, importAliases) {
|
|
3568
|
+
const nameNode = ann.childForFieldName?.("name") ?? this.findChildByType(ann, "identifier");
|
|
3569
|
+
if (!nameNode) return;
|
|
3570
|
+
const target = this.annotationTargetSpelling(nameNode, importAliases);
|
|
3571
|
+
if (!target) return;
|
|
3572
|
+
const carriers = this.annotationCarriers(ann, current, owner, moduleName);
|
|
3573
|
+
for (const carrier of carriers) {
|
|
3574
|
+
relationships.push({
|
|
3575
|
+
source: carrier.name,
|
|
3576
|
+
target,
|
|
3577
|
+
type: ASTEngine.RelationshipType.References,
|
|
3578
|
+
confidence: 0.95,
|
|
3579
|
+
// The NAME is written on the annotation's line; the declaration's header is another.
|
|
3580
|
+
line_number: this.nodeLine(nameNode),
|
|
3581
|
+
metadata: {
|
|
3582
|
+
relationshipType: "annotation",
|
|
3583
|
+
isAnnotation: true,
|
|
3584
|
+
annotatedKind: carrier.kind
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* The annotation's TYPE name, as this lane spells types.
|
|
3591
|
+
*
|
|
3592
|
+
* A `scoped_identifier` is ONE grammar shape for two different Dart facts, and the alias table
|
|
3593
|
+
* is what tells them apart:
|
|
3594
|
+
*
|
|
3595
|
+
* `@_i1.TestFor()` the qualifier is an IMPORT ALIAS -> the type is the LEAF
|
|
3596
|
+
* (`TestFor`), and publishing `_i1.TestFor` would name an
|
|
3597
|
+
* alias no declaration has.
|
|
3598
|
+
* `@TestFor.async_value_nullable_pattern`
|
|
3599
|
+
* the qualifier is the TYPE and the member selects a named
|
|
3600
|
+
* CONSTRUCTOR (riverpod's `@TestFor.<lint>()` family) or a
|
|
3601
|
+
* static; the type is the QUALIFIER. Taking the leaf here
|
|
3602
|
+
* published a target named after a constructor, which is
|
|
3603
|
+
* neither a type nor a declaration — and on a file whose
|
|
3604
|
+
* stem matched the constructor it even produced a
|
|
3605
|
+
* self-reference row (`async_value_nullable_pattern ->
|
|
3606
|
+
* async_value_nullable_pattern`), measured by the corpus
|
|
3607
|
+
* gate as UNEXPLAINED.
|
|
3608
|
+
*
|
|
3609
|
+
* Non-scoped names: the name field's own text. Arguments and type arguments are separate
|
|
3610
|
+
* children and never enter the target.
|
|
3611
|
+
*/
|
|
3612
|
+
annotationTargetSpelling(nameNode, importAliases) {
|
|
3613
|
+
if (nameNode.type === "identifier" || nameNode.type === "type_identifier") return nameNode.text;
|
|
3614
|
+
if (nameNode.type === "scoped_identifier") {
|
|
3615
|
+
const scope = nameNode.childForFieldName?.("scope")?.text ?? (nameNode.namedChildren ?? []).find((c) => c?.type === "identifier")?.text;
|
|
3616
|
+
const leaf = nameNode.childForFieldName?.("name")?.text ?? [...nameNode.namedChildren ?? []].reverse().find((c) => c?.type === "identifier")?.text;
|
|
3617
|
+
if (scope && importAliases.has(scope)) return leaf ?? null;
|
|
3618
|
+
return scope ?? leaf ?? null;
|
|
3619
|
+
}
|
|
3620
|
+
const id = this.findChildByType(nameNode, "identifier") ?? this.findChildByType(nameNode, "type_identifier");
|
|
3621
|
+
return id ? id.text : null;
|
|
3622
|
+
}
|
|
3623
|
+
/**
|
|
3624
|
+
* The declarations one annotation decorates, with the grammar node type of each carrier.
|
|
3625
|
+
* EMPTY when the position publishes no name this lane can attribute to — a library
|
|
3626
|
+
* directive's annotation (`library @TestOn('vm');`) is deliberately not attributed: the
|
|
3627
|
+
* library node is the module symbol, and an edge from the module to the annotation type
|
|
3628
|
+
* would be a claim about a directive the lane has not placed in this family.
|
|
3629
|
+
*/
|
|
3630
|
+
annotationCarriers(ann, current, owner, moduleName) {
|
|
3631
|
+
const parent = ann.parent;
|
|
3632
|
+
if (!parent) return [];
|
|
3633
|
+
const p = parent.type;
|
|
3634
|
+
if (p === "class_definition" || p === "mixin_declaration" || p === "enum_declaration" || p === "extension_declaration" || p === "extension_type_declaration") {
|
|
3635
|
+
const nm = parent.childForFieldName?.("name")?.text ?? this.findChildByType(parent, "identifier")?.text;
|
|
3636
|
+
return nm ? [{ name: nm, kind: p }] : [];
|
|
3637
|
+
}
|
|
3638
|
+
if (p === "enum_constant") {
|
|
3639
|
+
const nm = parent.childForFieldName?.("name")?.text ?? this.findChildByType(parent, "identifier")?.text;
|
|
3640
|
+
return nm ? [{ name: nm, kind: p }] : [];
|
|
3641
|
+
}
|
|
3642
|
+
if (p === "type_alias") {
|
|
3643
|
+
const nm = parent.childForFieldName?.("name")?.text ?? this.findChildByType(parent, "type_identifier")?.text;
|
|
3644
|
+
return nm ? [{ name: nm, kind: p }] : [];
|
|
3645
|
+
}
|
|
3646
|
+
if (p === "library_name") {
|
|
3647
|
+
const declared = this.findChildByType(parent, "dotted_identifier_list");
|
|
3648
|
+
const name = declared ? stripWhitespace(declared.text) : moduleName;
|
|
3649
|
+
return name ? [{ name, kind: "library_annotation" }] : [];
|
|
3650
|
+
}
|
|
3651
|
+
if (p === "local_variable_declaration") {
|
|
3652
|
+
const carrier = current ?? owner;
|
|
3653
|
+
return carrier ? [{ name: carrier, kind: p }] : [];
|
|
3654
|
+
}
|
|
3655
|
+
if (p === "formal_parameter" || p === "constructor_param" || p === "super_formal_parameter") {
|
|
3656
|
+
let sig = parent;
|
|
3657
|
+
let guard = 0;
|
|
3658
|
+
while (sig && guard++ < 8) {
|
|
3659
|
+
if (this.isSignatureNode(sig.type)) break;
|
|
3660
|
+
sig = sig.parent;
|
|
3661
|
+
}
|
|
3662
|
+
if (sig) {
|
|
3663
|
+
if (sig.parent?.type === "lambda_expression" || sig.parent?.type === "function_expression") {
|
|
3664
|
+
return current ? [{ name: current, kind: "lambda_parameter" }] : [];
|
|
3665
|
+
}
|
|
3666
|
+
const inner = this.findInnerSignature(sig);
|
|
3667
|
+
if (this.isConstructorSignature(sig.type) || this.isConstructorSignature(inner.type)) {
|
|
3668
|
+
return this.isRealConstructor(inner, this.enclosingTypeName(inner)) ? [{ name: "constructor", kind: "constructor_param" }] : [];
|
|
3669
|
+
}
|
|
3670
|
+
const nm = this.signatureName(inner);
|
|
3671
|
+
return nm ? [{ name: nm, kind: "parameter_annotation" }] : [];
|
|
3672
|
+
}
|
|
3673
|
+
return current ? [{ name: current, kind: "parameter_annotation" }] : [];
|
|
3674
|
+
}
|
|
3675
|
+
if (p === "class_body" || p === "program" || p === "enum_body") {
|
|
3676
|
+
let sib = ann.nextNamedSibling;
|
|
3677
|
+
let guard = 0;
|
|
3678
|
+
while (sib && sib.type === "annotation" && guard++ < 16) sib = sib.nextNamedSibling;
|
|
3679
|
+
let modGuard = 0;
|
|
3680
|
+
while (sib && sib.type.endsWith("_builtin") && modGuard++ < 8) sib = sib.nextNamedSibling;
|
|
3681
|
+
return sib ? this.carriersOfDeclarationNode(sib) : [];
|
|
3682
|
+
}
|
|
3683
|
+
return [];
|
|
3684
|
+
}
|
|
3685
|
+
/** The names a declaration node publishes, for the annotation sibling form. */
|
|
3686
|
+
carriersOfDeclarationNode(node) {
|
|
3687
|
+
if (node.type === "type_identifier" || node.type === "void_type" || node.type === "nullable_type" || node.type === "function_type" || node.type === "inferred_type") {
|
|
3688
|
+
const after = node.nextNamedSibling;
|
|
3689
|
+
if (after && (after.type === "initialized_identifier_list" || after.type === "static_final_declaration_list")) {
|
|
3690
|
+
return this.carriersOfDeclarationNode(after);
|
|
3691
|
+
}
|
|
3692
|
+
return [];
|
|
3693
|
+
}
|
|
3694
|
+
if (node.type === "initialized_identifier_list" || node.type === "static_final_declaration_list") {
|
|
3695
|
+
const out2 = [];
|
|
3696
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3697
|
+
const c = node.namedChild(i);
|
|
3698
|
+
if (!c) continue;
|
|
3699
|
+
const id = this.findChildByType(c, "identifier");
|
|
3700
|
+
if (id) out2.push({ name: id.text, kind: node.type });
|
|
3701
|
+
}
|
|
3702
|
+
return out2;
|
|
3703
|
+
}
|
|
3704
|
+
const t = node.type;
|
|
3705
|
+
if (t === "class_definition" || t === "mixin_declaration" || t === "enum_declaration" || t === "extension_declaration" || t === "extension_type_declaration" || t === "type_alias") {
|
|
3706
|
+
const nm = node.childForFieldName?.("name")?.text ?? this.findChildByType(node, "identifier")?.text;
|
|
3707
|
+
return nm ? [{ name: nm, kind: t }] : [];
|
|
3708
|
+
}
|
|
3709
|
+
let decl = node;
|
|
3710
|
+
if (decl.type === "declaration") {
|
|
3711
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
3712
|
+
const c = decl.namedChild(i);
|
|
3713
|
+
if (c && this.isSignatureNode(c.type)) {
|
|
3714
|
+
decl = c;
|
|
3715
|
+
break;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
if (this.isSignatureNode(decl.type)) {
|
|
3720
|
+
const inner = this.findInnerSignature(decl);
|
|
3721
|
+
if (this.isConstructorSignature(decl.type) || this.isConstructorSignature(inner.type)) {
|
|
3722
|
+
return this.isRealConstructor(inner, this.enclosingTypeName(inner)) ? [{ name: "constructor", kind: "constructor_signature" }] : [];
|
|
3723
|
+
}
|
|
3724
|
+
const nm = this.signatureName(inner);
|
|
3725
|
+
return nm ? [{ name: nm, kind: inner.type }] : [];
|
|
3726
|
+
}
|
|
3727
|
+
const out = [];
|
|
3728
|
+
const stack = [node];
|
|
3729
|
+
let guard = 0;
|
|
3730
|
+
while (stack.length > 0 && guard++ < 64) {
|
|
3731
|
+
const c = stack.pop();
|
|
3732
|
+
if (c.type === "initialized_identifier" || c.type === "static_final_declaration") {
|
|
3733
|
+
const id = this.findChildByType(c, "identifier");
|
|
3734
|
+
if (id) out.push({ name: id.text, kind: node.type });
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
for (let i = c.namedChildCount - 1; i >= 0; i--) {
|
|
3738
|
+
const ch = c.namedChild(i);
|
|
3739
|
+
if (ch && (ch.type === "initialized_identifier_list" || ch.type === "static_final_declaration_list" || ch.type === "initialized_identifier" || ch.type === "static_final_declaration")) {
|
|
3740
|
+
stack.push(ch);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
return out;
|
|
3745
|
+
}
|
|
3746
|
+
collectComments(node) {
|
|
3747
|
+
const comments = [];
|
|
3748
|
+
let sibling = node.previousSibling;
|
|
3749
|
+
let guard = 0;
|
|
3750
|
+
while (sibling && guard++ < 12) {
|
|
3751
|
+
if (sibling.type === "comment" || sibling.type === "documentation_comment") {
|
|
3752
|
+
comments.unshift(stripCommentMarkers(sibling.text));
|
|
3753
|
+
sibling = sibling.previousSibling;
|
|
3754
|
+
} else if (sibling.type === "annotation") {
|
|
3755
|
+
sibling = sibling.previousSibling;
|
|
3756
|
+
} else {
|
|
3757
|
+
break;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
return comments.length > 0 ? comments : void 0;
|
|
3761
|
+
}
|
|
3762
|
+
directIdentifiers(node) {
|
|
3763
|
+
const out = [];
|
|
3764
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3765
|
+
const c = node.namedChild(i);
|
|
3766
|
+
if (c && (c.type === "identifier" || c.type === "type_identifier")) out.push(c.text);
|
|
3767
|
+
}
|
|
3768
|
+
return out;
|
|
3769
|
+
}
|
|
3770
|
+
// ══ CYCLOMATIC COMPLEXITY IS COMPUTED ONCE PER FUNCTION, NOT ONCE PER CALL.
|
|
3771
|
+
//
|
|
3772
|
+
// `methodComplexity` walks the node's whole subtree, and `processTopLevelFunction` calls it for
|
|
3773
|
+
// every function-like node. In a file of DEEPLY NESTED statements those subtrees OVERLAP — every
|
|
3774
|
+
// nested function's walk includes everything below it — so the pass is O(functions × nodes). The
|
|
3775
|
+
// accessor counter measured it: **103,034,258 `.type` reads** on a 130,042-node file, with
|
|
3776
|
+
// `.text` at 1,596 and 1 MB of characters materialised, and the stack named this function. (That
|
|
3777
|
+
// also FALSIFIED the `.text` hypothesis: the materialisation was never the cost; the re-walking
|
|
3778
|
+
// was, and each visit pays ~10 `.type` reads that cross into WASM.)
|
|
3779
|
+
//
|
|
3780
|
+
// A function's complexity is a pure property of its subtree, so it is cached by the node's stable
|
|
3781
|
+
// SPAN — `startIndex:endIndex:type`, never the node object, because web-tree-sitter hands out
|
|
3782
|
+
// fresh wrappers. Cleared per file in `analyze()`.
|
|
3783
|
+
methodComplexityCache = /* @__PURE__ */ new Map();
|
|
3784
|
+
methodComplexity(node) {
|
|
3785
|
+
const cacheKey = `${node.startIndex}:${node.endIndex}:${node.type}`;
|
|
3786
|
+
const cached = this.methodComplexityCache.get(cacheKey);
|
|
3787
|
+
if (cached !== void 0) return cached;
|
|
3788
|
+
let c = 1;
|
|
3789
|
+
const stack = [node];
|
|
3790
|
+
let sib = node.nextNamedSibling;
|
|
3791
|
+
let guard = 0;
|
|
3792
|
+
while (sib && guard++ < 8) {
|
|
3793
|
+
if (sib.type === "function_body" || sib.type === "block" || sib.type === "function_expression_body") {
|
|
3794
|
+
stack.push(sib);
|
|
3795
|
+
break;
|
|
3796
|
+
}
|
|
3797
|
+
if (sib.type === "class_body" || sib.type === "extension_body" || sib.type === "}" || sib.type === "declaration" || sib.type === "method_signature" || sib.type === "function_signature") break;
|
|
3798
|
+
sib = sib.nextNamedSibling;
|
|
3799
|
+
}
|
|
3800
|
+
guard = 0;
|
|
3801
|
+
while (stack.length > 0 && guard++ < 2e5) {
|
|
3802
|
+
const n = stack.pop();
|
|
3803
|
+
if (n.type === "if_statement" || n.type === "for_statement" || n.type === "while_statement" || n.type === "do_statement" || n.type === "switch_statement_case" || n.type === "switch_expression_case" || n.type === "catch_clause" || n.type === "conditional_expression" || n.type === "logical_and_expression" || n.type === "logical_or_expression") {
|
|
3804
|
+
c++;
|
|
3805
|
+
}
|
|
3806
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3807
|
+
const child = n.namedChild(i);
|
|
3808
|
+
if (child) stack.push(child);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
this.methodComplexityCache.set(cacheKey, c);
|
|
3812
|
+
return c;
|
|
3813
|
+
}
|
|
3814
|
+
firstTypeIdentifier(node, excludeMixins) {
|
|
3815
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3816
|
+
const c = node.namedChild(i);
|
|
3817
|
+
if (!c) continue;
|
|
3818
|
+
if (excludeMixins && c.type === "mixins") continue;
|
|
3819
|
+
if (c.type === "type_identifier") {
|
|
3820
|
+
return c;
|
|
3821
|
+
}
|
|
3822
|
+
if (c.type === "identifier") return c;
|
|
3823
|
+
}
|
|
3824
|
+
return null;
|
|
3825
|
+
}
|
|
3826
|
+
/**
|
|
3827
|
+
* Resolved heritage names (DAD1/DAD2 contract): the real class names from a
|
|
3828
|
+
* heritage clause, with import-alias prefixes dropped and generic arguments
|
|
3829
|
+
* excluded.
|
|
3830
|
+
*
|
|
3831
|
+
* tree-sitter-dart parses a qualified supertype FLAT: `extends _i1.SmartFake`
|
|
3832
|
+
* is superclass > [_i1, `.`, SmartFake] — adjacent type_identifiers joined by
|
|
3833
|
+
* an anonymous `.` token where the FIRST is the import-alias container, not a
|
|
3834
|
+
* class name (the mocktail shape: `class _FakeX_0 extends _i1.SmartFake
|
|
3835
|
+
* implements _i2.Database`). Taking the first raw identifier named the base
|
|
3836
|
+
* after the alias (DAD1); enumerating all type_identifiers leaked the alias
|
|
3837
|
+
* into implements targets (DAD2).
|
|
3838
|
+
*
|
|
3839
|
+
* Rule over RAW children (anonymous tokens carry the disambiguation):
|
|
3840
|
+
* keep type_identifier/identifier nodes whose next raw sibling is NOT a `.`
|
|
3841
|
+
* token — a `.` means this name is an alias prefix; `,` or end-of-clause or
|
|
3842
|
+
* type_arguments means it is a real name. type_arguments are generic
|
|
3843
|
+
* arguments, never bases; mixins are handled separately as `uses`.
|
|
3844
|
+
* Probed shapes (dart-heritage-shapes.ts, 2026-09-03):
|
|
3845
|
+
* `_i1.SmartFake` -> [SmartFake]
|
|
3846
|
+
* `Alpha, Beta` -> [Alpha, Beta]
|
|
3847
|
+
* `_i2.Database, _i3.Future` -> [Database, Future]
|
|
3848
|
+
* `generics.Map<String, int>`-> [Map]
|
|
3849
|
+
*/
|
|
3850
|
+
resolvedHeritageNames(node) {
|
|
3851
|
+
return this.resolvedHeritageNodes(node).map((n) => n.text);
|
|
3852
|
+
}
|
|
3853
|
+
/** Node form of `resolvedHeritageNames` — the heritage-name node IS the node
|
|
3854
|
+
* that names an extends/implements/uses edge (M60 line contract), so text
|
|
3855
|
+
* and line come from one selection. */
|
|
3856
|
+
resolvedHeritageNodes(node) {
|
|
3857
|
+
const out = [];
|
|
3858
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3859
|
+
const c = node.child(i);
|
|
3860
|
+
if (!c) continue;
|
|
3861
|
+
if (c.type === "type_arguments" || c.type === "mixins") continue;
|
|
3862
|
+
if (c.type !== "type_identifier" && c.type !== "identifier") continue;
|
|
3863
|
+
const next = node.child(i + 1);
|
|
3864
|
+
if (next && next.type === ".") continue;
|
|
3865
|
+
out.push(c);
|
|
3866
|
+
}
|
|
3867
|
+
return out;
|
|
3868
|
+
}
|
|
3869
|
+
allTypeIdentifiers(node) {
|
|
3870
|
+
const out = [];
|
|
3871
|
+
const stack = [node];
|
|
3872
|
+
let guard = 0;
|
|
3873
|
+
while (stack.length > 0 && guard++ < 64) {
|
|
3874
|
+
const n = stack.pop();
|
|
3875
|
+
if (n.type === "type_identifier" && n.parent && n.parent.type !== "type_arguments") {
|
|
3876
|
+
out.push(n);
|
|
3877
|
+
} else if (n.type === "identifier" && n.parent && n.parent.type === "mixins") {
|
|
3878
|
+
out.push(n);
|
|
3879
|
+
}
|
|
3880
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3881
|
+
const c = n.namedChild(i);
|
|
3882
|
+
if (c) stack.push(c);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
return out;
|
|
3886
|
+
}
|
|
3887
|
+
findChildByType(node, type) {
|
|
3888
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3889
|
+
const child = node.namedChild(i);
|
|
3890
|
+
if (child && child.type === type) return child;
|
|
3891
|
+
}
|
|
3892
|
+
return null;
|
|
3893
|
+
}
|
|
3894
|
+
findDescendantByType(node, type) {
|
|
3895
|
+
const stack = [node];
|
|
3896
|
+
while (stack.length > 0) {
|
|
3897
|
+
const n = stack.pop();
|
|
3898
|
+
if (n.type === type) return n;
|
|
3899
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3900
|
+
const c = n.namedChild(i);
|
|
3901
|
+
if (c) stack.push(c);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
return null;
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* M60 (#929 lineage, dart lane): the line a relationship CLAIMS is the line
|
|
3908
|
+
* of the node that NAMES its target — the base clause for extends/implements,
|
|
3909
|
+
* the declared member for contains, the callee for calls, the constructed
|
|
3910
|
+
* class for instantiates, the overriding method for overrides, the uri
|
|
3911
|
+
* literal for an import/export/part, the redirect target for a factory
|
|
3912
|
+
* redirection. No node means NO field (absent is honest; `0` is the M59
|
|
3913
|
+
* fabrication).
|
|
3914
|
+
*/
|
|
3915
|
+
nodeLine(node) {
|
|
3916
|
+
if (!node) return void 0;
|
|
3917
|
+
return node.startPosition.row + 1;
|
|
3918
|
+
}
|
|
3919
|
+
nodeToRange(node) {
|
|
3920
|
+
return {
|
|
3921
|
+
startLine: node.startPosition.row + 1,
|
|
3922
|
+
startColumn: node.startPosition.column + 1,
|
|
3923
|
+
endLine: node.endPosition.row + 1,
|
|
3924
|
+
endColumn: node.endPosition.column + 1,
|
|
3925
|
+
startIndex: node.startIndex,
|
|
3926
|
+
endIndex: node.endIndex
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
};
|
|
3930
|
+
function dartNameLineKey(name, range) {
|
|
3931
|
+
return `${name}|${range?.startIndex ?? -1}|${range?.endIndex ?? -1}`;
|
|
3932
|
+
}
|
|
3933
|
+
function scopeOwnerKind(scopes) {
|
|
3934
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
3935
|
+
if (scopes[i].kind === "class" /* Class */) {
|
|
3936
|
+
return ASTEngine.SymbolKind.Class;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
return void 0;
|
|
3940
|
+
}
|
|
3941
|
+
export {
|
|
3942
|
+
DART_CITATION_ROOTS,
|
|
3943
|
+
RealDartAnalyzer
|
|
3944
|
+
};
|