@backbencher/brain 0.0.0-stage → 0.1.1
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-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -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-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -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-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.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-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.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,3145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import "./chunk-KAET47ZL.js";
|
|
8
|
+
import "./chunk-KCDECRJY.js";
|
|
9
|
+
import "./chunk-MLKGABMK.js";
|
|
10
|
+
|
|
11
|
+
// src/ast/real-vb-analyzer.ts
|
|
12
|
+
var { SymbolKind, RelationshipType } = ASTEngine;
|
|
13
|
+
var IS_IMPORT = "isImport";
|
|
14
|
+
var INCLUDE_DEPENDENCY = "include_dependency";
|
|
15
|
+
var VB_BUILTIN_TYPE_NAMES = [
|
|
16
|
+
"Boolean",
|
|
17
|
+
"Byte",
|
|
18
|
+
"SByte",
|
|
19
|
+
"Char",
|
|
20
|
+
"Date",
|
|
21
|
+
"Decimal",
|
|
22
|
+
"Double",
|
|
23
|
+
"Integer",
|
|
24
|
+
"Long",
|
|
25
|
+
"Object",
|
|
26
|
+
"Short",
|
|
27
|
+
"Single",
|
|
28
|
+
"String",
|
|
29
|
+
"UInteger",
|
|
30
|
+
"ULong",
|
|
31
|
+
"UShort"
|
|
32
|
+
];
|
|
33
|
+
var VB_BUILTIN_TYPE_CANONICAL = new Map(
|
|
34
|
+
VB_BUILTIN_TYPE_NAMES.map((n) => [n.toLowerCase(), n])
|
|
35
|
+
);
|
|
36
|
+
var WHITESPACE_CHARS = /* @__PURE__ */ new Set([" ", " ", "\n", "\r", "\f", "\v", "\xA0", "\u2028", "\u2029", "\uFEFF"]);
|
|
37
|
+
function stripWhitespace(text) {
|
|
38
|
+
let out = "";
|
|
39
|
+
for (const ch of text) if (!WHITESPACE_CHARS.has(ch)) out += ch;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
var RealVbAnalyzer = class _RealVbAnalyzer {
|
|
43
|
+
// Populated per analyze() call. The engine constructs one analyzer and reuses
|
|
44
|
+
// it across files, so nothing here may survive a call — see reset().
|
|
45
|
+
symbols = [];
|
|
46
|
+
relationships = [];
|
|
47
|
+
imports = [];
|
|
48
|
+
/** name -> the FIRST symbol declared under that name. */
|
|
49
|
+
index = /* @__PURE__ */ new Map();
|
|
50
|
+
/** Every name declared in THIS file, for same-file edge confidence. */
|
|
51
|
+
declaredNames = /* @__PURE__ */ new Set();
|
|
52
|
+
/** `<declaration>\u0000<attributeLeaf>\u0000<line>` — one `decorates` edge per attribute
|
|
53
|
+
* OCCURRENCE, so a node visited twice (a declared symbol and its declarator) cannot
|
|
54
|
+
* publish the same decoration twice. */
|
|
55
|
+
attributeEdgeKeys = /* @__PURE__ */ new Set();
|
|
56
|
+
/** The declaration chain we are inside, innermost last. */
|
|
57
|
+
ownerStack = [];
|
|
58
|
+
/** Scopes that take part in name resolution: namespaces and types. */
|
|
59
|
+
typeStack = [];
|
|
60
|
+
/** Member names of each type in this file, for unqualified call targets. */
|
|
61
|
+
memberNames = /* @__PURE__ */ new Map();
|
|
62
|
+
/**
|
|
63
|
+
* Names this file declares CALLABLE — a method, a property, a constructor, an
|
|
64
|
+
* operator or a delegate — and therefore names that cannot be an array index.
|
|
65
|
+
*
|
|
66
|
+
* This set exists because a VB call WITH ARGUMENTS is an `array_access_expression`
|
|
67
|
+
* in this grammar, so "is `Foo(1)` a call or an index?" has no syntactic answer
|
|
68
|
+
* and the only non-fabricating answer is: it is a call if `Foo` is something
|
|
69
|
+
* this file declares it could call. See `collectBodyEdges`.
|
|
70
|
+
*/
|
|
71
|
+
callablesInFile = /* @__PURE__ */ new Set();
|
|
72
|
+
/**
|
|
73
|
+
* Names this file declares as a VARIABLE or FIELD, used to keep a statement-level
|
|
74
|
+
* `arr(0)` from being published as a call. A local array is the one index shape
|
|
75
|
+
* that also looks like a call statement, and this is what excludes it.
|
|
76
|
+
*/
|
|
77
|
+
variablesInFile = /* @__PURE__ */ new Set();
|
|
78
|
+
/**
|
|
79
|
+
* Callables whose DECLARED TYPE is an array (`As T()`), because VB lets you index
|
|
80
|
+
* the result of a call: `SomePropertyReturningAnArray(0)` is legal and is an
|
|
81
|
+
* index, not a call. Read off the declaration's own `as_clause` -> `array_type`.
|
|
82
|
+
*/
|
|
83
|
+
arrayValuedInFile = /* @__PURE__ */ new Set();
|
|
84
|
+
/**
|
|
85
|
+
* Names bound by a LOCAL declaration. See `precollectDeclarations` for why a local
|
|
86
|
+
* has to be excluded from the statement-level call signal: `Dim s3() As Decimal`
|
|
87
|
+
* then a bare `s3(4)` is an index, and publishing it as a call is fabrication.
|
|
88
|
+
*/
|
|
89
|
+
localsInFile = /* @__PURE__ */ new Set();
|
|
90
|
+
/**
|
|
91
|
+
* `(source -> target)` pairs already published as `uses`, holding the EARLIEST
|
|
92
|
+
* site line seen for each — for the per-file dedup. A Map rather than a Set
|
|
93
|
+
* because "earliest" has to be compared, not just recorded.
|
|
94
|
+
*/
|
|
95
|
+
usesEmitted = /* @__PURE__ */ new Map();
|
|
96
|
+
/**
|
|
97
|
+
* Node ids of the CALLEE EXPRESSIONS this walk published a `calls` edge for.
|
|
98
|
+
*
|
|
99
|
+
* `uses` is defined as a value read that is NEITHER A CALL NOR A WRITE, so a
|
|
100
|
+
* member access this walk already called must not also be published as a read —
|
|
101
|
+
* otherwise one site carries two labels and the `calls` tally and the `uses`
|
|
102
|
+
* tally each count rows the other one already has.
|
|
103
|
+
*
|
|
104
|
+
* Why the ids and not a re-derived climb: the exclusion used to re-derive
|
|
105
|
+
* callee position by walking UP the tree, and the climb cannot see this grammar's
|
|
106
|
+
* argument-bearing call, because there is no invocation node to climb to.
|
|
107
|
+
*
|
|
108
|
+
* obj.M() -> invocation_expression(function: member_access) climb works
|
|
109
|
+
* obj.M(3) -> array_access_expression climb stops here
|
|
110
|
+
* member_access_expression
|
|
111
|
+
* return_statement
|
|
112
|
+
*
|
|
113
|
+
* So `obj.M(3)` was published as BOTH `calls -> M` and `uses -> M`. Measured over
|
|
114
|
+
* 400 files of roslyn-vb: 1,816 `uses` edges across 180 files carried both labels,
|
|
115
|
+
* 11.16% of the 16,276 `uses` edges in that sample (`script/m14/vb-uses-call-overlap.ts`).
|
|
116
|
+
*
|
|
117
|
+
* Recording the id where the decision is MADE is also the direction that cannot
|
|
118
|
+
* recur: a second copy of the callee test is a second thing to get wrong, and it
|
|
119
|
+
* had already been wrong once. Ordering is not a hazard — `collectBodyEdges`
|
|
120
|
+
* descends top-down, so a member access is always reached after its own enclosing
|
|
121
|
+
* call node.
|
|
122
|
+
*/
|
|
123
|
+
callCalleeNodes = /* @__PURE__ */ new Set();
|
|
124
|
+
/** Counters the lane discloses rather than hides. */
|
|
125
|
+
gaps = {};
|
|
126
|
+
/** `(source, target)` of every published `uses` edge, with its site count. */
|
|
127
|
+
useIndex = /* @__PURE__ */ new Map();
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
// Node-type and field vocabulary
|
|
130
|
+
//
|
|
131
|
+
// Read off the pinned wasm, not off a grammar tutorial. Kept as Sets so a test
|
|
132
|
+
// is membership, not a pattern match, and so an unclassified type is a MISS
|
|
133
|
+
// from the set rather than a silent fallthrough.
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
/** Grammar rules that declare a named type. Anything in the corpus that is not
|
|
136
|
+
* in here and not in SILENT_INTERNAL is a gap, and the census counts it. */
|
|
137
|
+
static DECLARATION_NODES = /* @__PURE__ */ new Set([
|
|
138
|
+
"namespace_declaration",
|
|
139
|
+
"class_declaration",
|
|
140
|
+
"structure_declaration",
|
|
141
|
+
"interface_declaration",
|
|
142
|
+
"module_declaration",
|
|
143
|
+
"enum_declaration",
|
|
144
|
+
// An enum member is a declaration, and `visitEnum` reads it directly rather
|
|
145
|
+
// than dispatching it, so it was never in this set. The behaviour was right
|
|
146
|
+
// and the classification was absent, which is precisely the state a census
|
|
147
|
+
// exists to make visible rather than to fix.
|
|
148
|
+
"enum_member_declaration",
|
|
149
|
+
"delegate_declaration",
|
|
150
|
+
"method_declaration",
|
|
151
|
+
"constructor_declaration",
|
|
152
|
+
"property_declaration",
|
|
153
|
+
"event_declaration",
|
|
154
|
+
"operator_declaration",
|
|
155
|
+
"declare_statement",
|
|
156
|
+
"field_declaration",
|
|
157
|
+
"imports_statement",
|
|
158
|
+
// An INTERFACE's members are declarations, and for a long time they were in
|
|
159
|
+
// neither this set nor SILENT_INTERNAL, which is the whole defect: nothing
|
|
160
|
+
// visited them, so an interface was published as an EMPTY SHELL. `IShape`
|
|
161
|
+
// appeared with a name and no members, while the class implementing it had
|
|
162
|
+
// all three. Found by the F8 node-type census, which found 104 unclassified
|
|
163
|
+
// node types against 42 classified -- the "every construct is accounted for"
|
|
164
|
+
// claim was 29% true and nothing had ever measured it.
|
|
165
|
+
//
|
|
166
|
+
// In an interface-driven codebase this is not a rare shape. DISMTools and
|
|
167
|
+
// P3D are both Windows Forms applications where the designer emits
|
|
168
|
+
// interfaces for every dialog, so a large fraction of the corpus's types had
|
|
169
|
+
// invisible members.
|
|
170
|
+
"interface_method_declaration",
|
|
171
|
+
"interface_property_declaration",
|
|
172
|
+
"interface_event_declaration"
|
|
173
|
+
]);
|
|
174
|
+
/**
|
|
175
|
+
* The node types that are STRUCTURE inside a type body rather than code in it:
|
|
176
|
+
* the declaration's own attributes and modifiers, its heritage clauses, its type
|
|
177
|
+
* parameter list and its name. Everything else that is not a declaration and
|
|
178
|
+
* appears directly in a type body is code that type executes, and is where a body
|
|
179
|
+
* edge comes from. See `dispatchBody`.
|
|
180
|
+
*/
|
|
181
|
+
static TYPE_BODY_STRUCTURE = /* @__PURE__ */ new Set([
|
|
182
|
+
"attribute_list",
|
|
183
|
+
"attribute",
|
|
184
|
+
"attribute_target",
|
|
185
|
+
"attribute_argument_list",
|
|
186
|
+
"named_argument",
|
|
187
|
+
"member_modifier",
|
|
188
|
+
"modifiers",
|
|
189
|
+
"local_declaration_modifier",
|
|
190
|
+
"inherits_statement",
|
|
191
|
+
"implements_statement",
|
|
192
|
+
"inherits_member_clause",
|
|
193
|
+
"implements_member_clause",
|
|
194
|
+
"type_parameter_list",
|
|
195
|
+
"type_parameter",
|
|
196
|
+
"type_parameter_constraints_clause",
|
|
197
|
+
"parameter_list",
|
|
198
|
+
"parameter",
|
|
199
|
+
"as_clause",
|
|
200
|
+
"identifier",
|
|
201
|
+
"simple_name",
|
|
202
|
+
"comment",
|
|
203
|
+
"preprocessor_directive",
|
|
204
|
+
"blank_line",
|
|
205
|
+
"option_statement",
|
|
206
|
+
"option_statements",
|
|
207
|
+
"global_attribute_section",
|
|
208
|
+
"end_statement",
|
|
209
|
+
"implements_clause",
|
|
210
|
+
"inherits_clause"
|
|
211
|
+
]);
|
|
212
|
+
/** Nodes the grammar produces that carry no symbol of their own: they are
|
|
213
|
+
* containers, modifiers, or the parts of a declaration already read. They are
|
|
214
|
+
* classified here so the spec's "every node type is classified" rule is
|
|
215
|
+
* satisfied by this list rather than by an omission. */
|
|
216
|
+
// A node type that publishes NO SYMBOL but DOES derive a relationship. This set
|
|
217
|
+
// exists because the census it feeds found two whole kinds of defect that a
|
|
218
|
+
// two-way classification could not express, and both were invisible:
|
|
219
|
+
//
|
|
220
|
+
// - `array_creation_expression` sat in NEITHER set, which is what let the
|
|
221
|
+
// analyzer read a construction's callee from a `function` field the node
|
|
222
|
+
// does not have. 15,577 `New Foo(a, b)` calls went unpublished.
|
|
223
|
+
// - `implements_member_clause` sat in SILENT_INTERNAL, which is a claim
|
|
224
|
+
// that the node carries nothing. It carries a relationship, and the
|
|
225
|
+
// member-level heritage form was therefore uncollected on every member in
|
|
226
|
+
// the language.
|
|
227
|
+
//
|
|
228
|
+
// "Classified" is not the same as "handled", and the difference between those
|
|
229
|
+
// two words is what a two-way split threw away.
|
|
230
|
+
static RELATIONSHIP_SOURCES = /* @__PURE__ */ new Set([
|
|
231
|
+
"invocation_expression",
|
|
232
|
+
"object_creation_expression",
|
|
233
|
+
"array_creation_expression",
|
|
234
|
+
"generic_invocation_expression",
|
|
235
|
+
"implements_member_clause",
|
|
236
|
+
"inherits_member_clause",
|
|
237
|
+
"inherits_clause",
|
|
238
|
+
"implements_clause",
|
|
239
|
+
"inherits_statement",
|
|
240
|
+
"implements_statement",
|
|
241
|
+
"operator_declaration",
|
|
242
|
+
"constructor_declaration",
|
|
243
|
+
// M60/VB2. These three publish NO SYMBOL but DO derive a relationship, and
|
|
244
|
+
// leaving them in SILENT_INTERNAL is a claim that they carry nothing --
|
|
245
|
+
// which is the claim that hid the `uses` class for a whole wave.
|
|
246
|
+
// `member_access_expression` / `with_member_access_expression`: a name
|
|
247
|
+
// READ that is neither the callee nor the index is a `uses` edge.
|
|
248
|
+
// `as_clause`: an `as_clause` whose span begins `As New` is a
|
|
249
|
+
// CONSTRUCTION, and the fourth of VB's four construction surfaces.
|
|
250
|
+
// The two-way split is what the four-way classification exists to avoid, and
|
|
251
|
+
// `array_creation_expression` was already moved here for the same reason.
|
|
252
|
+
"member_access_expression",
|
|
253
|
+
"with_member_access_expression",
|
|
254
|
+
"as_clause"
|
|
255
|
+
]);
|
|
256
|
+
// Classified as carrying nothing today, but a KNOWN candidate: the construct
|
|
257
|
+
// does name or bind something, and saying so here is what keeps "silent" from
|
|
258
|
+
// quietly meaning "nobody has looked". Each is a real relationship or symbol a
|
|
259
|
+
// future wave could derive; none is claimed to be derived now.
|
|
260
|
+
static RELATIONSHIP_CANDIDATES = /* @__PURE__ */ new Set([
|
|
261
|
+
// `: base(...)` / `: Me(...)` -- names the constructor being chained to. A
|
|
262
|
+
// `calls` edge, and the one call form this analyzer does not yet read.
|
|
263
|
+
"constructor_initializer",
|
|
264
|
+
// `AddHandler o.E, AddressOf h` -- an event binding between two names.
|
|
265
|
+
"add_handler_statement",
|
|
266
|
+
"remove_handler_statement",
|
|
267
|
+
// A lambda is a callable value: `AddressOf`, or an argument to `For Each`.
|
|
268
|
+
"lambda_expression",
|
|
269
|
+
// A property's accessors are members of the property.
|
|
270
|
+
"property_getter",
|
|
271
|
+
"property_setter",
|
|
272
|
+
// `RaiseEvent E` names the event being raised.
|
|
273
|
+
"raiseevent_statement",
|
|
274
|
+
// `Handles Me.Startup` -- the `Object.Event` half of a Handles clause, which
|
|
275
|
+
// names the event a handler is wired to. 2,602 in the corpus and a real
|
|
276
|
+
// `references` edge to each, so it is a candidate and not silence.
|
|
277
|
+
"event_member"
|
|
278
|
+
]);
|
|
279
|
+
static SILENT_INTERNAL = /* @__PURE__ */ new Set([
|
|
280
|
+
"source_file",
|
|
281
|
+
"identifier",
|
|
282
|
+
"comment",
|
|
283
|
+
"preprocessor_directive",
|
|
284
|
+
"blank_line",
|
|
285
|
+
"type_parameter_list",
|
|
286
|
+
"type_parameter",
|
|
287
|
+
"parameter_list",
|
|
288
|
+
"parameter",
|
|
289
|
+
"modifiers",
|
|
290
|
+
"member_modifier",
|
|
291
|
+
"attribute_list",
|
|
292
|
+
"attribute",
|
|
293
|
+
"attribute_target",
|
|
294
|
+
"attribute_argument_list",
|
|
295
|
+
"named_argument",
|
|
296
|
+
"handles_clause",
|
|
297
|
+
"variable_declarator",
|
|
298
|
+
"array_rank_specifier",
|
|
299
|
+
"array_initializer",
|
|
300
|
+
"collection_initializer",
|
|
301
|
+
"dictionary_initializer",
|
|
302
|
+
"dictionary_element",
|
|
303
|
+
"object_initializer",
|
|
304
|
+
"member_initializer",
|
|
305
|
+
// The types below were UNCLASSIFIED when the F8 census first ran -- 101 of
|
|
306
|
+
// them, against 45 classified. None of them declares a symbol and none of
|
|
307
|
+
// them names a relationship this analyzer derives; the list is here so that
|
|
308
|
+
// "every construct in the corpus is accounted for" is a checked statement
|
|
309
|
+
// rather than an assumption, and so a new grammar node shows up as a FAILING
|
|
310
|
+
// census instead of as silence.
|
|
311
|
+
//
|
|
312
|
+
// Literals: a literal names nothing, so there is no symbol and no target.
|
|
313
|
+
"boolean_literal",
|
|
314
|
+
"character_literal",
|
|
315
|
+
"floating_point_literal",
|
|
316
|
+
"integer_literal",
|
|
317
|
+
"string_literal",
|
|
318
|
+
"nothing_literal",
|
|
319
|
+
// Name and type syntax: read by whichever construct owns it; declares nothing.
|
|
320
|
+
"predefined_type",
|
|
321
|
+
"simple_name",
|
|
322
|
+
"qualified_name",
|
|
323
|
+
"global_qualified_name",
|
|
324
|
+
"generic_name",
|
|
325
|
+
"type_argument_list",
|
|
326
|
+
"array_type",
|
|
327
|
+
"nullable_type",
|
|
328
|
+
"tuple_type",
|
|
329
|
+
"tuple_element",
|
|
330
|
+
"type_declaration_in_type",
|
|
331
|
+
"type_parameter_constraint",
|
|
332
|
+
"type_parameter_constraints_clause",
|
|
333
|
+
// Expressions: their names are read by the constructs above; they are not
|
|
334
|
+
// structure. (The construction and invocation EXPRESSIONS are relationship
|
|
335
|
+
// SOURCES and are classified as such, which is the distinction that matters.)
|
|
336
|
+
"argument_list",
|
|
337
|
+
"array_access_expression",
|
|
338
|
+
"assignment_expression",
|
|
339
|
+
"binary_expression",
|
|
340
|
+
"cast_expression",
|
|
341
|
+
"conditional_expression",
|
|
342
|
+
"if_expression",
|
|
343
|
+
"me_expression",
|
|
344
|
+
"mybase_expression",
|
|
345
|
+
"parenthesized_expression",
|
|
346
|
+
"unary_expression",
|
|
347
|
+
"typeof_expression",
|
|
348
|
+
"typeof_is_expression",
|
|
349
|
+
"with_expression",
|
|
350
|
+
"lambda_parameter",
|
|
351
|
+
"lambda_parameter_list",
|
|
352
|
+
"omitted_argument",
|
|
353
|
+
"collection_range_variable",
|
|
354
|
+
// Control flow. The contract is a project SYMBOL TABLE: a `While` is not a
|
|
355
|
+
// symbol, and the family does not publish locals or control-flow statements.
|
|
356
|
+
"assignment_statement",
|
|
357
|
+
"declaration_statement",
|
|
358
|
+
"expression_statement",
|
|
359
|
+
"if_statement",
|
|
360
|
+
"elseif_clause",
|
|
361
|
+
"else_clause",
|
|
362
|
+
"case_clause",
|
|
363
|
+
"case_else_clause",
|
|
364
|
+
"case_value",
|
|
365
|
+
"select_statement",
|
|
366
|
+
"select_clause",
|
|
367
|
+
"select_element",
|
|
368
|
+
"select_or_group_clause",
|
|
369
|
+
"for_statement",
|
|
370
|
+
"for_each_statement",
|
|
371
|
+
"while_statement",
|
|
372
|
+
"do_statement",
|
|
373
|
+
"exit_statement",
|
|
374
|
+
"continue_statement",
|
|
375
|
+
"goto_statement",
|
|
376
|
+
"labeled_statement",
|
|
377
|
+
"return_statement",
|
|
378
|
+
"throw_statement",
|
|
379
|
+
"try_statement",
|
|
380
|
+
"catch_block",
|
|
381
|
+
"finally_block",
|
|
382
|
+
"with_statement",
|
|
383
|
+
"using_statement",
|
|
384
|
+
"synclock_statement",
|
|
385
|
+
"on_error_statement",
|
|
386
|
+
"call_statement",
|
|
387
|
+
"stop_statement",
|
|
388
|
+
"redim_statement",
|
|
389
|
+
"redim_clause",
|
|
390
|
+
"yield_statement",
|
|
391
|
+
"await_expression",
|
|
392
|
+
"empty_statement",
|
|
393
|
+
"end_statement",
|
|
394
|
+
// Modifiers, attributes, options.
|
|
395
|
+
"local_declaration_modifier",
|
|
396
|
+
"global_attribute_section",
|
|
397
|
+
"option_statement",
|
|
398
|
+
"option_statements",
|
|
399
|
+
// LINQ query syntax, which this grammar surfaces as clauses rather than
|
|
400
|
+
// structure.
|
|
401
|
+
"query_expression",
|
|
402
|
+
"query_body_clause",
|
|
403
|
+
"from_clause",
|
|
404
|
+
"order_by_clause",
|
|
405
|
+
"ordering",
|
|
406
|
+
"where_clause"
|
|
407
|
+
]);
|
|
408
|
+
// -------------------------------------------------------------------------
|
|
409
|
+
// Small AST helpers. Every one of these is a node-type or field test; there is
|
|
410
|
+
// no text matching in this file at all.
|
|
411
|
+
// -------------------------------------------------------------------------
|
|
412
|
+
/**
|
|
413
|
+
* The `index`-th NAMED child, or null when there is none.
|
|
414
|
+
*
|
|
415
|
+
* `web-tree-sitter` 0.25 types this `Node | null`, and it really can be null at
|
|
416
|
+
* the boundary, so every read goes through here rather than through a `!`. The
|
|
417
|
+
* alternative — asserting non-null at each of the ~40 call sites — would make
|
|
418
|
+
* "this index is in range" an unstated assumption repeated 40 times, which is
|
|
419
|
+
* the shape a crash takes when a grammar gains a child.
|
|
420
|
+
*/
|
|
421
|
+
nc(node, index) {
|
|
422
|
+
if (index < 0 || index >= node.namedChildCount) return null;
|
|
423
|
+
return node.namedChild(index);
|
|
424
|
+
}
|
|
425
|
+
/** The `index`-th child of ANY kind (named or anonymous token). */
|
|
426
|
+
ac(node, index) {
|
|
427
|
+
if (index < 0 || index >= node.childCount) return null;
|
|
428
|
+
return node.child(index);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The node's NAMED children, with the nulls dropped.
|
|
432
|
+
*
|
|
433
|
+
* `web-tree-sitter` 0.25 types `namedChild(i)` as `Node | null` and
|
|
434
|
+
* `namedChildren` as `(Node | null)[]`. A `!` at each of the ~20 read sites
|
|
435
|
+
* would turn "this index is in range" into an unstated assumption repeated 20
|
|
436
|
+
* times, and a throw would trade a type error for a crash -- the lane's
|
|
437
|
+
* measured state is zero crash files and that is worth keeping. So the null is
|
|
438
|
+
* handled once, here, and every loop downstream iterates a real `Node[]`.
|
|
439
|
+
*/
|
|
440
|
+
kids(node) {
|
|
441
|
+
const out = [];
|
|
442
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
443
|
+
const c = node.namedChild(i);
|
|
444
|
+
if (c) out.push(c);
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
/** The node's children of ANY kind, with the nulls dropped. */
|
|
449
|
+
allKids(node) {
|
|
450
|
+
const out = [];
|
|
451
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
452
|
+
const c = node.child(i);
|
|
453
|
+
if (c) out.push(c);
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The number of lines in `content`, counted with a character scan.
|
|
459
|
+
*
|
|
460
|
+
* This was `content.split(/\r?\n/).length`, and it was the ONE regex literal
|
|
461
|
+
* in this file — which made the header's "zero regex" claim false as written.
|
|
462
|
+
* Rather than soften the claim, the regex went.
|
|
463
|
+
*
|
|
464
|
+
* The scan counts a `\n` as a line break and treats a preceding `\r` as part of
|
|
465
|
+
* that break rather than as a character of its own, which is exactly what
|
|
466
|
+
* `\r?\n` meant. Equivalence was MEASURED, not argued: over 598 real `.vb`
|
|
467
|
+
* files from two corpora plus five edge cases, the two agree everywhere except
|
|
468
|
+
* the empty string, where the split form returns 1 and this returns 0 — and the
|
|
469
|
+
* old code's own `content.length === 0 ? 0 : …` guard already forced 0 there.
|
|
470
|
+
* (`script/m14/tmp/`-style probe: the fixture corpus (`probes/linecount-equiv.ts`).)
|
|
471
|
+
*/
|
|
472
|
+
lineCount(content) {
|
|
473
|
+
if (content.length === 0) return 0;
|
|
474
|
+
let lines = 1;
|
|
475
|
+
for (let i = 0; i < content.length; i++) {
|
|
476
|
+
if (content.charCodeAt(i) === 10) lines++;
|
|
477
|
+
}
|
|
478
|
+
return lines;
|
|
479
|
+
}
|
|
480
|
+
/** 1-based line of a node, or null when there is no node. M59.A7. */
|
|
481
|
+
nodeLine(node) {
|
|
482
|
+
return node ? node.startPosition.row + 1 : null;
|
|
483
|
+
}
|
|
484
|
+
range(node) {
|
|
485
|
+
return {
|
|
486
|
+
startLine: node.startPosition.row + 1,
|
|
487
|
+
startColumn: node.startPosition.column + 1,
|
|
488
|
+
endLine: node.endPosition.row + 1,
|
|
489
|
+
endColumn: node.endPosition.column + 1,
|
|
490
|
+
startIndex: node.startIndex,
|
|
491
|
+
endIndex: node.endIndex
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The node that NAMES a thing, descended to the last segment's own leaf.
|
|
496
|
+
*
|
|
497
|
+
* `qualified_name` spans `A.B.C` but the name a reader navigates to is written
|
|
498
|
+
* on the `C` leaf. A wrapped name — an alias, a qualified name inside a
|
|
499
|
+
* heritage clause — must therefore be stamped with ITS leaf's line, not the
|
|
500
|
+
* line the name starts on. This is the C# lane's `nodeNamingToken`, and it
|
|
501
|
+
* exists because a relationship stamped with the wrapper's line navigates to
|
|
502
|
+
* the wrong place in a file that mentions the same type on three lines.
|
|
503
|
+
*/
|
|
504
|
+
namingToken(node, text) {
|
|
505
|
+
if (!node) return null;
|
|
506
|
+
let cur = node;
|
|
507
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
508
|
+
const named = cur.childForFieldName("name");
|
|
509
|
+
if (named && named.id !== cur.id && named.type !== "identifier") {
|
|
510
|
+
cur = named;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (cur.type === "identifier") return cur;
|
|
514
|
+
if (cur.type === "simple_name") {
|
|
515
|
+
const inner = this.nc(cur, 0);
|
|
516
|
+
if (inner && inner.type === "identifier") return inner;
|
|
517
|
+
return cur;
|
|
518
|
+
}
|
|
519
|
+
if (cur.type === "nullable_type") {
|
|
520
|
+
const inner = this.nc(cur, 0);
|
|
521
|
+
if (inner && inner.id !== cur.id) {
|
|
522
|
+
cur = inner;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (cur.type === "array_type") {
|
|
527
|
+
const el = cur.childForFieldName("element_type");
|
|
528
|
+
if (el && el.id !== cur.id) {
|
|
529
|
+
cur = el;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (cur.type === "generic_name") {
|
|
534
|
+
const inner = this.nc(cur, 0);
|
|
535
|
+
if (inner) {
|
|
536
|
+
cur = inner;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
return cur;
|
|
540
|
+
}
|
|
541
|
+
if (cur.type === "qualified_name" || cur.type === "global_qualified_name") {
|
|
542
|
+
const inner = cur.childForFieldName("name");
|
|
543
|
+
if (inner && inner.id !== cur.id) {
|
|
544
|
+
cur = inner;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
return cur;
|
|
548
|
+
}
|
|
549
|
+
return cur;
|
|
550
|
+
}
|
|
551
|
+
return cur;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The leaf that NAMES a callee — the target a reader navigates to.
|
|
555
|
+
*
|
|
556
|
+
* The rule is the LAST `identifier` leaf at or under the callee node, and it
|
|
557
|
+
* has to be that rather than the node's own text, because VB spells a callee
|
|
558
|
+
* in three shapes that all reduce to one name:
|
|
559
|
+
*
|
|
560
|
+
* `EntityID.ToLower()` -> `ToLower` (a `member_access_expression`)
|
|
561
|
+
* `New AnimatedBlock()` -> `AnimatedBlock`
|
|
562
|
+
* `CType(x, W).Init()` -> `Init`
|
|
563
|
+
*
|
|
564
|
+
* The construction case was the one that was broken, and it was broken for a
|
|
565
|
+
* specific, findable reason: `New X()` parses as
|
|
566
|
+
* `(object_creation_expression type: (simple_name (identifier)))` — the type is
|
|
567
|
+
* in a `type` FIELD, and `namingToken` only ever followed a `name` field, so it
|
|
568
|
+
* fell through and returned the whole construction node. The published target
|
|
569
|
+
* was the literal string `"New AnimatedBlock"`: not a name in the file, not
|
|
570
|
+
* navigable, and unmatchable by any independent oracle. Measured on P3D's
|
|
571
|
+
* Entity.vb — 31 of them in one method.
|
|
572
|
+
*
|
|
573
|
+
* A `type_argument_list` is skipped, so `New List(Of Integer)` is `List` and not
|
|
574
|
+
* `Integer`: the argument is not what is being constructed. That is the same
|
|
575
|
+
* exclusion the heritage path and the F5 oracle both apply, so the three agree
|
|
576
|
+
* on one definition of "the name of this type expression".
|
|
577
|
+
*/
|
|
578
|
+
calleeLeaf(fn) {
|
|
579
|
+
const leaves = [];
|
|
580
|
+
const walk = (n, depth) => {
|
|
581
|
+
if (depth > 64) return;
|
|
582
|
+
if (n.type === "type_argument_list") return;
|
|
583
|
+
if (n.type === "identifier") {
|
|
584
|
+
leaves.push(n);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
588
|
+
const c = n.child(i);
|
|
589
|
+
if (c) walk(c, depth + 1);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
walk(fn, 0);
|
|
593
|
+
return leaves.length > 0 ? leaves[leaves.length - 1] : null;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The text of a name-bearing node, rebuilt from the tree.
|
|
597
|
+
*
|
|
598
|
+
* Char-scan, not regex: walk the node's named children and join the identifier
|
|
599
|
+
* leaves with the separator each wrapper contributes. `qualified_name` is
|
|
600
|
+
* `qualifier . name` in the grammar, so the dot is a CHILD TOKEN and is
|
|
601
|
+
* already in the node's text — reading `node.text` for the whole name and
|
|
602
|
+
* trimming is both simpler and more faithful than reassembling it.
|
|
603
|
+
*/
|
|
604
|
+
nameText(node) {
|
|
605
|
+
if (!node) return "";
|
|
606
|
+
return node.text.trim();
|
|
607
|
+
}
|
|
608
|
+
/** The `name` field of a declaration, or its first identifier child. */
|
|
609
|
+
declaredName(node) {
|
|
610
|
+
const field = node.childForFieldName("name");
|
|
611
|
+
if (field) {
|
|
612
|
+
const token = this.namingToken(field, field.text);
|
|
613
|
+
return { text: this.nameText(token ?? field), node: token ?? field };
|
|
614
|
+
}
|
|
615
|
+
for (const c of this.kids(node)) {
|
|
616
|
+
if (c.type === "identifier" || c.type === "simple_name") {
|
|
617
|
+
return { text: this.nameText(c), node: this.namingToken(c, c.text) ?? c };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { text: "", node: null };
|
|
621
|
+
}
|
|
622
|
+
namedChildrenOfType(node, type) {
|
|
623
|
+
const out = [];
|
|
624
|
+
for (const c of this.kids(node)) {
|
|
625
|
+
if (c.type === type) out.push(c);
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* VB visibility, read off the `member_modifier` list the grammar already
|
|
631
|
+
* separated out. A Set membership test per modifier, in the order VB resolves
|
|
632
|
+
* them, and the FIRST match wins — `Protected Friend` is `Protected` in VB's
|
|
633
|
+
* own spec ordering and getting that wrong changes access queries.
|
|
634
|
+
*/
|
|
635
|
+
static VISIBILITY_ORDER = [
|
|
636
|
+
["private", "private"],
|
|
637
|
+
["protected", "protected"],
|
|
638
|
+
["friend", "internal"],
|
|
639
|
+
["public", "public"]
|
|
640
|
+
];
|
|
641
|
+
/**
|
|
642
|
+
* The access levels ONE declaration actually grants, as a SET of level words.
|
|
643
|
+
*
|
|
644
|
+
* A SET and not one string, because VB has two COMBINED access modifiers and
|
|
645
|
+
* the combination is the meaning. `Protected Friend` is "protected OR friend",
|
|
646
|
+
* so it is reachable from another assembly precisely because of its friend
|
|
647
|
+
* half, while `Protected` alone is not. Reducing the pair to the single
|
|
648
|
+
* string `protected` before asking whether a declaration is exported answers
|
|
649
|
+
* `no` about a member the language says is assembly-visible, which is what an
|
|
650
|
+
* `exports` edge is for.
|
|
651
|
+
*/
|
|
652
|
+
accessLevelsOf(node) {
|
|
653
|
+
const levels = /* @__PURE__ */ new Set();
|
|
654
|
+
for (const m of this.modifierWordsOf(node)) {
|
|
655
|
+
if (_RealVbAnalyzer.ACCESS_WORDS.has(m)) levels.add(m);
|
|
656
|
+
}
|
|
657
|
+
return levels;
|
|
658
|
+
}
|
|
659
|
+
/** The four words VB spells an access level with. */
|
|
660
|
+
static ACCESS_WORDS = /* @__PURE__ */ new Set([
|
|
661
|
+
"public",
|
|
662
|
+
"protected",
|
|
663
|
+
"friend",
|
|
664
|
+
"private"
|
|
665
|
+
]);
|
|
666
|
+
/**
|
|
667
|
+
* The DEFAULT accessibility of a declaration that writes none.
|
|
668
|
+
*
|
|
669
|
+
* Taken from Microsoft's own table rather than from recollection, because the
|
|
670
|
+
* corpus exercises every row: the lane census counts 136 modifier-less
|
|
671
|
+
* members in a class, 20 in a module, 5 in a structure and 116 modifier-less
|
|
672
|
+
* types at namespace level (probes `exports:implicit_default:*` and
|
|
673
|
+
* `exports:type_implicit_default:*`), so a wrong answer here is wrong on real
|
|
674
|
+
* files rather than on a synthetic one. Source: "Declaration Contexts and
|
|
675
|
+
* Default Access Levels",
|
|
676
|
+
* learn.microsoft.com/dotnet/visual-basic/language-reference/statements/
|
|
677
|
+
* declaration-contexts-and-default-access-levels
|
|
678
|
+
*
|
|
679
|
+
* element namespace level | module level
|
|
680
|
+
* Variable / Constant not allowed | Private (Public in Structure)
|
|
681
|
+
* Class / Structure / Module / Interface / Friend | Public
|
|
682
|
+
* Enum / Delegate
|
|
683
|
+
* Procedure / Property / Event / Operator / not allowed | Public
|
|
684
|
+
* Declare
|
|
685
|
+
*
|
|
686
|
+
* The row that is easy to get wrong, and the one VB is famous for disagreeing
|
|
687
|
+
* with C# on, is Procedure/Property/Event: they default to PUBLIC. A
|
|
688
|
+
* `Sub M()` written without a modifier inside a Class is callable from another
|
|
689
|
+
* assembly. FIELDS and CONSTANTS are the ones that default to Private.
|
|
690
|
+
*
|
|
691
|
+
* `friend` at the namespace level is what makes a modifier-less `Class C`
|
|
692
|
+
* NOT an assembly export, which is the whole `exports` answer for the 116
|
|
693
|
+
* top-level types the corpus writes without a modifier.
|
|
694
|
+
*/
|
|
695
|
+
defaultAccessLevel(kind) {
|
|
696
|
+
if (kind === ASTEngine.SymbolKind.EnumMember) return "public";
|
|
697
|
+
const ownerKind = this.currentOwner()?.kind;
|
|
698
|
+
if (ownerKind === void 0 || ownerKind === ASTEngine.SymbolKind.Namespace) return "friend";
|
|
699
|
+
switch (kind) {
|
|
700
|
+
case ASTEngine.SymbolKind.Variable:
|
|
701
|
+
case ASTEngine.SymbolKind.Constant:
|
|
702
|
+
return ownerKind === ASTEngine.SymbolKind.Struct ? "public" : "private";
|
|
703
|
+
default:
|
|
704
|
+
return "public";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Does this set of access levels put the declaration in the assembly's public
|
|
709
|
+
* surface? The question `exports` exists to answer, asked on the levels
|
|
710
|
+
* rather than on the single `visibility` string, so `Protected Friend`
|
|
711
|
+
* (assembly-reachable through its friend half) is not confused with
|
|
712
|
+
* `Protected` (subclass-only).
|
|
713
|
+
*/
|
|
714
|
+
static isAssemblyVisible(levels) {
|
|
715
|
+
if (levels.has("public")) return true;
|
|
716
|
+
return levels.has("friend") && levels.has("protected");
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The single-string accessibility the symbol record carries.
|
|
720
|
+
*
|
|
721
|
+
* It applies the DOCUMENTED DEFAULT when the declaration writes none, rather
|
|
722
|
+
* than falling through to `internal`. That fall-through was a real published
|
|
723
|
+
* fact and it was wrong for every declaration that omits its modifier: VB's
|
|
724
|
+
* default for a Procedure / Property / Event is Public, not Private, so a
|
|
725
|
+
* modifier-less `Sub M()` was published `internal` and is in fact callable from
|
|
726
|
+
* another assembly. The corpus exercises this: 136 modifier-less members in a
|
|
727
|
+
* class and 20 in a module, of which the procedures, properties and events are
|
|
728
|
+
* Public by the language's own table.
|
|
729
|
+
*
|
|
730
|
+
* The set form is still available and is what `exports` asks, because the
|
|
731
|
+
* combined modifiers do not reduce to one word.
|
|
732
|
+
*/
|
|
733
|
+
visibilityOf(node, kind) {
|
|
734
|
+
const levels = this.accessLevelsOf(node);
|
|
735
|
+
for (const [mod, vis] of _RealVbAnalyzer.VISIBILITY_ORDER) {
|
|
736
|
+
if (levels.has(mod)) return vis;
|
|
737
|
+
}
|
|
738
|
+
return _RealVbAnalyzer.spellAccessLevel(this.defaultAccessLevel(kind ?? _RealVbAnalyzer.kindOfNode(node)));
|
|
739
|
+
}
|
|
740
|
+
/** The symbol kind a declaration node is, for the default table. */
|
|
741
|
+
static kindOfNode(node) {
|
|
742
|
+
switch (node.type) {
|
|
743
|
+
case "class_declaration":
|
|
744
|
+
return ASTEngine.SymbolKind.Class;
|
|
745
|
+
case "structure_declaration":
|
|
746
|
+
return ASTEngine.SymbolKind.Struct;
|
|
747
|
+
case "interface_declaration":
|
|
748
|
+
return ASTEngine.SymbolKind.Interface;
|
|
749
|
+
case "module_declaration":
|
|
750
|
+
return ASTEngine.SymbolKind.Module;
|
|
751
|
+
case "enum_declaration":
|
|
752
|
+
return ASTEngine.SymbolKind.Enum;
|
|
753
|
+
case "enum_member_declaration":
|
|
754
|
+
return ASTEngine.SymbolKind.EnumMember;
|
|
755
|
+
case "delegate_declaration":
|
|
756
|
+
return ASTEngine.SymbolKind.Type;
|
|
757
|
+
case "constructor_declaration":
|
|
758
|
+
return ASTEngine.SymbolKind.Constructor;
|
|
759
|
+
case "property_declaration":
|
|
760
|
+
case "interface_property_declaration":
|
|
761
|
+
return ASTEngine.SymbolKind.Property;
|
|
762
|
+
case "event_declaration":
|
|
763
|
+
case "interface_event_declaration":
|
|
764
|
+
return ASTEngine.SymbolKind.Event;
|
|
765
|
+
case "field_declaration":
|
|
766
|
+
case "variable_declarator":
|
|
767
|
+
return ASTEngine.SymbolKind.Variable;
|
|
768
|
+
default:
|
|
769
|
+
return ASTEngine.SymbolKind.Method;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/** The symbol-record spelling of an access-level word. */
|
|
773
|
+
static spellAccessLevel(level) {
|
|
774
|
+
return level === "friend" ? "internal" : level;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Every modifier word on a declaration, ONE ENTRY PER WORD.
|
|
778
|
+
*
|
|
779
|
+
* The split is load-bearing and it was absent. This grammar's
|
|
780
|
+
* `member_modifier` can hold a SEQUENCE of words: `Protected Friend Sub PF()`
|
|
781
|
+
* parses as ONE `member_modifier` node whose text is `Protected Friend`, so
|
|
782
|
+
* adding that node's text verbatim added the single token `"protected friend"`,
|
|
783
|
+
* which matches no entry in `VISIBILITY_ORDER` and falls through to
|
|
784
|
+
* `internal` -- a real declaration published with the wrong accessibility.
|
|
785
|
+
* Measured on the lane corpus: 12 occurrences of a multi-word
|
|
786
|
+
* `member_modifier`, every one of them `protected friend` (census probe
|
|
787
|
+
* `MULTIWORD_MODIFIER`, over 2,658 files).
|
|
788
|
+
*
|
|
789
|
+
* A word that is not a modifier this grammar's `VB_KEYWORD_HEADS` already
|
|
790
|
+
* names is DROPPED rather than admitted, so splitting cannot invent a
|
|
791
|
+
* modifier the file does not write.
|
|
792
|
+
*/
|
|
793
|
+
modifierWordsOf(node) {
|
|
794
|
+
const out = /* @__PURE__ */ new Set();
|
|
795
|
+
const addText = (text) => {
|
|
796
|
+
let start = 0;
|
|
797
|
+
const flush = (end) => {
|
|
798
|
+
if (end <= start) return;
|
|
799
|
+
const word = text.slice(start, end).toLowerCase();
|
|
800
|
+
if (_RealVbAnalyzer.VB_KEYWORD_HEADS.has(word)) out.add(word);
|
|
801
|
+
start = end + 1;
|
|
802
|
+
};
|
|
803
|
+
for (let i = 0; i < text.length; i++) {
|
|
804
|
+
const c = text.charCodeAt(i);
|
|
805
|
+
if (c === 32 || c === 9 || c === 13 || c === 10) flush(i);
|
|
806
|
+
}
|
|
807
|
+
flush(text.length);
|
|
808
|
+
};
|
|
809
|
+
const collect = (n, depth) => {
|
|
810
|
+
if (depth > 4) return;
|
|
811
|
+
for (const c of this.allKids(n)) {
|
|
812
|
+
if (c.type === "member_modifier" || c.type === "local_declaration_modifier") {
|
|
813
|
+
addText(c.text);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (_RealVbAnalyzer.DECLARATION_NODES.has(c.type)) continue;
|
|
817
|
+
collect(c, depth + 1);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
collect(node, 0);
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
modifierNames(node) {
|
|
824
|
+
return this.modifierWordsOf(node);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* The `overrides` edge for a member declared `Overrides`.
|
|
828
|
+
*
|
|
829
|
+
* Extracted so that METHODS, PROPERTIES and EVENTS all get one. It was inline
|
|
830
|
+
* in the method visitor only, so `Public Overrides Property Description` —
|
|
831
|
+
* which is how P3D's whole `Item` hierarchy is written, 56 of the missing rows
|
|
832
|
+
* in one frozen draw — published nothing while the oracle, reading the modifier
|
|
833
|
+
* off the declaration, derived the edge and reported the analyzer as
|
|
834
|
+
* incomplete. VB has overridable properties and events, and they are not rare:
|
|
835
|
+
* they are the shape a designer emits.
|
|
836
|
+
*
|
|
837
|
+
* The edge is QUALIFIED by the base type when this file says what the base type
|
|
838
|
+
* is, because the unqualified form is a self-edge (`Go -> Go`) and a self-edge
|
|
839
|
+
* cannot answer "what does this override?". The base type is usually right here
|
|
840
|
+
* — the owning type's own `Inherits` was already read to publish `extends` — so
|
|
841
|
+
* the edge can name `Item.Description` instead of guessing. When the base type
|
|
842
|
+
* is not derivable, the edge keeps the same name and says `baseTypeKnown: false`
|
|
843
|
+
* rather than inventing a type it never read.
|
|
844
|
+
*/
|
|
845
|
+
pushOverride(node, nameNode, sym, owner) {
|
|
846
|
+
const type = this.ownerStack.slice().reverse().find((e) => e !== sym && e.ownerKind === "class" || e !== sym && (e.kind === ASTEngine.SymbolKind.Class || e.kind === ASTEngine.SymbolKind.Struct || e.kind === ASTEngine.SymbolKind.Interface || e.kind === ASTEngine.SymbolKind.Module || e.kind === ASTEngine.SymbolKind.Enum));
|
|
847
|
+
const declaring = type ?? owner;
|
|
848
|
+
if (!declaring) return;
|
|
849
|
+
const baseType = this.relationships.find(
|
|
850
|
+
(r) => r.source === declaring.name && r.type === ASTEngine.RelationshipType.Extends
|
|
851
|
+
);
|
|
852
|
+
const baseKnown = typeof baseType?.target === "string" && baseType.target.length > 0;
|
|
853
|
+
this.relationships.push({
|
|
854
|
+
source: sym.name,
|
|
855
|
+
target: baseKnown ? `${baseType.target}.${sym.name}` : sym.name,
|
|
856
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
857
|
+
confidence: baseKnown ? 0.9 : 0.6,
|
|
858
|
+
line_number: this.nodeLine(nameNode ?? node),
|
|
859
|
+
metadata: {
|
|
860
|
+
baseMemberName: sym.name,
|
|
861
|
+
baseTypeName: baseKnown ? baseType.target : void 0,
|
|
862
|
+
baseTypeKnown: baseKnown,
|
|
863
|
+
fromModifier: "Overrides",
|
|
864
|
+
declaringType: declaring.name
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
isStatic(node) {
|
|
869
|
+
return this.modifierNames(node).has("shared");
|
|
870
|
+
}
|
|
871
|
+
isAbstract(node) {
|
|
872
|
+
const mods = this.modifierNames(node);
|
|
873
|
+
return mods.has("abstract") || mods.has("mustinherit");
|
|
874
|
+
}
|
|
875
|
+
// -------------------------------------------------------------------------
|
|
876
|
+
// Entry point
|
|
877
|
+
// -------------------------------------------------------------------------
|
|
878
|
+
analyze(rootNode, content, filePath, context) {
|
|
879
|
+
this.reset();
|
|
880
|
+
this.precollectDeclarations(rootNode);
|
|
881
|
+
this.walk(rootNode, content, context);
|
|
882
|
+
this.dedupeRelationships();
|
|
883
|
+
this.stampEvidenceClasses();
|
|
884
|
+
const lines = this.lineCount(content);
|
|
885
|
+
return {
|
|
886
|
+
symbols: this.symbols,
|
|
887
|
+
relationships: this.relationships,
|
|
888
|
+
imports: this.imports,
|
|
889
|
+
metadata: {
|
|
890
|
+
complexity: this.complexity(rootNode),
|
|
891
|
+
linesOfCode: lines,
|
|
892
|
+
cyclomaticComplexity: this.cyclomaticComplexity(rootNode),
|
|
893
|
+
analysisVersion: "m60-vb-1",
|
|
894
|
+
// The lane's honesty block. `gaps` is what the grammar could not express
|
|
895
|
+
// and is therefore NOT covered; it is disclosed so a consumer can see
|
|
896
|
+
// the boundary instead of inferring completeness from silence.
|
|
897
|
+
vbGaps: { ...this.gaps },
|
|
898
|
+
hasTypeInference: false,
|
|
899
|
+
hasFrameworkIntelligence: false
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* The same relationship row, emitted more than once in one file, is emitted
|
|
905
|
+
* ONCE.
|
|
906
|
+
*
|
|
907
|
+
* MEASURED DEFECT this closes. `script/m14/vb-edge-integrity.ts` over 2,658
|
|
908
|
+
* corpus files, keying on (source, target, type, line) within one file:
|
|
909
|
+
*
|
|
910
|
+
* references 3,813 rows
|
|
911
|
+
* instantiates 1,840 rows
|
|
912
|
+
* calls 935 rows
|
|
913
|
+
* imports 2 rows
|
|
914
|
+
* -------
|
|
915
|
+
* 6,590 rows that were the same row as a row already present
|
|
916
|
+
*
|
|
917
|
+
* Two shapes produce them, and both are real reads of the source rather than
|
|
918
|
+
* parser noise:
|
|
919
|
+
*
|
|
920
|
+
* - three `As String` on ONE line -- `Public Function F(a As String, b As
|
|
921
|
+
* String) As String` publishes `references F -> String` three times, once
|
|
922
|
+
* per parameter plus once for the return, and the three differ only in a
|
|
923
|
+
* `role` nobody queries.
|
|
924
|
+
* - two constructions of the same type on one line -- `New StackFrame(1)…
|
|
925
|
+
* & New StackFrame(2)…` publishes `instantiates LogMessage -> StackFrame`
|
|
926
|
+
* twice at the same line.
|
|
927
|
+
*
|
|
928
|
+
* WHY IT MATTERS DOWNSTREAM rather than aesthetically: `bb_impact` and
|
|
929
|
+
* `bb_graph` count DEGREES. A file with three `String` references in one
|
|
930
|
+
* signature reports a node three times as depended-upon, which is a number a
|
|
931
|
+
* consumer cannot tell from a real three-call dependency.
|
|
932
|
+
*
|
|
933
|
+
* WHAT IS KEPT. The FIRST row survives untouched apart from two added
|
|
934
|
+
* metadata keys, so the line and the confidence stay exactly as the emitting
|
|
935
|
+
* site set them:
|
|
936
|
+
*
|
|
937
|
+
* duplicateRows how many identical rows collapsed into this one — so the
|
|
938
|
+
* raw count is still visible and nothing is hidden
|
|
939
|
+
* roles the distinct `role` values of the collapsed copies, when
|
|
940
|
+
* they differ; omitted when they do not, because a key with
|
|
941
|
+
* one value in it is noise
|
|
942
|
+
*
|
|
943
|
+
* Rows that differ in ANY of the four key components are KEPT. Two calls to the
|
|
944
|
+
* same method on different lines are two facts about the program and stay two
|
|
945
|
+
* rows — this is not a per-pair dedupe, which would silently discard the
|
|
946
|
+
* per-site line the M59.A7 contract requires.
|
|
947
|
+
*/
|
|
948
|
+
dedupeRelationships() {
|
|
949
|
+
const kept = [];
|
|
950
|
+
const index = /* @__PURE__ */ new Map();
|
|
951
|
+
let collapsed = 0;
|
|
952
|
+
for (const rel of this.relationships) {
|
|
953
|
+
const key = `${rel.source}\0${rel.target}\0${rel.type}\0${rel.line_number ?? 0}`;
|
|
954
|
+
const prior = index.get(key);
|
|
955
|
+
if (prior === void 0) {
|
|
956
|
+
index.set(key, rel);
|
|
957
|
+
kept.push(rel);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
collapsed++;
|
|
961
|
+
const md = prior.metadata ?? {};
|
|
962
|
+
const n = typeof md.duplicateRows === "number" ? md.duplicateRows : 1;
|
|
963
|
+
const role = rel.metadata?.role;
|
|
964
|
+
let roles;
|
|
965
|
+
if (typeof role === "string" && role.length > 0) {
|
|
966
|
+
const existing = Array.isArray(md.roles) ? md.roles : typeof md.role === "string" ? [md.role] : [];
|
|
967
|
+
if (!existing.includes(role)) roles = [...existing, role].sort();
|
|
968
|
+
}
|
|
969
|
+
prior.metadata = { ...md, duplicateRows: n + 1, ...roles ? { roles } : {} };
|
|
970
|
+
}
|
|
971
|
+
if (collapsed > 0) {
|
|
972
|
+
this.note("relationship-duplicate-rows-collapsed");
|
|
973
|
+
this.relationships = kept;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* One `evidenceClass` per relationship, in one place, at the end.
|
|
978
|
+
*
|
|
979
|
+
* MEASURED DEFECT this closes: over 2,658 corpus files every one of the
|
|
980
|
+
* analyzer's 102,295 edges carried `evidenceClass: (none)` -- the label the
|
|
981
|
+
* honesty contract requires on any edge below confidence 1.0, and 94% of the
|
|
982
|
+
* `calls` population plus 100% of `references` is below it. `src/lsp/
|
|
983
|
+
* evidence_tier.ts` then re-derives a tier from the KIND, so the product had
|
|
984
|
+
* a badge and the stored evidence said nothing; the M14 contract's rule 4
|
|
985
|
+
* ("carry a machine-readable reason code, not just a number") was not
|
|
986
|
+
* satisfiable.
|
|
987
|
+
*
|
|
988
|
+
* The rule is the legend's own five tiers, applied here exactly once so no
|
|
989
|
+
* call site has to remember it and no call site can disagree with another:
|
|
990
|
+
*
|
|
991
|
+
* imports import_based (a stated import)
|
|
992
|
+
* references usage_contextual (a name, not a symbol)
|
|
993
|
+
* uses usage_contextual (a read; see pushUse)
|
|
994
|
+
* everything below the 0.7 confidence floor inferred (the legend's own
|
|
995
|
+
* wording for a guess)
|
|
996
|
+
* everything else structural
|
|
997
|
+
*
|
|
998
|
+
* `contains`, `exports`, `extends`, `implements`, `instantiates`, `calls` and
|
|
999
|
+
* `overrides` are structural reads of the tree; the sub-floor clause is what
|
|
1000
|
+
* catches the cross-file `calls`/`instantiates`/`references` edges whose target
|
|
1001
|
+
* is a name this file does not declare. An `evidenceClass` an analyzer already
|
|
1002
|
+
* wrote is never overwritten.
|
|
1003
|
+
*/
|
|
1004
|
+
stampEvidenceClasses() {
|
|
1005
|
+
for (const rel of this.relationships) {
|
|
1006
|
+
const md = rel.metadata;
|
|
1007
|
+
if (md && typeof md.evidenceClass === "string") continue;
|
|
1008
|
+
const conf = typeof rel.confidence === "number" ? rel.confidence : 1;
|
|
1009
|
+
let cls;
|
|
1010
|
+
if (conf < 0.7) cls = "inferred";
|
|
1011
|
+
else if (rel.type === RelationshipType.Imports) cls = "import_based";
|
|
1012
|
+
else if (rel.type === RelationshipType.References || rel.type === RelationshipType.Uses) cls = "usage_contextual";
|
|
1013
|
+
else cls = "structural";
|
|
1014
|
+
rel.metadata = { evidenceClass: cls, ...md ?? {} };
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
reset() {
|
|
1018
|
+
this.symbols = [];
|
|
1019
|
+
this.relationships = [];
|
|
1020
|
+
this.imports = [];
|
|
1021
|
+
this.index = /* @__PURE__ */ new Map();
|
|
1022
|
+
this.declaredNames = /* @__PURE__ */ new Set();
|
|
1023
|
+
this.attributeEdgeKeys = /* @__PURE__ */ new Set();
|
|
1024
|
+
this.ownerStack = [];
|
|
1025
|
+
this.typeStack = [];
|
|
1026
|
+
this.memberNames = /* @__PURE__ */ new Map();
|
|
1027
|
+
this.callablesInFile = /* @__PURE__ */ new Set();
|
|
1028
|
+
this.variablesInFile = /* @__PURE__ */ new Set();
|
|
1029
|
+
this.arrayValuedInFile = /* @__PURE__ */ new Set();
|
|
1030
|
+
this.localsInFile = /* @__PURE__ */ new Set();
|
|
1031
|
+
this.usesEmitted = /* @__PURE__ */ new Map();
|
|
1032
|
+
this.callCalleeNodes = /* @__PURE__ */ new Set();
|
|
1033
|
+
this.gaps = {};
|
|
1034
|
+
this.useIndex = /* @__PURE__ */ new Map();
|
|
1035
|
+
}
|
|
1036
|
+
note(kind) {
|
|
1037
|
+
this.gaps[kind] = (this.gaps[kind] ?? 0) + 1;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Read every declaration in the file BEFORE any body is walked, so the three
|
|
1041
|
+
* resolvability sets do not depend on where a declaration sits relative to its
|
|
1042
|
+
* use. See the `analyze()` note for the measured defect this removes.
|
|
1043
|
+
*
|
|
1044
|
+
* The node set is the same one `pushSymbol` acts on, and the array-ness of a
|
|
1045
|
+
* declaration is read off its own `as_clause` -> `array_type` rather than off its
|
|
1046
|
+
* text, so `SomePropertyReturningAnArray(0)` is recognised as an index and not a
|
|
1047
|
+
* call. One extra tree walk per file; the cost is in the lane's
|
|
1048
|
+
* payload/latency instrument.
|
|
1049
|
+
*/
|
|
1050
|
+
precollectDeclarations(root) {
|
|
1051
|
+
const CALLABLE = /* @__PURE__ */ new Set([
|
|
1052
|
+
"method_declaration",
|
|
1053
|
+
"constructor_declaration",
|
|
1054
|
+
"property_declaration",
|
|
1055
|
+
"operator_declaration",
|
|
1056
|
+
"interface_method_declaration",
|
|
1057
|
+
"interface_property_declaration",
|
|
1058
|
+
"declare_statement"
|
|
1059
|
+
]);
|
|
1060
|
+
const VARIABLE = /* @__PURE__ */ new Set(["field_declaration"]);
|
|
1061
|
+
const LOCAL_BINDING = /* @__PURE__ */ new Set([
|
|
1062
|
+
"declaration_statement",
|
|
1063
|
+
"local_declaration_modifier",
|
|
1064
|
+
"dim_statement"
|
|
1065
|
+
]);
|
|
1066
|
+
const stack = [root];
|
|
1067
|
+
while (stack.length > 0) {
|
|
1068
|
+
const n = stack.pop();
|
|
1069
|
+
if (CALLABLE.has(n.type)) {
|
|
1070
|
+
const field = n.childForFieldName("name");
|
|
1071
|
+
if (field) {
|
|
1072
|
+
const token = this.namingToken(field, field.text);
|
|
1073
|
+
const name = this.nameText(token ?? field);
|
|
1074
|
+
if (name.length > 0) {
|
|
1075
|
+
this.callablesInFile.add(name);
|
|
1076
|
+
if (this.isArrayValued(n)) this.arrayValuedInFile.add(name);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
} else if (n.type === "constructor_declaration") {
|
|
1080
|
+
this.callablesInFile.add("constructor");
|
|
1081
|
+
} else if (n.type === "delegate_declaration") {
|
|
1082
|
+
const field = n.childForFieldName("name");
|
|
1083
|
+
if (field) {
|
|
1084
|
+
const token = this.namingToken(field, field.text);
|
|
1085
|
+
const name = this.nameText(token ?? field);
|
|
1086
|
+
if (name.length > 0) this.callablesInFile.add(name);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (VARIABLE.has(n.type)) {
|
|
1090
|
+
for (const d of this.namedChildrenOfType(n, "variable_declarator")) {
|
|
1091
|
+
const { text } = this.declaredName(d);
|
|
1092
|
+
if (text.length > 0) this.variablesInFile.add(text);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (LOCAL_BINDING.has(n.type)) {
|
|
1096
|
+
for (const d of this.namedChildrenOfType(n, "variable_declarator")) {
|
|
1097
|
+
const { text } = this.declaredName(d);
|
|
1098
|
+
if (text.length > 0) this.localsInFile.add(text);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (n.type === "for_each_statement") {
|
|
1102
|
+
const v = n.childForFieldName("variable");
|
|
1103
|
+
if (v) {
|
|
1104
|
+
const { text } = this.declaredName(v);
|
|
1105
|
+
const name = text.length > 0 ? text : this.nameText(v);
|
|
1106
|
+
if (name.length > 0) this.localsInFile.add(name);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (n.type === "using_statement") {
|
|
1110
|
+
const resource = n.childForFieldName("resource");
|
|
1111
|
+
if (resource) {
|
|
1112
|
+
const { text } = this.declaredName(resource);
|
|
1113
|
+
if (text.length > 0) this.localsInFile.add(text);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
for (const c of this.kids(n)) stack.push(c);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/** Does this declaration's `as_clause` declare an ARRAY type? */
|
|
1120
|
+
isArrayValued(node) {
|
|
1121
|
+
const asClause = this.namedChildrenOfType(node, "as_clause")[0];
|
|
1122
|
+
if (!asClause) return false;
|
|
1123
|
+
const declared = asClause.childForFieldName("declared_type");
|
|
1124
|
+
if (!declared) return false;
|
|
1125
|
+
if (declared.type === "array_type") return true;
|
|
1126
|
+
for (const c of this.kids(declared)) {
|
|
1127
|
+
if (c.type === "array_type" || c.type === "array_rank_specifier") return true;
|
|
1128
|
+
}
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
// -------------------------------------------------------------------------
|
|
1132
|
+
// The walk
|
|
1133
|
+
// -------------------------------------------------------------------------
|
|
1134
|
+
walk(node, content, context) {
|
|
1135
|
+
switch (node.type) {
|
|
1136
|
+
case "namespace_declaration":
|
|
1137
|
+
return this.visitNamespace(node, content, context);
|
|
1138
|
+
case "class_declaration":
|
|
1139
|
+
case "structure_declaration":
|
|
1140
|
+
case "interface_declaration":
|
|
1141
|
+
case "module_declaration":
|
|
1142
|
+
return this.visitType(node, content, context);
|
|
1143
|
+
case "enum_declaration":
|
|
1144
|
+
return this.visitEnum(node, content, context);
|
|
1145
|
+
case "delegate_declaration":
|
|
1146
|
+
return this.visitDelegate(node, content, context);
|
|
1147
|
+
case "imports_statement":
|
|
1148
|
+
return this.visitImports(node);
|
|
1149
|
+
case "method_declaration":
|
|
1150
|
+
case "constructor_declaration":
|
|
1151
|
+
// An interface method has the SAME shape as a class method -- `name`,
|
|
1152
|
+
// `parameters`, `as_clause` -- and no body, so the visitor is reused
|
|
1153
|
+
// unchanged. The one thing it must not do is invent a body: `collectBodyEdges`
|
|
1154
|
+
// and the member-heritage read both walk children, and an interface member
|
|
1155
|
+
// has none to walk.
|
|
1156
|
+
case "interface_method_declaration":
|
|
1157
|
+
case "declare_statement":
|
|
1158
|
+
return this.visitMethod(node, content, context);
|
|
1159
|
+
case "property_declaration":
|
|
1160
|
+
case "interface_property_declaration":
|
|
1161
|
+
return this.visitProperty(node, content, context);
|
|
1162
|
+
case "event_declaration":
|
|
1163
|
+
case "interface_event_declaration":
|
|
1164
|
+
return this.visitEvent(node, content, context);
|
|
1165
|
+
case "operator_declaration":
|
|
1166
|
+
return this.visitOperator(node, content, context);
|
|
1167
|
+
case "field_declaration":
|
|
1168
|
+
return this.visitField(node, content);
|
|
1169
|
+
// AN `ERROR` NODE IS CODE THE GRAMMAR COULD NOT CLASSIFY, and it is often a
|
|
1170
|
+
// SIBLING of the declaration whose body it used to be.
|
|
1171
|
+
//
|
|
1172
|
+
// Measured: UseCompoundAssignment/Utilities.vb's
|
|
1173
|
+
// `ImmutableArray.Create((A,B,C), (D,E,F), ...)` initializer leaves a
|
|
1174
|
+
// `field_declaration` spanning rows 9-12 and puts rows 12-19 in ERROR siblings
|
|
1175
|
+
// beside it. Nothing else visits them, so 24 member reads in that one file
|
|
1176
|
+
// were unreachable — and the lane's differential derived all 24 and reported
|
|
1177
|
+
// them MISSING, which is how a grammar's recovery cost 24 edges in the index
|
|
1178
|
+
// and no gate could see it.
|
|
1179
|
+
//
|
|
1180
|
+
// Dispatching an ERROR node is the fix that generalises: its declaration-
|
|
1181
|
+
// shaped children go to `walk` and everything else is body code attributed to
|
|
1182
|
+
// the enclosing declaration, which is what it is.
|
|
1183
|
+
case "ERROR":
|
|
1184
|
+
return this.visitRecovery(node, content, context);
|
|
1185
|
+
default:
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Dispatch an `ERROR` node: declaration-shaped children to their own visitors,
|
|
1192
|
+
* everything else as body code of the enclosing declaration.
|
|
1193
|
+
*/
|
|
1194
|
+
visitRecovery(node, content, context) {
|
|
1195
|
+
this.dispatchBody(node, content, context);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Does this node, or anything under it, declare a symbol? Early-exits on the first
|
|
1199
|
+
* one found.
|
|
1200
|
+
*
|
|
1201
|
+
* The first version carried a `if (cur.id === n.id) continue` guard so the walk
|
|
1202
|
+
* would not re-test the node it started from — and that guard also SKIPPED THE
|
|
1203
|
+
* START NODE'S CHILDREN, so it answered "is this node itself a declaration?" for
|
|
1204
|
+
* every node and "is there a declaration below?" for none. It reported false for
|
|
1205
|
+
* `type_declaration_in_type`, whose only child is a `class_declaration`, and the
|
|
1206
|
+
* 168 such nodes across 60 of 403 sampled files were then dispatched as body code
|
|
1207
|
+
* and never reached `visitType` — 6 missing `contains`, 1 missing `extends` and 6
|
|
1208
|
+
* missing `overrides` in one file, which is how the differential found it.
|
|
1209
|
+
*/
|
|
1210
|
+
hasDeclarationDescendant(n) {
|
|
1211
|
+
const stack = [n];
|
|
1212
|
+
while (stack.length > 0) {
|
|
1213
|
+
const cur = stack.pop();
|
|
1214
|
+
if (_RealVbAnalyzer.DECLARATION_NODES.has(cur.type)) return true;
|
|
1215
|
+
for (const c of this.kids(cur)) stack.push(c);
|
|
1216
|
+
}
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
/** The nearest enclosing namespace or type, for `ownerName`. */
|
|
1220
|
+
/** The `role` recorded on a containment edge, derived from the KIND so that
|
|
1221
|
+
* every kind names itself and no visitor has to remember to say so. */
|
|
1222
|
+
containmentRole(kind) {
|
|
1223
|
+
switch (kind) {
|
|
1224
|
+
case ASTEngine.SymbolKind.Constructor:
|
|
1225
|
+
return "constructor";
|
|
1226
|
+
case ASTEngine.SymbolKind.Property:
|
|
1227
|
+
return "property";
|
|
1228
|
+
case ASTEngine.SymbolKind.Event:
|
|
1229
|
+
return "event";
|
|
1230
|
+
case ASTEngine.SymbolKind.Method:
|
|
1231
|
+
return "method";
|
|
1232
|
+
case ASTEngine.SymbolKind.Constant:
|
|
1233
|
+
return "const";
|
|
1234
|
+
case ASTEngine.SymbolKind.Variable:
|
|
1235
|
+
return "field";
|
|
1236
|
+
case ASTEngine.SymbolKind.EnumMember:
|
|
1237
|
+
return "enumMember";
|
|
1238
|
+
case ASTEngine.SymbolKind.Enum:
|
|
1239
|
+
return "enum";
|
|
1240
|
+
case ASTEngine.SymbolKind.Class:
|
|
1241
|
+
return "class";
|
|
1242
|
+
case ASTEngine.SymbolKind.Struct:
|
|
1243
|
+
return "structure";
|
|
1244
|
+
case ASTEngine.SymbolKind.Interface:
|
|
1245
|
+
return "interface";
|
|
1246
|
+
case ASTEngine.SymbolKind.Module:
|
|
1247
|
+
return "module";
|
|
1248
|
+
case ASTEngine.SymbolKind.Namespace:
|
|
1249
|
+
return "namespace";
|
|
1250
|
+
case ASTEngine.SymbolKind.Type:
|
|
1251
|
+
return "delegate";
|
|
1252
|
+
default:
|
|
1253
|
+
return String(kind);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
currentOwner() {
|
|
1257
|
+
return this.ownerStack.length > 0 ? this.ownerStack[this.ownerStack.length - 1] : null;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* THE NODE THAT CARRIES THIS DECLARATION'S ATTRIBUTE LISTS.
|
|
1261
|
+
*
|
|
1262
|
+
* The attributes do NOT always sit on the node the symbol is published from, and that is a
|
|
1263
|
+
* grammar fact rather than a choice: `field_declaration` carries
|
|
1264
|
+
* `repeat(field("attributes", $.attribute_list))` and then `commaSep1($.variable_declarator)`,
|
|
1265
|
+
* while this analyzer publishes one symbol per DECLARATOR (visitField's `pushSymbol(text, kind,
|
|
1266
|
+
* decl, ...)`). Reading only the declarator's own children would lose every attribute on every
|
|
1267
|
+
* field and every local — silently, which is the class of gap a completeness audit exists for.
|
|
1268
|
+
*
|
|
1269
|
+
* SO THE CLIMB IS EXACTLY TWO SHAPES AND NO MORE. A declarator is reached from
|
|
1270
|
+
* `field_declaration` or `declaration_statement`; anything else is refused rather than walked, so
|
|
1271
|
+
* a member can never inherit the enclosing TYPE's attributes (which is what an unbounded climb
|
|
1272
|
+
* would do on `Public Class C` followed by `Sub M()`: M is not decorated by C's `<Obsolete>`).
|
|
1273
|
+
*/
|
|
1274
|
+
attributeBearingNode(declNode) {
|
|
1275
|
+
let cur = declNode;
|
|
1276
|
+
for (let i = 0; cur !== null && i < 3; i++) {
|
|
1277
|
+
if (cur.namedChildCount > 0 && this.namedChildrenOfType(cur, "attribute_list").length > 0) {
|
|
1278
|
+
return cur;
|
|
1279
|
+
}
|
|
1280
|
+
const parent = cur.parent;
|
|
1281
|
+
if (parent === null) return null;
|
|
1282
|
+
if (parent.type !== "field_declaration" && parent.type !== "declaration_statement") {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
cur = parent;
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* DECORATES — a declaration's attribute lists, published as edges.
|
|
1291
|
+
*
|
|
1292
|
+
* `<Obsolete("use V2")> Public Class Legacy` is ONE syntactic event carrying TWO facts, and the
|
|
1293
|
+
* fleet's vocabulary keeps them apart: the attribute NAME is a type mention (not a value read,
|
|
1294
|
+
* which is why `inAttributePosition` suppresses it from `references` — a differential finding, not
|
|
1295
|
+
* an oversight), and the APPLYING declaration is what the attribute decorates. This publishes the
|
|
1296
|
+
* second as `decorates <declaration> -> <attribute leaf>`.
|
|
1297
|
+
*
|
|
1298
|
+
* WHY THE TARGET IS THE LEAF: the arbiter binds a target by NAME against the project's own
|
|
1299
|
+
* declarations, so `System.Obsolete` is unresolvable BY CONSTRUCTION while `Obsolete` resolves
|
|
1300
|
+
* exactly when the project declares its own attribute type (rust's ruling for this type, applied
|
|
1301
|
+
* here). The written spelling is not lost — it rides in `attributeName`, so the leaf is the KEY
|
|
1302
|
+
* and the spelling is the EVIDENCE.
|
|
1303
|
+
*
|
|
1304
|
+
* GRAMMAR BASIS, MEASURED RATHER THAN ASSUMED: in grammars/tree-sitter-vbnet/grammar.js every
|
|
1305
|
+
* declaration production carries `repeat(field("attributes", $.attribute_list))` — class,
|
|
1306
|
+
* structure, interface, module, enum, enum_member, delegate, field, property, getter, setter,
|
|
1307
|
+
* method, constructor, event, operator, the four interface member forms, parameter and
|
|
1308
|
+
* declaration_statement — and `attribute` is
|
|
1309
|
+
* `seq(field("name", $._name_reference), optional(field("arguments", $.attribute_argument_list)))`.
|
|
1310
|
+
* The name is therefore a FIELD read, not a positional guess.
|
|
1311
|
+
*
|
|
1312
|
+
* WHAT IS NOT REACHABLE, AND IS COUNTED RATHER THAN SILENT: `<Assembly: …>` / `<Module: …>` live in
|
|
1313
|
+
* `global_attribute_section` and a file-level `file_attribute_section` decorates the FILE. Neither
|
|
1314
|
+
* has a declaration to name as a source, so neither publishes an edge here; each is counted by
|
|
1315
|
+
* `note("attribute-without-declaration")` and ruled in the lane's spec, which is the difference
|
|
1316
|
+
* between a disclosed gap and a gap.
|
|
1317
|
+
*/
|
|
1318
|
+
pushAttributeDecorations(declNode, symName, kind) {
|
|
1319
|
+
const holder = this.attributeBearingNode(declNode);
|
|
1320
|
+
if (holder === null) return;
|
|
1321
|
+
for (const list of this.namedChildrenOfType(holder, "attribute_list")) {
|
|
1322
|
+
for (const attr of this.namedChildrenOfType(list, "attribute")) {
|
|
1323
|
+
const nameNode = attr.childForFieldName("name") ?? this.namedChildrenOfType(attr, "qualified_name")[0] ?? this.namedChildrenOfType(attr, "global_qualified_name")[0] ?? this.namedChildrenOfType(attr, "identifier")[0];
|
|
1324
|
+
if (!nameNode) {
|
|
1325
|
+
this.note("attribute-without-name");
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const written = this.nameText(nameNode);
|
|
1329
|
+
const leaf = written.split(".").pop() ?? written;
|
|
1330
|
+
if (leaf.length === 0) {
|
|
1331
|
+
this.note("attribute-without-name");
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
const line = this.nodeLine(nameNode);
|
|
1335
|
+
const key = `${symName}\0${leaf}\0${line}`;
|
|
1336
|
+
if (this.attributeEdgeKeys.has(key)) continue;
|
|
1337
|
+
this.attributeEdgeKeys.add(key);
|
|
1338
|
+
const resolved = this.declaredNames.has(leaf);
|
|
1339
|
+
const args = attr.childForFieldName("arguments");
|
|
1340
|
+
const attributeTarget = this.namedChildrenOfType(holder, "attribute_target")[0];
|
|
1341
|
+
this.relationships.push({
|
|
1342
|
+
source: symName,
|
|
1343
|
+
target: leaf,
|
|
1344
|
+
type: RelationshipType.Decorates,
|
|
1345
|
+
confidence: resolved ? 1 : 0.8,
|
|
1346
|
+
line_number: line,
|
|
1347
|
+
metadata: {
|
|
1348
|
+
// THE WRITTEN SPELLING, preserved: the edge's target is the resolvable key, this is
|
|
1349
|
+
// the evidence a reader (and the differential) checks it against.
|
|
1350
|
+
attributeName: written,
|
|
1351
|
+
attributeLeaf: leaf,
|
|
1352
|
+
// `declaration` for a member/type attribute; a target prefix (`Assembly:`) is carried
|
|
1353
|
+
// when the grammar recorded one, which is how a global section is visibly different.
|
|
1354
|
+
attributeForm: attributeTarget ? "target" : "declaration",
|
|
1355
|
+
...attributeTarget ? { attributeTarget: attributeTarget.text } : {},
|
|
1356
|
+
argumentCount: args ? args.namedChildCount : 0,
|
|
1357
|
+
decoratedKind: kind,
|
|
1358
|
+
resolved
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
pushSymbol(name, kind, node, extra = {}, nameNode = null) {
|
|
1365
|
+
if (name.length === 0) {
|
|
1366
|
+
this.note("declaration-without-name");
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
const owner = this.currentOwner();
|
|
1370
|
+
const { metadata: extraMetadata, ...rest } = extra;
|
|
1371
|
+
const qualifiedName = owner && owner.name !== name ? `${owner.name}.${name}` : name;
|
|
1372
|
+
const sym = {
|
|
1373
|
+
name,
|
|
1374
|
+
qualifiedName,
|
|
1375
|
+
kind,
|
|
1376
|
+
range: this.range(node),
|
|
1377
|
+
...owner ? { ownerName: owner.name, ownerKind: owner.kind } : {},
|
|
1378
|
+
// THE SYMBOL'S OWN ACCESSIBILITY, stamped where the symbol is published so a
|
|
1379
|
+
// consumer (and `pushExport`'s `ownerAccessLevel`) reads ONE record instead of
|
|
1380
|
+
// re-deriving the modifiers from the tree. It used to be absent, and the
|
|
1381
|
+
// `?? "assembly"` fallback in `pushExport` then reported a `Friend` type's
|
|
1382
|
+
// member as `ownerAccessLevel: assembly` -- the default standing in for a
|
|
1383
|
+
// measured fact, on the one row whose whole point is that the default differs.
|
|
1384
|
+
metadata: {
|
|
1385
|
+
...extraMetadata ?? {},
|
|
1386
|
+
accessLevel: [...this.exportLevelsOf(node, kind).levels].join("+")
|
|
1387
|
+
},
|
|
1388
|
+
...rest
|
|
1389
|
+
};
|
|
1390
|
+
this.symbols.push(sym);
|
|
1391
|
+
this.declaredNames.add(name);
|
|
1392
|
+
this.pushAttributeDecorations(node, name, kind);
|
|
1393
|
+
if (owner && owner.name !== name) {
|
|
1394
|
+
this.relationships.push({
|
|
1395
|
+
source: owner.name,
|
|
1396
|
+
target: name,
|
|
1397
|
+
type: RelationshipType.Contains,
|
|
1398
|
+
confidence: 1,
|
|
1399
|
+
line_number: this.nodeLine(nameNode ?? node),
|
|
1400
|
+
metadata: { role: this.containmentRole(kind) }
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
this.pushExport(node, name, kind, nameNode, owner);
|
|
1404
|
+
if (!this.index.has(name)) this.index.set(name, sym);
|
|
1405
|
+
if (kind === ASTEngine.SymbolKind.Method || kind === ASTEngine.SymbolKind.Constructor || kind === ASTEngine.SymbolKind.Property) {
|
|
1406
|
+
this.callablesInFile.add(name);
|
|
1407
|
+
}
|
|
1408
|
+
if (kind === ASTEngine.SymbolKind.Variable) this.variablesInFile.add(name);
|
|
1409
|
+
if (kind === ASTEngine.SymbolKind.Type && sym.metadata?.delegate === true) {
|
|
1410
|
+
this.callablesInFile.add(name);
|
|
1411
|
+
}
|
|
1412
|
+
const ownerType = this.currentType();
|
|
1413
|
+
if (ownerType) {
|
|
1414
|
+
let list = this.memberNames.get(ownerType.name);
|
|
1415
|
+
if (!list) {
|
|
1416
|
+
list = [];
|
|
1417
|
+
this.memberNames.set(ownerType.name, list);
|
|
1418
|
+
}
|
|
1419
|
+
list.push(name);
|
|
1420
|
+
}
|
|
1421
|
+
return sym;
|
|
1422
|
+
}
|
|
1423
|
+
currentType() {
|
|
1424
|
+
return this.typeStack.length > 0 ? this.typeStack[this.typeStack.length - 1] : null;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* The node whose ACCESS MODIFIERS describe this declaration.
|
|
1428
|
+
*
|
|
1429
|
+
* `pushSymbol` is called with the node that carries the NAME, and for a field
|
|
1430
|
+
* that is the `variable_declarator` -- while the modifiers are on its parent
|
|
1431
|
+
* `field_declaration` (`Public Value As Integer = 3` is
|
|
1432
|
+
* `field_declaration(members: member_modifier) variable_declarator(Value ...)`).
|
|
1433
|
+
* Reading modifiers off the declarator therefore found none, which is why a
|
|
1434
|
+
* `Public` field and an undecorated one were indistinguishable. One level up,
|
|
1435
|
+
* and only for that exact pair of node types, is the whole correction.
|
|
1436
|
+
*/
|
|
1437
|
+
declarationNodeOf(node) {
|
|
1438
|
+
if (node.type === "variable_declarator") {
|
|
1439
|
+
const parent = node.parent;
|
|
1440
|
+
if (parent && parent.type === "field_declaration") return parent;
|
|
1441
|
+
}
|
|
1442
|
+
return node;
|
|
1443
|
+
}
|
|
1444
|
+
/** The access levels in force for a symbol, explicit or defaulted. */
|
|
1445
|
+
exportLevelsOf(node, kind) {
|
|
1446
|
+
const levels = this.accessLevelsOf(this.declarationNodeOf(node));
|
|
1447
|
+
if (levels.size > 0) return { levels, explicit: true };
|
|
1448
|
+
const dflt = this.defaultAccessLevel(kind);
|
|
1449
|
+
return { levels: /* @__PURE__ */ new Set([dflt]), explicit: false };
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* `exports`: the assembly's public surface.
|
|
1453
|
+
*
|
|
1454
|
+
* VB HAS the construct, so this is a GAP and not a ruling. The language
|
|
1455
|
+
* expresses export through ACCESSIBILITY rather than through a keyword: a
|
|
1456
|
+
* declaration is reachable from outside its assembly when its effective
|
|
1457
|
+
* accessibility is `Public`, and there is no second mechanism -- measured over
|
|
1458
|
+
* 2,658 corpus files, `exports_statement`, `export_list` and `assembly_attribute`
|
|
1459
|
+
* are all node types the grammar HAS RULES for and that occur ZERO times
|
|
1460
|
+
* (census probe, node inventory), so a keyword-shaped `exports` in VB would be
|
|
1461
|
+
* an edge the language cannot express. `Public` is a `member_modifier` and a
|
|
1462
|
+
* grammar inventory is blind to a convention by construction, which is exactly
|
|
1463
|
+
* how json's `exports` ruling was overturned; so the number behind this
|
|
1464
|
+
* verdict is a MODIFIER count (15,489 `public` modifiers in 2,344 files) and a
|
|
1465
|
+
* DEFAULTED-accessibility count (136 modifier-less class members, 20 in a
|
|
1466
|
+
* module, 5 in a structure, 116 modifier-less top-level types), not a node
|
|
1467
|
+
* type count.
|
|
1468
|
+
*
|
|
1469
|
+
* DIRECTION. Source is the declaring type, or `root` for a type declared at
|
|
1470
|
+
* namespace level -- the assembly is the unit of export in a .NET assembly, and
|
|
1471
|
+
* `imports` already uses `root` as the source for the same reason.
|
|
1472
|
+
*
|
|
1473
|
+
* CONTAINMENT IS NOT COMPOUNDED INTO THE DECISION. VB's documented rule is
|
|
1474
|
+
* that a Public member of an internal type is not assembly-visible ("code that
|
|
1475
|
+
* cannot access a containing element cannot access any of its contained
|
|
1476
|
+
* elements"), so the edge COULD be suppressed for a member of a `Friend` type.
|
|
1477
|
+
* It is not, and the reason is that the edge as emitted answers a question a
|
|
1478
|
+
* consumer can ask ("which members does this type declare as Public?") without
|
|
1479
|
+
* the analyzer having to decide a transitive-visibility question it cannot
|
|
1480
|
+
* fully answer -- a `Friend` type is assembly-visible to a FRIEND ASSEMBLY,
|
|
1481
|
+
* which is per-project configuration this analyzer never sees. `ownerExported`
|
|
1482
|
+
* records the containing type's own visibility so the compounding is one
|
|
1483
|
+
* filter away instead of being baked into the edge's existence.
|
|
1484
|
+
*/
|
|
1485
|
+
pushExport(node, name, kind, nameNode, owner) {
|
|
1486
|
+
const { levels, explicit } = this.exportLevelsOf(node, kind);
|
|
1487
|
+
if (!_RealVbAnalyzer.isAssemblyVisible(levels)) return;
|
|
1488
|
+
if (owner && owner.kind === ASTEngine.SymbolKind.Namespace) return;
|
|
1489
|
+
this.relationships.push({
|
|
1490
|
+
source: owner ? owner.name : "root",
|
|
1491
|
+
target: name,
|
|
1492
|
+
type: RelationshipType.Exports,
|
|
1493
|
+
confidence: 1,
|
|
1494
|
+
line_number: this.nodeLine(nameNode ?? node),
|
|
1495
|
+
metadata: {
|
|
1496
|
+
role: this.containmentRole(kind),
|
|
1497
|
+
accessLevel: [...levels].join("+"),
|
|
1498
|
+
accessFromModifier: explicit,
|
|
1499
|
+
// The containing type's OWN accessibility, read off the symbol record
|
|
1500
|
+
// `pushSymbol` wrote when it published the owner. Recorded rather than
|
|
1501
|
+
// acted on; see the header for why the edge does not depend on it.
|
|
1502
|
+
ownerAccessLevel: owner?.metadata?.accessLevel ?? "assembly"
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
// -------------------------------------------------------------------------
|
|
1507
|
+
// Declarations
|
|
1508
|
+
// -------------------------------------------------------------------------
|
|
1509
|
+
visitNamespace(node, content, context) {
|
|
1510
|
+
const { text, node: nameNode } = this.declaredName(node);
|
|
1511
|
+
const sym = this.pushSymbol(text, SymbolKind.Namespace, node, {
|
|
1512
|
+
metadata: {
|
|
1513
|
+
// Uniform with every other declaration. The node-type yield audit walks
|
|
1514
|
+
// the corpus counting declaration NODES and then counts symbols by their
|
|
1515
|
+
// `grammarNode`, and a declaration that records none of its own node type
|
|
1516
|
+
// is invisible to that audit -- which is how a whole kind can stop being
|
|
1517
|
+
// produced with nothing left to notice. The audit found this one missing
|
|
1518
|
+
// and could not tell a dead visitor from an unlabelled symbol; the fix is
|
|
1519
|
+
// to label it.
|
|
1520
|
+
grammarNode: node.type,
|
|
1521
|
+
// The two namespace forms are ONE node type in this grammar (two
|
|
1522
|
+
// alternatives of one rule), and the analyzer's job is to say which one
|
|
1523
|
+
// it read. The discriminator is the ABSENCE of `End Namespace`: the
|
|
1524
|
+
// file-scoped form has no terminator to close it.
|
|
1525
|
+
//
|
|
1526
|
+
// (This line previously tested a `blank_line` node that this grammar
|
|
1527
|
+
// does not have -- an invented reference, which is the LUA-00 defect
|
|
1528
|
+
// class and which M14.0.F1 caught as an unknown node type. The
|
|
1529
|
+
// expression was also wrong independently of that: it ANDed the
|
|
1530
|
+
// discriminator with a test for a node that never appears, so it could
|
|
1531
|
+
// only ever be false.)
|
|
1532
|
+
fileScoped: !this.hasEndNamespace(node)
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
if (sym) {
|
|
1536
|
+
this.typeStack.push(sym);
|
|
1537
|
+
this.ownerStack.push(sym);
|
|
1538
|
+
}
|
|
1539
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
1540
|
+
if (sym) {
|
|
1541
|
+
this.typeStack.pop();
|
|
1542
|
+
this.ownerStack.pop();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Whether this namespace has an `End Namespace`.
|
|
1547
|
+
*
|
|
1548
|
+
* The grammar does not expose it as a node — `End` and `Namespace` are
|
|
1549
|
+
* anonymous tokens inside the rule. So this reads the node's LAST named
|
|
1550
|
+
* child's extent against the namespace's own text: the block form's last
|
|
1551
|
+
* named child ends at the `End Namespace`, the file-scoped form's last child
|
|
1552
|
+
* is the final declaration. A char-scan over the tail, not a regex: find the
|
|
1553
|
+
* last occurrence of the keyword by walking backwards.
|
|
1554
|
+
*/
|
|
1555
|
+
hasEndNamespace(node) {
|
|
1556
|
+
if (node.namedChildCount === 0) return false;
|
|
1557
|
+
const named = this.kids(node);
|
|
1558
|
+
const last = named[named.length - 1];
|
|
1559
|
+
if (!last) return false;
|
|
1560
|
+
const tail = node.text.slice(last.endIndex - node.startIndex);
|
|
1561
|
+
return _RealVbAnalyzer.endsWithKeyword(tail, "end namespace");
|
|
1562
|
+
}
|
|
1563
|
+
/** Char-scan: does `s` end with `words` (ASCII, case-insensitive, any run of spaces/tabs between)? */
|
|
1564
|
+
static endsWithKeyword(s, words) {
|
|
1565
|
+
let i = s.length - 1;
|
|
1566
|
+
let w = words.length - 1;
|
|
1567
|
+
while (w >= 0) {
|
|
1568
|
+
while (i >= 0 && (s.charCodeAt(i) === 32 || s.charCodeAt(i) === 9 || s.charCodeAt(i) === 13 || s.charCodeAt(i) === 10)) i--;
|
|
1569
|
+
if (w < 0) break;
|
|
1570
|
+
const want = words.charCodeAt(w);
|
|
1571
|
+
let got = s.charCodeAt(i);
|
|
1572
|
+
if (got >= 65 && got <= 90) got += 32;
|
|
1573
|
+
if (want >= 65 && want <= 90) want;
|
|
1574
|
+
const wantFolded = want >= 65 && want <= 90 ? want + 32 : want;
|
|
1575
|
+
if (got !== wantFolded) return false;
|
|
1576
|
+
i--;
|
|
1577
|
+
w--;
|
|
1578
|
+
}
|
|
1579
|
+
while (i >= 0) {
|
|
1580
|
+
const c = s.charCodeAt(i);
|
|
1581
|
+
if (c !== 32 && c !== 9 && c !== 13 && c !== 10 && c !== 59) return false;
|
|
1582
|
+
i--;
|
|
1583
|
+
}
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Dispatch a type's or a module's BODY: declarations to their own visitors,
|
|
1588
|
+
* structure to nothing, and everything else as body code of the type.
|
|
1589
|
+
*
|
|
1590
|
+
* This is what recovered 24 member reads in UseCompoundAssignment/Utilities.vb and
|
|
1591
|
+
* the whole class declaration in
|
|
1592
|
+
* VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.vb: the
|
|
1593
|
+
* grammar's recovery leaves a type body's statements as DIRECT children of the
|
|
1594
|
+
* type, outside any declaration, and nothing was reading them. A type that
|
|
1595
|
+
* executes code publishes nothing about what it executes.
|
|
1596
|
+
*/
|
|
1597
|
+
dispatchBody(node, content, context) {
|
|
1598
|
+
for (const child of this.kids(node)) {
|
|
1599
|
+
if (_RealVbAnalyzer.DECLARATION_NODES.has(child.type)) {
|
|
1600
|
+
this.walk(child, content, context);
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (_RealVbAnalyzer.TYPE_BODY_STRUCTURE.has(child.type)) continue;
|
|
1604
|
+
if (this.hasDeclarationDescendant(child)) {
|
|
1605
|
+
this.dispatchBody(child, content, context);
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
this.collectBodyEdges(child, null);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
visitType(node, content, context) {
|
|
1612
|
+
const kind = _RealVbAnalyzer.typeKindOf(node.type);
|
|
1613
|
+
const { text, node: nameNode } = this.declaredName(node);
|
|
1614
|
+
const mods = this.modifierNames(node);
|
|
1615
|
+
const sym = this.pushSymbol(text, kind, node, {
|
|
1616
|
+
visibility: this.visibilityOf(node, kind),
|
|
1617
|
+
isStatic: mods.has("shared"),
|
|
1618
|
+
isAbstract: this.isAbstract(node),
|
|
1619
|
+
metadata: {
|
|
1620
|
+
grammarNode: node.type,
|
|
1621
|
+
// `NotInheritable` is the VB spelling of C#'s `sealed`; recording it
|
|
1622
|
+
// under the same key the family's csharp lane uses is what lets a
|
|
1623
|
+
// cross-language query ask "sealed types" without knowing the language.
|
|
1624
|
+
sealed: mods.has("notinheritable") || mods.has("notoverridable"),
|
|
1625
|
+
partial: mods.has("partial")
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
if (sym) {
|
|
1629
|
+
this.typeStack.push(sym);
|
|
1630
|
+
this.ownerStack.push(sym);
|
|
1631
|
+
}
|
|
1632
|
+
if (sym) this.visitHeritage(node, text, sym, context);
|
|
1633
|
+
this.dispatchBody(node, content, context);
|
|
1634
|
+
if (sym) {
|
|
1635
|
+
this.typeStack.pop();
|
|
1636
|
+
this.ownerStack.pop();
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
static typeKindOf(nodeType) {
|
|
1640
|
+
switch (nodeType) {
|
|
1641
|
+
case "class_declaration":
|
|
1642
|
+
return SymbolKind.Class;
|
|
1643
|
+
case "structure_declaration":
|
|
1644
|
+
return SymbolKind.Struct;
|
|
1645
|
+
case "interface_declaration":
|
|
1646
|
+
return SymbolKind.Interface;
|
|
1647
|
+
case "module_declaration":
|
|
1648
|
+
return SymbolKind.Module;
|
|
1649
|
+
default:
|
|
1650
|
+
return SymbolKind.Class;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* VB spells heritage two ways and BOTH are emitted:
|
|
1655
|
+
* - a following line: `Inherits B` / `Implements I, J`
|
|
1656
|
+
* - on the declaration header: `Class C : Inherits B, Implements I`
|
|
1657
|
+
* and, for an interface or a structure, the keyword-less
|
|
1658
|
+
* `Interface I : IDisposable, IEnumerable`
|
|
1659
|
+
*
|
|
1660
|
+
* The header form is not a node — the grammar folds it into a hidden rule
|
|
1661
|
+
* whose children are anonymous tokens plus `name_reference` leaves. So the
|
|
1662
|
+
* heritage names are read as the identifier-bearing children of the header
|
|
1663
|
+
* position, and the keyword that precedes them decides whether each is an
|
|
1664
|
+
* `extends` or an `implements` edge. Which keyword it was comes from the
|
|
1665
|
+
* ORDER: `Inherits` precedes `Implements`, and a structure/interface list has
|
|
1666
|
+
* neither, so every name after the first `Inherits` run and before an
|
|
1667
|
+
* `Implements` token is an extends target and the rest are implements.
|
|
1668
|
+
*/
|
|
1669
|
+
visitHeritage(node, selfName, sym, context) {
|
|
1670
|
+
for (const st of this.namedChildrenOfType(node, "inherits_statement")) {
|
|
1671
|
+
for (const nameNode of this.nameReferencesOf(st)) {
|
|
1672
|
+
this.pushHeritage(selfName, nameNode, RelationshipType.Extends, sym, context);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
for (const st of this.namedChildrenOfType(node, "implements_statement")) {
|
|
1676
|
+
for (const nameNode of this.nameReferencesOf(st)) {
|
|
1677
|
+
this.pushHeritage(selfName, nameNode, RelationshipType.Implements, sym, context);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
for (const st of this.namedChildrenOfType(node, "implements_member_clause")) {
|
|
1681
|
+
for (const nameNode of this.nameReferencesOf(st)) {
|
|
1682
|
+
const ownerSym = this.index.get(selfName);
|
|
1683
|
+
if (ownerSym) this.pushHeritage(selfName, nameNode, RelationshipType.Implements, ownerSym, context);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
const colon = this.headerColonIndex(node);
|
|
1687
|
+
if (colon < 0) return;
|
|
1688
|
+
const headerKids = this.allKids(node);
|
|
1689
|
+
const firstNameIdx = headerKids.findIndex((c, i) => i > colon && this.isNameNode(c));
|
|
1690
|
+
const keyword = firstNameIdx < 0 ? "" : this.spanKeywordBetween(node, headerKids[colon], headerKids[firstNameIdx], ["inherits", "implements"]);
|
|
1691
|
+
const headerLabel = keyword === "inherits" ? RelationshipType.Extends : keyword === "implements" || node.type === "interface_declaration" ? RelationshipType.Implements : RelationshipType.Extends;
|
|
1692
|
+
for (let i = colon + 1; i < headerKids.length; i++) {
|
|
1693
|
+
const child = headerKids[i];
|
|
1694
|
+
if (child.type === ",") continue;
|
|
1695
|
+
if (!this.isNameNode(child)) break;
|
|
1696
|
+
const word = this.nameText(child).trim().toLowerCase();
|
|
1697
|
+
if (word === "implements" || word === "inherits") continue;
|
|
1698
|
+
this.pushHeritage(selfName, child, headerLabel, sym, context);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Which of `words` appears in the source span between two child nodes.
|
|
1703
|
+
*
|
|
1704
|
+
* A char scan, not a regex and not a node test, for the stated reason: this
|
|
1705
|
+
* grammar inlines the heritage keywords away, so the only place the word
|
|
1706
|
+
* survives is the node's own text. Word-bounded on both sides, so
|
|
1707
|
+
* `InheritsFoo` and `MyImplements` cannot match. Returns "" when none is
|
|
1708
|
+
* present, which is a real answer for the keyword-less interface header.
|
|
1709
|
+
*/
|
|
1710
|
+
spanKeywordBetween(parent, from, to, words) {
|
|
1711
|
+
const start = from.endIndex - parent.startIndex;
|
|
1712
|
+
const end = to.startIndex - parent.startIndex;
|
|
1713
|
+
if (end <= start || end > parent.text.length) return "";
|
|
1714
|
+
const span = parent.text.slice(start, end);
|
|
1715
|
+
const isWordChar = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95;
|
|
1716
|
+
for (const w of words) {
|
|
1717
|
+
for (let i = 0; i + w.length <= span.length; i++) {
|
|
1718
|
+
if (i > 0 && isWordChar(span.charCodeAt(i - 1))) continue;
|
|
1719
|
+
let ok = true;
|
|
1720
|
+
for (let k = 0; k < w.length; k++) {
|
|
1721
|
+
let c = span.charCodeAt(i + k);
|
|
1722
|
+
if (c >= 65 && c <= 90) c += 32;
|
|
1723
|
+
if (c !== w.charCodeAt(k)) {
|
|
1724
|
+
ok = false;
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!ok) continue;
|
|
1729
|
+
const after = i + w.length;
|
|
1730
|
+
if (after < span.length && isWordChar(span.charCodeAt(after))) continue;
|
|
1731
|
+
return w;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return "";
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* The index of the anonymous `:` token of the declaration header, or -1.
|
|
1738
|
+
*
|
|
1739
|
+
* THE ORDERING IS BY `startIndex`, and it used to be by `id` -- which made this
|
|
1740
|
+
* branch DEAD on every header clause in the corpus. A `web-tree-sitter` node
|
|
1741
|
+
* `id` is an internal subtree pointer, not a document position: measured on
|
|
1742
|
+
* `Public Class B : Inherits A`, the `:` token has id 4064760 and the name
|
|
1743
|
+
* field has id 4065216, so `kids[i].id > from` is FALSE for the colon this
|
|
1744
|
+
* method exists to find, and the whole header branch was unreachable. The
|
|
1745
|
+
* grammar audit could not see it: `id` is not a node type and not a field
|
|
1746
|
+
* name, so a vocabulary audit reads this line as a well-formed field read.
|
|
1747
|
+
*
|
|
1748
|
+
* What it cost, measured on the lane corpus by scanning for the shape
|
|
1749
|
+
* directly (2 header clauses exist, and this is every one of them):
|
|
1750
|
+
*
|
|
1751
|
+
* Public NotInheritable Class MyGroupCollectionAttribute : Inherits Attribute
|
|
1752
|
+
* Public Class entry_ : Implements IComparable(Of entry_)
|
|
1753
|
+
*
|
|
1754
|
+
* published NO heritage edge at all. Not a mislabelled one -- none. So the
|
|
1755
|
+
* mislabelling the `seenInherits = -1` flag would have caused was never
|
|
1756
|
+
* reached either, and both header forms were silently absent: `extends` was
|
|
1757
|
+
* 2 edges short and `implements` 1, on real declarations, and every number the
|
|
1758
|
+
* lane published counted them correctly as missing rather than as present.
|
|
1759
|
+
*/
|
|
1760
|
+
headerColonIndex(node) {
|
|
1761
|
+
const nameField = node.childForFieldName("name");
|
|
1762
|
+
const from = nameField ? nameField.startIndex : node.startIndex;
|
|
1763
|
+
const kids = this.allKids(node);
|
|
1764
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1765
|
+
if (kids[i].startIndex > from && kids[i].type === ":") return i;
|
|
1766
|
+
}
|
|
1767
|
+
return -1;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Is this node one of the grammar's NAME types?
|
|
1771
|
+
*
|
|
1772
|
+
* The set is the grammar's real vocabulary, read off the pinned wasm: an
|
|
1773
|
+
* identifier, or one of the three name wrappers (`simple_name`,
|
|
1774
|
+
* `qualified_name`, `global_qualified_name`) or the generic name. Two
|
|
1775
|
+
* earlier versions of this test named `name_reference` and `namespace_name`,
|
|
1776
|
+
* neither of which is a node type in this grammar -- invented references that
|
|
1777
|
+
* are dead branches, which is the LUA-00 defect and what M14.0.F1 is for.
|
|
1778
|
+
* Naming them from the pinned surface rather than from a habit is the fix.
|
|
1779
|
+
*/
|
|
1780
|
+
isNameNode(n) {
|
|
1781
|
+
switch (n.type) {
|
|
1782
|
+
case "identifier":
|
|
1783
|
+
case "simple_name":
|
|
1784
|
+
case "qualified_name":
|
|
1785
|
+
case "global_qualified_name":
|
|
1786
|
+
case "generic_name":
|
|
1787
|
+
return true;
|
|
1788
|
+
default:
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
/** The name-bearing children of a heritage statement. */
|
|
1793
|
+
nameReferencesOf(node) {
|
|
1794
|
+
const out = [];
|
|
1795
|
+
const visit = (n) => {
|
|
1796
|
+
for (const c of this.kids(n)) {
|
|
1797
|
+
if (this.isNameNode(c)) {
|
|
1798
|
+
out.push(c);
|
|
1799
|
+
} else {
|
|
1800
|
+
visit(c);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
visit(node);
|
|
1805
|
+
return out;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* One heritage edge, with the line of the node that NAMES the base.
|
|
1809
|
+
*
|
|
1810
|
+
* The target is the type's own name (`B`, not `Namespace.B`) because that is
|
|
1811
|
+
* what a symbol in this file would be called, and an edge is only navigable if
|
|
1812
|
+
* its target names something. Confidence is 1.0 for a name the file also
|
|
1813
|
+
* declares (an in-file base type is certain) and lower for one it does not,
|
|
1814
|
+
* because a cross-file target is a name, not a resolved symbol.
|
|
1815
|
+
*/
|
|
1816
|
+
/**
|
|
1817
|
+
* A MEMBER-LEVEL heritage clause, read on the member that carries it.
|
|
1818
|
+
*
|
|
1819
|
+
* `Public Sub Refresh() Implements IThing.Refresh` is a real, separate
|
|
1820
|
+
* relationship from the class's own `Implements IThing`, and it used to be
|
|
1821
|
+
* dropped: `visitHeritage` was called from exactly one place -- the TYPE
|
|
1822
|
+
* visitor -- so nothing ever read a member's clause. The whole member-level
|
|
1823
|
+
* form was simply not collected, on every member in the language.
|
|
1824
|
+
*
|
|
1825
|
+
* The TARGET IS THE FULL QUALIFIED NAME, deliberately, and this is ONE OF TWO
|
|
1826
|
+
* places the last-segment naming rule does not apply — the other is
|
|
1827
|
+
* `visitImports`, whose target is a namespace PATH (`System.Collections.Generic`),
|
|
1828
|
+
* changed 2026-10-28 for the reason recorded there. Everywhere else a dotted
|
|
1829
|
+
* name is reduced to its last segment, because that is the name a reader
|
|
1830
|
+
* navigates to. Here the member is only identified BY its interface: the method
|
|
1831
|
+
* is named `Refresh` precisely because it implements `IThing.Refresh`, so
|
|
1832
|
+
* last-segment reduction yields `implements Refresh -> Refresh` -- an edge from
|
|
1833
|
+
* a symbol to itself, which is not navigable and answers none of the questions
|
|
1834
|
+
* anyone asks of it ("where is `IThing.Refresh` implemented?"). The full
|
|
1835
|
+
* `IThing.Refresh` is what makes the edge mean something.
|
|
1836
|
+
*
|
|
1837
|
+
* Found by the full-corpus F5 sweep, not by a fixture: 17 of 23 missing rows on
|
|
1838
|
+
* a 483-file draw were this, all of them reading as self-edges for exactly the
|
|
1839
|
+
* reason above.
|
|
1840
|
+
*/
|
|
1841
|
+
visitMemberHeritage(node, sym, context) {
|
|
1842
|
+
for (const [clause, rel] of [
|
|
1843
|
+
["implements_member_clause", RelationshipType.Implements],
|
|
1844
|
+
["inherits_member_clause", RelationshipType.Extends]
|
|
1845
|
+
]) {
|
|
1846
|
+
for (const st of this.namedChildrenOfType(node, clause)) {
|
|
1847
|
+
for (const nameNode of this.nameReferencesOf(st)) {
|
|
1848
|
+
const target = this.typeArgumentsStrippedText(nameNode);
|
|
1849
|
+
if (target.length === 0) continue;
|
|
1850
|
+
this.relationships.push({
|
|
1851
|
+
source: sym.name,
|
|
1852
|
+
target,
|
|
1853
|
+
type: rel,
|
|
1854
|
+
// A member-level interface is a type this file may not declare, and
|
|
1855
|
+
// the honest confidence for that is the unresolved-heritage one.
|
|
1856
|
+
confidence: this.declaredNames.has(target) ? 1 : 0.7,
|
|
1857
|
+
line_number: this.nodeLine(nameNode),
|
|
1858
|
+
metadata: {
|
|
1859
|
+
[rel === RelationshipType.Extends ? "baseType" : "interfaceName"]: target,
|
|
1860
|
+
heritageForm: "member",
|
|
1861
|
+
resolved: this.declaredNames.has(target),
|
|
1862
|
+
// The reduction is recorded rather than silent, so a reader can
|
|
1863
|
+
// see that `IEnumerable(Of T).GetEnumerator` and
|
|
1864
|
+
// `IEnumerable.GetEnumerator` are the same edge and not two.
|
|
1865
|
+
openGenericArity: this.typeArgumentArity(nameNode)
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* The name node with every `type_argument_list` REMOVED from its own text.
|
|
1874
|
+
*
|
|
1875
|
+
* `qualified_name` is `qualifier . name`, and the qualifier may be a
|
|
1876
|
+
* `generic_name`, so `IEnumerable(Of CustomEvent).GetEnumerator` is
|
|
1877
|
+
* (qualified_name
|
|
1878
|
+
* qualifier: (generic_name name: (identifier IEnumerable)
|
|
1879
|
+
* (type_argument_list (simple_name (identifier))))
|
|
1880
|
+
* name: (simple_name (identifier GetEnumerator)))
|
|
1881
|
+
* The type arguments live inside the qualifier, not beside the target, so the
|
|
1882
|
+
* `(Of …)` has to be removed from the SPAN rather than from a trailing child.
|
|
1883
|
+
*
|
|
1884
|
+
* A char scan with the equivalence argued, and it has to be the same reduction
|
|
1885
|
+
* `calleeLeaf` applies so the two agree: every `type_argument_list` SPAN is
|
|
1886
|
+
* dropped and the text around it is kept, so the span between the wrapper's
|
|
1887
|
+
* start and the list's start, and everything from the last list's end to the
|
|
1888
|
+
* wrapper's end, both survive. A span scan is required because `node.text` is
|
|
1889
|
+
* the only place the parentheses survive — the `type_argument_list` node's own
|
|
1890
|
+
* text is `(Of CustomEvent)` but no field says where inside the qualifier it
|
|
1891
|
+
* sits, and it is the QUALIFIER that is generic, not the target.
|
|
1892
|
+
*
|
|
1893
|
+
* The result is a STRING rather than a synthesised `Node`. An earlier version
|
|
1894
|
+
* returned `{ text } as unknown as Node`, which happened to satisfy the single
|
|
1895
|
+
* caller because it only read `.text` — and a lie that satisfies its one caller
|
|
1896
|
+
* is exactly the kind of thing that survives until a second caller reads
|
|
1897
|
+
* `startIndex` off it. The signature says what it is.
|
|
1898
|
+
*
|
|
1899
|
+
* When there is nothing to remove the node's own text is returned, so the
|
|
1900
|
+
* common case costs one tree walk and no allocation.
|
|
1901
|
+
*/
|
|
1902
|
+
typeArgumentsStrippedText(n) {
|
|
1903
|
+
if (!this.containsTypeArgumentList(n)) return this.nameText(n).trim();
|
|
1904
|
+
const start = n.startIndex - n.startIndex;
|
|
1905
|
+
const end = n.endIndex - n.startIndex;
|
|
1906
|
+
if (!(end > start)) return this.nameText(n).trim();
|
|
1907
|
+
const text = n.text;
|
|
1908
|
+
const spans = [];
|
|
1909
|
+
const walk = (x, depth) => {
|
|
1910
|
+
if (depth > 16) return;
|
|
1911
|
+
if (x.type === "type_argument_list") {
|
|
1912
|
+
spans.push([x.startIndex - n.startIndex, x.endIndex - n.startIndex]);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
for (const c of this.allKids(x)) walk(c, depth + 1);
|
|
1916
|
+
};
|
|
1917
|
+
walk(n, 0);
|
|
1918
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
1919
|
+
let out = "";
|
|
1920
|
+
let cursor = start;
|
|
1921
|
+
for (const [s, e] of spans) {
|
|
1922
|
+
if (s < cursor || e > end) continue;
|
|
1923
|
+
out += text.slice(cursor, s);
|
|
1924
|
+
cursor = e;
|
|
1925
|
+
}
|
|
1926
|
+
out += text.slice(cursor, end);
|
|
1927
|
+
return out.trim();
|
|
1928
|
+
}
|
|
1929
|
+
/** Is there a `type_argument_list` anywhere at or under this node? */
|
|
1930
|
+
containsTypeArgumentList(n) {
|
|
1931
|
+
let found = false;
|
|
1932
|
+
const walk = (x, depth) => {
|
|
1933
|
+
if (found || depth > 16) return;
|
|
1934
|
+
if (x.type === "type_argument_list") {
|
|
1935
|
+
found = true;
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
for (const c of this.allKids(x)) walk(c, depth + 1);
|
|
1939
|
+
};
|
|
1940
|
+
walk(n, 0);
|
|
1941
|
+
return found;
|
|
1942
|
+
}
|
|
1943
|
+
/** How many type arguments the name carries, for the edge's own metadata. */
|
|
1944
|
+
typeArgumentArity(n) {
|
|
1945
|
+
let arity = 0;
|
|
1946
|
+
const walk = (x, depth) => {
|
|
1947
|
+
if (depth > 16) return;
|
|
1948
|
+
if (x.type === "type_argument_list") {
|
|
1949
|
+
arity += x.namedChildCount;
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
for (const c of this.allKids(x)) walk(c, depth + 1);
|
|
1953
|
+
};
|
|
1954
|
+
walk(n, 0);
|
|
1955
|
+
return arity;
|
|
1956
|
+
}
|
|
1957
|
+
pushHeritage(selfName, nameNode, type, selfSym, context) {
|
|
1958
|
+
const token = this.namingToken(nameNode, nameNode.text);
|
|
1959
|
+
const target = this.nameText(token ?? nameNode);
|
|
1960
|
+
if (target.length === 0) return;
|
|
1961
|
+
if (target === selfName) {
|
|
1962
|
+
this.note("self-heritage");
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const inFile = this.declaredNames.has(target);
|
|
1966
|
+
this.relationships.push({
|
|
1967
|
+
source: selfName,
|
|
1968
|
+
target,
|
|
1969
|
+
type,
|
|
1970
|
+
confidence: inFile ? 1 : 0.7,
|
|
1971
|
+
line_number: this.nodeLine(token ?? nameNode),
|
|
1972
|
+
metadata: {
|
|
1973
|
+
[type === RelationshipType.Extends ? "baseType" : "interfaceName"]: target,
|
|
1974
|
+
heritageForm: this.heritageFormOf(nameNode),
|
|
1975
|
+
resolved: inFile
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
/** `line` for a heritage statement's names, `header` for a header-clause name. */
|
|
1980
|
+
heritageFormOf(nameNode) {
|
|
1981
|
+
let cur = nameNode;
|
|
1982
|
+
for (let i = 0; cur !== null && i < 8; i++) {
|
|
1983
|
+
const p = cur.parent;
|
|
1984
|
+
if (p === null) break;
|
|
1985
|
+
if (p.type === "inherits_statement" || p.type === "implements_statement" || p.type === "implements_member_clause") return "statement";
|
|
1986
|
+
if (p.type === "class_declaration" || p.type === "structure_declaration" || p.type === "interface_declaration" || p.type === "module_declaration") return "header";
|
|
1987
|
+
cur = p;
|
|
1988
|
+
}
|
|
1989
|
+
return "unknown";
|
|
1990
|
+
}
|
|
1991
|
+
visitEnum(node, content, context) {
|
|
1992
|
+
const { text } = this.declaredName(node);
|
|
1993
|
+
const sym = this.pushSymbol(text, SymbolKind.Enum, node, {
|
|
1994
|
+
visibility: this.visibilityOf(node, SymbolKind.Enum),
|
|
1995
|
+
metadata: { grammarNode: node.type }
|
|
1996
|
+
});
|
|
1997
|
+
if (sym) {
|
|
1998
|
+
this.typeStack.push(sym);
|
|
1999
|
+
this.ownerStack.push(sym);
|
|
2000
|
+
const enumAsClause = this.namedChildrenOfType(node, "as_clause")[0];
|
|
2001
|
+
const enumDeclared = enumAsClause ? enumAsClause.childForFieldName("declared_type") : null;
|
|
2002
|
+
if (enumDeclared) this.pushTypeReference(sym, enumDeclared, "enumUnderlyingType");
|
|
2003
|
+
}
|
|
2004
|
+
for (const child of this.kids(node)) {
|
|
2005
|
+
if (child.type !== "enum_member_declaration" && child.type !== "variable_declarator") continue;
|
|
2006
|
+
const mName = this.declaredName(child);
|
|
2007
|
+
if (mName.text.length === 0) {
|
|
2008
|
+
this.note("enum-member-without-name");
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
const init = child.childForFieldName("initializer");
|
|
2012
|
+
const member = this.pushSymbol(mName.text, SymbolKind.EnumMember, child, {
|
|
2013
|
+
ownerName: sym ? sym.name : void 0,
|
|
2014
|
+
ownerKind: sym ? sym.kind : void 0,
|
|
2015
|
+
metadata: {
|
|
2016
|
+
grammarNode: child.type,
|
|
2017
|
+
...init ? { value: init.text.trim() } : {},
|
|
2018
|
+
valueLine: init ? this.nodeLine(this.namingToken(init, init.text) ?? init) : null
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
2023
|
+
if (sym) {
|
|
2024
|
+
this.typeStack.pop();
|
|
2025
|
+
this.ownerStack.pop();
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
visitDelegate(node, content, context) {
|
|
2029
|
+
const { text } = this.declaredName(node);
|
|
2030
|
+
const sym = this.pushSymbol(text, SymbolKind.Type, node, {
|
|
2031
|
+
visibility: this.visibilityOf(node, SymbolKind.Type),
|
|
2032
|
+
metadata: { grammarNode: node.type, delegate: true }
|
|
2033
|
+
});
|
|
2034
|
+
if (sym) {
|
|
2035
|
+
this.typeStack.push(sym);
|
|
2036
|
+
this.ownerStack.push(sym);
|
|
2037
|
+
}
|
|
2038
|
+
const params = node.childForFieldName("parameters");
|
|
2039
|
+
if (params && sym) this.readParameters(params, sym);
|
|
2040
|
+
const asClause = this.namedChildrenOfType(node, "as_clause")[0];
|
|
2041
|
+
if (asClause) {
|
|
2042
|
+
const declared = asClause.childForFieldName("declared_type");
|
|
2043
|
+
if (declared) {
|
|
2044
|
+
sym.returnType = this.nameText(declared);
|
|
2045
|
+
this.pushTypeReference(sym, declared, "returnType");
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
2049
|
+
if (sym) {
|
|
2050
|
+
this.typeStack.pop();
|
|
2051
|
+
this.ownerStack.pop();
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
// -------------------------------------------------------------------------
|
|
2055
|
+
// Imports
|
|
2056
|
+
// -------------------------------------------------------------------------
|
|
2057
|
+
/**
|
|
2058
|
+
* `Imports A, B` — one edge PER NAME, each stamped with the line of the name it
|
|
2059
|
+
* writes, and each carrying `isImport` + `include_dependency` so the family's
|
|
2060
|
+
* completeness section-B pin and the tier classifier can mark it import-based.
|
|
2061
|
+
* An import is a name in VB, not a module path, so `module` holds the name and
|
|
2062
|
+
* `isNamespace` is true.
|
|
2063
|
+
*/
|
|
2064
|
+
visitImports(node) {
|
|
2065
|
+
const names = this.nameReferencesOf(node);
|
|
2066
|
+
for (const nameNode of names) {
|
|
2067
|
+
const token = this.namingToken(nameNode, nameNode.text);
|
|
2068
|
+
const written = stripWhitespace(nameNode.text);
|
|
2069
|
+
const text = written.length > 0 ? written : this.nameText(token ?? nameNode);
|
|
2070
|
+
if (text.length === 0) continue;
|
|
2071
|
+
const range = this.range(nameNode);
|
|
2072
|
+
this.imports.push({
|
|
2073
|
+
module: text,
|
|
2074
|
+
items: [],
|
|
2075
|
+
isNamespace: true,
|
|
2076
|
+
isNamespaceImport: true,
|
|
2077
|
+
importType: "namespace",
|
|
2078
|
+
range,
|
|
2079
|
+
metadata: { grammarNode: node.type }
|
|
2080
|
+
});
|
|
2081
|
+
this.relationships.push({
|
|
2082
|
+
source: "root",
|
|
2083
|
+
target: text,
|
|
2084
|
+
type: RelationshipType.Imports,
|
|
2085
|
+
confidence: 1,
|
|
2086
|
+
line_number: this.nodeLine(token ?? nameNode),
|
|
2087
|
+
metadata: {
|
|
2088
|
+
[IS_IMPORT]: true,
|
|
2089
|
+
[INCLUDE_DEPENDENCY]: true,
|
|
2090
|
+
importForm: "imports-statement"
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
// -------------------------------------------------------------------------
|
|
2096
|
+
// Members
|
|
2097
|
+
// -------------------------------------------------------------------------
|
|
2098
|
+
visitMethod(node, content, context) {
|
|
2099
|
+
const isCtor = node.type === "constructor_declaration";
|
|
2100
|
+
const { text, node: nameNode } = this.declaredName(node);
|
|
2101
|
+
if (text.length === 0 && !isCtor) {
|
|
2102
|
+
this.note("method-without-name");
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const owner = this.currentOwner();
|
|
2106
|
+
const mods = this.modifierNames(node);
|
|
2107
|
+
const isDeclare = node.type === "declare_statement";
|
|
2108
|
+
const kind = isCtor ? SymbolKind.Constructor : SymbolKind.Method;
|
|
2109
|
+
const effectiveName = isCtor ? "constructor" : text;
|
|
2110
|
+
const overridesBase = mods.has("overrides");
|
|
2111
|
+
const sym = this.pushSymbol(effectiveName, kind, node, {
|
|
2112
|
+
visibility: this.visibilityOf(node, kind),
|
|
2113
|
+
isStatic: this.isStatic(node),
|
|
2114
|
+
isAbstract: this.isAbstract(node),
|
|
2115
|
+
isAsync: mods.has("async"),
|
|
2116
|
+
...isDeclare ? { metadata: { pInvoke: true } } : {},
|
|
2117
|
+
metadata: {
|
|
2118
|
+
grammarNode: node.type,
|
|
2119
|
+
...isDeclare ? { pInvoke: true } : {},
|
|
2120
|
+
...overridesBase ? { overridesModifier: true } : {},
|
|
2121
|
+
...mods.has("partial") ? { partial: true } : {},
|
|
2122
|
+
// The P/Invoke's target library and entry point are on the DECLARATION,
|
|
2123
|
+
// and they are the only place a VB.NET file names an unmanaged symbol.
|
|
2124
|
+
...isDeclare ? this.readPInvoke(node) : {}
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
if (!sym) return;
|
|
2128
|
+
const params = node.childForFieldName("parameters");
|
|
2129
|
+
if (params && sym) this.readParameters(params, sym);
|
|
2130
|
+
const asClause = this.namedChildrenOfType(node, "as_clause")[0];
|
|
2131
|
+
if (asClause) {
|
|
2132
|
+
const declared = asClause.childForFieldName("declared_type");
|
|
2133
|
+
if (declared) {
|
|
2134
|
+
sym.returnType = this.nameText(declared);
|
|
2135
|
+
this.pushTypeReference(sym, declared, "returnType");
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
this.ownerStack.push(sym);
|
|
2139
|
+
this.visitMemberHeritage(node, sym, context);
|
|
2140
|
+
if (this.modifierNames(node).has("overrides")) this.pushOverride(node, null, sym, this.currentOwner());
|
|
2141
|
+
this.collectBodyEdges(node, sym);
|
|
2142
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
2143
|
+
this.ownerStack.pop();
|
|
2144
|
+
}
|
|
2145
|
+
/** `Lib "user32.dll" Alias "MessageBoxA"` — the unmanaged target of a Declare. */
|
|
2146
|
+
readPInvoke(node) {
|
|
2147
|
+
const out = {};
|
|
2148
|
+
const lib = node.childForFieldName("library");
|
|
2149
|
+
if (lib) out.pInvokeLibrary = lib.text.trim();
|
|
2150
|
+
const alias = node.childForFieldName("alias");
|
|
2151
|
+
if (alias) out.pInvokeEntryPoint = alias.text.trim();
|
|
2152
|
+
const kind = node.childForFieldName("kind");
|
|
2153
|
+
if (kind) out.pInvokeKind = kind.text.trim();
|
|
2154
|
+
return out;
|
|
2155
|
+
}
|
|
2156
|
+
readParameters(params, sym) {
|
|
2157
|
+
const out = [];
|
|
2158
|
+
for (const p of this.kids(params)) {
|
|
2159
|
+
if (p.type !== "parameter") continue;
|
|
2160
|
+
const nameNode = p.childForFieldName("name") ?? (p.namedChildCount > 0 ? this.nc(p, 0) : null);
|
|
2161
|
+
const name = this.nameText(nameNode);
|
|
2162
|
+
if (name.length === 0) continue;
|
|
2163
|
+
const byVal = p.text.includes("ByVal") || p.text.includes("byval");
|
|
2164
|
+
const asClause = this.namedChildrenOfType(p, "as_clause")[0];
|
|
2165
|
+
const declared = asClause ? asClause.childForFieldName("declared_type") : null;
|
|
2166
|
+
const optional = p.text.includes("Optional") || p.text.includes("optional");
|
|
2167
|
+
const param = {
|
|
2168
|
+
name,
|
|
2169
|
+
...declared ? { type: this.nameText(declared) } : {},
|
|
2170
|
+
...optional ? { isOptional: true } : {},
|
|
2171
|
+
defaultValue: this.readDefaultOf(p),
|
|
2172
|
+
metadata: { passingMode: byVal ? "ByVal" : "ByRef" }
|
|
2173
|
+
};
|
|
2174
|
+
out.push(param);
|
|
2175
|
+
if (declared) this.pushTypeReference(sym, declared, "parameter");
|
|
2176
|
+
}
|
|
2177
|
+
if (out.length > 0) sym.parameters = out;
|
|
2178
|
+
}
|
|
2179
|
+
/** An `= default` on a parameter, read off the node's own text span. */
|
|
2180
|
+
readDefaultOf(p) {
|
|
2181
|
+
const idx = p.text.indexOf("=");
|
|
2182
|
+
if (idx < 0) return void 0;
|
|
2183
|
+
const tail = p.text.slice(idx + 1).trim();
|
|
2184
|
+
return tail.length > 0 ? tail : void 0;
|
|
2185
|
+
}
|
|
2186
|
+
visitProperty(node, content, context) {
|
|
2187
|
+
const { text, node: nameNode } = this.declaredName(node);
|
|
2188
|
+
if (text.length === 0) {
|
|
2189
|
+
this.note("property-without-name");
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
const mods = this.modifierNames(node);
|
|
2193
|
+
const sym = this.pushSymbol(text, SymbolKind.Property, node, {
|
|
2194
|
+
visibility: this.visibilityOf(node, SymbolKind.Property),
|
|
2195
|
+
isStatic: this.isStatic(node),
|
|
2196
|
+
isAbstract: this.isAbstract(node),
|
|
2197
|
+
metadata: {
|
|
2198
|
+
grammarNode: node.type,
|
|
2199
|
+
...mods.has("readonly") ? { readOnly: true } : {},
|
|
2200
|
+
...mods.has("writeonly") ? { writeOnly: true } : {},
|
|
2201
|
+
...mods.has("default") ? { defaultMember: true } : {},
|
|
2202
|
+
...mods.has("overrides") ? { overridesModifier: true } : {},
|
|
2203
|
+
autoImplemented: this.namedChildrenOfType(node, "property_getter").length === 0
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
if (!sym) return;
|
|
2207
|
+
const asClause = this.namedChildrenOfType(node, "as_clause")[0];
|
|
2208
|
+
if (asClause) {
|
|
2209
|
+
const declared = asClause.childForFieldName("declared_type");
|
|
2210
|
+
if (declared) {
|
|
2211
|
+
sym.returnType = this.nameText(declared);
|
|
2212
|
+
this.pushTypeReference(sym, declared, "propertyType");
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
const owner = this.currentOwner();
|
|
2216
|
+
this.ownerStack.push(sym);
|
|
2217
|
+
this.visitMemberHeritage(node, sym, context);
|
|
2218
|
+
if (this.modifierNames(node).has("overrides")) this.pushOverride(node, null, sym, this.currentOwner());
|
|
2219
|
+
this.collectBodyEdges(node, sym);
|
|
2220
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
2221
|
+
this.ownerStack.pop();
|
|
2222
|
+
}
|
|
2223
|
+
visitEvent(node, content, context) {
|
|
2224
|
+
const { text, node: nameNode } = this.declaredName(node);
|
|
2225
|
+
if (text.length === 0) {
|
|
2226
|
+
this.note("event-without-name");
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const sym = this.pushSymbol(text, SymbolKind.Event, node, {
|
|
2230
|
+
visibility: this.visibilityOf(node, SymbolKind.Event),
|
|
2231
|
+
isStatic: this.isStatic(node),
|
|
2232
|
+
metadata: { grammarNode: node.type, withEvents: this.modifierNames(node).has("withevents") }
|
|
2233
|
+
});
|
|
2234
|
+
if (!sym) return;
|
|
2235
|
+
const owner = this.currentOwner();
|
|
2236
|
+
const asClause = this.namedChildrenOfType(node, "as_clause")[0];
|
|
2237
|
+
if (asClause) {
|
|
2238
|
+
const declared = asClause.childForFieldName("declared_type");
|
|
2239
|
+
if (declared) {
|
|
2240
|
+
sym.returnType = this.nameText(declared);
|
|
2241
|
+
this.pushTypeReference(sym, declared, "eventType");
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
this.ownerStack.push(sym);
|
|
2245
|
+
this.visitMemberHeritage(node, sym, context);
|
|
2246
|
+
if (this.modifierNames(node).has("overrides")) this.pushOverride(node, null, sym, this.currentOwner());
|
|
2247
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
2248
|
+
this.ownerStack.pop();
|
|
2249
|
+
}
|
|
2250
|
+
visitOperator(node, content, context) {
|
|
2251
|
+
const STRUCTURAL = /* @__PURE__ */ new Set(["(", ")", ",", ":", "=", "New", "Operator"]);
|
|
2252
|
+
let opName = "";
|
|
2253
|
+
for (const c of this.allKids(node)) {
|
|
2254
|
+
if (c.isNamed) continue;
|
|
2255
|
+
const t = c.text.trim();
|
|
2256
|
+
if (t.length === 0) continue;
|
|
2257
|
+
if (STRUCTURAL.has(t)) continue;
|
|
2258
|
+
if (c.type === "=") continue;
|
|
2259
|
+
if (_RealVbAnalyzer.isWordLike(t) && _RealVbAnalyzer.isKeyword(t)) continue;
|
|
2260
|
+
opName = t;
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
const sym = this.pushSymbol("operator", SymbolKind.Method, node, {
|
|
2264
|
+
visibility: this.visibilityOf(node, ASTEngine.SymbolKind.Method),
|
|
2265
|
+
isStatic: this.isStatic(node),
|
|
2266
|
+
metadata: {
|
|
2267
|
+
grammarNode: node.type,
|
|
2268
|
+
...opName.length > 0 ? { operatorSymbol: opName } : {}
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
if (!sym) return;
|
|
2272
|
+
const opParams = node.childForFieldName("parameters");
|
|
2273
|
+
if (opParams) this.readParameters(opParams, sym);
|
|
2274
|
+
const opAs = this.namedChildrenOfType(node, "as_clause")[0];
|
|
2275
|
+
if (opAs) {
|
|
2276
|
+
const declared = opAs.childForFieldName("declared_type");
|
|
2277
|
+
if (declared) {
|
|
2278
|
+
sym.returnType = this.nameText(declared);
|
|
2279
|
+
this.pushTypeReference(sym, declared, "operatorReturnType");
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
this.ownerStack.push(sym);
|
|
2283
|
+
this.collectBodyEdges(node, sym);
|
|
2284
|
+
for (const child of this.kids(node)) this.walk(child, content, context);
|
|
2285
|
+
this.ownerStack.pop();
|
|
2286
|
+
}
|
|
2287
|
+
/** Does this token start with a letter or underscore? */
|
|
2288
|
+
static isWordLike(t) {
|
|
2289
|
+
const c = t.charCodeAt(0);
|
|
2290
|
+
return Number.isFinite(c) && (c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95);
|
|
2291
|
+
}
|
|
2292
|
+
/** A char-set membership test, not a keyword list lookup by regex. */
|
|
2293
|
+
static isKeyword(t) {
|
|
2294
|
+
let i = 0;
|
|
2295
|
+
let sawAlpha = false;
|
|
2296
|
+
for (; i < t.length; i++) {
|
|
2297
|
+
const c = t.charCodeAt(i);
|
|
2298
|
+
const alpha = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95;
|
|
2299
|
+
if (!alpha) break;
|
|
2300
|
+
sawAlpha = true;
|
|
2301
|
+
}
|
|
2302
|
+
if (!sawAlpha) return true;
|
|
2303
|
+
return _RealVbAnalyzer.VB_KEYWORD_HEADS.has(t.slice(0, i).toLowerCase());
|
|
2304
|
+
}
|
|
2305
|
+
static VB_KEYWORD_HEADS = /* @__PURE__ */ new Set([
|
|
2306
|
+
"public",
|
|
2307
|
+
"private",
|
|
2308
|
+
"protected",
|
|
2309
|
+
"friend",
|
|
2310
|
+
"shared",
|
|
2311
|
+
"overridable",
|
|
2312
|
+
"overrides",
|
|
2313
|
+
"notoverridable",
|
|
2314
|
+
"mustoverride",
|
|
2315
|
+
"notinheritable",
|
|
2316
|
+
"mustinherit",
|
|
2317
|
+
"partial",
|
|
2318
|
+
"readonly",
|
|
2319
|
+
"writeonly",
|
|
2320
|
+
"default",
|
|
2321
|
+
"withevents",
|
|
2322
|
+
"shadows",
|
|
2323
|
+
"overloads",
|
|
2324
|
+
"widening",
|
|
2325
|
+
"narrowing",
|
|
2326
|
+
"async",
|
|
2327
|
+
"iterator",
|
|
2328
|
+
"static",
|
|
2329
|
+
"const"
|
|
2330
|
+
]);
|
|
2331
|
+
visitField(node, content) {
|
|
2332
|
+
const mods = this.modifierNames(node);
|
|
2333
|
+
const isConst = mods.has("const");
|
|
2334
|
+
const owner = this.currentOwner();
|
|
2335
|
+
for (const decl of this.kids(node)) {
|
|
2336
|
+
if (decl.type !== "variable_declarator") continue;
|
|
2337
|
+
const { text, node: nameNode } = this.declaredName(decl);
|
|
2338
|
+
if (text.length === 0) {
|
|
2339
|
+
this.note("field-without-name");
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
const init = decl.childForFieldName("initializer");
|
|
2343
|
+
const asClause = this.namedChildrenOfType(decl, "as_clause")[0];
|
|
2344
|
+
const declared = asClause ? asClause.childForFieldName("declared_type") : null;
|
|
2345
|
+
const sym = this.pushSymbol(text, isConst ? SymbolKind.Constant : SymbolKind.Variable, decl, {
|
|
2346
|
+
visibility: this.visibilityOf(node, isConst ? SymbolKind.Constant : SymbolKind.Variable),
|
|
2347
|
+
isStatic: this.isStatic(node) || isConst || mods.has("shared"),
|
|
2348
|
+
...declared ? { returnType: this.nameText(declared) } : {},
|
|
2349
|
+
...init ? { metadata: { initializer: init.text.trim() } } : {},
|
|
2350
|
+
metadata: {
|
|
2351
|
+
grammarNode: decl.type,
|
|
2352
|
+
...isConst ? { constant: true } : {},
|
|
2353
|
+
...mods.has("withevents") ? { withEvents: true } : {},
|
|
2354
|
+
...mods.has("readonly") ? { readOnly: true } : {},
|
|
2355
|
+
fieldLine: this.nodeLine(decl)
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
if (!sym) continue;
|
|
2359
|
+
if (declared) this.pushTypeReference(sym, declared, "fieldType");
|
|
2360
|
+
if (asClause && this.asClauseIsConstruction(asClause)) {
|
|
2361
|
+
const tyNode = declared ?? asClause;
|
|
2362
|
+
this.pushConstruction(tyNode, owner ?? sym);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
this.collectBodyEdges(node, null);
|
|
2366
|
+
}
|
|
2367
|
+
// -------------------------------------------------------------------------
|
|
2368
|
+
// Calls, instantiations, member reads and type references
|
|
2369
|
+
// -------------------------------------------------------------------------
|
|
2370
|
+
/**
|
|
2371
|
+
* Every body edge in a subtree: `calls`, `instantiates` and `uses`, all stamped
|
|
2372
|
+
* with the line of the node that NAMES the thing.
|
|
2373
|
+
*
|
|
2374
|
+
* ## THE CALL SHAPE THIS GRAMMAR ACTUALLY PRODUCES, and why `calls` was silent
|
|
2375
|
+
*
|
|
2376
|
+
* Measured through the pinned wasm, minimal repro, tree-only:
|
|
2377
|
+
*
|
|
2378
|
+
* Foo() -> invocation_expression
|
|
2379
|
+
* Foo(1) -> array_access_expression <- with ARGUMENTS
|
|
2380
|
+
* obj.Foo(1) -> array_access_expression
|
|
2381
|
+
* a(i) -> array_access_expression <- an INDEX
|
|
2382
|
+
* New Foo(1) -> array_creation_expression
|
|
2383
|
+
*
|
|
2384
|
+
* **A VB call with arguments is an `array_access_expression`, and this analyzer
|
|
2385
|
+
* read only `invocation_expression`.** Every argument-bearing call in the
|
|
2386
|
+
* language was therefore un-published. Measured over 636 corpus files:
|
|
2387
|
+
*
|
|
2388
|
+
* invocation_expression 5,120 (the only shape read, before)
|
|
2389
|
+
* array_access_expression 21,392 (not read at all)
|
|
2390
|
+
*
|
|
2391
|
+
* A 100-line file published 3 call edges from ~46 member accesses and calls. The
|
|
2392
|
+
* F5 differential could not see it: the lane's oracle block was removed from
|
|
2393
|
+
* `script/m14/differential.ts` (the lane has 4 DERIVATION findings — calls,
|
|
2394
|
+
* contains, imports, references — which is the gate's word for "compared against
|
|
2395
|
+
* nothing"), and a whole-corpus COUNT cannot see a missing class of edge either,
|
|
2396
|
+
* because the counts stayed healthy.
|
|
2397
|
+
*
|
|
2398
|
+
* ## WHY NOT EVERY `array_access_expression` IS A CALL
|
|
2399
|
+
*
|
|
2400
|
+
* The grammar cannot tell `Foo(1)` (a call) from `a(i)` (an index). Emitting an
|
|
2401
|
+
* edge for all of them would be a fabricated call edge, which the workstream
|
|
2402
|
+
* calls out as the most expensive mistake available. So two SIGNALS are
|
|
2403
|
+
* required, and a site with neither is COUNTED in `vbGaps` rather than guessed:
|
|
2404
|
+
*
|
|
2405
|
+
* A. The callee names something THIS FILE DECLARES CALLABLE -- a method, a
|
|
2406
|
+
* property, a constructor, an operator or a delegate -- and that declaration
|
|
2407
|
+
* is NOT array-valued. An index needs an array, so a name this file declares
|
|
2408
|
+
* as a method is a call and cannot be an index. The array-valued exclusion is
|
|
2409
|
+
* read off the declaration's own `as_clause` -> `array_type`, because
|
|
2410
|
+
* `SomePropertyReturningAnArray(0)` is legal VB and would otherwise be
|
|
2411
|
+
* published as a call to an array.
|
|
2412
|
+
*
|
|
2413
|
+
* B. The node is the WHOLE content of an `expression_statement` -- a call
|
|
2414
|
+
* statement -- AND the callee name is not a variable/field this file
|
|
2415
|
+
* declares. VB has no statement-level index expression, so a parenthesised
|
|
2416
|
+
* name at statement level is a call; excluding the file's own variables
|
|
2417
|
+
* removes the `arr(0)` case where `arr` is declared locally.
|
|
2418
|
+
*
|
|
2419
|
+
* Measured over 636 files: 11,269 of the 21,392 carry at least one signal. The
|
|
2420
|
+
* other 10,123 are counted as `index-or-call-ambiguous` and stay un-emitted,
|
|
2421
|
+
* because guessing between an index and a call is exactly the fabrication the
|
|
2422
|
+
* ontology forbids.
|
|
2423
|
+
*
|
|
2424
|
+
* ## WHAT ELSE IS DECIDED HERE, each a real question
|
|
2425
|
+
*
|
|
2426
|
+
* - WHICH node names the callee. For `a.b.C(1)` the callee is the `C` leaf, so
|
|
2427
|
+
* the edge is stamped on the line `C` is written on, not the line the
|
|
2428
|
+
* statement started on. A three-line chained call then has one site line.
|
|
2429
|
+
* - WHICH node is the SOURCE. The innermost enclosing declaration, not the
|
|
2430
|
+
* file: an edge from `root` to a callee cannot be navigated to a method.
|
|
2431
|
+
* - WHEN there is no source, there is no edge. A call in a field initializer
|
|
2432
|
+
* has no enclosing method, and an edge from nothing is a phantom — so it is
|
|
2433
|
+
* counted as a gap instead of invented.
|
|
2434
|
+
* - `uses` DEDUPLICATES per (source, target) per file, keeping the FIRST site.
|
|
2435
|
+
* That is the family's own choice for this label (csharp's CS-D4 field-access
|
|
2436
|
+
* edge does exactly this) and it is a payload decision as much as a semantic
|
|
2437
|
+
* one: 536,141 member reads exist in the corpus and `uses` at one edge per
|
|
2438
|
+
* site would more than double this lane's whole graph.
|
|
2439
|
+
*/
|
|
2440
|
+
collectBodyEdges(scope, owner) {
|
|
2441
|
+
const source = owner ?? this.currentOwner();
|
|
2442
|
+
const scopeId = scope.id;
|
|
2443
|
+
const visit = (n) => {
|
|
2444
|
+
if (n.id !== scopeId && _RealVbAnalyzer.DECLARATION_NODES.has(n.type)) {
|
|
2445
|
+
for (const c of this.kids(n)) visit(c);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (n.type === "variable_declarator" && source) {
|
|
2449
|
+
let p = n.parent;
|
|
2450
|
+
let fieldOwned = false;
|
|
2451
|
+
for (let guard = 0; guard < 8 && p; guard++, p = p.parent) {
|
|
2452
|
+
if (p.type === "field_declaration") {
|
|
2453
|
+
fieldOwned = true;
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
if (p.type === "declaration_statement" || p.type === "using_statement" || p.type === "for_each_statement") break;
|
|
2457
|
+
}
|
|
2458
|
+
if (!fieldOwned) {
|
|
2459
|
+
const asClause = this.namedChildrenOfType(n, "as_clause")[0];
|
|
2460
|
+
if (asClause && this.asClauseIsConstruction(asClause)) {
|
|
2461
|
+
this.pushConstruction(asClause.childForFieldName("declared_type") ?? asClause, source);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
if (n.type === "array_creation_expression") {
|
|
2466
|
+
this.pushConstruction(n, source);
|
|
2467
|
+
for (const c of this.kids(n)) visit(c);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
if (n.type === "member_access_expression" || n.type === "with_member_access_expression") {
|
|
2471
|
+
if (!this.isCalleeOrIndexSlot(n) && !this.isAssignmentLhs(n) && !this.inAttributePosition(n)) {
|
|
2472
|
+
const token = this.calleeLeaf(n);
|
|
2473
|
+
const target = this.nameText(token);
|
|
2474
|
+
if (target.length > 0) this.pushUse(source, target, n, token);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (n.type === "invocation_expression") {
|
|
2478
|
+
const fn = n.childForFieldName("function");
|
|
2479
|
+
const args = n.childForFieldName("arguments");
|
|
2480
|
+
if (fn && !this.calleeIsConstruction(fn)) {
|
|
2481
|
+
const token = this.calleeLeaf(fn);
|
|
2482
|
+
const target = this.nameText(token);
|
|
2483
|
+
if (target.length > 0) {
|
|
2484
|
+
if (source) {
|
|
2485
|
+
const argCount = args ? args.namedChildCount : 0;
|
|
2486
|
+
this.relationships.push({
|
|
2487
|
+
source: source.name,
|
|
2488
|
+
target,
|
|
2489
|
+
type: RelationshipType.Calls,
|
|
2490
|
+
confidence: this.declaredNames.has(target) ? 1 : 0.6,
|
|
2491
|
+
line_number: this.nodeLine(token ?? fn),
|
|
2492
|
+
metadata: {
|
|
2493
|
+
// The argument count is read off the AST's own child list, so
|
|
2494
|
+
// it excludes `comment` extras rather than counting them the
|
|
2495
|
+
// way a text scan of the parens would.
|
|
2496
|
+
argumentCount: argCount,
|
|
2497
|
+
calleeIsQualified: fn.type === "member_access_expression",
|
|
2498
|
+
callLine: this.nodeLine(token ?? fn),
|
|
2499
|
+
constructed: fn.type === "object_creation_expression",
|
|
2500
|
+
...fn.type === "object_creation_expression" ? { constructionNode: fn.type } : {}
|
|
2501
|
+
}
|
|
2502
|
+
});
|
|
2503
|
+
if (fn.type === "object_creation_expression") {
|
|
2504
|
+
this.pushInstantiation(source, target, token ?? fn, fn.type);
|
|
2505
|
+
}
|
|
2506
|
+
this.callCalleeNodes.add(fn.id);
|
|
2507
|
+
} else {
|
|
2508
|
+
this.note("call-without-enclosing-declaration");
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (n.type === "object_creation_expression") {
|
|
2514
|
+
this.pushConstruction(n, source);
|
|
2515
|
+
}
|
|
2516
|
+
if (n.type === "array_access_expression") {
|
|
2517
|
+
const calleeNode = this.nc(n, 0);
|
|
2518
|
+
const token = calleeNode ? this.calleeLeaf(calleeNode) : null;
|
|
2519
|
+
const target = this.nameText(token);
|
|
2520
|
+
if (target.length > 0) {
|
|
2521
|
+
const argCount = n.namedChildCount > 1 ? n.namedChildCount - 1 : 0;
|
|
2522
|
+
const viaDeclaration = this.callablesInFile.has(target) && !this.arrayValuedInFile.has(target);
|
|
2523
|
+
const viaStatement = this.isStatementLevelCall(n) && !this.variablesInFile.has(target) && !this.localsInFile.has(target);
|
|
2524
|
+
if (viaDeclaration || viaStatement) {
|
|
2525
|
+
if (source) {
|
|
2526
|
+
this.relationships.push({
|
|
2527
|
+
source: source.name,
|
|
2528
|
+
target,
|
|
2529
|
+
type: RelationshipType.Calls,
|
|
2530
|
+
confidence: viaDeclaration && this.declaredNames.has(target) ? 1 : 0.7,
|
|
2531
|
+
line_number: this.nodeLine(token ?? n),
|
|
2532
|
+
metadata: {
|
|
2533
|
+
argumentCount: argCount,
|
|
2534
|
+
calleeIsQualified: calleeNode?.type === "member_access_expression",
|
|
2535
|
+
callLine: this.nodeLine(token ?? n),
|
|
2536
|
+
constructed: false,
|
|
2537
|
+
// `callShape` names the node the grammar gave this call, because
|
|
2538
|
+
// `array_access_expression` is not what a reader expects to see on
|
|
2539
|
+
// a `calls` edge and a consumer filtering on the node type would
|
|
2540
|
+
// otherwise conclude the edge is an index.
|
|
2541
|
+
callShape: n.type,
|
|
2542
|
+
callSignal: viaDeclaration ? "callee-declared-callable-in-file" : "call-statement"
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
if (calleeNode) this.callCalleeNodes.add(calleeNode.id);
|
|
2546
|
+
} else {
|
|
2547
|
+
this.note("call-without-enclosing-declaration");
|
|
2548
|
+
}
|
|
2549
|
+
} else {
|
|
2550
|
+
this.note("index-or-call-ambiguous");
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (n.type === "member_access_expression" || n.type === "with_member_access_expression") {
|
|
2555
|
+
const leaf = this.calleeLeaf(n);
|
|
2556
|
+
const target = this.nameText(leaf);
|
|
2557
|
+
if (source && target.length > 0 && !this.isCalleePosition(n) && !this.isAssignmentLhs(n) && !this.inAttributePosition(n)) {
|
|
2558
|
+
const key = `${source.name}->${target}`;
|
|
2559
|
+
const line = this.nodeLine(leaf ?? n);
|
|
2560
|
+
const prior = this.usesEmitted.get(key);
|
|
2561
|
+
if (prior === void 0) {
|
|
2562
|
+
this.usesEmitted.set(key, { key, line_number: line });
|
|
2563
|
+
this.relationships.push({
|
|
2564
|
+
source: source.name,
|
|
2565
|
+
target,
|
|
2566
|
+
type: RelationshipType.Uses,
|
|
2567
|
+
confidence: this.declaredNames.has(target) ? 0.9 : 0.7,
|
|
2568
|
+
line_number: line,
|
|
2569
|
+
metadata: {
|
|
2570
|
+
relationshipType: "vb_member_read",
|
|
2571
|
+
readKind: n.type,
|
|
2572
|
+
receiverIsQualified: this.calleeLeafIsNotFirst(n),
|
|
2573
|
+
// The reason code the honesty contract §4 requires for any
|
|
2574
|
+
// confidence below 1.0: without it, 0.7 is a number with no
|
|
2575
|
+
// machine-readable reason attached.
|
|
2576
|
+
resolvedInFile: this.declaredNames.has(target)
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
} else if (line !== null && line < (prior.line_number ?? Number.MAX_SAFE_INTEGER)) {
|
|
2580
|
+
prior.line_number = line;
|
|
2581
|
+
this.relineUses(key, line);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
for (const c of this.kids(n)) visit(c);
|
|
2586
|
+
};
|
|
2587
|
+
visit(scope);
|
|
2588
|
+
}
|
|
2589
|
+
/** One `instantiates` edge, sourced at the declaration that constructs. */
|
|
2590
|
+
pushInstantiation(source, target, nameNode, nodeType) {
|
|
2591
|
+
this.relationships.push({
|
|
2592
|
+
source: source.name,
|
|
2593
|
+
target,
|
|
2594
|
+
type: RelationshipType.Instantiates,
|
|
2595
|
+
confidence: this.declaredNames.has(target) ? 1 : 0.6,
|
|
2596
|
+
line_number: this.nodeLine(nameNode),
|
|
2597
|
+
metadata: {
|
|
2598
|
+
isNew: true,
|
|
2599
|
+
constructedType: target,
|
|
2600
|
+
// `New Integer(10)` is an ARRAY construction and the grammar reports it as
|
|
2601
|
+
// `array_creation_expression`. Recording that rather than hiding it is the
|
|
2602
|
+
// difference between "instantiates Integer" being a fact a reader can check
|
|
2603
|
+
// and being a claim they have to take on trust.
|
|
2604
|
+
constructionNode: nodeType,
|
|
2605
|
+
constructsArray: nodeType === "array_creation_expression",
|
|
2606
|
+
resolvedInFile: this.declaredNames.has(target)
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
/** Re-stamp an already-published `uses` edge with an earlier site line. */
|
|
2611
|
+
relineUses(key, line) {
|
|
2612
|
+
for (const r of this.relationships) {
|
|
2613
|
+
if (r.type !== RelationshipType.Uses) continue;
|
|
2614
|
+
if (`${r.source}->${r.target}` === key) {
|
|
2615
|
+
r.line_number = line;
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Is this `array_access_expression` the whole content of an expression
|
|
2622
|
+
* statement? `CheckMemberAndReportDiagnostic(context, member)` is a CALL
|
|
2623
|
+
* statement; `x = a(i)` is an index inside an assignment. VB has no
|
|
2624
|
+
* statement-level index expression, so the shape decides — and the caller adds
|
|
2625
|
+
* the second half of the test (the callee is not one of this file's own
|
|
2626
|
+
* variables), which is what removes `arr(0)`.
|
|
2627
|
+
*/
|
|
2628
|
+
isStatementLevelCall(n) {
|
|
2629
|
+
const p = n.parent;
|
|
2630
|
+
if (!p) return false;
|
|
2631
|
+
if (p.type !== "expression_statement") return false;
|
|
2632
|
+
return p.namedChildCount === 1;
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Is this member access (or the chain above it) the CALLEE of an invocation?
|
|
2636
|
+
*
|
|
2637
|
+
* `obj.M()` is `invocation_expression(function: member_access_expression)`. And
|
|
2638
|
+
* `obj.M(args)` — the shape this grammar produces for a call WITH arguments — is
|
|
2639
|
+
* an `array_access_expression` whose first named child is the member access, so
|
|
2640
|
+
* the climb has to pass through `array_access_expression` as well or every
|
|
2641
|
+
* chained call's leading segment is published as a read of the wrong member.
|
|
2642
|
+
*/
|
|
2643
|
+
isCalleePosition(n) {
|
|
2644
|
+
if (this.callCalleeNodes.has(n.id)) return true;
|
|
2645
|
+
let a = n;
|
|
2646
|
+
for (let i = 0; a !== null && i < 12; i++) {
|
|
2647
|
+
const p = a.parent;
|
|
2648
|
+
if (!p) return false;
|
|
2649
|
+
if (p.type === "member_access_expression" || p.type === "with_member_access_expression" || p.type === "array_access_expression" || p.type === "parenthesized_expression") {
|
|
2650
|
+
a = p;
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
if (p.type === "invocation_expression") {
|
|
2654
|
+
const fn = p.childForFieldName("function");
|
|
2655
|
+
return fn !== null && fn !== void 0 && fn.id === a.id;
|
|
2656
|
+
}
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2659
|
+
return false;
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Is this identifier on the LEFT of an assignment? `this.count = 0` is a write,
|
|
2663
|
+
* not a read — the family's SCD1 contract, shared by java JAD1, kotlin KAD1 and
|
|
2664
|
+
* scala SCD1. Containment is by SPAN, so `this.count = this.count + 1` keeps its
|
|
2665
|
+
* right-hand read.
|
|
2666
|
+
*/
|
|
2667
|
+
isAssignmentLhs(n) {
|
|
2668
|
+
let a = n.parent;
|
|
2669
|
+
for (let i = 0; a !== null && i < 6; i++) {
|
|
2670
|
+
if (a.type === "member_access_expression" || a.type === "with_member_access_expression" || a.type === "identifier" || a.type === "array_access_expression") {
|
|
2671
|
+
a = a.parent;
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
break;
|
|
2675
|
+
}
|
|
2676
|
+
if (!a) return false;
|
|
2677
|
+
if (a.type !== "assignment_expression" && a.type !== "assignment_statement") return false;
|
|
2678
|
+
const left = a.childForFieldName("left") ?? a.childForFieldName("target");
|
|
2679
|
+
if (!left) return false;
|
|
2680
|
+
return left.startIndex <= n.startIndex && left.endIndex >= n.endIndex;
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Is this member access inside an ATTRIBUTE rather than inside code?
|
|
2684
|
+
*
|
|
2685
|
+
* `<Obsolete(MefConstruction.ImportingConstructorMessage, True)>` on a
|
|
2686
|
+
* constructor is read by the COMPILER at type-load time, not by the constructor,
|
|
2687
|
+
* so `uses -> constructor -> ImportingConstructorMessage` is not a read the
|
|
2688
|
+
* constructor performs. The lane's differential reported exactly these three rows
|
|
2689
|
+
* as UNEXPLAINED, on three separate corpus files.
|
|
2690
|
+
*/
|
|
2691
|
+
inAttributePosition(n) {
|
|
2692
|
+
let a = n.parent;
|
|
2693
|
+
for (let i = 0; a !== null && i < 12; i++) {
|
|
2694
|
+
if (a.type === "attribute_list" || a.type === "attribute" || a.type === "attribute_argument_list" || a.type === "global_attribute_section") return true;
|
|
2695
|
+
if (a.type === "identifier" || a.type === "simple_name" || a.type === "qualified_name" || a.type === "global_qualified_name" || a.type === "generic_name" || a.type === "member_access_expression" || a.type === "with_member_access_expression" || a.type === "named_argument") {
|
|
2696
|
+
a = a.parent;
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
return false;
|
|
2700
|
+
}
|
|
2701
|
+
return false;
|
|
2702
|
+
}
|
|
2703
|
+
/** Is the accessed member the SECOND half of the chain, i.e. is there a receiver? */
|
|
2704
|
+
calleeLeafIsNotFirst(n) {
|
|
2705
|
+
return this.namedChildrenOfType(n, "identifier").length > 1;
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Is the thing being invoked a CONSTRUCTION?
|
|
2709
|
+
*
|
|
2710
|
+
* Read down the receiver chain to the leftmost node: `New Foo()` is an
|
|
2711
|
+
* `invocation_expression` whose `function` is the `object_creation_expression`
|
|
2712
|
+
* itself, and `New System.Drawing.Size()` is an `invocation_expression` whose
|
|
2713
|
+
* `function` is a three-deep `member_access_expression` whose innermost
|
|
2714
|
+
* `object` is the construction. Both are constructions the grammar wrapped in
|
|
2715
|
+
* an invocation, and both already produce an `instantiates` edge naming the
|
|
2716
|
+
* same type, so publishing a `calls` edge as well would count one syntactic
|
|
2717
|
+
* event twice under two labels.
|
|
2718
|
+
*/
|
|
2719
|
+
calleeIsConstruction(fn) {
|
|
2720
|
+
let cur = fn;
|
|
2721
|
+
for (let guard = 0; guard < 32; guard++) {
|
|
2722
|
+
if (cur.type === "object_creation_expression" || cur.type === "array_creation_expression") return true;
|
|
2723
|
+
if (cur.type !== "member_access_expression" && cur.type !== "with_member_access_expression") return false;
|
|
2724
|
+
const obj = cur.childForFieldName("object");
|
|
2725
|
+
if (!obj || obj.id === cur.id) return false;
|
|
2726
|
+
cur = obj;
|
|
2727
|
+
}
|
|
2728
|
+
return false;
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Is this qualified access the thing being invoked, or the thing being indexed?
|
|
2732
|
+
*
|
|
2733
|
+
* `function` is the callee slot and `array` is the indexed slot, and those two
|
|
2734
|
+
* are the only positions in this grammar where a name is being APPLIED rather
|
|
2735
|
+
* than read. `with_member_access_expression` has neither field -- the census
|
|
2736
|
+
* measured 1,017 of them and 1,017 with no `member` field either -- so it
|
|
2737
|
+
* always reads, which is the right answer for a `With` block: `.Value = 1`
|
|
2738
|
+
* inside `With obj` reads `Value` off `obj`, it does not call it.
|
|
2739
|
+
*/
|
|
2740
|
+
isCalleeOrIndexSlot(n) {
|
|
2741
|
+
const p = n.parent;
|
|
2742
|
+
if (!p) return false;
|
|
2743
|
+
if (p.type === "with_member_access_expression") return false;
|
|
2744
|
+
const fn = p.childForFieldName("function");
|
|
2745
|
+
if (fn && fn.id === n.id) return true;
|
|
2746
|
+
const arr = p.childForFieldName("array");
|
|
2747
|
+
if (arr && arr.id === n.id) return true;
|
|
2748
|
+
return false;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* `uses`: the enclosing declaration READS this name.
|
|
2752
|
+
*
|
|
2753
|
+
* VB HAS the construct -- it is the commonest expression in the language -- so
|
|
2754
|
+
* this is a GAP and not a ruling. Measured on the lane corpus: 323,540
|
|
2755
|
+
* `member_access_expression` and 1,017 `with_member_access_expression` nodes
|
|
2756
|
+
* are a name read rather than a callee or an index, in 1,921 of 2,658 files.
|
|
2757
|
+
*
|
|
2758
|
+
* WHAT THE TARGET IS. The NAME the file writes, which is the last identifier
|
|
2759
|
+
* leaf of the access chain -- `TimeSpan.FromSeconds` gives `FromSeconds`, and
|
|
2760
|
+
* `System.Collections.Generic.List(Of String)` gives `List` because
|
|
2761
|
+
* `calleeLeaf` skips a `type_argument_list`. That is the same name the
|
|
2762
|
+
* `calls` edge would carry for the same expression, so the two labels are
|
|
2763
|
+
* comparable and a consumer can ask either question of the same grammar.
|
|
2764
|
+
*
|
|
2765
|
+
* WHAT THE TARGET IS NOT. A resolved symbol. A qualified read names a member
|
|
2766
|
+
* of a type this file usually does not declare, so the edge resolves at both
|
|
2767
|
+
* ends about as often as a `calls` edge does (12.0% measured on the current
|
|
2768
|
+
* `calls` population) and that is the honest number for a cross-file name, not
|
|
2769
|
+
* a defect. The family convention agrees: `real-csharp-analyzer.ts` publishes
|
|
2770
|
+
* its identifier reads as `uses` at confidence 1.0 with the same meaning --
|
|
2771
|
+
* the name is written in the file, so the READ is a fact, and whether the
|
|
2772
|
+
* name binds to something elsewhere is a resolution question the edge does not
|
|
2773
|
+
* claim to have answered.
|
|
2774
|
+
*
|
|
2775
|
+
* DEDUPLICATED PER (source, target), keeping the FIRST site, with the site
|
|
2776
|
+
* count on the edge. This matches the family's own `uses` emission (csharp
|
|
2777
|
+
* keys on `source::uses::name`) and it is what makes the class affordable:
|
|
2778
|
+
* 323,540 read sites collapse to the distinct pairs. `sites` keeps the raw
|
|
2779
|
+
* count visible, so the dedup hides nothing.
|
|
2780
|
+
*/
|
|
2781
|
+
pushUse(source, target, n, token) {
|
|
2782
|
+
const src = source ?? this.currentOwner();
|
|
2783
|
+
if (!src) {
|
|
2784
|
+
this.note("use-without-enclosing-declaration");
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
const key = `${src.name}\0${target}`;
|
|
2788
|
+
const line = this.nodeLine(token ?? n);
|
|
2789
|
+
const prior = this.useIndex.get(key);
|
|
2790
|
+
if (prior !== void 0) {
|
|
2791
|
+
prior.sites++;
|
|
2792
|
+
prior.edge.metadata = { ...prior.edge.metadata, sites: prior.sites };
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const edge = {
|
|
2796
|
+
source: src.name,
|
|
2797
|
+
target,
|
|
2798
|
+
type: RelationshipType.Uses,
|
|
2799
|
+
confidence: 1,
|
|
2800
|
+
line_number: line,
|
|
2801
|
+
metadata: { readNode: n.type, sites: 1 }
|
|
2802
|
+
};
|
|
2803
|
+
this.useIndex.set(key, { sites: 1, line, edge });
|
|
2804
|
+
this.relationships.push(edge);
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* The type name a CONSTRUCTION builds, when the construction is the leading
|
|
2808
|
+
* segment of a qualified name rather than a whole one.
|
|
2809
|
+
*
|
|
2810
|
+
* MEASURED, not guessed. `New System.Drawing.Size(3, 4)` does not parse as
|
|
2811
|
+
* one construction: it is
|
|
2812
|
+
* (array_access_expression
|
|
2813
|
+
* array: (member_access_expression
|
|
2814
|
+
* object: (member_access_expression
|
|
2815
|
+
* object: (object_creation_expression System)
|
|
2816
|
+
* member: Drawing)
|
|
2817
|
+
* member: Size)
|
|
2818
|
+
* indices: 3, 4)
|
|
2819
|
+
* so the `object_creation_expression` holds only `New System` and the real
|
|
2820
|
+
* target is the LAST leaf of the enclosing access chain. Publishing the node's
|
|
2821
|
+
* own `type` field here would publish `instantiates -> System`, which is a
|
|
2822
|
+
* namespace and not a type anybody constructs -- the grammar's fragmentation
|
|
2823
|
+
* turned into a fact about the program, which is the fabrication the
|
|
2824
|
+
* workstream names. Counted over the lane corpus: 15,233
|
|
2825
|
+
* `object_creation_expression` and 310 `array_creation_expression` nodes are
|
|
2826
|
+
* fragments of a qualified name, against 1,612 and 15,181 that stand alone
|
|
2827
|
+
* (census probes `instantiates:*:fragment_of_qualified` /
|
|
2828
|
+
* `instantiates:*:standalone`) -- so this is the COMMON case in this grammar,
|
|
2829
|
+
* not an edge one.
|
|
2830
|
+
*/
|
|
2831
|
+
constructionTargetLeaf(n) {
|
|
2832
|
+
if (this.constructionIsFragment(n)) {
|
|
2833
|
+
let outer = n;
|
|
2834
|
+
for (let guard = 0; guard < 32; guard++) {
|
|
2835
|
+
const p = outer.parent;
|
|
2836
|
+
if (!p) break;
|
|
2837
|
+
const isAccess = p.type === "member_access_expression" || p.type === "with_member_access_expression";
|
|
2838
|
+
if (!isAccess) break;
|
|
2839
|
+
const obj = p.childForFieldName("object");
|
|
2840
|
+
if (!obj || obj.id !== outer.id) break;
|
|
2841
|
+
outer = p;
|
|
2842
|
+
}
|
|
2843
|
+
const leaf = this.calleeLeaf(outer);
|
|
2844
|
+
if (leaf) return leaf;
|
|
2845
|
+
}
|
|
2846
|
+
const ty = n.childForFieldName("type");
|
|
2847
|
+
if (ty) return this.calleeLeaf(ty);
|
|
2848
|
+
return this.calleeLeaf(n);
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Is this construction the leading NAMESPACE SEGMENT of a qualified type name,
|
|
2852
|
+
* rather than a whole construction that happens to be the RECEIVER of an access?
|
|
2853
|
+
*
|
|
2854
|
+
* THE PARENT TEST ALONE WAS NOT ENOUGH, and it fabricated an edge on 322
|
|
2855
|
+
* construction nodes across the lane corpus. "Is my parent a
|
|
2856
|
+
* `member_access_expression` whose `object` I am?" is true for two completely
|
|
2857
|
+
* different programs:
|
|
2858
|
+
*
|
|
2859
|
+
* `New System.Text.StringBuilder` the construction holds `New System` and
|
|
2860
|
+
* `Text.StringBuilder` continues the TYPE in the chain, so the last leaf
|
|
2861
|
+
* of the chain (`StringBuilder`) is the constructed type. CLIMB.
|
|
2862
|
+
*
|
|
2863
|
+
* `New StackFrame(1).GetMethod().Name` the construction is COMPLETE and
|
|
2864
|
+
* `.GetMethod` is a method call ON THE OBJECT JUST BUILT, so the last
|
|
2865
|
+
* leaf of the chain (`Name`) is not a type at all. DO NOT CLIMB.
|
|
2866
|
+
*
|
|
2867
|
+
* The second shape published `instantiates -> GetMethod` — a method, not a
|
|
2868
|
+
* type, which is the fabrication `docs/analyzers/ONTOLOGY-WORKSTREAM.md`
|
|
2869
|
+
* Part 2 names as the thing that poisons `bb_graph`. An independent
|
|
2870
|
+
* source-text oracle (`script/m14/vb-instantiates-oracle.ts`, which reads no
|
|
2871
|
+
* grammar at all) put the error at 1.07% of all `instantiates` edges on a
|
|
2872
|
+
* 200-file draw, and named the targets: `ToList` 59, `Message` 6, `Length` 4,
|
|
2873
|
+
* `GetMethod` 4, `Contains` 4, `Clone` 2, `Status`, `LayeredDriver`, `Get`,
|
|
2874
|
+
* `TextInfo`. None of them is a constructed type.
|
|
2875
|
+
*
|
|
2876
|
+
* THE DISCRIMINATOR, measured over all 15,543 construction nodes that sit in an
|
|
2877
|
+
* access chain across the corpus (`script/m14/tmp-vb/frag3.mts`):
|
|
2878
|
+
*
|
|
2879
|
+
* a construction is a NAMESPACE PREFIX iff its `type` field is a BARE
|
|
2880
|
+
* `simple_name` and it has no other named child
|
|
2881
|
+
*
|
|
2882
|
+
* 15,221 bare `type`, no other child -> a namespace prefix; climbing is right
|
|
2883
|
+
* 322 anything else -> the construction is complete
|
|
2884
|
+
*
|
|
2885
|
+
* Every one of the 322 is a real defect of one of two kinds, and both are in
|
|
2886
|
+
* the sample set:
|
|
2887
|
+
*
|
|
2888
|
+
* - the construction carries its own arguments, so the chain is a member
|
|
2889
|
+
* access on the value: `New StackFrame(1).GetMethod()`,
|
|
2890
|
+
* `New CultureInfo("en-US", False).TextInfo`,
|
|
2891
|
+
* `BattleQuery.Add(New TextQueryObject(...))` -> `-> Add`, and
|
|
2892
|
+
* `New String() {…}.ToList()` (whose arguments are an `array_initializer`,
|
|
2893
|
+
* not a `bounds` field, so a `bounds`-only test misses it — hence the
|
|
2894
|
+
* named-child count rather than a field test).
|
|
2895
|
+
* - the construction node's SPAN was mis-anchored by the grammar's error
|
|
2896
|
+
* recovery onto the following statement: `Dim SpawnedEntity = New
|
|
2897
|
+
* BattleAnimation3D(...)` followed by `SpawnedEntity.Opacity = Opacity`
|
|
2898
|
+
* yields `-> Opacity`. Its `type` is a `generic_name` rather than a bare
|
|
2899
|
+
* `simple_name`, which is what the second half of the test catches.
|
|
2900
|
+
*/
|
|
2901
|
+
constructionIsFragment(n) {
|
|
2902
|
+
const p = n.parent;
|
|
2903
|
+
if (!p) return false;
|
|
2904
|
+
if (p.type !== "member_access_expression" && p.type !== "with_member_access_expression") return false;
|
|
2905
|
+
const obj = p.childForFieldName("object");
|
|
2906
|
+
if (!obj || obj.id !== n.id) return false;
|
|
2907
|
+
if (n.namedChildCount !== 1) return false;
|
|
2908
|
+
const ty = n.childForFieldName("type");
|
|
2909
|
+
if (!ty) return false;
|
|
2910
|
+
return ty.type === "simple_name" || ty.type === "identifier";
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* `instantiates`: a construction is not a call.
|
|
2914
|
+
*
|
|
2915
|
+
* VB HAS four distinct construction surfaces and the grammar gives each of
|
|
2916
|
+
* them a different shape, so the count is four counts and not one:
|
|
2917
|
+
*
|
|
2918
|
+
* `New Foo` / `New Foo()` object_creation_expression
|
|
2919
|
+
* `New Foo(a, b)` array_creation_expression (a `type` field
|
|
2920
|
+
* and a `bounds` field, and NO `function`
|
|
2921
|
+
* field -- the node a caller must read to find
|
|
2922
|
+
* the constructed type)
|
|
2923
|
+
* `As New Foo()` an `as_clause` whose text begins `As New`,
|
|
2924
|
+
* where the declared_type is an `array_type`
|
|
2925
|
+
* because the `()` reads as bounds
|
|
2926
|
+
* `New A.B.C(x)` an access chain (see
|
|
2927
|
+
* `constructionTargetLeaf`)
|
|
2928
|
+
*
|
|
2929
|
+
* Measured over 2,658 corpus files: 16,845 `object_creation_expression` and
|
|
2930
|
+
* 15,491 `array_creation_expression` nodes, plus 2,499 `as_clause`s that begin
|
|
2931
|
+
* `As New`. The `As New` half cannot be read from the tree at all -- both
|
|
2932
|
+
* keywords are inlined away by the grammar's `as_clause` rule, measured at 0
|
|
2933
|
+
* of 30,001 `as_clause` nodes carrying either token -- so it is a char scan
|
|
2934
|
+
* over the node's own span with the equivalence argued in
|
|
2935
|
+
* `startsWithAsNewClause`, and the census runs the same predicate with the
|
|
2936
|
+
* same controls.
|
|
2937
|
+
*
|
|
2938
|
+
* LABEL. This edge REPLACES the `calls` edge the analyzer used to publish for
|
|
2939
|
+
* a construction rather than joining it. Publishing both would double-count
|
|
2940
|
+
* one syntactic event under two labels and put a construction in the call
|
|
2941
|
+
* graph, which is what makes `bb_graph`'s call view wrong. The family's own
|
|
2942
|
+
* convention agrees: `real-csharp-analyzer.ts` emits `instantiates` for
|
|
2943
|
+
* `object_creation_expression` and its `calls` rule explicitly excludes it.
|
|
2944
|
+
*
|
|
2945
|
+
* CONFIDENCE follows the same rule every other VB edge follows: 1.0 when this
|
|
2946
|
+
* file also declares the constructed type, 0.6 when it does not, because the
|
|
2947
|
+
* target is then a name and not a resolved symbol. Under the honesty
|
|
2948
|
+
* contract's 0.7 floor the 0.6 edges read `inferred`, which is stamped by the
|
|
2949
|
+
* one evidence-class pass at the end of `analyze()`.
|
|
2950
|
+
*/
|
|
2951
|
+
pushConstruction(n, source) {
|
|
2952
|
+
const token = this.constructionTargetLeaf(n);
|
|
2953
|
+
const tyField = n.childForFieldName("type");
|
|
2954
|
+
const predefined = token === null && tyField !== null && tyField.type === "predefined_type" ? tyField : null;
|
|
2955
|
+
const target = this.nameText(token ?? predefined);
|
|
2956
|
+
if (target.length === 0) {
|
|
2957
|
+
this.note("construction-without-readable-name");
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
if (!source) {
|
|
2961
|
+
this.note("instantiation-without-enclosing-declaration");
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
this.relationships.push({
|
|
2965
|
+
source: source.name,
|
|
2966
|
+
target,
|
|
2967
|
+
type: RelationshipType.Instantiates,
|
|
2968
|
+
confidence: this.declaredNames.has(target) ? 1 : 0.6,
|
|
2969
|
+
line_number: this.nodeLine(token ?? predefined ?? n),
|
|
2970
|
+
metadata: {
|
|
2971
|
+
constructionNode: n.type,
|
|
2972
|
+
constructionForm: this.constructionFormOf(n),
|
|
2973
|
+
qualifiedConstruction: this.constructionIsFragment(n),
|
|
2974
|
+
// THE ARRAY-NODE DISTINCTION, kept on the row because the two facts differ:
|
|
2975
|
+
// this grammar gives every construction WITH arguments an
|
|
2976
|
+
// `array_creation_expression`, so `New Gauge(1)` and `New Gauge()` are the
|
|
2977
|
+
// same language construct on two different nodes. Recording which node
|
|
2978
|
+
// produced the edge is what stops a reader inferring an array from it.
|
|
2979
|
+
constructsArray: n.type === "array_creation_expression",
|
|
2980
|
+
// The instantiation facts `pushInstantiation` used to stamp on the second
|
|
2981
|
+
// copy of this row, kept here so one emitter carries the whole shape.
|
|
2982
|
+
isNew: true,
|
|
2983
|
+
constructedType: target,
|
|
2984
|
+
resolvedInFile: this.declaredNames.has(target)
|
|
2985
|
+
}
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
/** Which of the four VB construction surfaces this is, for a reader triaging. */
|
|
2989
|
+
constructionFormOf(n) {
|
|
2990
|
+
if (n.type === "array_creation_expression") {
|
|
2991
|
+
return n.childForFieldName("bounds") !== null ? "new-with-bounds" : "new-array-initializer";
|
|
2992
|
+
}
|
|
2993
|
+
if (n.parent?.type === "as_clause" && this.asClauseIsConstruction(n.parent)) return "as-new";
|
|
2994
|
+
return n.parent?.type === "invocation_expression" ? "new-then-invoke" : "new";
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Does this `as_clause` begin with the two VB words `As` `New`?
|
|
2998
|
+
*
|
|
2999
|
+
* A char scan with the equivalence argued, because the tree cannot answer it:
|
|
3000
|
+
* `as_clause` is `seq(ci("As"), ci("New"), field("declared_type", ...))` and
|
|
3001
|
+
* both keywords are ABSENT from the produced tree -- measured over the corpus,
|
|
3002
|
+
* 0 of 30,001 `as_clause` nodes carry an `As` or `New` token, and a printed
|
|
3003
|
+
* tree of `Dim l As New List(Of String)` shows an `as_clause` whose FIRST
|
|
3004
|
+
* child is the `generic_name`. A node-type or token test on the tree has
|
|
3005
|
+
* nothing to read, so this reads the node's own span instead.
|
|
3006
|
+
*
|
|
3007
|
+
* Word boundary means: the byte before the word is start-of-span or not a
|
|
3008
|
+
* letter/digit/underscore, and the byte after it is the same or end-of-span.
|
|
3009
|
+
* `As Namespace` and `As NewType` must NOT match, and both are rejected
|
|
3010
|
+
* because the adjacent byte is a letter. The leading whitespace is skipped
|
|
3011
|
+
* first because `as_clause`'s span can begin at the line's indentation.
|
|
3012
|
+
*/
|
|
3013
|
+
static startsWithAsNewClause(s) {
|
|
3014
|
+
const isWordChar = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95;
|
|
3015
|
+
const skipSpace = (i2) => {
|
|
3016
|
+
let k = i2;
|
|
3017
|
+
while (k < s.length) {
|
|
3018
|
+
const c = s.charCodeAt(k);
|
|
3019
|
+
if (c === 32 || c === 9 || c === 13 || c === 10) k++;
|
|
3020
|
+
else break;
|
|
3021
|
+
}
|
|
3022
|
+
return k;
|
|
3023
|
+
};
|
|
3024
|
+
const startsAt = (i2, w) => {
|
|
3025
|
+
if (i2 > 0 && isWordChar(s.charCodeAt(i2 - 1))) return false;
|
|
3026
|
+
if (i2 + w.length > s.length) return false;
|
|
3027
|
+
for (let k = 0; k < w.length; k++) {
|
|
3028
|
+
let c = s.charCodeAt(i2 + k);
|
|
3029
|
+
if (c >= 65 && c <= 90) c += 32;
|
|
3030
|
+
if (c !== w.charCodeAt(k)) return false;
|
|
3031
|
+
}
|
|
3032
|
+
return true;
|
|
3033
|
+
};
|
|
3034
|
+
const endsAt = (i2) => i2 >= s.length || !isWordChar(s.charCodeAt(i2));
|
|
3035
|
+
const i = skipSpace(0);
|
|
3036
|
+
if (!startsAt(i, "as")) return false;
|
|
3037
|
+
const j = skipSpace(i + 2);
|
|
3038
|
+
if (!startsAt(j, "new")) return false;
|
|
3039
|
+
return endsAt(j + 3);
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Is this `as_clause` a CONSTRUCTION (`As New T`) rather than a plain type
|
|
3043
|
+
* annotation (`As T`)? Two signals, one per shape, because the grammar
|
|
3044
|
+
* inlines both keywords away and the argument-less form is otherwise
|
|
3045
|
+
* byte-identical to the plain one:
|
|
3046
|
+
*
|
|
3047
|
+
* `As New T(a, b)` carries an `arguments` field the plain form never has;
|
|
3048
|
+
* `As New T` / `As New T()` does not, so the node's own span is read with
|
|
3049
|
+
* the word-bounded scan above (`As Namespace` and `As NewType` are both
|
|
3050
|
+
* rejected there by the boundary test, which is why the span is scanned
|
|
3051
|
+
* and not matched by substring).
|
|
3052
|
+
*/
|
|
3053
|
+
asClauseIsConstruction(asClause) {
|
|
3054
|
+
if (asClause.childForFieldName("arguments") !== null) return true;
|
|
3055
|
+
return _RealVbAnalyzer.startsWithAsNewClause(asClause.text);
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* A type NAME this file uses, as a `references` edge.
|
|
3059
|
+
*
|
|
3060
|
+
* Emitted only for a name this file does not itself declare, because a type
|
|
3061
|
+
* this file declares has a `contains`/`extends` edge already and a second edge
|
|
3062
|
+
* between the same pair is noise a consumer has to filter.
|
|
3063
|
+
*/
|
|
3064
|
+
pushTypeReference(from, typeNode, role) {
|
|
3065
|
+
if (typeNode.type === "tuple_type") {
|
|
3066
|
+
let emitted = 0;
|
|
3067
|
+
for (const el of this.namedChildrenOfType(typeNode, "tuple_element")) {
|
|
3068
|
+
const elType = el.childForFieldName("type") ?? el;
|
|
3069
|
+
this.pushTypeReference(from, elType, role);
|
|
3070
|
+
emitted++;
|
|
3071
|
+
}
|
|
3072
|
+
if (emitted === 0) this.note("tuple-type-has-no-single-name");
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
const token = this.namingToken(typeNode, typeNode.text);
|
|
3076
|
+
const builtin = token !== null && token.type === "predefined_type" ? VB_BUILTIN_TYPE_CANONICAL.get(this.nameText(token).toLowerCase()) : void 0;
|
|
3077
|
+
const target = builtin ?? this.nameText(token ?? typeNode);
|
|
3078
|
+
if (target.length === 0) return;
|
|
3079
|
+
if (this.declaredNames.has(target)) return;
|
|
3080
|
+
this.relationships.push({
|
|
3081
|
+
source: from.name,
|
|
3082
|
+
target,
|
|
3083
|
+
type: RelationshipType.References,
|
|
3084
|
+
confidence: 0.6,
|
|
3085
|
+
line_number: this.nodeLine(token ?? typeNode),
|
|
3086
|
+
metadata: {
|
|
3087
|
+
role,
|
|
3088
|
+
qualified: typeNode.type === "qualified_name" || typeNode.type === "global_qualified_name",
|
|
3089
|
+
...builtin !== void 0 ? {
|
|
3090
|
+
languageNamespace: [...VB_BUILTIN_TYPE_NAMES],
|
|
3091
|
+
languageNamespaceBasis: "vb-grammar-predefined-type"
|
|
3092
|
+
} : {}
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
// -------------------------------------------------------------------------
|
|
3097
|
+
// Complexity
|
|
3098
|
+
// -------------------------------------------------------------------------
|
|
3099
|
+
/**
|
|
3100
|
+
* Cyclomatic complexity, counted off DECISION NODES in the AST.
|
|
3101
|
+
*
|
|
3102
|
+
* The node set is the set of grammar rules that introduce a branch, and each
|
|
3103
|
+
* is named rather than matched, so a grammar change that adds a branching rule
|
|
3104
|
+
* shows up as a number that moved and a spec entry that is now wrong. No text
|
|
3105
|
+
* scan: counting `If` in a comment would be a fabrication.
|
|
3106
|
+
*/
|
|
3107
|
+
static DECISION_NODES = /* @__PURE__ */ new Set([
|
|
3108
|
+
"if_statement",
|
|
3109
|
+
"while_statement",
|
|
3110
|
+
"do_statement",
|
|
3111
|
+
"for_statement",
|
|
3112
|
+
"for_each_statement",
|
|
3113
|
+
"case_clause",
|
|
3114
|
+
"catch_clause",
|
|
3115
|
+
"catch_block",
|
|
3116
|
+
"case_else_clause",
|
|
3117
|
+
"else_clause",
|
|
3118
|
+
"catch_filter_clause",
|
|
3119
|
+
"when_clause"
|
|
3120
|
+
]);
|
|
3121
|
+
cyclomaticComplexity(root) {
|
|
3122
|
+
let n = 1;
|
|
3123
|
+
const visit = (node) => {
|
|
3124
|
+
if (_RealVbAnalyzer.DECISION_NODES.has(node.type)) n++;
|
|
3125
|
+
for (const c of this.kids(node)) visit(c);
|
|
3126
|
+
};
|
|
3127
|
+
visit(root);
|
|
3128
|
+
return n;
|
|
3129
|
+
}
|
|
3130
|
+
/** A file-level figure: nodes per 100 lines, so it is comparable across files. */
|
|
3131
|
+
complexity(root) {
|
|
3132
|
+
let count = 0;
|
|
3133
|
+
const visit = (node) => {
|
|
3134
|
+
count++;
|
|
3135
|
+
for (const c of this.kids(node)) visit(c);
|
|
3136
|
+
};
|
|
3137
|
+
visit(root);
|
|
3138
|
+
return count;
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
var real_vb_analyzer_default = RealVbAnalyzer;
|
|
3142
|
+
export {
|
|
3143
|
+
RealVbAnalyzer,
|
|
3144
|
+
real_vb_analyzer_default as default
|
|
3145
|
+
};
|