@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,1957 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FILE_CLAIM_MAX_BYTES,
|
|
3
|
+
dirOf,
|
|
4
|
+
fileClaimForTarget,
|
|
5
|
+
foldProjectPath
|
|
6
|
+
} from "./chunk-EWMWK7WN.js";
|
|
7
|
+
import {
|
|
8
|
+
ASTEngine
|
|
9
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
10
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
11
|
+
import "./chunk-SK6YVHFZ.js";
|
|
12
|
+
import "./chunk-2BSKD7O3.js";
|
|
13
|
+
import "./chunk-KAET47ZL.js";
|
|
14
|
+
import "./chunk-KCDECRJY.js";
|
|
15
|
+
import "./chunk-MLKGABMK.js";
|
|
16
|
+
|
|
17
|
+
// src/ast/real-lua-analyzer.ts
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
var LUA54_TABLES = /* @__PURE__ */ new Set([
|
|
20
|
+
"string",
|
|
21
|
+
"table",
|
|
22
|
+
"math",
|
|
23
|
+
"io",
|
|
24
|
+
"os",
|
|
25
|
+
"coroutine",
|
|
26
|
+
"debug",
|
|
27
|
+
"utf8"
|
|
28
|
+
]);
|
|
29
|
+
var LUA54_GLOBALS = /* @__PURE__ */ new Set([
|
|
30
|
+
"assert",
|
|
31
|
+
"collectgarbage",
|
|
32
|
+
"dofile",
|
|
33
|
+
"error",
|
|
34
|
+
"getmetatable",
|
|
35
|
+
"ipairs",
|
|
36
|
+
"load",
|
|
37
|
+
"loadfile",
|
|
38
|
+
"next",
|
|
39
|
+
"pairs",
|
|
40
|
+
"pcall",
|
|
41
|
+
"print",
|
|
42
|
+
"rawequal",
|
|
43
|
+
"rawget",
|
|
44
|
+
"rawlen",
|
|
45
|
+
"rawset",
|
|
46
|
+
"require",
|
|
47
|
+
"select",
|
|
48
|
+
"setmetatable",
|
|
49
|
+
"tonumber",
|
|
50
|
+
"tostring",
|
|
51
|
+
"type",
|
|
52
|
+
"warn",
|
|
53
|
+
"xpcall",
|
|
54
|
+
"_G",
|
|
55
|
+
"_VERSION"
|
|
56
|
+
]);
|
|
57
|
+
function luaModuleBases(fileRel) {
|
|
58
|
+
const dir = dirOf(fileRel);
|
|
59
|
+
const segs = fileRel.split("/");
|
|
60
|
+
const all = [{ label: "root", base: "" }, { label: "fileDir", base: dir }];
|
|
61
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
62
|
+
if (segs[i] === "lua") {
|
|
63
|
+
all.push({ label: "luaRoot", base: segs.slice(0, i + 1).join("/") + "/" });
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
all.push({ label: "fileDir+lua", base: dir + "lua/" });
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
return all.filter((b) => {
|
|
70
|
+
if (seen.has(b.base)) return false;
|
|
71
|
+
seen.add(b.base);
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function luaModuleFileCandidates(moduleName, fileRel) {
|
|
76
|
+
const stem = moduleName.trim().split(".").join("/");
|
|
77
|
+
if (stem.length === 0) return [];
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const { base } of luaModuleBases(fileRel)) {
|
|
80
|
+
for (const suffix of [".lua", "/init.lua"]) {
|
|
81
|
+
const folded = foldProjectPath(base, stem + suffix);
|
|
82
|
+
if (folded !== null && !out.includes(folded)) out.push(folded);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
var RealLuaAnalyzer = class {
|
|
88
|
+
analyze(rootNode, content, filePath, context) {
|
|
89
|
+
const projectRootRaw = String(context?.projectRoot ?? "");
|
|
90
|
+
const filePathRaw = String(filePath ?? "");
|
|
91
|
+
const rooted = projectRootRaw.length > 0 && filePathRaw.length > 0 && path.isAbsolute(projectRootRaw) && path.isAbsolute(filePathRaw);
|
|
92
|
+
const ctx = {
|
|
93
|
+
symbols: [],
|
|
94
|
+
relationships: [],
|
|
95
|
+
imports: [],
|
|
96
|
+
exports: [],
|
|
97
|
+
errors: [],
|
|
98
|
+
globalVariables: /* @__PURE__ */ new Set(),
|
|
99
|
+
declaredTables: /* @__PURE__ */ new Set(),
|
|
100
|
+
unpublishedSurface: [],
|
|
101
|
+
frameworks: /* @__PURE__ */ new Set(),
|
|
102
|
+
patterns: /* @__PURE__ */ new Set(),
|
|
103
|
+
seenRelationships: /* @__PURE__ */ new Set(),
|
|
104
|
+
coroutineCalls: 0,
|
|
105
|
+
declarationsByLabel: /* @__PURE__ */ new Map(),
|
|
106
|
+
membersByOwner: /* @__PURE__ */ new Map(),
|
|
107
|
+
supertypeLinks: /* @__PURE__ */ new Map(),
|
|
108
|
+
factorySupertypeLines: /* @__PURE__ */ new Map(),
|
|
109
|
+
declaredFunctions: /* @__PURE__ */ new Set(),
|
|
110
|
+
wrapperFactories: /* @__PURE__ */ new Set(),
|
|
111
|
+
stdlibCandidates: [],
|
|
112
|
+
stdlibGlobalCandidates: [],
|
|
113
|
+
...rooted ? { projectRoot: projectRootRaw, filePath: filePathRaw } : {},
|
|
114
|
+
moduleFileClaims: /* @__PURE__ */ new Map()
|
|
115
|
+
};
|
|
116
|
+
this.collectErrors(rootNode, ctx);
|
|
117
|
+
this.collectDeclaredTables(rootNode, ctx);
|
|
118
|
+
this.collectSupertypeLinks(rootNode, ctx);
|
|
119
|
+
this.collectFactorySupertypeLinks(rootNode, ctx);
|
|
120
|
+
this.collectDecoratorFacts(rootNode, ctx);
|
|
121
|
+
const rootFrame = { localNames: /* @__PURE__ */ new Set(), inFunction: false };
|
|
122
|
+
const stack = [{ node: rootNode, frame: rootFrame }];
|
|
123
|
+
while (stack.length > 0) {
|
|
124
|
+
const entry = stack.pop();
|
|
125
|
+
const node = entry.node;
|
|
126
|
+
const frame = entry.frame;
|
|
127
|
+
if (node.isMissing) continue;
|
|
128
|
+
this.processNode(node, frame, ctx);
|
|
129
|
+
let childFrame = frame;
|
|
130
|
+
if (node.type === "function_declaration" || node.type === "function_definition") {
|
|
131
|
+
const functionName = node.type === "function_declaration" ? this.functionName(node) : void 0;
|
|
132
|
+
const locals = new Set(frame.localNames);
|
|
133
|
+
for (const parameter of this.extractParameters(node)) locals.add(parameter.name);
|
|
134
|
+
if (functionName || node.type === "function_definition") {
|
|
135
|
+
const nameNode = node.childForFieldName("name");
|
|
136
|
+
const bareOwner = (nameNode ? this.functionOwner(nameNode) : void 0) ?? functionName ?? frame.owner ?? frame.callable;
|
|
137
|
+
const selfName = (nameNode ? this.splitMemberName(nameNode, functionName ?? "").name : functionName) || frame.selfName;
|
|
138
|
+
childFrame = {
|
|
139
|
+
callable: functionName ?? frame.callable,
|
|
140
|
+
owner: bareOwner,
|
|
141
|
+
selfName,
|
|
142
|
+
localNames: locals,
|
|
143
|
+
inFunction: true
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
148
|
+
const child = node.namedChild(i);
|
|
149
|
+
if (child) stack.push({ node: child, frame: childFrame });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.addContainsRelationships(ctx);
|
|
153
|
+
this.addFactorySupertypeEdges(ctx);
|
|
154
|
+
this.addOverrideRelationships(ctx);
|
|
155
|
+
this.markStdlibExternal(ctx);
|
|
156
|
+
const detectedFrameworks = [...ctx.frameworks];
|
|
157
|
+
const detectedPatterns = [...ctx.patterns];
|
|
158
|
+
return {
|
|
159
|
+
language: "lua",
|
|
160
|
+
symbols: ctx.symbols,
|
|
161
|
+
relationships: ctx.relationships,
|
|
162
|
+
imports: ctx.imports,
|
|
163
|
+
exports: ctx.exports,
|
|
164
|
+
errors: ctx.errors,
|
|
165
|
+
metadata: {
|
|
166
|
+
// M60 LUA-35. Present ONLY when there is something to say, and every field
|
|
167
|
+
// is derived from the parse: `shape` from the node type, `detail` from the
|
|
168
|
+
// node's own text, `line` from its start position, `why` a fixed reason per
|
|
169
|
+
// shape. Nothing here is a constant shaped like a measurement, which is the
|
|
170
|
+
// LUA-11 rule — a count or score that did not vary with the file would be
|
|
171
|
+
// worse than an absent field, and this is a list that is empty whenever the
|
|
172
|
+
// module's surface is nameable.
|
|
173
|
+
...ctx.unpublishedSurface.length ? { unpublishedSurface: ctx.unpublishedSurface } : {},
|
|
174
|
+
complexity: ctx.symbols.length + ctx.relationships.length,
|
|
175
|
+
linesOfCode: this.lineCount(content),
|
|
176
|
+
cyclomaticComplexity: this.calculateCyclomaticComplexity(rootNode),
|
|
177
|
+
framework: detectedFrameworks[0],
|
|
178
|
+
patterns: detectedPatterns,
|
|
179
|
+
detectedFrameworks,
|
|
180
|
+
frameworkPatterns: detectedFrameworks.map((framework) => ({
|
|
181
|
+
framework,
|
|
182
|
+
patterns: [{ type: "usage", detected: true, description: `${framework} usage detected` }]
|
|
183
|
+
})),
|
|
184
|
+
architecturalInsights: this.generateArchitecturalInsights(ctx),
|
|
185
|
+
architecturalScore: this.calculateArchitecturalScore(ctx),
|
|
186
|
+
codeQuality: this.assessCodeQuality(ctx),
|
|
187
|
+
hasTypeInference: false,
|
|
188
|
+
// M60 LUA-11: performanceScore and memoryEfficiency were the literals
|
|
189
|
+
// 85 and 85 — the two most checkable-looking numbers in this block and
|
|
190
|
+
// the two that were invented. They are gone under those NAMES as well as
|
|
191
|
+
// those values, because a derived number published as "performanceScore"
|
|
192
|
+
// is still a lie: it claims to measure performance, and this analyzer
|
|
193
|
+
// measures no time at all. What replaces them is named for what it is —
|
|
194
|
+
// relationshipDensity and retainedRowKb — and each is either derived
|
|
195
|
+
// from the analysed file or ABSENT.
|
|
196
|
+
relationshipDensity: this.relationshipDensity(ctx),
|
|
197
|
+
retainedRowKb: this.retainedRowKb(ctx),
|
|
198
|
+
// M60 LUA-02: counted from the CALLS the walk actually saw, not from a
|
|
199
|
+
// symbol flag nothing ever set — which made this a structural zero.
|
|
200
|
+
coroutineUsage: this.countCoroutineSymbols(ctx),
|
|
201
|
+
analysisVersion: "4.0.0-lua-m12.32"
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
processNode(node, frame, ctx) {
|
|
206
|
+
switch (node.type) {
|
|
207
|
+
case "function_declaration":
|
|
208
|
+
this.extractFunction(node, frame, ctx, this.isLocalFunction(node));
|
|
209
|
+
break;
|
|
210
|
+
case "variable_declaration":
|
|
211
|
+
if (this.isLocalVariableDeclaration(node)) this.extractLocalDeclaration(node, frame, ctx);
|
|
212
|
+
this.extractDecoratorApplication(node, frame, ctx);
|
|
213
|
+
break;
|
|
214
|
+
case "assignment_statement":
|
|
215
|
+
this.extractAssignment(node, frame, ctx);
|
|
216
|
+
this.extractDecoratorApplication(node, frame, ctx);
|
|
217
|
+
break;
|
|
218
|
+
case "function_call":
|
|
219
|
+
this.processCall(node, frame, ctx);
|
|
220
|
+
break;
|
|
221
|
+
case "return_statement":
|
|
222
|
+
this.extractExport(node, frame, ctx);
|
|
223
|
+
break;
|
|
224
|
+
case "for_statement":
|
|
225
|
+
this.registerLoopBindings(node, frame);
|
|
226
|
+
break;
|
|
227
|
+
case "identifier":
|
|
228
|
+
this.detectFrameworks(node.text, ctx);
|
|
229
|
+
this.emitNameRead(node, frame, ctx);
|
|
230
|
+
break;
|
|
231
|
+
case "dot_index_expression":
|
|
232
|
+
case "method_index_expression":
|
|
233
|
+
case "bracket_index_expression":
|
|
234
|
+
this.emitMemberRead(node, frame, ctx);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* M60 LUA-40 — `uses`: a name this scope READS.
|
|
240
|
+
*
|
|
241
|
+
* Measured before it was written (script/m60/lua-ontology-census.ts, probe
|
|
242
|
+
* B4-bare-name-read): 295,019 bare-name reads across 3,250 corpus files. The
|
|
243
|
+
* type was absent, so the largest read surface the language has was invisible.
|
|
244
|
+
*
|
|
245
|
+
* The rule is a ROLE, read off the parent node, never a text shape. Each
|
|
246
|
+
* exclusion is a different relationship that is already published or is not a
|
|
247
|
+
* declaration at all:
|
|
248
|
+
*
|
|
249
|
+
* callee of a function_call -> that is `calls`, published by processCall
|
|
250
|
+
* a `variable_list` child -> a DECLARATION, published as a symbol and
|
|
251
|
+
* as `contains`
|
|
252
|
+
* the LHS of an assignment -> a WRITE, not a read
|
|
253
|
+
* a `field`'s key -> a table-literal KEY, a declaration site
|
|
254
|
+
* a `function_declaration` name -> a member DECLARATION
|
|
255
|
+
* the RECEIVER of an index expr -> part of a qualified name; that read is
|
|
256
|
+
* `references` (LUA-41), and publishing both
|
|
257
|
+
* would double-count one fact under two labels
|
|
258
|
+
* a `parameters` child -> a parameter BINDING, not a use of it
|
|
259
|
+
*
|
|
260
|
+
* Confidence is 0.9, not 1.0, and the reason is not a hedge: the node proves
|
|
261
|
+
* the file READS A NAME. Which binding that name denotes — a local, an
|
|
262
|
+
* upvalue, a global, a stdlib function, a field of a required module — is not
|
|
263
|
+
* written anywhere in the tree. The machine-readable reason code the honesty
|
|
264
|
+
* contract §4 requires for anything under 1.0 is `evidenceClass:
|
|
265
|
+
* "usage_contextual"`, which is this codebase's existing word for a link
|
|
266
|
+
* established by a NAME in a value position (real-css-analyzer.ts:737).
|
|
267
|
+
*/
|
|
268
|
+
/**
|
|
269
|
+
* M60 LUA-45: node identity, the only way that works on this runtime.
|
|
270
|
+
*
|
|
271
|
+
* `web-tree-sitter` returns a FRESH wrapper object on every access, so
|
|
272
|
+
* `parent.namedChild(0) === node` is false even when both name the same span —
|
|
273
|
+
* measured on validation/m14/fixtures/lua/name-reads.lua, where the receiver
|
|
274
|
+
* test silently never fired and published 3 edges it exists to suppress:
|
|
275
|
+
* `apply -> config` at L22, `apply -> helper` at L24 and `never -> helper` at
|
|
276
|
+
* L29. Every one of them states a qualified access twice under two labels.
|
|
277
|
+
*
|
|
278
|
+
* The identity that IS stable is the SPAN, which is what this compares. It is
|
|
279
|
+
* the family's own idiom and not a local invention: real-c-analyzer.ts:2336,
|
|
280
|
+
* :2525 and :6012 all compare `startIndex`/`endIndex` for exactly this reason.
|
|
281
|
+
* `type` is compared too because a span can be covered by more than one node
|
|
282
|
+
* type in a grammar that reuses a token, and a wrong-type match here would
|
|
283
|
+
* suppress a real edge.
|
|
284
|
+
*/
|
|
285
|
+
isSameNode(a, b) {
|
|
286
|
+
if (!a || !b) return false;
|
|
287
|
+
return a.type === b.type && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
|
|
288
|
+
}
|
|
289
|
+
emitNameRead(node, frame, ctx) {
|
|
290
|
+
const parent = node.parent;
|
|
291
|
+
if (!parent) return;
|
|
292
|
+
const pt = parent.type;
|
|
293
|
+
if (pt === "function_call") {
|
|
294
|
+
if (this.isSameNode(parent.childForFieldName("name"), node) || this.isSameNode(parent.namedChild(0), node)) return;
|
|
295
|
+
} else if (pt === "variable_list" || pt === "parameters") {
|
|
296
|
+
return;
|
|
297
|
+
} else if (pt === "assignment_statement") {
|
|
298
|
+
if (this.isSameNode(parent.namedChild(0), node)) return;
|
|
299
|
+
} else if (pt === "field") {
|
|
300
|
+
if (this.isSameNode(parent.childForFieldName("name"), node)) return;
|
|
301
|
+
} else if (pt === "function_declaration") {
|
|
302
|
+
if (this.isSameNode(parent.childForFieldName("name"), node)) return;
|
|
303
|
+
} else if (pt === "dot_index_expression" || pt === "method_index_expression") {
|
|
304
|
+
if (this.isSameNode(parent.namedChild(0), node)) return;
|
|
305
|
+
if (this.isSameNode(parent.namedChild(parent.namedChildCount - 1), node)) return;
|
|
306
|
+
} else if (pt === "bracket_index_expression") {
|
|
307
|
+
if (this.isSameNode(parent.namedChild(0), node)) return;
|
|
308
|
+
}
|
|
309
|
+
this.addRelationship(
|
|
310
|
+
ctx,
|
|
311
|
+
frame.callable || "global",
|
|
312
|
+
node.text,
|
|
313
|
+
ASTEngine.RelationshipType.Uses,
|
|
314
|
+
{ luaSpecific: true, relationshipType: "lua_name_read" },
|
|
315
|
+
node,
|
|
316
|
+
{ confidence: 0.9 }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* M60 LUA-41 — `references`: a MEMBER of a table read without calling it.
|
|
321
|
+
*
|
|
322
|
+
* Measured first, the same way: probe B3-member-read counted 198,720 qualified
|
|
323
|
+
* reads across 2,643 files. A qualified read is the shape the family's
|
|
324
|
+
* `references` is for and the shape this lane was missing: css's `var(--x)`
|
|
325
|
+
* asserts a dependency on a NAME that may be declared in another file, and
|
|
326
|
+
* `M.field` asserts exactly that — `M` is written, `field` is written, and
|
|
327
|
+
* which module owns `field` is not written anywhere.
|
|
328
|
+
*
|
|
329
|
+
* The same four exclusions as `calls`/`contains` already rely on, each for the
|
|
330
|
+
* same reason: a member's CALLEE is a `calls` edge (processCall), a member's
|
|
331
|
+
* DECLARATION is a symbol plus a `contains` edge, a member ASSIGNED TO is a
|
|
332
|
+
* write, and a `field`'s key is a table-literal key.
|
|
333
|
+
*
|
|
334
|
+
* A COMPUTED index (`dao[method or context .. "_by_" .. endpoint_key]`) has no
|
|
335
|
+
* name, and LUA-22's payload lesson applies unchanged: a literal STRING index
|
|
336
|
+
* does (`dao["rows"]`, read as `rows`), a number and a variable-held key do not.
|
|
337
|
+
*
|
|
338
|
+
* The TARGET is the member's own field name, which is what every other lua edge
|
|
339
|
+
* uses for a member (LUA-13/LUA-15: `obj:method()` yields target `method`), and
|
|
340
|
+
* the RECEIVER is recorded in metadata so a consumer that needs the qualified
|
|
341
|
+
* path has it without this edge having to claim an identity it cannot prove.
|
|
342
|
+
* The cost of that choice is measured and declared: two tables in one file can
|
|
343
|
+
* both have a member `run`, and the bare name fuses them. The qualified path is
|
|
344
|
+
* recorded per edge, so the ambiguity is recoverable rather than lost.
|
|
345
|
+
*/
|
|
346
|
+
emitMemberRead(node, frame, ctx) {
|
|
347
|
+
const parent = node.parent;
|
|
348
|
+
if (parent) {
|
|
349
|
+
const pt = parent.type;
|
|
350
|
+
if (pt === "function_call" && (this.isSameNode(parent.childForFieldName("name"), node) || this.isSameNode(parent.namedChild(0), node))) return;
|
|
351
|
+
if (pt === "function_declaration" && this.isSameNode(parent.childForFieldName("name"), node)) return;
|
|
352
|
+
if (pt === "field" && this.isSameNode(parent.childForFieldName("name"), node)) return;
|
|
353
|
+
if (pt === "variable_list") return;
|
|
354
|
+
}
|
|
355
|
+
let member;
|
|
356
|
+
let memberNode;
|
|
357
|
+
if (node.type === "bracket_index_expression") {
|
|
358
|
+
const key = node.childForFieldName("field");
|
|
359
|
+
if (key?.type !== "string") return;
|
|
360
|
+
member = this.stringValue(key);
|
|
361
|
+
memberNode = key;
|
|
362
|
+
} else {
|
|
363
|
+
const last = node.namedChildCount > 0 ? node.namedChild(node.namedChildCount - 1) : void 0;
|
|
364
|
+
if (last?.type !== "identifier") return;
|
|
365
|
+
member = last.text;
|
|
366
|
+
memberNode = last;
|
|
367
|
+
}
|
|
368
|
+
if (!member || !memberNode) return;
|
|
369
|
+
const receiver = this.nameBearing(node.childForFieldName("table"));
|
|
370
|
+
this.addRelationship(
|
|
371
|
+
ctx,
|
|
372
|
+
frame.callable || "global",
|
|
373
|
+
member,
|
|
374
|
+
ASTEngine.RelationshipType.References,
|
|
375
|
+
{
|
|
376
|
+
luaSpecific: true,
|
|
377
|
+
relationshipType: "lua_member_read",
|
|
378
|
+
...receiver ? { receiver } : {}
|
|
379
|
+
},
|
|
380
|
+
memberNode,
|
|
381
|
+
{ confidence: 0.9 }
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
extractFunction(node, frame, ctx, isLocal) {
|
|
385
|
+
const nameNode = node.childForFieldName("name") || node.namedChild(0);
|
|
386
|
+
if (!nameNode) return;
|
|
387
|
+
const label = nameNode.text;
|
|
388
|
+
if (!label) return;
|
|
389
|
+
const { name, owner } = this.splitMemberName(nameNode, label);
|
|
390
|
+
const parameters = this.extractParameters(node);
|
|
391
|
+
const isMethod = !!owner;
|
|
392
|
+
const lexicallyLocal = isLocal || !!frame.callable;
|
|
393
|
+
const symbol = {
|
|
394
|
+
name,
|
|
395
|
+
kind: isMethod ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
|
|
396
|
+
range: this.nodeToRange(node),
|
|
397
|
+
scope: lexicallyLocal ? frame.callable || "global" : "global",
|
|
398
|
+
visibility: lexicallyLocal ? "private" : "public",
|
|
399
|
+
parameters,
|
|
400
|
+
ownerName: owner,
|
|
401
|
+
ownerKind: owner ? ASTEngine.SymbolKind.Class : void 0,
|
|
402
|
+
// M60 LUA-37: the enclosing body's own symbol name, so a declaration nested
|
|
403
|
+
// in this one can be contained BY IT rather than by its label. See
|
|
404
|
+
// addContainsRelationships, which reads this and never reads `scope`.
|
|
405
|
+
frameSelfName: frame.selfName,
|
|
406
|
+
// qualifiedName keeps the full path, which is what it is FOR. The `name`
|
|
407
|
+
// field is the identity; this is the path. Before the fix the path was
|
|
408
|
+
// doing the identity's job and no path was recorded at all.
|
|
409
|
+
qualifiedName: label,
|
|
410
|
+
metadata: { luaSpecific: true, isLocal: lexicallyLocal, isMethod, ownerTable: owner, qualifiedName: label }
|
|
411
|
+
};
|
|
412
|
+
ctx.symbols.push(symbol);
|
|
413
|
+
if (owner) {
|
|
414
|
+
ctx.declaredTables.add(owner);
|
|
415
|
+
ctx.patterns.add("Object-Oriented Programming");
|
|
416
|
+
const members = ctx.membersByOwner.get(owner) ?? /* @__PURE__ */ new Map();
|
|
417
|
+
if (!members.has(name)) members.set(name, node.startPosition.row + 1);
|
|
418
|
+
ctx.membersByOwner.set(owner, members);
|
|
419
|
+
}
|
|
420
|
+
const byLabel = ctx.declarationsByLabel.get(label) ?? [];
|
|
421
|
+
byLabel.push({ line: node.startPosition.row + 1, owner: owner ?? null, member: owner ? name : null });
|
|
422
|
+
ctx.declarationsByLabel.set(label, byLabel);
|
|
423
|
+
if (lexicallyLocal) ctx.globalVariables.delete(name);
|
|
424
|
+
}
|
|
425
|
+
// M60 LUA-15: split a declaration's name into its OWN name and its OWNER.
|
|
426
|
+
//
|
|
427
|
+
// In this grammar `function Buffer:initialize()` and `function
|
|
428
|
+
// Buffer.readUInt8()` both put an index expression in the `name` field, with
|
|
429
|
+
// the RECEIVER first and the FIELD last:
|
|
430
|
+
// method_index_expression
|
|
431
|
+
// table: (identifier "Buffer")
|
|
432
|
+
// : (identifier "initialize")
|
|
433
|
+
// so the field is the LAST named child and the receiver the first. Taking the
|
|
434
|
+
// first yields "Buffer" — the table, not the method.
|
|
435
|
+
splitMemberName(nameNode, label) {
|
|
436
|
+
if (nameNode.type !== "dot_index_expression" && nameNode.type !== "method_index_expression") {
|
|
437
|
+
return { name: label };
|
|
438
|
+
}
|
|
439
|
+
const first = nameNode.namedChild(0);
|
|
440
|
+
const last = nameNode.namedChildCount > 1 ? nameNode.namedChild(nameNode.namedChildCount - 1) : void 0;
|
|
441
|
+
const owner = first?.type === "identifier" ? first.text : void 0;
|
|
442
|
+
return { name: last?.text || label, owner };
|
|
443
|
+
}
|
|
444
|
+
extractLocalDeclaration(node, frame, ctx) {
|
|
445
|
+
const first = node.type === "variable_declaration" ? node.namedChild(0) : node;
|
|
446
|
+
const assignment = first?.type === "assignment_statement" ? first : void 0;
|
|
447
|
+
const variableList = assignment ? this.childOfType(assignment, "variable_list") : first?.type === "variable_list" ? first : void 0;
|
|
448
|
+
if (!variableList) return;
|
|
449
|
+
for (let i = 0; i < variableList.namedChildCount; i++) {
|
|
450
|
+
const variable = variableList.namedChild(i);
|
|
451
|
+
const name = this.plainVariableName(variable);
|
|
452
|
+
if (!variable || !name) continue;
|
|
453
|
+
frame.localNames.add(name);
|
|
454
|
+
const value = assignment ? this.assignmentValue(assignment) : void 0;
|
|
455
|
+
const isTable = this.isTableNode(value);
|
|
456
|
+
if (isTable) ctx.declaredTables.add(name);
|
|
457
|
+
ctx.symbols.push({
|
|
458
|
+
name,
|
|
459
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
460
|
+
range: this.nodeToRange(variable),
|
|
461
|
+
scope: frame.callable || "global",
|
|
462
|
+
// M60 LUA-37: a declaration inside a body is contained by the FUNCTION whose
|
|
463
|
+
// body it is, so the owner is that function's own name (`selfName`), not the
|
|
464
|
+
// function's own owner. `function fs.appendFile()` has owner "fs" and self
|
|
465
|
+
// "appendFile", and a local inside it must read `appendFile -> localName`:
|
|
466
|
+
// "fs" is the owner of the METHOD, and using it filed the local as a sibling
|
|
467
|
+
// of the method rather than a member of it, which cost 525 unexplained
|
|
468
|
+
// contains edges on an 83-file kong slice.
|
|
469
|
+
ownerName: frame.selfName ?? frame.owner ?? frame.callable,
|
|
470
|
+
frameSelfName: frame.selfName,
|
|
471
|
+
ownerKind: frame.callable ? ASTEngine.SymbolKind.Function : void 0,
|
|
472
|
+
visibility: frame.callable ? "private" : "public",
|
|
473
|
+
metadata: { luaSpecific: true, isLocal: true, isTable, tableFields: isTable ? this.tableFieldNames(value) : [] }
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* M60 LUA-49: a for-clause BINDS a name, and the frame never heard of it.
|
|
479
|
+
*
|
|
480
|
+
* `extractAssignment` suppresses a local re-assignment with
|
|
481
|
+
* `frame.localNames.has(name)` — the one line standing between a re-assignment and
|
|
482
|
+
* a fabricated declaration. `localNames` was filled from exactly one place,
|
|
483
|
+
* `variable_declaration` (real-lua-analyzer.ts:649), so every OTHER binding form in
|
|
484
|
+
* the language fell through it. A generic-for clause is the common one:
|
|
485
|
+
*
|
|
486
|
+
* for _, line in ipairs(lines) do
|
|
487
|
+
* line = line:match("(.-)%%") or line <- a re-assignment
|
|
488
|
+
* end
|
|
489
|
+
*
|
|
490
|
+
* `line` is bound by the clause, so the statement above re-assigns a local and
|
|
491
|
+
* declares nothing. The analyzer had no record of the binding, published a
|
|
492
|
+
* `variable` SYMBOL for it, and `addContainsRelationships` then published
|
|
493
|
+
* `contains get_packages -> line` — a containment edge for a declaration that never
|
|
494
|
+
* happened, sitting on the assignment's line rather than the clause's.
|
|
495
|
+
*
|
|
496
|
+
* Found by the ORACLE rather than by reading: `lua-truth-oracle.ts` records
|
|
497
|
+
* for-clause names in `localsIn` (for its own LUA-26 exemption), so it correctly
|
|
498
|
+
* declined to derive the re-assignment, and the two disagreed. Four unexplained
|
|
499
|
+
* `contains` edges over ~600 corpus files, every one this shape:
|
|
500
|
+
* `contains get_packages -> line` (snacks.nvim/lua/snacks/image/doc.lua:131),
|
|
501
|
+
* `contains json -> item` (snacks.nvim/lua/snacks/picker/source/proc.lua:172),
|
|
502
|
+
* and one each in kong and luarocks.
|
|
503
|
+
*
|
|
504
|
+
* WHAT THIS DOES NOT DO, stated because the omission is the other half of the
|
|
505
|
+
* defect: the loop BINDING itself is still not published as a declaration. Lua
|
|
506
|
+
* binds `line` there, and neither a symbol nor a `contains` edge says so — the
|
|
507
|
+
* edge that used to appear was on the wrong line and for the wrong reason.
|
|
508
|
+
* Publishing the binding is the larger change and it is not made here: `for k, v in
|
|
509
|
+
* pairs(t)` is one of the most frequent constructs in Lua, so it is a symbol-count
|
|
510
|
+
* and payload decision for the whole graph surface, not a lane-local fix. Reported
|
|
511
|
+
* rather than done (§8 of the workstream, and limitation 14 of the doc).
|
|
512
|
+
*
|
|
513
|
+
* Scope: the name is recorded WITH THE LOOP'S END OFFSET, and the re-assignment
|
|
514
|
+
* test asks whether that loop is still lexically open at the assignment. That is
|
|
515
|
+
* not a refinement, it is the whole correctness of the rule: Lua scopes a loop
|
|
516
|
+
* variable to the loop body, so
|
|
517
|
+
*
|
|
518
|
+
* for _, v in ipairs(t) do v = f(v) end -- a RE-assignment
|
|
519
|
+
* v = 5 -- a DIFFERENT binding
|
|
520
|
+
*
|
|
521
|
+
* are different facts. Recording the name in `localNames` unconditionally, which is
|
|
522
|
+
* what the first version of this fix did, got the first line right and the second
|
|
523
|
+
* one wrong: it suppressed 18 real `contains` edges on kong alone. The oracle caught
|
|
524
|
+
* it as 18 MISSING, which is the oracle working — it derives the post-loop
|
|
525
|
+
* assignment because the loop really has closed.
|
|
526
|
+
*
|
|
527
|
+
* Lua has no block scope for locals — a `do ... end` does not open one — so the
|
|
528
|
+
* binding goes in the ENCLOSING FUNCTION's frame rather than a block frame, and the
|
|
529
|
+
* loop's own extent is what bounds it. At file scope `frame.callable` is undefined
|
|
530
|
+
* and `extractAssignment`'s existing `frame.callable &&` guard still lets a
|
|
531
|
+
* file-scope assignment declare a global, which is correct: after
|
|
532
|
+
* `for _, v in ...` ends at file scope, `v = 5` assigns a global.
|
|
533
|
+
*/
|
|
534
|
+
registerLoopBindings(node, frame) {
|
|
535
|
+
const clause = node.namedChild(0);
|
|
536
|
+
if (!clause) return;
|
|
537
|
+
const end = node.endIndex;
|
|
538
|
+
const add = (n) => {
|
|
539
|
+
if (n?.type !== "identifier") return;
|
|
540
|
+
let list = frame.loopBindings;
|
|
541
|
+
if (!list) frame.loopBindings = list = [];
|
|
542
|
+
list.push({ name: n.text, endIndex: end });
|
|
543
|
+
};
|
|
544
|
+
if (clause.type === "for_numeric_clause") {
|
|
545
|
+
add(clause.childForFieldName("name"));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (clause.type !== "for_generic_clause") return;
|
|
549
|
+
for (let i = 0; i < clause.namedChildCount; i++) {
|
|
550
|
+
const list = clause.namedChild(i);
|
|
551
|
+
if (list?.type !== "variable_list") continue;
|
|
552
|
+
for (let j = 0; j < list.namedChildCount; j++) add(list.namedChild(j));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** Is `name` bound by a for-clause whose loop is still open at byte offset `at`? */
|
|
556
|
+
inOpenLoopBinding(frame, name, at) {
|
|
557
|
+
const list = frame.loopBindings;
|
|
558
|
+
if (!list) return false;
|
|
559
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
560
|
+
if (list[i].name === name && at < list[i].endIndex) return true;
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
extractAssignment(node, frame, ctx) {
|
|
565
|
+
if (node.parent?.type === "variable_declaration" && this.isLocalVariableDeclaration(node.parent)) return;
|
|
566
|
+
const variableList = this.childOfType(node, "variable_list");
|
|
567
|
+
if (!variableList) return;
|
|
568
|
+
const value = this.assignmentValue(node);
|
|
569
|
+
for (let i = 0; i < variableList.namedChildCount; i++) {
|
|
570
|
+
const variable = variableList.namedChild(i);
|
|
571
|
+
const name = this.plainVariableName(variable);
|
|
572
|
+
if (!variable || !name) continue;
|
|
573
|
+
if (frame.callable && frame.localNames.has(name) || this.inOpenLoopBinding(frame, name, node.startIndex)) continue;
|
|
574
|
+
const isTable = this.isTableNode(value);
|
|
575
|
+
if (isTable) ctx.declaredTables.add(name);
|
|
576
|
+
ctx.symbols.push({
|
|
577
|
+
name,
|
|
578
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
579
|
+
range: this.nodeToRange(variable),
|
|
580
|
+
scope: frame.callable || "global",
|
|
581
|
+
ownerName: frame.selfName ?? frame.owner ?? frame.callable,
|
|
582
|
+
frameSelfName: frame.selfName,
|
|
583
|
+
ownerKind: frame.callable ? ASTEngine.SymbolKind.Function : void 0,
|
|
584
|
+
visibility: frame.callable ? "private" : "public",
|
|
585
|
+
metadata: { luaSpecific: true, isGlobal: !frame.callable, isTable, tableFields: isTable ? this.tableFieldNames(value) : [] }
|
|
586
|
+
});
|
|
587
|
+
if (!frame.callable) ctx.globalVariables.add(name);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
processCall(node, frame, ctx) {
|
|
591
|
+
const functionNode = node.childForFieldName("name") || node.namedChild(0);
|
|
592
|
+
if (!functionNode) return;
|
|
593
|
+
const callee = this.callee(functionNode);
|
|
594
|
+
if (!callee) return;
|
|
595
|
+
const callName = callee.name;
|
|
596
|
+
const qualified = functionNode.text;
|
|
597
|
+
this.detectFrameworks(qualified, ctx);
|
|
598
|
+
const values = this.callArguments(node);
|
|
599
|
+
const firstString = this.stringValue(values[0]);
|
|
600
|
+
if (callName === "require" || callName === "module") {
|
|
601
|
+
if (firstString) this.addImport(firstString, frame, node, ctx);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (callName === "pcall" || callName === "xpcall") {
|
|
605
|
+
if (values[0]?.text === "require") {
|
|
606
|
+
const moduleName = this.stringValue(values[1]);
|
|
607
|
+
if (moduleName) this.addImport(moduleName, frame, node, ctx);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const source = frame.callable || "global";
|
|
611
|
+
if (callName !== "pcall" && callName !== "xpcall") {
|
|
612
|
+
this.addRelationship(ctx, source, callName, ASTEngine.RelationshipType.Calls, { luaSpecific: true, ...callee.shapeMark }, callee.nameNode);
|
|
613
|
+
}
|
|
614
|
+
if (qualified === "coroutine.create" || qualified === "coroutine.wrap") {
|
|
615
|
+
ctx.patterns.add("Coroutines");
|
|
616
|
+
ctx.coroutineCalls++;
|
|
617
|
+
}
|
|
618
|
+
if (callName === "setmetatable") {
|
|
619
|
+
ctx.patterns.add("Object-Oriented Programming (Metatables)");
|
|
620
|
+
const target = values[0];
|
|
621
|
+
const metatable = values[1];
|
|
622
|
+
const srcName = this.nameBearing(target);
|
|
623
|
+
const mtName = this.nameBearing(metatable);
|
|
624
|
+
if (target && metatable && !this.isTableNode(target) && srcName && mtName && srcName !== mtName) {
|
|
625
|
+
this.addRelationship(ctx, srcName, mtName, ASTEngine.RelationshipType.Extends, { luaSpecific: true, relationshipType: "lua_metatable" }, metatable);
|
|
626
|
+
}
|
|
627
|
+
if (metatable?.type === "identifier") {
|
|
628
|
+
const tableName = metatable.text;
|
|
629
|
+
ctx.declaredTables.add(tableName);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const constructor = this.constructorTarget(functionNode, ctx);
|
|
633
|
+
if (constructor) {
|
|
634
|
+
this.addRelationship(ctx, source, constructor, ASTEngine.RelationshipType.Instantiates, { luaSpecific: true, relationshipType: "instantiates" }, callee.nameNode);
|
|
635
|
+
}
|
|
636
|
+
if (callName === "class" || qualified === "Object:extend" || qualified === "Object.extend") {
|
|
637
|
+
const className = firstString;
|
|
638
|
+
if (className) {
|
|
639
|
+
ctx.symbols.push({
|
|
640
|
+
name: className,
|
|
641
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
642
|
+
range: this.nodeToRange(node),
|
|
643
|
+
scope: source,
|
|
644
|
+
ownerName: frame.selfName ?? frame.owner ?? frame.callable,
|
|
645
|
+
frameSelfName: frame.selfName,
|
|
646
|
+
metadata: { luaSpecific: true, isLibraryClass: true }
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
ctx.patterns.add("Object-Oriented Programming (Libraries)");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
addImport(moduleName, frame, node, ctx) {
|
|
653
|
+
ctx.imports.push({
|
|
654
|
+
module: moduleName,
|
|
655
|
+
items: [],
|
|
656
|
+
isDefaultImport: true,
|
|
657
|
+
isNamespaceImport: false,
|
|
658
|
+
range: this.nodeToRange(node),
|
|
659
|
+
metadata: { luaSpecific: true }
|
|
660
|
+
});
|
|
661
|
+
const fileClaim = this.moduleFileClaim(moduleName, ctx);
|
|
662
|
+
this.addRelationship(ctx, frame.callable || "global", moduleName, ASTEngine.RelationshipType.Imports, {
|
|
663
|
+
luaSpecific: true,
|
|
664
|
+
relationshipType: "lua_require",
|
|
665
|
+
...fileClaim !== null ? { importProjectFile: fileClaim } : {}
|
|
666
|
+
}, node);
|
|
667
|
+
if (moduleName.startsWith("resty")) ctx.frameworks.add("OpenResty");
|
|
668
|
+
if (moduleName === "socket" || moduleName === "lfs" || moduleName === "cjson" || moduleName === "ssl") ctx.frameworks.add("LuaRocks Core Modules");
|
|
669
|
+
if (moduleName === "lapis") ctx.frameworks.add("Lapis");
|
|
670
|
+
if (moduleName === "middleclass" || moduleName === "classic" || moduleName === "30log") ctx.patterns.add("Object-Oriented Programming (Libraries)");
|
|
671
|
+
if (moduleName === "hs" || moduleName.startsWith("hs.")) ctx.frameworks.add("Hammerspoon");
|
|
672
|
+
if (moduleName.startsWith("lovr")) ctx.frameworks.add("L\xD6VR (VR/AR)");
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* THE FILE CLAIM for one module name, or null when no candidate can be confirmed.
|
|
676
|
+
*
|
|
677
|
+
* WHAT IT ASSERTS, in the exact words of the guard's own contract: the project carries an admitted
|
|
678
|
+
* `.lua` file that this `require` names. It asserts NOTHING about symbols, so it can never raise
|
|
679
|
+
* `resolved`, and it is written ONLY where the guard confirmed a candidate on disk — a claim the
|
|
680
|
+
* project would refute is worse than no claim, because the gate checks a claim BEFORE the count.
|
|
681
|
+
*
|
|
682
|
+
* WHEN IT IS SILENT, and both silences are deliberate:
|
|
683
|
+
* * NO ROOT, NO CLAIM. `analyze` without an absolute `projectRoot` (the fixture runner, a
|
|
684
|
+
* one-off parse) states no claim at all rather than guessing a base. A caller that wants the
|
|
685
|
+
* claim must hand the analyzer the project it is reading inside;
|
|
686
|
+
* * NO CANDIDATE, NO CLAIM. `require("luarocks.fs")`, `require("tap")` and the rest of the
|
|
687
|
+
* residue name files this corpus does not carry — those edges keep their target and stay
|
|
688
|
+
* unresolved, which is TRUE, rather than being marked external to make a column smaller.
|
|
689
|
+
*
|
|
690
|
+
* The candidate arithmetic is `luaModuleFileCandidates`; the ADMITTED-EXTENSION clause, the
|
|
691
|
+
* inside-the-project clause, the size cap and the `statSync` are the shared guard's, so this lane
|
|
692
|
+
* cannot relax them by editing its own spelling of them.
|
|
693
|
+
*/
|
|
694
|
+
moduleFileClaim(moduleName, ctx) {
|
|
695
|
+
const { projectRoot, filePath } = ctx;
|
|
696
|
+
if (projectRoot === void 0 || filePath === void 0) return null;
|
|
697
|
+
const cached = ctx.moduleFileClaims?.get(moduleName);
|
|
698
|
+
if (cached !== void 0) return cached;
|
|
699
|
+
const fileRel = path.relative(projectRoot, filePath).split(path.sep).join("/");
|
|
700
|
+
const claim = fileClaimForTarget({
|
|
701
|
+
projectRoot,
|
|
702
|
+
filePath,
|
|
703
|
+
// THE TARGET AS WRITTEN. The guard folds it itself before the extras below; the claim is a
|
|
704
|
+
// statement ABOUT this string and never a rewrite of it.
|
|
705
|
+
written: moduleName,
|
|
706
|
+
// THE LANE'S OWN ADMITTED LIST out of the router: lua admits `.lua` alone, so a candidate of
|
|
707
|
+
// any other extension can never verify while a miss still refutes — which is why this clause
|
|
708
|
+
// is the guard's and not this file's.
|
|
709
|
+
admittedExtensions: ["lua"],
|
|
710
|
+
extraCandidates: luaModuleFileCandidates(moduleName, fileRel),
|
|
711
|
+
// The indexer's own admission cap: a file above it is not in the population a claim is
|
|
712
|
+
// checked against, so a claim about it would be REFUTED rather than unverified.
|
|
713
|
+
maxFileSizeBytes: FILE_CLAIM_MAX_BYTES
|
|
714
|
+
});
|
|
715
|
+
ctx.moduleFileClaims?.set(moduleName, claim);
|
|
716
|
+
return claim;
|
|
717
|
+
}
|
|
718
|
+
// M60 LUA-06: this was ORDER-DEPENDENT, and therefore wrong. `declaredTables`
|
|
719
|
+
// was filled during the same single traversal that read it, so whether a
|
|
720
|
+
// constructor call produced an edge depended on whether the table's
|
|
721
|
+
// declaration happened to appear EARLIER IN THE FILE. Proven by probe, with
|
|
722
|
+
// the two files differing only in statement order:
|
|
723
|
+
//
|
|
724
|
+
// local A = {} A.new()
|
|
725
|
+
// function A.new() end local A = {}
|
|
726
|
+
// A.new() function A.new() end
|
|
727
|
+
//
|
|
728
|
+
// first -> instantiates global->A (edge present)
|
|
729
|
+
// second -> NO EDGE (same semantics, no edge)
|
|
730
|
+
//
|
|
731
|
+
// A relationship that depends on where its declaration sits is not a fact
|
|
732
|
+
// about the program, it is a fact about the traversal. The fix is a PRE-PASS:
|
|
733
|
+
// collect every name this file binds to a table, before any call is examined.
|
|
734
|
+
// One traversal, order-independent.
|
|
735
|
+
constructorTarget(functionNode, ctx) {
|
|
736
|
+
if (!["dot_index_expression", "method_index_expression"].includes(functionNode.type)) return void 0;
|
|
737
|
+
const method = functionNode.childForFieldName("method") || functionNode.childForFieldName("field");
|
|
738
|
+
const table = functionNode.childForFieldName("table");
|
|
739
|
+
if ((method?.text === "new" || method?.text === "constructor") && table?.text && ctx.declaredTables.has(table.text)) return table.text;
|
|
740
|
+
return void 0;
|
|
741
|
+
}
|
|
742
|
+
// M60 LUA-06: collect the file's table-valued names BEFORE the walk, so a
|
|
743
|
+
// constructor call does not depend on the order of the declaration.
|
|
744
|
+
collectDeclaredTables(root, ctx) {
|
|
745
|
+
const stack = [root];
|
|
746
|
+
while (stack.length > 0) {
|
|
747
|
+
const node = stack.pop();
|
|
748
|
+
if (node.type === "variable_declaration" || node.type === "assignment_statement") {
|
|
749
|
+
const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
|
|
750
|
+
const list = inner?.type === "assignment_statement" ? inner.namedChild(0) : void 0;
|
|
751
|
+
const values = inner?.type === "assignment_statement" ? inner.namedChild(1) : void 0;
|
|
752
|
+
const first = values?.namedChild(0);
|
|
753
|
+
if (list?.type === "variable_list" && first?.type === "table_constructor") {
|
|
754
|
+
for (let i = 0; i < list.namedChildCount; i++) {
|
|
755
|
+
const id = list.namedChild(i);
|
|
756
|
+
if (id?.type === "identifier") ctx.declaredTables.add(id.text);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (node.type === "function_declaration") {
|
|
761
|
+
const nm = node.childForFieldName("name");
|
|
762
|
+
if (nm && (nm.type === "dot_index_expression" || nm.type === "method_index_expression")) {
|
|
763
|
+
const recv = nm.namedChild(0);
|
|
764
|
+
if (recv?.type === "identifier") ctx.declaredTables.add(recv.text);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
768
|
+
const c = node.namedChild(i);
|
|
769
|
+
if (c) stack.push(c);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* M60 LUA-44: derived table -> the tables its member lookup falls back to.
|
|
775
|
+
*
|
|
776
|
+
* Lua has NO inheritance keyword, so a grammar inventory can never find this
|
|
777
|
+
* relation — it is a CONVENTION over three ordinary constructs, and the
|
|
778
|
+
* ontology-coverage gate's own json ruling says in as many words that a grammar
|
|
779
|
+
* cannot see a convention. All three forms are read here because all three mean
|
|
780
|
+
* the same thing for an override:
|
|
781
|
+
*
|
|
782
|
+
* setmetatable(D, B) D's members fall back to B's (direct)
|
|
783
|
+
* setmetatable(D, {__index = B}) the same, through an inline metatable
|
|
784
|
+
* local Mt = {__index = B}; setmetatable(D, Mt) the same, through a named
|
|
785
|
+
* metatable — kong's idiom, and the shape
|
|
786
|
+
* probe B2 actually found
|
|
787
|
+
* D.__index = B the same, assigned after the fact
|
|
788
|
+
*
|
|
789
|
+
* The named-metatable form needs TWO HOPS and is why this method does not simply
|
|
790
|
+
* read the second argument: `setmetatable(_lmdb, _lmdb_mt)` names a METATABLE,
|
|
791
|
+
* and reading it as a supertype would make every metatable in the corpus look
|
|
792
|
+
* like a base class. Resolving `Mt -> Mt.__index -> Base` is the difference
|
|
793
|
+
* between the two, and it is a derivation over parse facts rather than a guess:
|
|
794
|
+
* both hops are written in the file.
|
|
795
|
+
*
|
|
796
|
+
* The third form is deliberately absent from the B2 probe in
|
|
797
|
+
* script/m60/lua-ontology-census.ts, which reads only the inline
|
|
798
|
+
* `setmetatable` forms and therefore UNDER-counts (1 occurrence). That probe is
|
|
799
|
+
* the falsifiable instrument for the ruling; this is the analyzer's wider read,
|
|
800
|
+
* and the gap between them is disclosed rather than reconciled by widening the
|
|
801
|
+
* probe until the numbers agree.
|
|
802
|
+
*/
|
|
803
|
+
collectSupertypeLinks(root, ctx) {
|
|
804
|
+
const link = (derived, base) => {
|
|
805
|
+
if (!derived || !base || derived === base) return;
|
|
806
|
+
if (ctx.declaredTables.has(derived) || ctx.declaredTables.has(base)) {
|
|
807
|
+
const set = ctx.supertypeLinks.get(derived) ?? /* @__PURE__ */ new Set();
|
|
808
|
+
set.add(base);
|
|
809
|
+
ctx.supertypeLinks.set(derived, set);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const indexField = (table) => {
|
|
813
|
+
if (table?.type !== "table_constructor") return void 0;
|
|
814
|
+
for (let i = 0; i < table.namedChildCount; i++) {
|
|
815
|
+
const f = table.namedChild(i);
|
|
816
|
+
if (f?.type !== "field") continue;
|
|
817
|
+
const key = f.childForFieldName("name") ?? f.namedChild(0);
|
|
818
|
+
const value = f.namedChild(1);
|
|
819
|
+
if (key?.text === "__index" && value?.type === "identifier") return value.text;
|
|
820
|
+
}
|
|
821
|
+
return void 0;
|
|
822
|
+
};
|
|
823
|
+
const metatableIndex = /* @__PURE__ */ new Map();
|
|
824
|
+
const pending = [];
|
|
825
|
+
const stack = [root];
|
|
826
|
+
while (stack.length > 0) {
|
|
827
|
+
const node = stack.pop();
|
|
828
|
+
if (node.type === "function_call") {
|
|
829
|
+
const fn = node.childForFieldName("name");
|
|
830
|
+
const args = node.childForFieldName("arguments");
|
|
831
|
+
if (fn?.type === "identifier" && (fn.text === "setmetatable" || fn.text === "setmetatable2")) {
|
|
832
|
+
const a0 = args?.namedChild(0) ?? null;
|
|
833
|
+
pending.push({ derived: a0?.type === "identifier" ? a0.text : void 0, second: args?.namedChild(1) ?? null });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (node.type === "variable_declaration" || node.type === "assignment_statement") {
|
|
837
|
+
const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
|
|
838
|
+
if (inner?.type === "assignment_statement") {
|
|
839
|
+
const list = inner.namedChild(0);
|
|
840
|
+
const values = inner.namedChild(1);
|
|
841
|
+
const table = values?.type === "expression_list" ? values.namedChild(0) : void 0;
|
|
842
|
+
const base = indexField(table);
|
|
843
|
+
if (base && list?.type === "variable_list") {
|
|
844
|
+
for (let i = 0; i < list.namedChildCount; i++) {
|
|
845
|
+
const id = list.namedChild(i);
|
|
846
|
+
if (id?.type === "identifier") metatableIndex.set(id.text, base);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
852
|
+
const c = node.namedChild(i);
|
|
853
|
+
if (c) stack.push(c);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
for (const site of pending) {
|
|
857
|
+
const second = site.second;
|
|
858
|
+
if (second?.type === "identifier") {
|
|
859
|
+
link(site.derived, metatableIndex.get(second.text) ?? second.text);
|
|
860
|
+
} else {
|
|
861
|
+
const base = indexField(second);
|
|
862
|
+
if (base) link(site.derived, base);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const stack2 = [root];
|
|
866
|
+
while (stack2.length > 0) {
|
|
867
|
+
const node = stack2.pop();
|
|
868
|
+
if (node.type === "assignment_statement") {
|
|
869
|
+
const list = node.namedChild(0);
|
|
870
|
+
const values = node.namedChild(1);
|
|
871
|
+
const lhs = list?.type === "variable_list" ? list.namedChild(0) ?? null : null;
|
|
872
|
+
const rhs = values?.type === "expression_list" ? values.namedChild(0) ?? null : null;
|
|
873
|
+
if ((lhs?.type === "dot_index_expression" || lhs?.type === "method_index_expression") && lhs.namedChild(lhs.namedChildCount - 1)?.text === "__index" && rhs?.type === "identifier") {
|
|
874
|
+
const receiver = lhs.namedChild(0);
|
|
875
|
+
link(receiver?.type === "identifier" ? receiver.text : void 0, rhs.text);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
879
|
+
const c = node.namedChild(i);
|
|
880
|
+
if (c) stack2.push(c);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* M60 LUA-51 — a WRITTEN supertype CLAUSE, expressed as an ordinary call argument.
|
|
886
|
+
*
|
|
887
|
+
* MEASURED BEFORE THIS METHOD EXISTED (script/m60/lua-supertype-arg-probe.ts, over
|
|
888
|
+
* the 3,250 admitted corpus files): 7 written supertype arguments across 5 files,
|
|
889
|
+
* of which 2 are inheritance and neither was emitted. The two:
|
|
890
|
+
*
|
|
891
|
+
* luarocks/vendor/argparse.lua:277 local Command = class({...}, {...}, Parser)
|
|
892
|
+
* luarocks/vendor/argparse.lua:328 local Option = class({...}, {...}, Argument)
|
|
893
|
+
*
|
|
894
|
+
* `extends` read only `setmetatable` (real-lua-analyzer.ts:924) and the `class`
|
|
895
|
+
* branch read only `firstString`, the class's own NAME — so a parent passed in ANY
|
|
896
|
+
* argument position was invisible.
|
|
897
|
+
*
|
|
898
|
+
* ## Why the position is READ and not GUESSED
|
|
899
|
+
*
|
|
900
|
+
* The obvious implementation is "the third argument of a `class` call is the
|
|
901
|
+
* parent", and it is wrong in a way the corpus demonstrates: middleclass's own form
|
|
902
|
+
* is `class("Name", Super)` — second position — and luarocks' is
|
|
903
|
+
* `class(prototype, properties, parent)` — third. A fixed index publishes a wrong
|
|
904
|
+
* edge for one of them and misses the other.
|
|
905
|
+
*
|
|
906
|
+
* So the position comes from the CALLEE'S OWN PARAMETER LIST, which the same file
|
|
907
|
+
* writes down:
|
|
908
|
+
*
|
|
909
|
+
* local function class(prototype, properties, parent)
|
|
910
|
+
* 0 1 2 <- the parent slot
|
|
911
|
+
*
|
|
912
|
+
* A parameter whose NAME states the supertype role (`parent`, `super`, `base`,
|
|
913
|
+
* `supertype`, `superclass`, …) identifies its own slot. If the file does not
|
|
914
|
+
* declare such a parameter there is nothing to read, and no edge is published — so
|
|
915
|
+
* the rule cannot invent a relation from a call that never claimed one.
|
|
916
|
+
*
|
|
917
|
+
* ## Why this is `extends` and not `implements`
|
|
918
|
+
*
|
|
919
|
+
* Because the callee's body composes the parent into the object it returns:
|
|
920
|
+
*
|
|
921
|
+
* if parent then cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
|
|
922
|
+
*
|
|
923
|
+
* The new class's prototype is built FROM the parent's. That is inheritance, and it
|
|
924
|
+
* is the same relation the `setmetatable` forms already publish, so it carries the
|
|
925
|
+
* same label and feeds the same `supertypeLinks` map that `overrides` reads.
|
|
926
|
+
*
|
|
927
|
+
* ## Confidence
|
|
928
|
+
*
|
|
929
|
+
* 0.95, `usage_contextual` — NOT 1.0 and NOT `structural`, and deliberately so.
|
|
930
|
+
* Both endpoint NAMES are written in the file, which is what earns `extends` 1.0
|
|
931
|
+
* elsewhere; what is a convention rather than a fact is the ROLE: that a parameter
|
|
932
|
+
* named `parent` means the supertype. `overrides`' `lua_inherited_member` takes the
|
|
933
|
+
* same 0.95 for the same reason and cites the same family precedent
|
|
934
|
+
* (real-objective-c-analyzer.ts:1020). The corpus shows the cost of reading it as
|
|
935
|
+
* certain: `snacks.nvim/lua/snacks/layout.lua` and three other files declare a
|
|
936
|
+
* `parent` parameter for TREE TRAVERSAL (`_each(widget, parent)`,
|
|
937
|
+
* `add(result, parent)`), and the probe returns all 5 of those alongside the 2 real
|
|
938
|
+
* ones.
|
|
939
|
+
*
|
|
940
|
+
* So the read is narrowed to the case the corpus proves is inheritance: the factory
|
|
941
|
+
* must be ASSIGNED to a top-level name, because `local Command = class(...)` is what
|
|
942
|
+
* makes the edge's SOURCE a name a consumer can follow, and the derived and base
|
|
943
|
+
* names must differ.
|
|
944
|
+
*/
|
|
945
|
+
collectFactorySupertypeLinks(root, ctx) {
|
|
946
|
+
const SUPERTYPE_ROLES = /* @__PURE__ */ new Set([
|
|
947
|
+
"parent",
|
|
948
|
+
"super",
|
|
949
|
+
"base",
|
|
950
|
+
"supertype",
|
|
951
|
+
"superclass",
|
|
952
|
+
"super_class",
|
|
953
|
+
"parentclass",
|
|
954
|
+
"parent_class"
|
|
955
|
+
]);
|
|
956
|
+
const paramNames = (params) => {
|
|
957
|
+
const out = [];
|
|
958
|
+
if (!params) return out;
|
|
959
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
960
|
+
const c = params.namedChild(i);
|
|
961
|
+
if (!c) continue;
|
|
962
|
+
if (c.type === "identifier") out.push(c.text);
|
|
963
|
+
else if (c.type === "vararg_expression") continue;
|
|
964
|
+
else out.push("");
|
|
965
|
+
}
|
|
966
|
+
return out;
|
|
967
|
+
};
|
|
968
|
+
const foldsIntoMetamethod = (fn, param) => {
|
|
969
|
+
let found = false;
|
|
970
|
+
const isRole = (n) => n?.type === "identifier" && n.text === param;
|
|
971
|
+
const visit = (n) => {
|
|
972
|
+
if (found) return;
|
|
973
|
+
if (n.type === "dot_index_expression" || n.type === "method_index_expression") {
|
|
974
|
+
if (isRole(n.namedChild(0))) {
|
|
975
|
+
const last = n.namedChild(n.namedChildCount - 1);
|
|
976
|
+
if (last?.text?.startsWith("__")) {
|
|
977
|
+
found = true;
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (n.type === "assignment_statement") {
|
|
983
|
+
const lhs = n.namedChild(0);
|
|
984
|
+
const lhsNode = lhs?.type === "variable_list" ? lhs.namedChild(0) : void 0;
|
|
985
|
+
if (lhsNode && (lhsNode.type === "dot_index_expression" || lhsNode.type === "method_index_expression")) {
|
|
986
|
+
const field = lhsNode.namedChild(lhsNode.namedChildCount - 1);
|
|
987
|
+
const rhs = n.namedChild(1);
|
|
988
|
+
const rhsNode = rhs?.type === "expression_list" ? rhs.namedChild(0) : void 0;
|
|
989
|
+
if (field?.text?.startsWith("__") && isRole(rhsNode)) {
|
|
990
|
+
found = true;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
996
|
+
const c = n.namedChild(i);
|
|
997
|
+
if (c) visit(c);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const body = fn.childForFieldName("body") ?? fn;
|
|
1001
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
1002
|
+
const c = body.namedChild(i);
|
|
1003
|
+
if (c) visit(c);
|
|
1004
|
+
}
|
|
1005
|
+
return found;
|
|
1006
|
+
};
|
|
1007
|
+
const factoryParams = /* @__PURE__ */ new Map();
|
|
1008
|
+
const stack0 = [root];
|
|
1009
|
+
while (stack0.length > 0) {
|
|
1010
|
+
const node = stack0.pop();
|
|
1011
|
+
if (node.type === "function_declaration" || node.type === "function_definition") {
|
|
1012
|
+
const nameNode = node.childForFieldName("name");
|
|
1013
|
+
if (nameNode?.type === "identifier" && nameNode.text && !factoryParams.has(nameNode.text)) {
|
|
1014
|
+
const ps = paramNames(node.childForFieldName("parameters"));
|
|
1015
|
+
if (ps.some((p) => SUPERTYPE_ROLES.has(p.toLowerCase()) && foldsIntoMetamethod(node, p))) {
|
|
1016
|
+
factoryParams.set(nameNode.text, ps);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1021
|
+
const c = node.namedChild(i);
|
|
1022
|
+
if (c) stack0.push(c);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (factoryParams.size === 0) return;
|
|
1026
|
+
const stack1 = [root];
|
|
1027
|
+
while (stack1.length > 0) {
|
|
1028
|
+
const node = stack1.pop();
|
|
1029
|
+
if (node.type === "variable_declaration" || node.type === "assignment_statement") {
|
|
1030
|
+
const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
|
|
1031
|
+
const call = inner?.type === "assignment_statement" ? inner.namedChild(1) : node;
|
|
1032
|
+
const value = call?.type === "expression_list" ? call.namedChild(0) : void 0;
|
|
1033
|
+
if (value?.type === "function_call" && inner) {
|
|
1034
|
+
const callee = value.childForFieldName("name");
|
|
1035
|
+
const args = value.childForFieldName("arguments");
|
|
1036
|
+
if (callee?.type === "identifier" && args) {
|
|
1037
|
+
const ps = factoryParams.get(callee.text);
|
|
1038
|
+
if (ps) {
|
|
1039
|
+
const list = inner.namedChild(0);
|
|
1040
|
+
const derived = list?.type === "variable_list" ? list.namedChild(0) : void 0;
|
|
1041
|
+
const derivedName = derived?.type === "identifier" ? derived.text : void 0;
|
|
1042
|
+
ps.forEach((p, slot) => {
|
|
1043
|
+
if (!SUPERTYPE_ROLES.has(p.toLowerCase())) return;
|
|
1044
|
+
const argNode = args.namedChild(slot);
|
|
1045
|
+
if (argNode?.type !== "identifier") return;
|
|
1046
|
+
const baseName = argNode.text;
|
|
1047
|
+
if (!derivedName || derivedName === baseName) return;
|
|
1048
|
+
const existing = ctx.supertypeLinks.get(derivedName) ?? /* @__PURE__ */ new Set();
|
|
1049
|
+
existing.add(baseName);
|
|
1050
|
+
ctx.supertypeLinks.set(derivedName, existing);
|
|
1051
|
+
ctx.factorySupertypeLines.set(`${derivedName}|${baseName}`, {
|
|
1052
|
+
derived: derivedName,
|
|
1053
|
+
base: baseName,
|
|
1054
|
+
line: argNode.startPosition.row + 1
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1062
|
+
const c = node.namedChild(i);
|
|
1063
|
+
if (c) stack1.push(c);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* M60 LUA-51 — publish the `extends` edge for a factory supertype clause.
|
|
1069
|
+
*
|
|
1070
|
+
* `extends` was emitted only from the `setmetatable` call site, so a link collected
|
|
1071
|
+
* by `collectFactorySupertypeLinks` reached `overrides` and nothing else. This runs
|
|
1072
|
+
* beside it and emits the same relation, deduplicated against the `extends` edges
|
|
1073
|
+
* already in the result so a link both readers find is published once.
|
|
1074
|
+
*
|
|
1075
|
+
* It iterates `factorySupertypeLines` and NOT `supertypeLinks`, and that is the whole
|
|
1076
|
+
* correction. The first version walked `supertypeLinks`, which is the union of THREE
|
|
1077
|
+
* conventions, and so published the third one — `D.__index = Base` — under this
|
|
1078
|
+
* method's `lua_factory_supertype` label. Measured on the file that exposed it:
|
|
1079
|
+
*
|
|
1080
|
+
* luarocks/vendor/argparse.lua:99 class_metatable.__index = parent
|
|
1081
|
+
*
|
|
1082
|
+
* produced `class_metatable -[extends 0.95 lua_factory_supertype]-> parent` — a link
|
|
1083
|
+
* the walk had deliberately never published, carrying a label that says "factory" for
|
|
1084
|
+
* a construct with no factory in it. `factorySupertypeLines` is keyed only by links
|
|
1085
|
+
* `collectFactorySupertypeLinks` created, so iterating it cannot reach the other two.
|
|
1086
|
+
*/
|
|
1087
|
+
addFactorySupertypeEdges(ctx) {
|
|
1088
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1089
|
+
for (const e of ctx.relationships) {
|
|
1090
|
+
if (e.type !== ASTEngine.RelationshipType.Extends) continue;
|
|
1091
|
+
seen.add(`${e.source}|${e.target}`);
|
|
1092
|
+
}
|
|
1093
|
+
for (const [key, link] of ctx.factorySupertypeLines) {
|
|
1094
|
+
const { derived, base, line } = link;
|
|
1095
|
+
if (seen.has(key)) continue;
|
|
1096
|
+
seen.add(key);
|
|
1097
|
+
this.addRelationship(
|
|
1098
|
+
ctx,
|
|
1099
|
+
derived,
|
|
1100
|
+
base,
|
|
1101
|
+
ASTEngine.RelationshipType.Extends,
|
|
1102
|
+
{ luaSpecific: true, relationshipType: "lua_factory_supertype", evidenceClass: "usage_contextual" },
|
|
1103
|
+
null,
|
|
1104
|
+
{ confidence: 0.95, symbolRange: { startLine: line } }
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* `decorates` — THE LUA DECORATOR IDIOM, and the measurement that made it a
|
|
1110
|
+
* relation instead of a ruling.
|
|
1111
|
+
*
|
|
1112
|
+
* ## What the construct is
|
|
1113
|
+
*
|
|
1114
|
+
* Lua has no annotation syntax. Its decorator idiom is the PYTHON DESUGARING
|
|
1115
|
+
* WRITTEN OUT: a declared function is wrapped by a declared higher-order
|
|
1116
|
+
* function and rebound to the same name at file scope.
|
|
1117
|
+
*
|
|
1118
|
+
* function fib(n) ... end
|
|
1119
|
+
* function cache(f) -- a WRAPPER FACTORY: returns a function
|
|
1120
|
+
* local c = {}
|
|
1121
|
+
* return function(x) ... end
|
|
1122
|
+
* end
|
|
1123
|
+
* fib = cache(fib) -- redis/deps/lua/test/fib.lua:39
|
|
1124
|
+
*
|
|
1125
|
+
* `@cache def fib` in Python compiles to exactly this sequence — the function
|
|
1126
|
+
* object is created, then `cache` is applied to it, then the name is rebound. So
|
|
1127
|
+
* the relation is a real runtime application to a DECLARATION, and the edge is
|
|
1128
|
+
* `fib -> cache` (the vocabulary's direction: decorated declaration -> decorator).
|
|
1129
|
+
*
|
|
1130
|
+
* ## Why the discriminator is structural, and what each clause excludes
|
|
1131
|
+
*
|
|
1132
|
+
* MEASURED over the whole admitted population (3,255 tracked files, artifact
|
|
1133
|
+
* validation/m14/measurements/lua-extended-census.json): 1,185 `X = f(X)` sites;
|
|
1134
|
+
* 186 whose name is declared somewhere in the file; 3 at file scope; **1** that is
|
|
1135
|
+
* a decoration. The clauses below are what separate that one from the other 1,184:
|
|
1136
|
+
*
|
|
1137
|
+
* FILE SCOPE ONLY. A decorator is applied while the declaration is created, and
|
|
1138
|
+
* in Lua that is chunk scope. An in-function `X = f(X)` is a value assignment —
|
|
1139
|
+
* measured class: `value = sub(value, 2, -2)`, `payload = json_encode(payload)`.
|
|
1140
|
+
* Publishing those would attach a decoration to a local parameter.
|
|
1141
|
+
*
|
|
1142
|
+
* X IS A DECLARED FUNCTION. The decorated endpoint must be a declaration this
|
|
1143
|
+
* file writes (`function X(...)`, including `local function X`). A rebinding of
|
|
1144
|
+
* a table or a scalar is not a declaration being decorated.
|
|
1145
|
+
*
|
|
1146
|
+
* f IS A DECLARED WRAPPER FACTORY. `f`'s own body must RETURN a function
|
|
1147
|
+
* definition — that is the structural evidence it is a decorator and not a
|
|
1148
|
+
* transform. `cache` returns `function(x) ... end`; `sub`, `concat`,
|
|
1149
|
+
* `json_encode` return values. This clause is the whole difference between the
|
|
1150
|
+
* 185 value rebindings and the 1 decoration, and a name-based filter would not
|
|
1151
|
+
* find it (the dart lane's lesson: a filter on the NAME does not separate the
|
|
1152
|
+
* construct from its impostors).
|
|
1153
|
+
*
|
|
1154
|
+
* THE ARGUMENT IS THE DECORATED NAME. `f(X)` with the same identifier that is
|
|
1155
|
+
* rebound; `fib = cache(x)` decorates `x`'s value, not `fib`.
|
|
1156
|
+
*
|
|
1157
|
+
* ## Confidence and tier
|
|
1158
|
+
*
|
|
1159
|
+
* 0.95 and `usage_contextual`, NOT 1.0 and NOT `structural`. The application is a
|
|
1160
|
+
* real call site and both names are written in the file; what is a CONVENTION is
|
|
1161
|
+
* the READING — that a wrapper-factory application to a declaration is a
|
|
1162
|
+
* decoration rather than a memoization rebinding. Same number and tier this
|
|
1163
|
+
* analyzer already assigns its two other convention edges (`lua_factory_supertype`,
|
|
1164
|
+
* `lua_inherited_member`), and the same reason: "a 0.95 edge must not claim
|
|
1165
|
+
* `structural`" (see `addFactorySupertypeEdges`).
|
|
1166
|
+
*/
|
|
1167
|
+
extractDecoratorApplication(node, frame, ctx) {
|
|
1168
|
+
if (frame.inFunction) return;
|
|
1169
|
+
const inner = node.type === "variable_declaration" ? node.namedChild(0) : node;
|
|
1170
|
+
if (inner?.type !== "assignment_statement") return;
|
|
1171
|
+
const list = this.childOfType(inner, "variable_list");
|
|
1172
|
+
const values = this.childOfType(inner, "expression_list");
|
|
1173
|
+
const lhs = list?.namedChild(0);
|
|
1174
|
+
const call = values?.namedChild(0);
|
|
1175
|
+
if (lhs?.type !== "identifier" || call?.type !== "function_call") return;
|
|
1176
|
+
const callee = call.childForFieldName("name");
|
|
1177
|
+
if (callee?.type !== "identifier") return;
|
|
1178
|
+
const args = call.childForFieldName("arguments");
|
|
1179
|
+
const arg0 = args?.namedChild(0);
|
|
1180
|
+
if (arg0?.type !== "identifier" || arg0.text !== lhs.text) return;
|
|
1181
|
+
if (callee.text === lhs.text) return;
|
|
1182
|
+
if (!ctx.declaredFunctions.has(lhs.text)) return;
|
|
1183
|
+
if (!ctx.wrapperFactories.has(callee.text)) return;
|
|
1184
|
+
this.addRelationship(
|
|
1185
|
+
ctx,
|
|
1186
|
+
lhs.text,
|
|
1187
|
+
callee.text,
|
|
1188
|
+
ASTEngine.RelationshipType.Decorates,
|
|
1189
|
+
{
|
|
1190
|
+
luaSpecific: true,
|
|
1191
|
+
relationshipType: "lua_decorator_application",
|
|
1192
|
+
decorator: callee.text,
|
|
1193
|
+
evidenceClass: "usage_contextual"
|
|
1194
|
+
},
|
|
1195
|
+
call,
|
|
1196
|
+
{ confidence: 0.95 }
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* THE PRE-PASS for `decorates`: the file's declared function names and, of those,
|
|
1201
|
+
* the ones whose body RETURNS a function (wrapper factories).
|
|
1202
|
+
*
|
|
1203
|
+
* Order-independent by construction, for the same reason `collectDeclaredTables`
|
|
1204
|
+
* and `collectSupertypeLinks` are: a file may apply a factory declared below the
|
|
1205
|
+
* site, and a relation that depends on traversal order is a fact about the
|
|
1206
|
+
* traversal, not the program.
|
|
1207
|
+
*
|
|
1208
|
+
* `function X()` and `local function X()` both parse as `function_declaration` with
|
|
1209
|
+
* an `identifier` name (verified on the pinned build), so both are declarations for
|
|
1210
|
+
* this purpose. A `function M.x()` binds a FIELD — reachable only through the
|
|
1211
|
+
* table and not a bare name a decoration site could rebind — and is not collected.
|
|
1212
|
+
*/
|
|
1213
|
+
collectDecoratorFacts(root, ctx) {
|
|
1214
|
+
const stack = [root];
|
|
1215
|
+
while (stack.length > 0) {
|
|
1216
|
+
const node = stack.pop();
|
|
1217
|
+
if (node.type === "function_declaration") {
|
|
1218
|
+
const nm = node.childForFieldName("name");
|
|
1219
|
+
if (nm?.type === "identifier") {
|
|
1220
|
+
ctx.declaredFunctions.add(nm.text);
|
|
1221
|
+
if (this.returnsFunctionDefinition(node)) ctx.wrapperFactories.add(nm.text);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1225
|
+
const c = node.namedChild(i);
|
|
1226
|
+
if (c) stack.push(c);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Does this function's own body RETURN a function definition, at ITS OWN level?
|
|
1232
|
+
*
|
|
1233
|
+
* The walk stops at a nested function body, because a return inside it belongs to
|
|
1234
|
+
* that inner function. `function cache(f) ... return function(x) ... end end`
|
|
1235
|
+
* answers true; `function sub(s, i) return s end` answers false; a factory whose
|
|
1236
|
+
* inner wrapper itself returns a value (`return function(x) return f(x) end end`)
|
|
1237
|
+
* answers true from the OUTER return, which is the one that matters.
|
|
1238
|
+
*/
|
|
1239
|
+
returnsFunctionDefinition(fn) {
|
|
1240
|
+
const body = fn.childForFieldName("body");
|
|
1241
|
+
if (!body) return false;
|
|
1242
|
+
const visit = (n) => {
|
|
1243
|
+
if (n.type === "return_statement") {
|
|
1244
|
+
const list = this.childOfType(n, "expression_list");
|
|
1245
|
+
const first = list?.namedChild(0) ?? n.namedChild(0);
|
|
1246
|
+
if (first && (first.type === "function_definition" || first.type === "function_declaration")) return true;
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
if (n.type === "function_definition" || n.type === "function_declaration") return false;
|
|
1250
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1251
|
+
const c = n.namedChild(i);
|
|
1252
|
+
if (c && visit(c)) return true;
|
|
1253
|
+
}
|
|
1254
|
+
return false;
|
|
1255
|
+
};
|
|
1256
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
1257
|
+
const c = body.namedChild(i);
|
|
1258
|
+
if (c && visit(c)) return true;
|
|
1259
|
+
}
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
extractExport(node, frame, ctx) {
|
|
1263
|
+
if (frame.inFunction) return;
|
|
1264
|
+
const values = this.childOfType(node, "expression_list");
|
|
1265
|
+
const target = values?.namedChild(0);
|
|
1266
|
+
if (!target) return;
|
|
1267
|
+
if (target.type === "identifier") {
|
|
1268
|
+
ctx.exports.push({ name: target.text, isDefault: true, range: this.nodeToRange(node), metadata: { luaSpecific: true } });
|
|
1269
|
+
this.addExportEdge(ctx, target.text, target);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (target.type === "function_call") {
|
|
1273
|
+
const nameNode = target.childForFieldName("name") ?? target.namedChild(0);
|
|
1274
|
+
const calleeName = nameNode ? this.callee(nameNode)?.name : void 0;
|
|
1275
|
+
if (calleeName === "setmetatable") {
|
|
1276
|
+
const first = this.callArgument(target, 0);
|
|
1277
|
+
const named = this.nameBearing(first);
|
|
1278
|
+
if (named) {
|
|
1279
|
+
ctx.exports.push({ name: named, isDefault: true, range: this.nodeToRange(first), metadata: { luaSpecific: true, viaMetatable: true } });
|
|
1280
|
+
this.addExportEdge(ctx, named, first ?? null);
|
|
1281
|
+
} else {
|
|
1282
|
+
ctx.unpublishedSurface.push({ shape: "call-result", detail: calleeName ?? null, line: target.startPosition.row + 1, why: "setmetatable's first argument is an expression, so the surface has no name" });
|
|
1283
|
+
}
|
|
1284
|
+
} else if (calleeName === "require") {
|
|
1285
|
+
const arg = this.callArgument(target, 0);
|
|
1286
|
+
const path2 = arg?.type === "string" ? this.stringValue(arg) : null;
|
|
1287
|
+
ctx.unpublishedSurface.push({ shape: "re-export", detail: path2 ?? null, line: target.startPosition.row + 1, why: "the surface is another module's, not this module's" });
|
|
1288
|
+
} else {
|
|
1289
|
+
ctx.unpublishedSurface.push({ shape: "call-result", detail: calleeName ?? null, line: target.startPosition.row + 1, why: "the result of a call is statically unknowable" });
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (target.type === "string") {
|
|
1294
|
+
ctx.unpublishedSurface.push({ shape: "value-literal", detail: this.stringValue(target) ?? null, line: target.startPosition.row + 1, why: "a value is not a name" });
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (target.type === "function_definition") {
|
|
1298
|
+
ctx.unpublishedSurface.push({ shape: "anonymous-function", detail: null, line: target.startPosition.row + 1, why: "an anonymous function has no name" });
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (target.type === "table_constructor") {
|
|
1302
|
+
for (let i = 0; i < target.namedChildCount; i++) {
|
|
1303
|
+
const f = target.namedChild(i);
|
|
1304
|
+
if (!f || f.type !== "field") continue;
|
|
1305
|
+
const key = f.childForFieldName("name");
|
|
1306
|
+
if (!key) continue;
|
|
1307
|
+
let name;
|
|
1308
|
+
if (key.type === "identifier") name = key.text;
|
|
1309
|
+
else if (key.type === "string") name = this.stringValue(key);
|
|
1310
|
+
if (!name) continue;
|
|
1311
|
+
ctx.exports.push({ name, isDefault: false, range: this.nodeToRange(key), metadata: { luaSpecific: true } });
|
|
1312
|
+
this.addExportEdge(ctx, name, key);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/** M60 LUA-42: the `exports` edge for one nameable module-surface name.
|
|
1317
|
+
*
|
|
1318
|
+
* Confidence 1.0 and `evidenceClass: "structural"`, because both halves of the
|
|
1319
|
+
* claim are written in the file: a `return` at chunk scope names this name. That
|
|
1320
|
+
* is the difference from `uses`/`references` (LUA-40/LUA-41), where the name is
|
|
1321
|
+
* written but the BINDING it denotes is not — the reason those two carry 0.9
|
|
1322
|
+
* and those two only.
|
|
1323
|
+
*/
|
|
1324
|
+
addExportEdge(ctx, name, nameNode) {
|
|
1325
|
+
this.addRelationship(
|
|
1326
|
+
ctx,
|
|
1327
|
+
"global",
|
|
1328
|
+
name,
|
|
1329
|
+
ASTEngine.RelationshipType.Exports,
|
|
1330
|
+
{ luaSpecific: true, relationshipType: "lua_module_return" },
|
|
1331
|
+
nameNode
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* M60 LUA-44 — `overrides`, the type this lane was missing.
|
|
1336
|
+
*
|
|
1337
|
+
* MEASURED BEFORE THIS METHOD EXISTED, on 3,250 corpus files through
|
|
1338
|
+
* `parseFile` (script/m60/lua-ontology-census.ts):
|
|
1339
|
+
*
|
|
1340
|
+
* B1-override-redefinition 65 occurrences / 37 files
|
|
1341
|
+
* B2-override-inherited 1 occurrence / 1 file
|
|
1342
|
+
*
|
|
1343
|
+
* Both non-zero, so `overrides` could NOT be ruled and had to be built. That is
|
|
1344
|
+
* the whole reason this method exists: the ontology-coverage gate refuses a
|
|
1345
|
+
* ruling whose probe finds a non-zero count, and the lua grammar's 100 node
|
|
1346
|
+
* types include no `override_specifier` and no `redefinition` node, so no
|
|
1347
|
+
* grammar inventory would ever have found it — instrument A is silent here by
|
|
1348
|
+
* construction, and only instrument B could falsify the ruling.
|
|
1349
|
+
*
|
|
1350
|
+
* TWO SHAPES, two labels, because Lua expresses two different things and fusing
|
|
1351
|
+
* them would state one of them falsely:
|
|
1352
|
+
*
|
|
1353
|
+
* lua_redefinition the SAME name declared twice in one chunk. The later
|
|
1354
|
+
* declaration replaces the earlier one. 1.0, structural —
|
|
1355
|
+
* both endpoints are nodes this parse saw, and the
|
|
1356
|
+
* `overridesLine` metadata carries the prior declaration's
|
|
1357
|
+
* line so the two are distinguishable, exactly as
|
|
1358
|
+
* real-bash-analyzer.ts:343 does for the same construct.
|
|
1359
|
+
*
|
|
1360
|
+
* lua_inherited_member a member of D that D also inherits from a base B. 0.95,
|
|
1361
|
+
* structural — the family number for this edge is objc's
|
|
1362
|
+
* (real-objective-c-analyzer.ts:1020), and perl uses 0.95
|
|
1363
|
+
* for the same verified-base case. It is not 1.0 because
|
|
1364
|
+
* the corpus's inheritance is a CONVENTION over
|
|
1365
|
+
* `setmetatable`/`__index`, and nothing in the tree says
|
|
1366
|
+
* the link is meant as inheritance.
|
|
1367
|
+
*
|
|
1368
|
+
* The DIRECTION and the ENDPOINTS deliberately do NOT copy objc's
|
|
1369
|
+
* `Derived.member -> Base.member`. This lane's member-identity contract (LUA-15,
|
|
1370
|
+
* pinned by validation/m14/fixtures/lua/member-identity.expectation.json) holds
|
|
1371
|
+
* that a member's identity is its container plus its own name and that the
|
|
1372
|
+
* qualified path must not BE the name — that fixture FORBIDS `Buffer:initialize`
|
|
1373
|
+
* as a symbol name. So the edge runs `Derived -> Base`, the same pair `extends`
|
|
1374
|
+
* uses, and the overridden member travels in metadata as `member` plus the
|
|
1375
|
+
* qualified `memberPath`. Two tables in one file can therefore produce an
|
|
1376
|
+
* `overrides` and an `extends` edge between the same pair, under different type
|
|
1377
|
+
* labels; the store treats a distinct type on the same pair as a distinct link,
|
|
1378
|
+
* which is the property that keeps them from being read as one fact.
|
|
1379
|
+
*/
|
|
1380
|
+
addOverrideRelationships(ctx) {
|
|
1381
|
+
for (const [label, decls] of ctx.declarationsByLabel) {
|
|
1382
|
+
if (decls.length < 2) continue;
|
|
1383
|
+
const byLine = [...decls].sort((a, b) => a.line - b.line);
|
|
1384
|
+
const first = byLine[0];
|
|
1385
|
+
for (let i = 1; i < byLine.length; i++) {
|
|
1386
|
+
const d = byLine[i];
|
|
1387
|
+
this.addRelationship(
|
|
1388
|
+
ctx,
|
|
1389
|
+
d.owner ?? "global",
|
|
1390
|
+
d.owner ? d.member ?? label : label,
|
|
1391
|
+
ASTEngine.RelationshipType.Overrides,
|
|
1392
|
+
{
|
|
1393
|
+
luaSpecific: true,
|
|
1394
|
+
relationshipType: "lua_redefinition",
|
|
1395
|
+
redefinition: true,
|
|
1396
|
+
qualifiedName: label,
|
|
1397
|
+
overridesLine: first.line,
|
|
1398
|
+
overriddenOwner: first.owner
|
|
1399
|
+
},
|
|
1400
|
+
null,
|
|
1401
|
+
{ confidence: 1, symbolRange: { startLine: d.line } }
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
for (const [derived, bases] of ctx.supertypeLinks) {
|
|
1406
|
+
const derivedMembers = ctx.membersByOwner.get(derived);
|
|
1407
|
+
if (!derivedMembers || derivedMembers.size === 0) continue;
|
|
1408
|
+
for (const base of bases) {
|
|
1409
|
+
const baseMembers = ctx.membersByOwner.get(base);
|
|
1410
|
+
if (!baseMembers) continue;
|
|
1411
|
+
for (const [member, line] of derivedMembers) {
|
|
1412
|
+
if (!baseMembers.has(member)) continue;
|
|
1413
|
+
this.addRelationship(
|
|
1414
|
+
ctx,
|
|
1415
|
+
derived,
|
|
1416
|
+
base,
|
|
1417
|
+
ASTEngine.RelationshipType.Overrides,
|
|
1418
|
+
{
|
|
1419
|
+
luaSpecific: true,
|
|
1420
|
+
relationshipType: "lua_inherited_member",
|
|
1421
|
+
isOverride: true,
|
|
1422
|
+
member,
|
|
1423
|
+
memberPath: `${derived}.${member} -> ${base}.${member}`,
|
|
1424
|
+
// M60 LUA-52: the tier is `usage_contextual`, NOT the `structural` that
|
|
1425
|
+
// `evidenceClassFor` hands every type which is not imports/references/uses.
|
|
1426
|
+
// The rule this analyzer already wrote for ITSELF is at
|
|
1427
|
+
// `addFactorySupertypeEdges` (:1381): "a 0.95 edge must not claim
|
|
1428
|
+
// `structural`". `structural` asserts both endpoints are nodes this parse
|
|
1429
|
+
// saw, which is true here — and is not the claim. The claim is that a
|
|
1430
|
+
// member SHADOWS another through an `__index` chain, and Lua's inheritance
|
|
1431
|
+
// is a CONVENTION over `setmetatable`/`__index` with nothing in the tree
|
|
1432
|
+
// saying the link is meant as inheritance. Same reason, same 0.95, same
|
|
1433
|
+
// tier as `lua_factory_supertype`.
|
|
1434
|
+
//
|
|
1435
|
+
// MEASURED (validation/m60/lua/ontology-census.json, 3,252 files): 2 of
|
|
1436
|
+
// 67 `overrides` edges carried `structural` at confidence 0.95, and the
|
|
1437
|
+
// census counted 67 edges before and after this change — the tier is the
|
|
1438
|
+
// whole difference. Pinned by
|
|
1439
|
+
// `validation/m14/fixtures/lua/override-shapes.expectation.json`, whose
|
|
1440
|
+
// forbid half fails an implementation that derives the tier from the
|
|
1441
|
+
// type, and by the mutant in script/m60/mutate-lua-evidence-tier.ts.
|
|
1442
|
+
evidenceClass: "usage_contextual"
|
|
1443
|
+
},
|
|
1444
|
+
null,
|
|
1445
|
+
{ confidence: 0.95, symbolRange: { startLine: line } }
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
addContainsRelationships(ctx) {
|
|
1452
|
+
for (const symbol of ctx.symbols) {
|
|
1453
|
+
const owner = symbol.ownerName || symbol.frameSelfName || (symbol.scope === "global" || !symbol.scope ? "global" : symbol.scope);
|
|
1454
|
+
const selfNamed = owner === symbol.name;
|
|
1455
|
+
const ownerWrittenAsReceiver = symbol.metadata?.isMethod === true;
|
|
1456
|
+
const containerExists = selfNamed && ownerWrittenAsReceiver && ctx.symbols.some(
|
|
1457
|
+
(o) => o.name === owner && o !== symbol && (o.range?.startLine !== symbol.range?.startLine || o.kind !== symbol.kind)
|
|
1458
|
+
);
|
|
1459
|
+
if (owner && (owner !== symbol.name || containerExists)) {
|
|
1460
|
+
this.addRelationship(ctx, owner, symbol.name, ASTEngine.RelationshipType.Contains, {}, void 0, { symbolRange: symbol.range });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
addRelationship(ctx, source, target, type, metadata, nameNode, opts) {
|
|
1465
|
+
if (!source || !target) return;
|
|
1466
|
+
const key = `${type}${source}${target}`;
|
|
1467
|
+
if (ctx.seenRelationships.has(key)) return;
|
|
1468
|
+
ctx.seenRelationships.add(key);
|
|
1469
|
+
const relationship = {
|
|
1470
|
+
source,
|
|
1471
|
+
target,
|
|
1472
|
+
type,
|
|
1473
|
+
confidence: opts?.confidence ?? 1,
|
|
1474
|
+
metadata
|
|
1475
|
+
};
|
|
1476
|
+
if (nameNode) relationship.line_number = nameNode.startPosition.row + 1;
|
|
1477
|
+
else if (opts?.symbolRange) relationship.line_number = opts.symbolRange.startLine;
|
|
1478
|
+
relationship.metadata = { evidenceClass: this.evidenceClassFor(type), ...relationship.metadata };
|
|
1479
|
+
const receiver = nameNode ? this.stdlibReceiverOf(nameNode) : null;
|
|
1480
|
+
if (receiver) ctx.stdlibCandidates.push({ rel: relationship, receiver });
|
|
1481
|
+
const globalName = (type === ASTEngine.RelationshipType.Calls || type === ASTEngine.RelationshipType.Uses) && nameNode !== null && nameNode !== void 0 && target === nameNode.text ? this.stdlibGlobalNameOf(nameNode) : null;
|
|
1482
|
+
if (globalName !== null && LUA54_GLOBALS.has(globalName)) {
|
|
1483
|
+
ctx.stdlibGlobalCandidates.push({ rel: relationship, name: globalName });
|
|
1484
|
+
}
|
|
1485
|
+
ctx.relationships.push(relationship);
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* M60 LUA-54 — the standard-library receiver of a member read, or `null`.
|
|
1489
|
+
*
|
|
1490
|
+
* The name node is the edge's own target, so the test is structural rather than
|
|
1491
|
+
* a name comparison: walk up to the `dot_index_expression` /
|
|
1492
|
+
* `method_index_expression` that OWNS this node, require its receiver to be an
|
|
1493
|
+
* `identifier`, and return that identifier's text. `bracket_index_expression` is
|
|
1494
|
+
* excluded deliberately — `t["insert"]` is a member read whose key is a STRING
|
|
1495
|
+
* literal and never an identifier, so it cannot carry a receiver name, and
|
|
1496
|
+
* `stdlibReceiverOf` returning `null` for it is the correct answer rather than a
|
|
1497
|
+
* miss. `t[table.insert]` is a real `table.insert` read and is NOT marked: the
|
|
1498
|
+
* subscript is a computed value, so the name it denotes is not in the tree.
|
|
1499
|
+
*
|
|
1500
|
+
* Returns the RECEIVER's text and nothing else. Whether that name is a standard
|
|
1501
|
+
* library table is decided by the caller against a transcribed list, so this
|
|
1502
|
+
* method holds no list and cannot drift from one.
|
|
1503
|
+
*/
|
|
1504
|
+
stdlibReceiverOf(nameNode) {
|
|
1505
|
+
const owner = nameNode.parent;
|
|
1506
|
+
if (!owner) return null;
|
|
1507
|
+
if (owner.type !== "dot_index_expression" && owner.type !== "method_index_expression") return null;
|
|
1508
|
+
const field = owner.childForFieldName("field") ?? owner.childForFieldName("method");
|
|
1509
|
+
if (!field || !this.isSameNode(field, nameNode)) return null;
|
|
1510
|
+
const receiver = owner.childForFieldName("table");
|
|
1511
|
+
if (!receiver || receiver.type !== "identifier") return null;
|
|
1512
|
+
return receiver.text;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* M60 LUA-57 — the node's text when the node is a BARE identifier, or `null` when
|
|
1516
|
+
* it is a member's name.
|
|
1517
|
+
*
|
|
1518
|
+
* The inverse question to `stdlibReceiverOf`, and it is answered structurally for
|
|
1519
|
+
* the same reason: `("abc"):type()` and `obj.type()` both publish target `type`
|
|
1520
|
+
* with a node whose TEXT is `type`, and neither is the standard library global —
|
|
1521
|
+
* the first because its receiver is a literal, the second because its receiver is
|
|
1522
|
+
* an identifier that is not a documented table. Text alone cannot tell either from
|
|
1523
|
+
* a bare `type(x)`, so the shape decides: a name that is the FIELD or METHOD of an
|
|
1524
|
+
* index expression is a member's name and never a global read. The RECEIVER half
|
|
1525
|
+
* of an index expression is deliberately NOT excluded — it is a genuine bare read
|
|
1526
|
+
* of that identifier — but no `calls`/`uses` edge is ever published under it, so
|
|
1527
|
+
* the case is unreachable from the only two call sites that consult this.
|
|
1528
|
+
*/
|
|
1529
|
+
stdlibGlobalNameOf(nameNode) {
|
|
1530
|
+
if (nameNode.type !== "identifier") return null;
|
|
1531
|
+
const owner = nameNode.parent;
|
|
1532
|
+
if (owner && (owner.type === "dot_index_expression" || owner.type === "method_index_expression")) {
|
|
1533
|
+
const field = owner.childForFieldName("field") ?? owner.childForFieldName("method");
|
|
1534
|
+
if (field && this.isSameNode(field, nameNode)) return null;
|
|
1535
|
+
}
|
|
1536
|
+
return nameNode.text;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* M60 LUA-54 — mark the standard-library member edges, from ONE post-pass, after
|
|
1540
|
+
* every symbol this file declares is known.
|
|
1541
|
+
*
|
|
1542
|
+
* ## WHAT IT CLAIMS, AND THE MEASUREMENT BEHIND THE CLAIM
|
|
1543
|
+
*
|
|
1544
|
+
* An edge is marked `external` when its target is the field of a member read
|
|
1545
|
+
* whose receiver is an identifier naming one of the eight documented Lua 5.4
|
|
1546
|
+
* standard tables (`string`, `table`, `math`, `io`, `os`, `coroutine`, `debug`,
|
|
1547
|
+
* `utf8` — reference manual §6, transcribed into `LUA54_TABLES`), AND this file
|
|
1548
|
+
* publishes no symbol under that receiver.
|
|
1549
|
+
*
|
|
1550
|
+
* Measured on the whole-corpus draw of 3,252 .lua files from 199 repos
|
|
1551
|
+
* (drawDigest 502ef2f4..., 3,250 parsed, 219,981 relationships, production
|
|
1552
|
+
* `parseFile`): 3,525 edges are marked — 2,738 `calls` and 787 `references` —
|
|
1553
|
+
* out of 219,981, by `script/m60/verify-lua-stdlib-external.mts`. It changes NO
|
|
1554
|
+
* edge count and creates NO edge: the edge digest is `bc0c38b6...` with and
|
|
1555
|
+
* without this method, because the key is additive metadata and every consumer
|
|
1556
|
+
* that ignores it sees the identical graph.
|
|
1557
|
+
*
|
|
1558
|
+
* `script/m60/lua-external-marking.ts` puts the same rule at 3,956 from outside
|
|
1559
|
+
* the analyzer. The 431 difference is the outer instrument keying on LINE,
|
|
1560
|
+
* which credits a hit to every edge on that line — including the 398 `contains`
|
|
1561
|
+
* edges that share a line with a `references` edge and carry no node at all. The
|
|
1562
|
+
* two agree exactly on `calls` at 2,725, so the gap sits entirely in the types
|
|
1563
|
+
* with no node to test, which is what the analyzer can prove.
|
|
1564
|
+
*
|
|
1565
|
+
* ## THE RULE THAT WAS REJECTED, because it is the interesting half
|
|
1566
|
+
*
|
|
1567
|
+
* The first version of this marked any target whose bare name is in a
|
|
1568
|
+
* transcribed stdlib surface — 16,195 edges, 4.1x the above — and it is a
|
|
1569
|
+
* NAME-MATCH GUESS. This analyzer publishes the FINAL SEGMENT of a member read
|
|
1570
|
+
* (LUA-13), so `table.insert(x)` arrives as target `insert`, and a bare
|
|
1571
|
+
* `insert(x)` with no receiver arrives as target `insert` too. Lua 5.4
|
|
1572
|
+
* publishes no global named `insert`, so the name alone cannot tell the standard
|
|
1573
|
+
* library from a user's own global, and marking the second case `external`
|
|
1574
|
+
* would move it out of `unresolved` without moving anything toward truth — the
|
|
1575
|
+
* arbiter's PASS would then mean less than it did. Measured: **0** edge targets
|
|
1576
|
+
* a dotted `table.field` spelling anywhere in the corpus, which is why the name
|
|
1577
|
+
* list cannot recover the receiver from the target.
|
|
1578
|
+
*
|
|
1579
|
+
* ## WHAT IS DELIBERATELY LEFT UNMARKED
|
|
1580
|
+
*
|
|
1581
|
+
* `imports` targets (6,601 on the same draw) are Lua MODULE paths, not
|
|
1582
|
+
* declarations. One file cannot tell a sibling module from a luarocks rock, and
|
|
1583
|
+
* `classb-origin` measured both populations — 2,425 dotted targets whose module
|
|
1584
|
+
* file is in the corpus against 3,060 whose file is absent. Marking all 6,601
|
|
1585
|
+
* would assert the second count for the first.
|
|
1586
|
+
*
|
|
1587
|
+
* UPDATED 2026-10-11 (LUA-55), because the sentence that used to close this
|
|
1588
|
+
* paragraph — "the gate has no bucket for 'this type's target is a module', so they
|
|
1589
|
+
* are reported rather than swept" — stopped being true, and a comment that outlives
|
|
1590
|
+
* its code is this lane's most expensive defect class (LUA-21, 41811754). The gate
|
|
1591
|
+
* HAS a bucket now: `metadata.importProjectFile`, the shared file claim, and
|
|
1592
|
+
* `addImport` writes it. The split above is unchanged and is still why an `isExternal`
|
|
1593
|
+
* MARK is wrong here (1,560 of 5,485 REFUTED) — but the file claim is a different
|
|
1594
|
+
* statement about the same edge, and it is checkable rather than asserted. See
|
|
1595
|
+
* LUA-55 in docs/analyzers/lua.md.
|
|
1596
|
+
*/
|
|
1597
|
+
markStdlibExternal(ctx) {
|
|
1598
|
+
if (ctx.stdlibCandidates.length === 0 && ctx.stdlibGlobalCandidates.length === 0) return;
|
|
1599
|
+
const declaredAt = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (const s of ctx.symbols) {
|
|
1601
|
+
const line = Number(s.range?.startLine ?? s.startLine ?? 0);
|
|
1602
|
+
if (!s.name) continue;
|
|
1603
|
+
const arr = declaredAt.get(s.name) ?? [];
|
|
1604
|
+
arr.push(line);
|
|
1605
|
+
declaredAt.set(s.name, arr);
|
|
1606
|
+
}
|
|
1607
|
+
for (const { rel, receiver } of ctx.stdlibCandidates) {
|
|
1608
|
+
if (!LUA54_TABLES.has(receiver)) continue;
|
|
1609
|
+
const line = Number(rel.line_number ?? 0);
|
|
1610
|
+
const lines = declaredAt.get(receiver);
|
|
1611
|
+
if (lines && lines.some((l) => l <= 0 || line > 0 && l <= line)) continue;
|
|
1612
|
+
rel.metadata.external = true;
|
|
1613
|
+
rel.metadata.externalKind = "lua54-stdlib";
|
|
1614
|
+
rel.metadata.externalReceiver = receiver;
|
|
1615
|
+
}
|
|
1616
|
+
for (const { rel, name } of ctx.stdlibGlobalCandidates) {
|
|
1617
|
+
if (!LUA54_GLOBALS.has(name)) continue;
|
|
1618
|
+
const line = Number(rel.line_number ?? 0);
|
|
1619
|
+
const lines = declaredAt.get(name);
|
|
1620
|
+
if (lines && lines.some((l) => l <= 0 || line > 0 && l <= line)) continue;
|
|
1621
|
+
rel.metadata.external = true;
|
|
1622
|
+
rel.metadata.externalKind = "lua54-global";
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
/** M60 LUA-43: the evidence tier for a relationship type. See the call site for
|
|
1626
|
+
* why these three words and not a fourth. */
|
|
1627
|
+
evidenceClassFor(type) {
|
|
1628
|
+
if (type === ASTEngine.RelationshipType.Imports || type === ASTEngine.RelationshipType.References) return "import_based";
|
|
1629
|
+
if (type === ASTEngine.RelationshipType.Uses) return "usage_contextual";
|
|
1630
|
+
return "structural";
|
|
1631
|
+
}
|
|
1632
|
+
functionName(node) {
|
|
1633
|
+
return node.childForFieldName("name")?.text || node.namedChild(0)?.text;
|
|
1634
|
+
}
|
|
1635
|
+
hasChildType(node, type) {
|
|
1636
|
+
for (let i = 0; i < node.childCount; i++) if (node.child(i)?.type === type) return true;
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
isLocalFunction(node) {
|
|
1640
|
+
return this.hasChildType(node, "local");
|
|
1641
|
+
}
|
|
1642
|
+
isLocalVariableDeclaration(node) {
|
|
1643
|
+
return this.hasChildType(node, "local");
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The resolvable CONTAINER of a member: the HEAD of the receiver path.
|
|
1647
|
+
*
|
|
1648
|
+
* M60 LUA-37. `function Buffer:initialize()` has receiver `Buffer`; `function
|
|
1649
|
+
* Buffer.meta.index()` has receiver `Buffer.meta`. Returning the receiver's
|
|
1650
|
+
* text verbatim makes the second an owner of `"Buffer.meta"`, which no symbol is
|
|
1651
|
+
* filed under, so the owner still does not resolve. The head is the only part
|
|
1652
|
+
* of the path that is a declared name, and the analyzer already files
|
|
1653
|
+
* `Buffer` as a symbol. Returns undefined for a bare name — a plain function
|
|
1654
|
+
* has no container, and its own name is its scope.
|
|
1655
|
+
*/
|
|
1656
|
+
functionOwner(nameNode) {
|
|
1657
|
+
if (!["dot_index_expression", "method_index_expression"].includes(nameNode.type)) return void 0;
|
|
1658
|
+
const recv = nameNode.childForFieldName("table")?.text;
|
|
1659
|
+
if (!recv) return void 0;
|
|
1660
|
+
const head = recv.split(".")[0];
|
|
1661
|
+
return head.length > 0 ? head : void 0;
|
|
1662
|
+
}
|
|
1663
|
+
extractParameters(node) {
|
|
1664
|
+
const list = node.childForFieldName("parameters");
|
|
1665
|
+
if (!list) return [];
|
|
1666
|
+
const parameters = [];
|
|
1667
|
+
for (let i = 0; i < list.namedChildCount; i++) {
|
|
1668
|
+
const child = list.namedChild(i);
|
|
1669
|
+
if (!child) continue;
|
|
1670
|
+
if (child.type === "identifier") parameters.push({ name: child.text, type: "Any" });
|
|
1671
|
+
else if (child.type === "vararg_expression") parameters.push({ name: "...", type: "Any", isVariadic: true });
|
|
1672
|
+
}
|
|
1673
|
+
return parameters;
|
|
1674
|
+
}
|
|
1675
|
+
plainVariableName(node) {
|
|
1676
|
+
return node?.type === "identifier" ? node.text : void 0;
|
|
1677
|
+
}
|
|
1678
|
+
isTableNode(node) {
|
|
1679
|
+
return node?.type === "table_constructor";
|
|
1680
|
+
}
|
|
1681
|
+
assignmentValue(node) {
|
|
1682
|
+
const expressionList = node.childForFieldName("value") || node.namedChild(1);
|
|
1683
|
+
return expressionList?.namedChild(0) || expressionList || void 0;
|
|
1684
|
+
}
|
|
1685
|
+
callArguments(node) {
|
|
1686
|
+
const argumentList = node.childForFieldName("arguments");
|
|
1687
|
+
if (!argumentList) return [];
|
|
1688
|
+
const source = argumentList;
|
|
1689
|
+
const values = [];
|
|
1690
|
+
for (let i = 0; i < source.namedChildCount; i++) {
|
|
1691
|
+
const child = source.namedChild(i);
|
|
1692
|
+
if (child) values.push(child);
|
|
1693
|
+
}
|
|
1694
|
+
return values;
|
|
1695
|
+
}
|
|
1696
|
+
callArgument(node, index) {
|
|
1697
|
+
return this.callArguments(node)[index];
|
|
1698
|
+
}
|
|
1699
|
+
stringValue(node) {
|
|
1700
|
+
if (!node || node.type !== "string") return void 0;
|
|
1701
|
+
return this.cleanLuaString(node.text);
|
|
1702
|
+
}
|
|
1703
|
+
tableFieldNames(node) {
|
|
1704
|
+
const result = [];
|
|
1705
|
+
const fieldList = node;
|
|
1706
|
+
for (let i = 0; i < (fieldList?.namedChildCount || 0); i++) {
|
|
1707
|
+
const field = fieldList?.namedChild(i);
|
|
1708
|
+
if (!field || field.type !== "field") continue;
|
|
1709
|
+
const key = field.childForFieldName("name") || field.namedChild(0);
|
|
1710
|
+
if (key?.text) result.push(key.text);
|
|
1711
|
+
}
|
|
1712
|
+
return result;
|
|
1713
|
+
}
|
|
1714
|
+
// M60 LUA-13 + LUA-01: the callee's NAME and the node that CARRIES ITS LINE
|
|
1715
|
+
// must be the same node, returned together so they cannot drift apart.
|
|
1716
|
+
//
|
|
1717
|
+
// Getting this wrong took three measurements, and each wrong answer was
|
|
1718
|
+
// plausible enough to nearly ship:
|
|
1719
|
+
// 1. `node.text` on the callee — on a call split across lines that is a
|
|
1720
|
+
// SOURCE SNIPPET ("obj\n .method"), which the pool-worker's
|
|
1721
|
+
// garbage-target filter deletes (pool-worker.ts:391 rejects any target
|
|
1722
|
+
// containing a newline). Consequence: `obj\n.method()` produced NO EDGE
|
|
1723
|
+
// while the identical one-line call produced one. A line break silently
|
|
1724
|
+
// removed a call from the graph.
|
|
1725
|
+
// 2. the first identifier in the index expression — that is the RECEIVER
|
|
1726
|
+
// (`obj`), not the thing called.
|
|
1727
|
+
// 3. the index expression itself — its start is the receiver's line, so
|
|
1728
|
+
// `obj\n .method()` reported the `obj` line and the gate caught it.
|
|
1729
|
+
//
|
|
1730
|
+
// The callee of `table . field` / `table : field` is `field`, and `field` is
|
|
1731
|
+
// the LAST named child, so its own start position is the line the method name
|
|
1732
|
+
// is written on — which is exactly M59 (#825)'s rule and exactly what java
|
|
1733
|
+
// gets from `childForFieldName("name")` on its invocation node.
|
|
1734
|
+
// M60 LUA-32: the ONE rule for "is this node a name", shared by the callee
|
|
1735
|
+
// whitelist (LUA-29/LUA-30) and the setmetatable path, so the two cannot drift
|
|
1736
|
+
// apart the way `table_index_expression` did (LUA-27). Three node forms carry a
|
|
1737
|
+
// name; everything else is an expression, and an expression used as a target is
|
|
1738
|
+
// a payload — unmatchable, unreferenceable, and unbounded in shape.
|
|
1739
|
+
//
|
|
1740
|
+
// `dot_index_expression` and `method_index_expression` yield their FULL TEXT
|
|
1741
|
+
// (`Account.Meta`), which is a path rather than a bare name. That is kept
|
|
1742
|
+
// deliberately: these are relationship ENDPOINTS, not symbols, and the
|
|
1743
|
+
// member-identity rule that forbids '.' in a SYMBOL name (verify-lua-member-identity)
|
|
1744
|
+
// is a different contract from what a reference to `Account.Meta` may look like.
|
|
1745
|
+
nameBearing(node) {
|
|
1746
|
+
if (!node) return void 0;
|
|
1747
|
+
if (node.type === "identifier") return node.text;
|
|
1748
|
+
if (node.type === "dot_index_expression" || node.type === "method_index_expression") return node.text;
|
|
1749
|
+
return void 0;
|
|
1750
|
+
}
|
|
1751
|
+
// M60 LUA-56 — WHY THE SHAPE IS RETURNED WITH THE NAME. `calls` is this lane's largest
|
|
1752
|
+
// remaining cause (42,889 not-resolved edges, 30,370 ambiguous + 12,519 unresolved, measured
|
|
1753
|
+
// on the whole admitted population), and the class is TWO mechanisms wearing one label:
|
|
1754
|
+
//
|
|
1755
|
+
// `foo()` an IDENTIFIER callee — the name the language looks up through the file's
|
|
1756
|
+
// lexical scope and then `_ENV`. This is the only shape about which an
|
|
1757
|
+
// externality question can even be asked, because the target IS the name.
|
|
1758
|
+
// `M.foo()` an INDEX callee — `callee()` publishes the FIELD (LUA-13), so the target is a
|
|
1759
|
+
// `M:foo()` member name whose CONTAINER the edge does not carry. "declared nowhere in the
|
|
1760
|
+
// `M["foo"]()` project" is then a statement about a field, not about a global.
|
|
1761
|
+
//
|
|
1762
|
+
// The distinction is DECIDED BY THE BRANCH THAT PRODUCES THE NAME — the same node test that
|
|
1763
|
+
// returns the name decides which form it is — so it is returned from here rather than
|
|
1764
|
+
// re-derived at the call site, where a second copy of the node test could silently drift from
|
|
1765
|
+
// this one. That is the "one spelling, two places" hazard this lane has paid for repeatedly.
|
|
1766
|
+
//
|
|
1767
|
+
// THE TWO KEYS ARE A PARTITION of the `calls` population (every edge carries exactly one), and
|
|
1768
|
+
// they are marker KEYS rather than one value-carrying key on purpose: the gate's marker table
|
|
1769
|
+
// and this lane's own cause reader both group by KEY ONLY ("a value would split one marker into
|
|
1770
|
+
// a signature per symbol it names"), so a fact that must be separable there has to be a key.
|
|
1771
|
+
//
|
|
1772
|
+
// THE THIRD STATE IS DELIBERATE. The fallthrough below publishes a name taken from arbitrary
|
|
1773
|
+
// node text — the payload-as-a-name defect LUA-22/LUA-29/LUA-30 closed for every form the
|
|
1774
|
+
// pinned grammar can produce — and it carries NO shape key. An edge in that state is therefore
|
|
1775
|
+
// visible as a hole (the lane's `calls-file-scope-pricing.mts` reports it as UNPUBLISHED, and a
|
|
1776
|
+
// non-zero reading there means the grammar grew a callee form this whitelist has not seen)
|
|
1777
|
+
// rather than being silently filed under one of the two shapes.
|
|
1778
|
+
callee(functionNode) {
|
|
1779
|
+
if (functionNode.type === "identifier") return { name: functionNode.text, nameNode: functionNode, shapeMark: { calleeIdentifier: true } };
|
|
1780
|
+
if (functionNode.type === "dot_index_expression" || functionNode.type === "method_index_expression") {
|
|
1781
|
+
const last = functionNode.namedChildCount > 0 ? functionNode.namedChild(functionNode.namedChildCount - 1) : void 0;
|
|
1782
|
+
if (last?.text) return { name: last.text, nameNode: last, shapeMark: { calleeIndexed: true } };
|
|
1783
|
+
}
|
|
1784
|
+
if (functionNode.type === "bracket_index_expression") {
|
|
1785
|
+
const field = functionNode.childForFieldName("field");
|
|
1786
|
+
if (field?.type !== "string") return void 0;
|
|
1787
|
+
const literal = this.stringValue(field);
|
|
1788
|
+
return literal ? { name: literal, nameNode: field, shapeMark: { calleeIndexed: true } } : void 0;
|
|
1789
|
+
}
|
|
1790
|
+
if (functionNode.type === "function_call" || functionNode.type === "table_constructor" || functionNode.type === "binary_expression" || functionNode.type === "parenthesized_expression" || functionNode.type === "string" || functionNode.type === "number") return void 0;
|
|
1791
|
+
if (functionNode.text) return { name: functionNode.text, nameNode: functionNode, shapeMark: {} };
|
|
1792
|
+
return void 0;
|
|
1793
|
+
}
|
|
1794
|
+
// M60 LUA-01: reach a child by node TYPE. The five field literals this
|
|
1795
|
+
// analyzer used to ask for (`name_list`, `function`, `expression_list`, `key`)
|
|
1796
|
+
// do not exist in the pinned grammar, and every one of them sat behind a
|
|
1797
|
+
// `|| namedChild(0)` fallback that turned "this field does not exist" into
|
|
1798
|
+
// "quietly use whatever happens to be first" — a wrong answer that could not
|
|
1799
|
+
// raise, log, or fail. Type-directed lookup has no fallback to hide behind:
|
|
1800
|
+
// either the child is there or the path does not run.
|
|
1801
|
+
childOfType(node, type) {
|
|
1802
|
+
if (!node) return void 0;
|
|
1803
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1804
|
+
const child = node.namedChild(i);
|
|
1805
|
+
if (child?.type === type) return child;
|
|
1806
|
+
}
|
|
1807
|
+
return void 0;
|
|
1808
|
+
}
|
|
1809
|
+
cleanLuaString(text) {
|
|
1810
|
+
const cleaned = text.trim();
|
|
1811
|
+
if (cleaned.length >= 2) {
|
|
1812
|
+
const first = cleaned[0];
|
|
1813
|
+
const last = cleaned[cleaned.length - 1];
|
|
1814
|
+
if ((first === '"' || first === "'") && first === last) return cleaned.substring(1, cleaned.length - 1);
|
|
1815
|
+
}
|
|
1816
|
+
if (cleaned.startsWith("[[") && cleaned.endsWith("]]")) return cleaned.substring(2, cleaned.length - 2);
|
|
1817
|
+
return cleaned;
|
|
1818
|
+
}
|
|
1819
|
+
// M60 LUA-04: framework detection fired on EVERY identifier node and matched
|
|
1820
|
+
// BARE NAMES — `use`, `setup`, `GetTime`, `C_Timer`, `Proto`, `UIParent`. So a
|
|
1821
|
+
// file containing `local setup = 1` was labelled "Neovim Configuration", and
|
|
1822
|
+
// the committed gate for this defect class caught a probe labelled BOTH
|
|
1823
|
+
// "Neovim Configuration" AND "World of Warcraft Addon" purely from three
|
|
1824
|
+
// ordinary variable names.
|
|
1825
|
+
//
|
|
1826
|
+
// The analyzer's own header claims "No regex fallback or source-wide scanning
|
|
1827
|
+
// is used". A bare-name equality test applied to every identifier is
|
|
1828
|
+
// source-wide scanning wearing an AST costume, so this is a contract failure
|
|
1829
|
+
// and not only a precision one.
|
|
1830
|
+
//
|
|
1831
|
+
// The rule now: a framework is claimed only from a QUALIFIED member access
|
|
1832
|
+
// (`vim.api`, `love.graphics`, `ngx.timer`) or from a `require`d module name
|
|
1833
|
+
// that names it. A bare identifier never names a framework. That is narrower
|
|
1834
|
+
// than the old rule on purpose — and the gate asserts the CONVERSE too, so it
|
|
1835
|
+
// cannot be satisfied by deleting detection: a file that really does use
|
|
1836
|
+
// `vim.api` must still be labelled Neovim.
|
|
1837
|
+
detectFrameworks(text, ctx) {
|
|
1838
|
+
if (!text.includes(".")) return;
|
|
1839
|
+
const head = text.split(".")[0];
|
|
1840
|
+
if (!head) return;
|
|
1841
|
+
if (head === "vim") ctx.frameworks.add("Neovim Configuration");
|
|
1842
|
+
if (head === "love") ctx.frameworks.add("Love2D");
|
|
1843
|
+
if (head === "ngx" || head === "resty") ctx.frameworks.add("OpenResty");
|
|
1844
|
+
if (head === "display" || head === "system" || head === "transition" || head === "Runtime") ctx.frameworks.add("Corona SDK / Solar2D");
|
|
1845
|
+
if (head === "lovr") ctx.frameworks.add("L\xD6VR (VR/AR)");
|
|
1846
|
+
if (head === "redis") ctx.frameworks.add("Redis Scripting");
|
|
1847
|
+
}
|
|
1848
|
+
collectErrors(rootNode, ctx) {
|
|
1849
|
+
if (!rootNode.hasError) return;
|
|
1850
|
+
const stack = [rootNode];
|
|
1851
|
+
while (stack.length) {
|
|
1852
|
+
const node = stack.pop();
|
|
1853
|
+
if (node.type === "ERROR" || node.isMissing) {
|
|
1854
|
+
ctx.errors.push({ message: `Syntax ${node.type} detected.`, range: this.nodeToRange(node), severity: "warning" });
|
|
1855
|
+
}
|
|
1856
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1857
|
+
const child = node.namedChild(i);
|
|
1858
|
+
if (child) stack.push(child);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
calculateCyclomaticComplexity(rootNode) {
|
|
1863
|
+
let complexity = 1;
|
|
1864
|
+
const stack = [rootNode];
|
|
1865
|
+
while (stack.length) {
|
|
1866
|
+
const node = stack.pop();
|
|
1867
|
+
if (node.type === "if_statement" || node.type === "for_statement" || node.type === "while_statement" || node.type === "repeat_statement") complexity++;
|
|
1868
|
+
if (node.type === "binary_expression") {
|
|
1869
|
+
const operator = node.childForFieldName("operator");
|
|
1870
|
+
if (operator?.text === "and" || operator?.text === "or") complexity++;
|
|
1871
|
+
}
|
|
1872
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1873
|
+
const child = node.namedChild(i);
|
|
1874
|
+
if (child) stack.push(child);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return complexity;
|
|
1878
|
+
}
|
|
1879
|
+
// M60 LUA-02: this counted `symbol.metadata?.isCoroutine`, which NO code path
|
|
1880
|
+
// in the analyzer ever set — processCall adds the string "Coroutines" to
|
|
1881
|
+
// ctx.patterns and stops. So the count could only ever read 0, and it was
|
|
1882
|
+
// published as a count. It is now counted from the calls the walk actually
|
|
1883
|
+
// saw: `coroutine.create` and `coroutine.wrap` are ordinary function_call
|
|
1884
|
+
// nodes, so a file that uses them reports a positive number and a file that
|
|
1885
|
+
// does not reports 0 that means something.
|
|
1886
|
+
// M60 LUA-02. Counted during the walk, not afterwards and not from the stored
|
|
1887
|
+
// edges: the edge carries the callee FIELD, so after the LUA-13 fix there is
|
|
1888
|
+
// no relationship whose target is "coroutine.create" — the field is "create".
|
|
1889
|
+
// Two earlier versions of this fix were wrong in ways the gate caught: the
|
|
1890
|
+
// first counted ctx.symbols (a call is not a declaration, so it read 0), the
|
|
1891
|
+
// second counted relationship targets (which no longer exist under that name,
|
|
1892
|
+
// so it also read 0). Counting at the point where the qualified name is still
|
|
1893
|
+
// in hand is the only place the fact exists.
|
|
1894
|
+
countCoroutineSymbols(ctx) {
|
|
1895
|
+
return ctx.coroutineCalls;
|
|
1896
|
+
}
|
|
1897
|
+
// M60 LUA-11: a derived figure, or absent. Returns undefined when there is
|
|
1898
|
+
// nothing to measure rather than a number that means nothing.
|
|
1899
|
+
relationshipDensity(ctx) {
|
|
1900
|
+
if (ctx.symbols.length === 0 && ctx.relationships.length === 0) return void 0;
|
|
1901
|
+
const perSymbol = ctx.symbols.length === 0 ? 0 : ctx.relationships.length / ctx.symbols.length;
|
|
1902
|
+
return Math.max(0, Math.min(100, Math.round(100 - perSymbol * 10)));
|
|
1903
|
+
}
|
|
1904
|
+
retainedRowKb(ctx) {
|
|
1905
|
+
if (ctx.symbols.length === 0 && ctx.relationships.length === 0) return void 0;
|
|
1906
|
+
const SYMBOL_ROW_KB = 0.5;
|
|
1907
|
+
const RELATIONSHIP_ROW_KB = 0.25;
|
|
1908
|
+
const kb = ctx.symbols.length * SYMBOL_ROW_KB + ctx.relationships.length * RELATIONSHIP_ROW_KB;
|
|
1909
|
+
return Math.round(kb * 100) / 100;
|
|
1910
|
+
}
|
|
1911
|
+
// M60 LUA-10: the +5 branch for "Module/Table Pattern" rewarded a string
|
|
1912
|
+
// nothing ever added, so a published score was computed from a dead branch —
|
|
1913
|
+
// and codeQuality thresholds on that score, so the error propagated into a
|
|
1914
|
+
// second published field. The branch is gone rather than wired up: a module
|
|
1915
|
+
// table IS this language's module idiom, so awarding points for it would
|
|
1916
|
+
// award points to most Lua files, which measures nothing.
|
|
1917
|
+
calculateArchitecturalScore(ctx) {
|
|
1918
|
+
let score = 70;
|
|
1919
|
+
if (ctx.relationships.length > 0) score += 10;
|
|
1920
|
+
if (ctx.exports.length > 0) score += 5;
|
|
1921
|
+
if (ctx.patterns.has("Object-Oriented Programming (Metatables)")) score += 10;
|
|
1922
|
+
if (ctx.patterns.has("Object-Oriented Programming (Libraries)")) score += 5;
|
|
1923
|
+
return Math.min(100, score);
|
|
1924
|
+
}
|
|
1925
|
+
assessCodeQuality(ctx) {
|
|
1926
|
+
const score = this.calculateArchitecturalScore(ctx);
|
|
1927
|
+
if (score >= 90) return "excellent";
|
|
1928
|
+
if (score >= 80) return "good";
|
|
1929
|
+
if (score >= 60) return "fair";
|
|
1930
|
+
return "poor";
|
|
1931
|
+
}
|
|
1932
|
+
generateArchitecturalInsights(ctx) {
|
|
1933
|
+
const insights = [];
|
|
1934
|
+
if (ctx.patterns.has("Object-Oriented Programming (Metatables)")) {
|
|
1935
|
+
insights.push({ type: "architecture", category: "lua_oop", message: "Lua object-oriented patterns detected through metatables.", severity: "info" });
|
|
1936
|
+
}
|
|
1937
|
+
return insights;
|
|
1938
|
+
}
|
|
1939
|
+
lineCount(content) {
|
|
1940
|
+
let count = 1;
|
|
1941
|
+
for (let i = 0; i < content.length; i++) if (content[i] === "\n") count++;
|
|
1942
|
+
return count;
|
|
1943
|
+
}
|
|
1944
|
+
nodeToRange(node) {
|
|
1945
|
+
return {
|
|
1946
|
+
startLine: node.startPosition.row + 1,
|
|
1947
|
+
startColumn: node.startPosition.column + 1,
|
|
1948
|
+
endLine: node.endPosition.row + 1,
|
|
1949
|
+
endColumn: node.endPosition.column + 1,
|
|
1950
|
+
startIndex: node.startIndex,
|
|
1951
|
+
endIndex: node.endIndex
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
export {
|
|
1956
|
+
RealLuaAnalyzer
|
|
1957
|
+
};
|