@colbymchenry/codegraph-darwin-x64 0.9.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dist/bin/codegraph.d.ts +1 -0
- package/lib/dist/bin/codegraph.d.ts.map +1 -1
- package/lib/dist/bin/codegraph.js +247 -39
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/context/index.d.ts +9 -0
- package/lib/dist/context/index.d.ts.map +1 -1
- package/lib/dist/context/index.js +102 -6
- package/lib/dist/context/index.js.map +1 -1
- package/lib/dist/context/markers.d.ts +19 -0
- package/lib/dist/context/markers.d.ts.map +1 -0
- package/lib/dist/context/markers.js +22 -0
- package/lib/dist/context/markers.js.map +1 -0
- package/lib/dist/db/index.d.ts.map +1 -1
- package/lib/dist/db/index.js +2 -1
- package/lib/dist/db/index.js.map +1 -1
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +10 -1
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/queries.d.ts +43 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +103 -7
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/db/schema.sql +1 -0
- package/lib/dist/db/sqlite-adapter.d.ts +7 -0
- package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/lib/dist/db/sqlite-adapter.js +3 -0
- package/lib/dist/db/sqlite-adapter.js.map +1 -1
- package/lib/dist/directory.d.ts +34 -2
- package/lib/dist/directory.d.ts.map +1 -1
- package/lib/dist/directory.js +129 -35
- package/lib/dist/directory.js.map +1 -1
- package/lib/dist/extraction/astro-extractor.d.ts +79 -0
- package/lib/dist/extraction/astro-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/astro-extractor.js +320 -0
- package/lib/dist/extraction/astro-extractor.js.map +1 -0
- package/lib/dist/extraction/extraction-version.d.ts +25 -0
- package/lib/dist/extraction/extraction-version.d.ts.map +1 -0
- package/lib/dist/extraction/extraction-version.js +28 -0
- package/lib/dist/extraction/extraction-version.js.map +1 -0
- package/lib/dist/extraction/function-ref.d.ts +118 -0
- package/lib/dist/extraction/function-ref.d.ts.map +1 -0
- package/lib/dist/extraction/function-ref.js +727 -0
- package/lib/dist/extraction/function-ref.js.map +1 -0
- package/lib/dist/extraction/generated-detection.d.ts.map +1 -1
- package/lib/dist/extraction/generated-detection.js +3 -0
- package/lib/dist/extraction/generated-detection.js.map +1 -1
- package/lib/dist/extraction/grammars.d.ts +7 -1
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +52 -4
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +34 -0
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +346 -62
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.d.ts +8 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.js +87 -28
- package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
- package/lib/dist/extraction/languages/csharp.d.ts +22 -0
- package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/csharp.js +84 -2
- package/lib/dist/extraction/languages/csharp.js.map +1 -1
- package/lib/dist/extraction/languages/dart.d.ts.map +1 -1
- package/lib/dist/extraction/languages/dart.js +161 -1
- package/lib/dist/extraction/languages/dart.js.map +1 -1
- package/lib/dist/extraction/languages/go.d.ts.map +1 -1
- package/lib/dist/extraction/languages/go.js +43 -2
- package/lib/dist/extraction/languages/go.js.map +1 -1
- package/lib/dist/extraction/languages/index.d.ts.map +1 -1
- package/lib/dist/extraction/languages/index.js +2 -0
- package/lib/dist/extraction/languages/index.js.map +1 -1
- package/lib/dist/extraction/languages/java.d.ts.map +1 -1
- package/lib/dist/extraction/languages/java.js +42 -1
- package/lib/dist/extraction/languages/java.js.map +1 -1
- package/lib/dist/extraction/languages/javascript.d.ts.map +1 -1
- package/lib/dist/extraction/languages/javascript.js +16 -0
- package/lib/dist/extraction/languages/javascript.js.map +1 -1
- package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/lib/dist/extraction/languages/kotlin.js +69 -0
- package/lib/dist/extraction/languages/kotlin.js.map +1 -1
- package/lib/dist/extraction/languages/objc.d.ts.map +1 -1
- package/lib/dist/extraction/languages/objc.js +42 -0
- package/lib/dist/extraction/languages/objc.js.map +1 -1
- package/lib/dist/extraction/languages/pascal.d.ts.map +1 -1
- package/lib/dist/extraction/languages/pascal.js +11 -0
- package/lib/dist/extraction/languages/pascal.js.map +1 -1
- package/lib/dist/extraction/languages/php.d.ts.map +1 -1
- package/lib/dist/extraction/languages/php.js +90 -1
- package/lib/dist/extraction/languages/php.js.map +1 -1
- package/lib/dist/extraction/languages/r.d.ts +3 -0
- package/lib/dist/extraction/languages/r.d.ts.map +1 -0
- package/lib/dist/extraction/languages/r.js +314 -0
- package/lib/dist/extraction/languages/r.js.map +1 -0
- package/lib/dist/extraction/languages/ruby.d.ts.map +1 -1
- package/lib/dist/extraction/languages/ruby.js +35 -0
- package/lib/dist/extraction/languages/ruby.js.map +1 -1
- package/lib/dist/extraction/languages/rust.d.ts.map +1 -1
- package/lib/dist/extraction/languages/rust.js +35 -2
- package/lib/dist/extraction/languages/rust.js.map +1 -1
- package/lib/dist/extraction/languages/scala.d.ts.map +1 -1
- package/lib/dist/extraction/languages/scala.js +61 -1
- package/lib/dist/extraction/languages/scala.js.map +1 -1
- package/lib/dist/extraction/languages/swift.d.ts.map +1 -1
- package/lib/dist/extraction/languages/swift.js +61 -0
- package/lib/dist/extraction/languages/swift.js.map +1 -1
- package/lib/dist/extraction/languages/typescript.d.ts +13 -0
- package/lib/dist/extraction/languages/typescript.d.ts.map +1 -1
- package/lib/dist/extraction/languages/typescript.js +38 -0
- package/lib/dist/extraction/languages/typescript.js.map +1 -1
- package/lib/dist/extraction/liquid-extractor.d.ts +7 -0
- package/lib/dist/extraction/liquid-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/liquid-extractor.js +53 -9
- package/lib/dist/extraction/liquid-extractor.js.map +1 -1
- package/lib/dist/extraction/razor-extractor.d.ts +42 -0
- package/lib/dist/extraction/razor-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/razor-extractor.js +285 -0
- package/lib/dist/extraction/razor-extractor.js.map +1 -0
- package/lib/dist/extraction/svelte-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/svelte-extractor.js +6 -3
- package/lib/dist/extraction/svelte-extractor.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.js +59 -10
- package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +33 -0
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +237 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +1820 -68
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/extraction/vue-extractor.d.ts +15 -0
- package/lib/dist/extraction/vue-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/vue-extractor.js +94 -3
- package/lib/dist/extraction/vue-extractor.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
- package/lib/dist/graph/queries.d.ts.map +1 -1
- package/lib/dist/graph/queries.js +13 -40
- package/lib/dist/graph/queries.js.map +1 -1
- package/lib/dist/graph/traversal.d.ts.map +1 -1
- package/lib/dist/graph/traversal.js +16 -4
- package/lib/dist/graph/traversal.js.map +1 -1
- package/lib/dist/index.d.ts +41 -3
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +99 -9
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +52 -2
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/instructions-template.d.ts +34 -11
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +44 -12
- package/lib/dist/installer/instructions-template.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +6 -10
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/codex.js +4 -6
- package/lib/dist/installer/targets/codex.js.map +1 -1
- package/lib/dist/installer/targets/gemini.js +4 -6
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/opencode.d.ts +9 -1
- package/lib/dist/installer/targets/opencode.d.ts.map +1 -1
- package/lib/dist/installer/targets/opencode.js +91 -40
- package/lib/dist/installer/targets/opencode.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +14 -0
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +19 -2
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/mcp/daemon.d.ts +60 -1
- package/lib/dist/mcp/daemon.d.ts.map +1 -1
- package/lib/dist/mcp/daemon.js +221 -8
- package/lib/dist/mcp/daemon.js.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.d.ts +41 -0
- package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
- package/lib/dist/mcp/dynamic-boundaries.js +359 -0
- package/lib/dist/mcp/dynamic-boundaries.js.map +1 -0
- package/lib/dist/mcp/index.d.ts.map +1 -1
- package/lib/dist/mcp/index.js +18 -9
- package/lib/dist/mcp/index.js.map +1 -1
- package/lib/dist/mcp/ppid-watchdog.d.ts +44 -0
- package/lib/dist/mcp/ppid-watchdog.d.ts.map +1 -0
- package/lib/dist/mcp/ppid-watchdog.js +27 -0
- package/lib/dist/mcp/ppid-watchdog.js.map +1 -0
- package/lib/dist/mcp/proxy.d.ts +6 -0
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +153 -24
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +12 -1
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +58 -32
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.d.ts +2 -0
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +49 -2
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/stdin-teardown.d.ts +27 -0
- package/lib/dist/mcp/stdin-teardown.d.ts.map +1 -0
- package/lib/dist/mcp/stdin-teardown.js +49 -0
- package/lib/dist/mcp/stdin-teardown.js.map +1 -0
- package/lib/dist/mcp/tools.d.ts +110 -49
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +1222 -972
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/mcp/transport.d.ts.map +1 -1
- package/lib/dist/mcp/transport.js +18 -2
- package/lib/dist/mcp/transport.js.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts +3 -3
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +549 -21
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/frameworks/astro.d.ts +9 -0
- package/lib/dist/resolution/frameworks/astro.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/astro.js +169 -0
- package/lib/dist/resolution/frameworks/astro.js.map +1 -0
- package/lib/dist/resolution/frameworks/expo-modules.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/expo-modules.js +6 -1
- package/lib/dist/resolution/frameworks/expo-modules.js.map +1 -1
- package/lib/dist/resolution/frameworks/index.d.ts +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/index.js +5 -1
- package/lib/dist/resolution/frameworks/index.js.map +1 -1
- package/lib/dist/resolution/frameworks/java.js +6 -1
- package/lib/dist/resolution/frameworks/java.js.map +1 -1
- package/lib/dist/resolution/frameworks/python.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/python.js +7 -3
- package/lib/dist/resolution/frameworks/python.js.map +1 -1
- package/lib/dist/resolution/frameworks/react-native.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/react-native.js +53 -3
- package/lib/dist/resolution/frameworks/react-native.js.map +1 -1
- package/lib/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/react.js +15 -3
- package/lib/dist/resolution/frameworks/react.js.map +1 -1
- package/lib/dist/resolution/frameworks/svelte.js +5 -1
- package/lib/dist/resolution/frameworks/svelte.js.map +1 -1
- package/lib/dist/resolution/frameworks/vue.js +24 -27
- package/lib/dist/resolution/frameworks/vue.js.map +1 -1
- package/lib/dist/resolution/import-resolver.d.ts +10 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +564 -2
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +80 -0
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +457 -7
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts +61 -0
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +590 -14
- package/lib/dist/resolution/name-matcher.js.map +1 -1
- package/lib/dist/resolution/types.d.ts +27 -3
- package/lib/dist/resolution/types.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.d.ts +48 -0
- package/lib/dist/resolution/workspace-packages.d.ts.map +1 -0
- package/lib/dist/resolution/workspace-packages.js +208 -0
- package/lib/dist/resolution/workspace-packages.js.map +1 -0
- package/lib/dist/search/query-utils.d.ts +35 -1
- package/lib/dist/search/query-utils.d.ts.map +1 -1
- package/lib/dist/search/query-utils.js +109 -10
- package/lib/dist/search/query-utils.js.map +1 -1
- package/lib/dist/sync/watcher.d.ts +124 -32
- package/lib/dist/sync/watcher.d.ts.map +1 -1
- package/lib/dist/sync/watcher.js +326 -111
- package/lib/dist/sync/watcher.js.map +1 -1
- package/lib/dist/telemetry/index.d.ts +146 -0
- package/lib/dist/telemetry/index.d.ts.map +1 -0
- package/lib/dist/telemetry/index.js +544 -0
- package/lib/dist/telemetry/index.js.map +1 -0
- package/lib/dist/types.d.ts +25 -2
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +3 -0
- package/lib/dist/types.js.map +1 -1
- package/lib/dist/upgrade/index.d.ts +132 -0
- package/lib/dist/upgrade/index.d.ts.map +1 -0
- package/lib/dist/upgrade/index.js +462 -0
- package/lib/dist/upgrade/index.js.map +1 -0
- package/lib/dist/utils.d.ts +30 -24
- package/lib/dist/utils.d.ts.map +1 -1
- package/lib/dist/utils.js +64 -48
- package/lib/dist/utils.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -29
- package/lib/package.json +1 -2
- package/package.json +1 -1
- package/lib/node_modules/chokidar/LICENSE +0 -21
- package/lib/node_modules/chokidar/README.md +0 -305
- package/lib/node_modules/chokidar/esm/handler.d.ts +0 -90
- package/lib/node_modules/chokidar/esm/handler.js +0 -629
- package/lib/node_modules/chokidar/esm/index.d.ts +0 -215
- package/lib/node_modules/chokidar/esm/index.js +0 -798
- package/lib/node_modules/chokidar/esm/package.json +0 -1
- package/lib/node_modules/chokidar/handler.d.ts +0 -90
- package/lib/node_modules/chokidar/handler.js +0 -635
- package/lib/node_modules/chokidar/index.d.ts +0 -215
- package/lib/node_modules/chokidar/index.js +0 -804
- package/lib/node_modules/chokidar/package.json +0 -69
- package/lib/node_modules/readdirp/LICENSE +0 -21
- package/lib/node_modules/readdirp/README.md +0 -120
- package/lib/node_modules/readdirp/esm/index.d.ts +0 -108
- package/lib/node_modules/readdirp/esm/index.js +0 -257
- package/lib/node_modules/readdirp/esm/package.json +0 -1
- package/lib/node_modules/readdirp/index.d.ts +0 -108
- package/lib/node_modules/readdirp/index.js +0 -263
- package/lib/node_modules/readdirp/package.json +0 -70
|
@@ -43,9 +43,13 @@ exports.extractFromSource = extractFromSource;
|
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const grammars_1 = require("./grammars");
|
|
45
45
|
const tree_sitter_helpers_1 = require("./tree-sitter-helpers");
|
|
46
|
+
const function_ref_1 = require("./function-ref");
|
|
47
|
+
const generated_detection_1 = require("./generated-detection");
|
|
46
48
|
const languages_1 = require("./languages");
|
|
47
49
|
const liquid_extractor_1 = require("./liquid-extractor");
|
|
50
|
+
const razor_extractor_1 = require("./razor-extractor");
|
|
48
51
|
const svelte_extractor_1 = require("./svelte-extractor");
|
|
52
|
+
const astro_extractor_1 = require("./astro-extractor");
|
|
49
53
|
const dfm_extractor_1 = require("./dfm-extractor");
|
|
50
54
|
const vue_extractor_1 = require("./vue-extractor");
|
|
51
55
|
const mybatis_extractor_1 = require("./mybatis-extractor");
|
|
@@ -130,6 +134,78 @@ function extractName(node, source, extractor) {
|
|
|
130
134
|
}
|
|
131
135
|
return '<anonymous>';
|
|
132
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a Scala type node to its base type NAME for name-matching — unwrapping
|
|
139
|
+
* `generic_type` (`Monoid[Int]` → `Monoid`), taking the last segment of a
|
|
140
|
+
* qualified `stable_type_identifier` (`cats.Functor` → `Functor`), and falling
|
|
141
|
+
* back to a descendant `type_identifier`. Returns null for non-type nodes.
|
|
142
|
+
* Shared by Scala inheritance and type-reference extraction.
|
|
143
|
+
*/
|
|
144
|
+
function scalaBaseTypeName(node, source) {
|
|
145
|
+
if (!node)
|
|
146
|
+
return null;
|
|
147
|
+
switch (node.type) {
|
|
148
|
+
case 'type_identifier':
|
|
149
|
+
case 'identifier':
|
|
150
|
+
return (0, tree_sitter_helpers_1.getNodeText)(node, source);
|
|
151
|
+
case 'generic_type':
|
|
152
|
+
// `<base> type_arguments` — the base type is the first named child.
|
|
153
|
+
return scalaBaseTypeName(node.namedChild(0), source);
|
|
154
|
+
case 'stable_type_identifier':
|
|
155
|
+
case 'stable_identifier': {
|
|
156
|
+
// Qualified `a.b.C` — match on the simple (last) segment.
|
|
157
|
+
const ids = node.namedChildren.filter((c) => c.type === 'type_identifier' || c.type === 'identifier');
|
|
158
|
+
const last = ids[ids.length - 1];
|
|
159
|
+
return last ? (0, tree_sitter_helpers_1.getNodeText)(last, source) : null;
|
|
160
|
+
}
|
|
161
|
+
default: {
|
|
162
|
+
const id = node.namedChildren.find((c) => c.type === 'type_identifier');
|
|
163
|
+
return id ? (0, tree_sitter_helpers_1.getNodeText)(id, source) : null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* PHP type-position wrapper node kinds (a type-hint is `named_type`,
|
|
169
|
+
* `?Foo` is `optional_type`, `A|B` is `union_type`, `A&B` is
|
|
170
|
+
* `intersection_type`). Used to find the type subtree inside a parameter /
|
|
171
|
+
* property / return position before walking it for class references.
|
|
172
|
+
*/
|
|
173
|
+
const PHP_TYPE_NODES = new Set([
|
|
174
|
+
'named_type', 'optional_type', 'nullable_type',
|
|
175
|
+
'union_type', 'intersection_type', 'disjunctive_normal_form_type',
|
|
176
|
+
'primitive_type',
|
|
177
|
+
]);
|
|
178
|
+
/**
|
|
179
|
+
* Member-access node kinds whose receiver, when it's a capitalized
|
|
180
|
+
* type/enum/class name, is a real dependency — `Enum.value`, `Type.CONST`,
|
|
181
|
+
* `Foo::BAR`. These VALUE reads (as opposed to `Type.method()` calls, already
|
|
182
|
+
* handled) produced no edge, so a type used only via a static member or enum
|
|
183
|
+
* value looked like nothing depended on it. See {@link extractStaticMemberRef}.
|
|
184
|
+
*/
|
|
185
|
+
const MEMBER_ACCESS_TYPES = new Set([
|
|
186
|
+
'field_access', // java (`Foo.BAR`)
|
|
187
|
+
'member_access_expression', // c# (`Foo.Bar`)
|
|
188
|
+
'navigation_expression', // kotlin / swift (`Foo.bar`)
|
|
189
|
+
'field_expression', // scala (`Foo.bar`)
|
|
190
|
+
'class_constant_access_expression', // php (`Foo::CONST`, `Foo::class`)
|
|
191
|
+
'scoped_property_access_expression', // php (`Foo::$bar`)
|
|
192
|
+
'qualified_identifier', // c++ (`Foo::bar`)
|
|
193
|
+
]);
|
|
194
|
+
/**
|
|
195
|
+
* Languages whose types are Capitalized by convention, so a capitalized
|
|
196
|
+
* member-access receiver is reliably a type (not a local/variable). The
|
|
197
|
+
* static-member/value-read pass is gated to these — the ones where it was the
|
|
198
|
+
* confirmed residual frontier (enum-value / static-field reads). TS/JS/Python
|
|
199
|
+
* are deliberately excluded, and a measured A/B confirms the call: extending the
|
|
200
|
+
* pass to them adds ZERO coverage — in import-based languages you must `import` a
|
|
201
|
+
* type before any `Type.MEMBER` read, so the import edge already covers it (the
|
|
202
|
+
* static read is pure duplication) — while adding real graph noise (+1813 edges /
|
|
203
|
+
* +2448 `references` on excalidraw, the retrieval-perf benchmark, all pointing at
|
|
204
|
+
* already-covered types). Don't re-add `member_expression`/`attribute` here.
|
|
205
|
+
*/
|
|
206
|
+
const STATIC_MEMBER_LANGS = new Set([
|
|
207
|
+
'java', 'csharp', 'kotlin', 'swift', 'scala', 'dart', 'php', 'cpp',
|
|
208
|
+
]);
|
|
133
209
|
/**
|
|
134
210
|
* Tree-sitter node kinds that represent constructor invocations
|
|
135
211
|
* (`new Foo()` and friends). Used by extractInstantiation to emit
|
|
@@ -139,6 +215,9 @@ const INSTANTIATION_KINDS = new Set([
|
|
|
139
215
|
'new_expression', // typescript / javascript / tsx / jsx
|
|
140
216
|
'object_creation_expression', // java / c#
|
|
141
217
|
'instance_creation_expression', // some grammars
|
|
218
|
+
'composite_literal', // go — `Widget{...}` / `pkga.Widget{...}`
|
|
219
|
+
'struct_expression', // rust — `Widget { n: 1 }` / `m::Widget { .. }`
|
|
220
|
+
'instance_expression', // scala — `new Monoid[Int] { ... }`
|
|
142
221
|
]);
|
|
143
222
|
/**
|
|
144
223
|
* TreeSitterExtractor - Main extraction class
|
|
@@ -155,11 +234,17 @@ class TreeSitterExtractor {
|
|
|
155
234
|
extractor = null;
|
|
156
235
|
nodeStack = []; // Stack of parent node IDs
|
|
157
236
|
methodIndex = null; // lookup key → node ID for Pascal defProc lookup
|
|
237
|
+
// Function-as-value capture (#756): per-language spec + candidates collected
|
|
238
|
+
// during the walk, gated & flushed into unresolvedReferences at end-of-file
|
|
239
|
+
// (see flushFnRefCandidates).
|
|
240
|
+
fnRefSpec;
|
|
241
|
+
fnRefCandidates = [];
|
|
158
242
|
constructor(filePath, source, language) {
|
|
159
243
|
this.filePath = filePath;
|
|
160
244
|
this.source = source;
|
|
161
245
|
this.language = language || (0, grammars_1.detectLanguage)(filePath, source);
|
|
162
246
|
this.extractor = languages_1.EXTRACTORS[this.language] || null;
|
|
247
|
+
this.fnRefSpec = function_ref_1.FN_REF_SPECS[this.language];
|
|
163
248
|
}
|
|
164
249
|
/**
|
|
165
250
|
* Parse and extract from the source code
|
|
@@ -200,6 +285,14 @@ class TreeSitterExtractor {
|
|
|
200
285
|
};
|
|
201
286
|
}
|
|
202
287
|
try {
|
|
288
|
+
// Optional pre-parse source transform (offset-preserving) to work around
|
|
289
|
+
// grammar gaps — e.g. C# blanks conditional-compilation directive lines
|
|
290
|
+
// the grammar mis-parses inside enum bodies (#237). We reassign
|
|
291
|
+
// this.source so downstream getNodeText reads the same bytes the parser
|
|
292
|
+
// saw (identical outside the blanked directive lines).
|
|
293
|
+
if (this.extractor?.preParse) {
|
|
294
|
+
this.source = this.extractor.preParse(this.source);
|
|
295
|
+
}
|
|
203
296
|
this.tree = parser.parse(this.source) ?? null;
|
|
204
297
|
if (!this.tree) {
|
|
205
298
|
throw new Error('Parser returned null tree');
|
|
@@ -230,6 +323,9 @@ class TreeSitterExtractor {
|
|
|
230
323
|
if (packageNodeId)
|
|
231
324
|
this.nodeStack.push(packageNodeId);
|
|
232
325
|
this.visitNode(this.tree.rootNode);
|
|
326
|
+
// Gate + flush function-as-value candidates (#756) while the file's
|
|
327
|
+
// nodes and import refs are complete and the file node is still pushed.
|
|
328
|
+
this.flushFnRefCandidates();
|
|
233
329
|
if (packageNodeId)
|
|
234
330
|
this.nodeStack.pop();
|
|
235
331
|
this.nodeStack.pop();
|
|
@@ -267,6 +363,157 @@ class TreeSitterExtractor {
|
|
|
267
363
|
durationMs: Date.now() - startTime,
|
|
268
364
|
};
|
|
269
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Function-as-value capture (#756): if this node is one of the language's
|
|
368
|
+
* value-position containers (call arguments, assignment RHS, struct/object
|
|
369
|
+
* initializer, array/table literal), collect candidate function names from
|
|
370
|
+
* it. Candidates are gated & flushed at end-of-file (flushFnRefCandidates).
|
|
371
|
+
*/
|
|
372
|
+
maybeCaptureFnRefs(node, nodeType) {
|
|
373
|
+
const spec = this.fnRefSpec;
|
|
374
|
+
if (!spec)
|
|
375
|
+
return;
|
|
376
|
+
const rule = spec.dispatch.get(nodeType);
|
|
377
|
+
if (!rule || this.nodeStack.length === 0)
|
|
378
|
+
return;
|
|
379
|
+
const fromNodeId = this.nodeStack[this.nodeStack.length - 1];
|
|
380
|
+
if (!fromNodeId)
|
|
381
|
+
return;
|
|
382
|
+
for (const cand of (0, function_ref_1.captureFnRefCandidates)(node, rule, spec, this.source)) {
|
|
383
|
+
this.fnRefCandidates.push({ ...cand, fromNodeId });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Candidates-only scan of a subtree the main walkers won't traverse
|
|
388
|
+
* (top-level variable initializers). No extraction side effects. Halts at
|
|
389
|
+
* nested function definitions: their bodies are walked — and their
|
|
390
|
+
* candidates attributed — by extractFunction's own body walk.
|
|
391
|
+
*/
|
|
392
|
+
scanFnRefSubtree(node, depth) {
|
|
393
|
+
if (!this.fnRefSpec || depth > 12)
|
|
394
|
+
return;
|
|
395
|
+
const nodeType = node.type;
|
|
396
|
+
if (depth > 0 && (this.extractor?.functionTypes.includes(nodeType) ||
|
|
397
|
+
nodeType === 'arrow_function' ||
|
|
398
|
+
nodeType === 'function_expression' ||
|
|
399
|
+
nodeType === 'lambda_literal' ||
|
|
400
|
+
nodeType === 'lambda_expression')) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.maybeCaptureFnRefs(node, nodeType);
|
|
404
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
405
|
+
const child = node.namedChild(i);
|
|
406
|
+
if (child)
|
|
407
|
+
this.scanFnRefSubtree(child, depth + 1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Gate captured function-as-value candidates and push survivors as
|
|
412
|
+
* `function_ref` unresolved references.
|
|
413
|
+
*
|
|
414
|
+
* The gate bounds volume and protects precision: a candidate survives only
|
|
415
|
+
* if its name matches a function/method DEFINED IN THIS FILE or a name this
|
|
416
|
+
* file imports/references. Everything else (locals, params, fields passed
|
|
417
|
+
* as arguments) is dropped before it ever reaches the database. Resolution
|
|
418
|
+
* then matches survivors against function/method nodes only
|
|
419
|
+
* (matchFunctionRef) and emits `references` edges — which callers/impact
|
|
420
|
+
* already traverse.
|
|
421
|
+
*
|
|
422
|
+
* Known v1 limit, deliberate: a C/C++ callback registered in a DIFFERENT
|
|
423
|
+
* translation unit than its definition (extern, no symbol imports to match)
|
|
424
|
+
* is not captured. Same-file registration — the dominant C pattern (static
|
|
425
|
+
* callback + same-file ops struct) — is.
|
|
426
|
+
*/
|
|
427
|
+
flushFnRefCandidates() {
|
|
428
|
+
if (this.fnRefCandidates.length === 0)
|
|
429
|
+
return;
|
|
430
|
+
const candidates = this.fnRefCandidates;
|
|
431
|
+
this.fnRefCandidates = [];
|
|
432
|
+
// Generated/minified files (vendored jquery.min.js and friends): their
|
|
433
|
+
// function-as-value edges are noise — single-letter minified symbols
|
|
434
|
+
// resolve everywhere. Same policy as the callback synthesizer.
|
|
435
|
+
if ((0, generated_detection_1.isGeneratedFile)(this.filePath))
|
|
436
|
+
return;
|
|
437
|
+
const definedHere = new Set();
|
|
438
|
+
for (const n of this.nodes) {
|
|
439
|
+
if (n.kind === 'function' || n.kind === 'method')
|
|
440
|
+
definedHere.add(n.name);
|
|
441
|
+
}
|
|
442
|
+
// Import-binding names only (all binding emitters push kind 'imports').
|
|
443
|
+
// Deliberately NOT 'references': those carry type-annotation and
|
|
444
|
+
// interface-member names, which let local variables that share a type
|
|
445
|
+
// member's name slip through the gate (excalidraw A/B finding). A dotted
|
|
446
|
+
// import (JVM `import com.example.OtherClass`) also contributes its LAST
|
|
447
|
+
// segment — the simple name Java/Kotlin code uses in `OtherClass::method`
|
|
448
|
+
// references.
|
|
449
|
+
const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
450
|
+
// JVM imports are dotted (`com.example.OtherClass`); PHP `use` imports
|
|
451
|
+
// are backslashed (`App\Services\Mailer`). Both contribute their last
|
|
452
|
+
// segment — the simple name code uses to reference them.
|
|
453
|
+
const QUALIFIED_IMPORT = /^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
|
454
|
+
const importedNames = new Set();
|
|
455
|
+
for (const r of this.unresolvedReferences) {
|
|
456
|
+
if (r.referenceKind !== 'imports')
|
|
457
|
+
continue;
|
|
458
|
+
if (SIMPLE_NAME.test(r.referenceName)) {
|
|
459
|
+
importedNames.add(r.referenceName);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const qualified = r.referenceName.match(QUALIFIED_IMPORT);
|
|
463
|
+
if (qualified)
|
|
464
|
+
importedNames.add(qualified[1]);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const ungated = this.fnRefSpec?.ungatedModes;
|
|
468
|
+
const addressOfOnly = this.fnRefSpec?.addressOfOnly === true;
|
|
469
|
+
const seen = new Set();
|
|
470
|
+
for (const c of candidates) {
|
|
471
|
+
const atFileScope = c.fromNodeId.startsWith('file:');
|
|
472
|
+
// C++ (addressOfOnly): a BARE identifier qualifies only inside a
|
|
473
|
+
// file-scope initializer table. Everywhere else — args, assignments,
|
|
474
|
+
// local braced-init lists like `{begin, size}` — only explicit `&`
|
|
475
|
+
// forms count (fmt A/B finding: generic names `begin`/`out`/`size`
|
|
476
|
+
// collide with locals and members).
|
|
477
|
+
if (addressOfOnly &&
|
|
478
|
+
!c.explicitRef &&
|
|
479
|
+
!(atFileScope && (c.mode === 'value' || c.mode === 'list'))) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
// Gate policy by candidate shape:
|
|
483
|
+
// - `this.<member>`: ALWAYS flush — the member may be inherited from a
|
|
484
|
+
// class in another file (definedHere can't see it), volume is
|
|
485
|
+
// naturally bounded by real `this.X` expressions, and resolution is
|
|
486
|
+
// strictly class-scoped (own members or the validated supertype
|
|
487
|
+
// pass), so nothing fuzzy can leak.
|
|
488
|
+
// - `Scope::member` (C++ member-pointers, Java/Kotlin type-qualified
|
|
489
|
+
// method refs, PHP `'Cls::m'`): ALWAYS flush — the explicit-ref
|
|
490
|
+
// syntax is self-selecting, the referenced type often needs NO
|
|
491
|
+
// import (Java/Kotlin same-package, Kotlin companions), and
|
|
492
|
+
// resolution is scope-suffix-anchored + unique-or-drop, so a
|
|
493
|
+
// same-named member on another class can't match.
|
|
494
|
+
// - C-family file-scope initializers skip the gate entirely
|
|
495
|
+
// (constant-expression context — see FnRefSpec.ungatedModes).
|
|
496
|
+
// - everything else: name ∈ same-file functions/methods ∪ imports.
|
|
497
|
+
if (!c.name.startsWith('this.') && !c.name.includes('::')) {
|
|
498
|
+
const skipGate = (ungated?.has(c.mode) === true && atFileScope) ||
|
|
499
|
+
c.skipGate === true; // PHP HOF-position string callables (see FnRefCandidate.skipGate)
|
|
500
|
+
if (!skipGate && !definedHere.has(c.name) && !importedNames.has(c.name)) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const key = `${c.fromNodeId}|${c.name}`;
|
|
505
|
+
if (seen.has(key))
|
|
506
|
+
continue;
|
|
507
|
+
seen.add(key);
|
|
508
|
+
this.unresolvedReferences.push({
|
|
509
|
+
fromNodeId: c.fromNodeId,
|
|
510
|
+
referenceName: c.name,
|
|
511
|
+
referenceKind: 'function_ref',
|
|
512
|
+
line: c.line,
|
|
513
|
+
column: c.column,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
270
517
|
/**
|
|
271
518
|
* Visit a node and extract information
|
|
272
519
|
*/
|
|
@@ -279,8 +526,14 @@ class TreeSitterExtractor {
|
|
|
279
526
|
if (this.extractor.visitNode) {
|
|
280
527
|
const ctx = this.makeExtractorContext();
|
|
281
528
|
const handled = this.extractor.visitNode(node, ctx);
|
|
282
|
-
if (handled)
|
|
529
|
+
if (handled) {
|
|
530
|
+
// The hook consumed this subtree, so the walkers below never descend
|
|
531
|
+
// into it — scan it for function-as-value candidates (#756). Scala's
|
|
532
|
+
// hook handles val/var definitions (`val table = Seq(targetCb)`), for
|
|
533
|
+
// example. The scan is capture-only and halts at nested functions.
|
|
534
|
+
this.scanFnRefSubtree(node, 0);
|
|
283
535
|
return;
|
|
536
|
+
}
|
|
284
537
|
}
|
|
285
538
|
// Pascal-specific AST handling
|
|
286
539
|
if (this.language === 'pascal') {
|
|
@@ -288,6 +541,10 @@ class TreeSitterExtractor {
|
|
|
288
541
|
if (skipChildren)
|
|
289
542
|
return;
|
|
290
543
|
}
|
|
544
|
+
// Function-as-value capture (#756) — independent of the dispatch ladder
|
|
545
|
+
// below (the captured container types have no other handler there), so it
|
|
546
|
+
// can never shadow or be shadowed by an extraction branch.
|
|
547
|
+
this.maybeCaptureFnRefs(node, nodeType);
|
|
291
548
|
// Check for function declarations
|
|
292
549
|
// For Python/Ruby, function_definition inside a class should be treated as method
|
|
293
550
|
if (this.extractor.functionTypes.includes(nodeType)) {
|
|
@@ -329,8 +586,31 @@ class TreeSitterExtractor {
|
|
|
329
586
|
}
|
|
330
587
|
// Check for method declarations (only if not already handled by functionTypes)
|
|
331
588
|
else if (this.extractor.methodTypes.includes(nodeType)) {
|
|
332
|
-
|
|
333
|
-
|
|
589
|
+
// TS/JS class fields parse as a methodTypes node; only function-valued
|
|
590
|
+
// fields are methods — a plain field (`public fonts: Fonts;`) is a
|
|
591
|
+
// property (#808). classifyMethodNode is absent for other languages.
|
|
592
|
+
if (this.extractor.classifyMethodNode?.(node) === 'property') {
|
|
593
|
+
const propNode = this.extractProperty(node);
|
|
594
|
+
// Walk the initializer so its calls/instantiations attribute to the
|
|
595
|
+
// property (`history = createHistory()` → history calls
|
|
596
|
+
// createHistory). The old field-as-method path never walked these
|
|
597
|
+
// (resolveBody only resolves function bodies), so this is additive.
|
|
598
|
+
const valueNode = (0, tree_sitter_helpers_1.getChildByField)(node, 'value');
|
|
599
|
+
if (propNode && valueNode) {
|
|
600
|
+
this.nodeStack.push(propNode.id);
|
|
601
|
+
this.visitFunctionBody(valueNode, '');
|
|
602
|
+
this.nodeStack.pop();
|
|
603
|
+
}
|
|
604
|
+
// A field initializer can also register callbacks
|
|
605
|
+
// (`static handlers = { click: onClick }`) — scan it for
|
|
606
|
+
// function-as-value candidates (capture-only, halts at functions).
|
|
607
|
+
this.scanFnRefSubtree(node, 0);
|
|
608
|
+
skipChildren = true;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.extractMethod(node);
|
|
612
|
+
skipChildren = true; // extractMethod visits children via visitFunctionBody
|
|
613
|
+
}
|
|
334
614
|
}
|
|
335
615
|
// Check for interface/protocol/trait declarations
|
|
336
616
|
else if (this.extractor.interfaceTypes.includes(nodeType)) {
|
|
@@ -356,19 +636,70 @@ class TreeSitterExtractor {
|
|
|
356
636
|
// Check for class properties (e.g. C# property_declaration)
|
|
357
637
|
else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
|
|
358
638
|
this.extractProperty(node);
|
|
639
|
+
// Property initializers aren't walked — scan for function-as-value
|
|
640
|
+
// candidates (#756): Scala `val table = Seq(targetCb)` in an object,
|
|
641
|
+
// Kotlin `val cb = ::handler` class properties.
|
|
642
|
+
this.scanFnRefSubtree(node, 0);
|
|
359
643
|
skipChildren = true;
|
|
360
644
|
}
|
|
361
645
|
// Check for class fields (e.g. Java field_declaration, C# field_declaration)
|
|
362
646
|
else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
|
|
363
647
|
this.extractField(node);
|
|
648
|
+
// Field initializers aren't walked — scan for function-as-value
|
|
649
|
+
// candidates (#756): Java `List<IntConsumer> table = List.of(Main::cb)`,
|
|
650
|
+
// C# `List<Action<int>> table = new() { TargetCb }`.
|
|
651
|
+
this.scanFnRefSubtree(node, 0);
|
|
364
652
|
skipChildren = true;
|
|
365
653
|
}
|
|
366
654
|
// Check for variable declarations (const, let, var, etc.)
|
|
367
655
|
// Only extract top-level variables (not inside functions/methods)
|
|
368
656
|
else if (this.extractor.variableTypes.includes(nodeType) && !this.isInsideClassLikeNode()) {
|
|
369
657
|
this.extractVariable(node);
|
|
658
|
+
// extractVariable doesn't walk every initializer shape (object literals
|
|
659
|
+
// are deliberately skipped; Python/Ruby don't walk at all), so scan the
|
|
660
|
+
// declaration subtree for function-as-value candidates — `const routes =
|
|
661
|
+
// { home: renderHome }`, `handlers = {"recv": target_cb}`. The scan halts
|
|
662
|
+
// at nested function definitions (their bodies are walked — and
|
|
663
|
+
// attributed — separately) and flush-time dedup absorbs any overlap with
|
|
664
|
+
// initializers extractVariable DOES walk.
|
|
665
|
+
this.scanFnRefSubtree(node, 0);
|
|
370
666
|
skipChildren = true; // extractVariable handles children
|
|
371
667
|
}
|
|
668
|
+
// Swift stored properties inside a type. Swift instance properties aren't
|
|
669
|
+
// extracted as their own nodes, but a property's PROPERTY WRAPPER
|
|
670
|
+
// (`@Argument`/`@Published`/`@State`/custom) and declared type ARE
|
|
671
|
+
// dependencies — attribute them to the enclosing type so the wrapper/type
|
|
672
|
+
// files get dependents. Don't skipChildren: an initializer's calls still
|
|
673
|
+
// matter. (Other languages extract properties via property/field types.)
|
|
674
|
+
else if (this.language === 'swift' &&
|
|
675
|
+
nodeType === 'property_declaration' &&
|
|
676
|
+
this.isInsideClassLikeNode()) {
|
|
677
|
+
const ownerId = this.nodeStack[this.nodeStack.length - 1];
|
|
678
|
+
if (ownerId) {
|
|
679
|
+
this.extractDecoratorsFor(node, ownerId);
|
|
680
|
+
this.extractVariableTypeAnnotation(node, ownerId);
|
|
681
|
+
// Fluent / SwiftUI property-wrapper attributes often reference a model or
|
|
682
|
+
// type by metatype in their ARGUMENTS — `@Siblings(through: Pivot.self,
|
|
683
|
+
// …)`, `@Group(…)`. extractDecoratorsFor captures the wrapper type
|
|
684
|
+
// (`Siblings`); this pulls the TYPE out of the argument expressions
|
|
685
|
+
// (`Pivot.self` → a dependency on Pivot), so a model reached ONLY through
|
|
686
|
+
// a relationship (a many-to-many pivot/join model) isn't left orphaned.
|
|
687
|
+
// extractStaticMemberRef self-filters to `Type.member` navigation, so the
|
|
688
|
+
// `\.$keypath` arguments and the wrapper `user_type` are skipped.
|
|
689
|
+
const modifiers = node.namedChildren.find((c) => c.type === 'modifiers');
|
|
690
|
+
if (modifiers) {
|
|
691
|
+
const walkAttrArgs = (n) => {
|
|
692
|
+
this.extractStaticMemberRef(n);
|
|
693
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
694
|
+
const c = n.namedChild(i);
|
|
695
|
+
if (c)
|
|
696
|
+
walkAttrArgs(c);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
walkAttrArgs(modifiers);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
372
703
|
// `export_statement` itself is not extracted — the walker descends
|
|
373
704
|
// into children, where the inner declaration (lexical_declaration,
|
|
374
705
|
// function_declaration, class_declaration, etc.) is dispatched to
|
|
@@ -386,6 +717,21 @@ class TreeSitterExtractor {
|
|
|
386
717
|
else if (this.extractor.importTypes.includes(nodeType)) {
|
|
387
718
|
this.extractImport(node);
|
|
388
719
|
}
|
|
720
|
+
// Re-export from another module — `export { X } from './y'` (TS/JS). A
|
|
721
|
+
// re-export is a dependency on the source module just like an import, but
|
|
722
|
+
// the export_statement is otherwise only descended into (no declaration to
|
|
723
|
+
// extract), so a barrel that ONLY re-exports produced zero edges and showed
|
|
724
|
+
// 0 dependents. Link each re-exported name to its definition. Children are
|
|
725
|
+
// still visited (a non-re-export `export const X = …` has no `source` and
|
|
726
|
+
// falls through to its normal declaration extraction).
|
|
727
|
+
else if (nodeType === 'export_statement' &&
|
|
728
|
+
(this.language === 'typescript' || this.language === 'tsx' ||
|
|
729
|
+
this.language === 'javascript' || this.language === 'jsx') &&
|
|
730
|
+
(0, tree_sitter_helpers_1.getChildByField)(node, 'source')) {
|
|
731
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
732
|
+
if (parentId)
|
|
733
|
+
this.emitReExportRefs(node, parentId);
|
|
734
|
+
}
|
|
389
735
|
// Check for function calls
|
|
390
736
|
else if (this.extractor.callTypes.includes(nodeType)) {
|
|
391
737
|
this.extractCall(node);
|
|
@@ -477,6 +823,14 @@ class TreeSitterExtractor {
|
|
|
477
823
|
updatedAt: Date.now(),
|
|
478
824
|
...extra,
|
|
479
825
|
};
|
|
826
|
+
// Persist extra symbol-level modifiers (e.g. Kotlin `expect`/`actual`) onto
|
|
827
|
+
// the node's decorators list so the resolver can pair multiplatform
|
|
828
|
+
// declarations with their implementations. Merged, not overwritten, so a
|
|
829
|
+
// language that also captures real annotations keeps both.
|
|
830
|
+
const mods = this.extractor?.extractModifiers?.(node);
|
|
831
|
+
if (mods && mods.length > 0) {
|
|
832
|
+
newNode.decorators = [...(newNode.decorators ?? []), ...mods];
|
|
833
|
+
}
|
|
480
834
|
this.nodes.push(newNode);
|
|
481
835
|
// Add containment edge from parent
|
|
482
836
|
if (this.nodeStack.length > 0) {
|
|
@@ -616,8 +970,19 @@ class TreeSitterExtractor {
|
|
|
616
970
|
}
|
|
617
971
|
}
|
|
618
972
|
}
|
|
619
|
-
if (name === '<anonymous>')
|
|
620
|
-
|
|
973
|
+
if (name === '<anonymous>') {
|
|
974
|
+
// Don't emit a node for the anonymous wrapper itself, but still visit its
|
|
975
|
+
// body: AMD/RequireJS and CommonJS module wrappers (`define([], function(){…})`,
|
|
976
|
+
// `(function(){…})()`) hold named inner functions and calls that would
|
|
977
|
+
// otherwise be lost — the dispatcher set skipChildren, so nothing else
|
|
978
|
+
// descends into this subtree. (#528)
|
|
979
|
+
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
|
|
980
|
+
?? (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.bodyField);
|
|
981
|
+
if (body) {
|
|
982
|
+
this.visitFunctionBody(body, '');
|
|
983
|
+
}
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
621
986
|
// Check for misparse artifacts (e.g. C++ macros causing "namespace detail" functions)
|
|
622
987
|
// Skip the node but still visit the body for calls and structural nodes
|
|
623
988
|
if (this.extractor.isMisparsedFunction?.(name, node)) {
|
|
@@ -634,6 +999,7 @@ class TreeSitterExtractor {
|
|
|
634
999
|
const isExported = this.extractor.isExported?.(node, this.source);
|
|
635
1000
|
const isAsync = this.extractor.isAsync?.(node);
|
|
636
1001
|
const isStatic = this.extractor.isStatic?.(node);
|
|
1002
|
+
const returnType = this.extractor.getReturnType?.(node, this.source);
|
|
637
1003
|
const funcNode = this.createNode('function', name, node, {
|
|
638
1004
|
docstring,
|
|
639
1005
|
signature,
|
|
@@ -641,6 +1007,7 @@ class TreeSitterExtractor {
|
|
|
641
1007
|
isExported,
|
|
642
1008
|
isAsync,
|
|
643
1009
|
isStatic,
|
|
1010
|
+
returnType,
|
|
644
1011
|
});
|
|
645
1012
|
if (!funcNode)
|
|
646
1013
|
return;
|
|
@@ -678,6 +1045,8 @@ class TreeSitterExtractor {
|
|
|
678
1045
|
return;
|
|
679
1046
|
// Extract extends/implements
|
|
680
1047
|
this.extractInheritance(node, classNode.id);
|
|
1048
|
+
// C# primary-constructor parameter dependencies (`class Svc(IRepo r, …)`).
|
|
1049
|
+
this.extractCsharpPrimaryCtorParamRefs(node, classNode.id);
|
|
681
1050
|
// Extract decorators applied to the class (`@Foo class X {}`).
|
|
682
1051
|
this.extractDecoratorsFor(node, classNode.id);
|
|
683
1052
|
// Push to stack and visit body
|
|
@@ -738,12 +1107,14 @@ class TreeSitterExtractor {
|
|
|
738
1107
|
const visibility = this.extractor.getVisibility?.(node);
|
|
739
1108
|
const isAsync = this.extractor.isAsync?.(node);
|
|
740
1109
|
const isStatic = this.extractor.isStatic?.(node);
|
|
1110
|
+
const returnType = this.extractor.getReturnType?.(node, this.source);
|
|
741
1111
|
const extraProps = {
|
|
742
1112
|
docstring,
|
|
743
1113
|
signature,
|
|
744
1114
|
visibility,
|
|
745
1115
|
isAsync,
|
|
746
1116
|
isStatic,
|
|
1117
|
+
returnType,
|
|
747
1118
|
};
|
|
748
1119
|
if (receiverType) {
|
|
749
1120
|
extraProps.qualifiedName = `${receiverType}::${name}`;
|
|
@@ -817,8 +1188,10 @@ class TreeSitterExtractor {
|
|
|
817
1188
|
if (!this.extractor)
|
|
818
1189
|
return;
|
|
819
1190
|
// Skip forward declarations and type references (no body = not a definition)
|
|
1191
|
+
// — EXCEPT C# positional records (`record struct M(decimal Amount);`),
|
|
1192
|
+
// complete definitions with no body block. (#831)
|
|
820
1193
|
const body = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.bodyField);
|
|
821
|
-
if (!body)
|
|
1194
|
+
if (!body && node.type !== 'record_declaration')
|
|
822
1195
|
return;
|
|
823
1196
|
const name = extractName(node, this.source, this.extractor);
|
|
824
1197
|
const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
|
|
@@ -833,15 +1206,21 @@ class TreeSitterExtractor {
|
|
|
833
1206
|
return;
|
|
834
1207
|
// Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable)
|
|
835
1208
|
this.extractInheritance(node, structNode.id);
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1209
|
+
// C# primary-constructor parameter dependencies (`struct P(int x)`, and
|
|
1210
|
+
// `record struct M(decimal Amount)` which the grammar nests here).
|
|
1211
|
+
this.extractCsharpPrimaryCtorParamRefs(node, structNode.id);
|
|
1212
|
+
// Push to stack for field extraction (bodiless positional records have
|
|
1213
|
+
// no members to visit)
|
|
1214
|
+
if (body) {
|
|
1215
|
+
this.nodeStack.push(structNode.id);
|
|
1216
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
1217
|
+
const child = body.namedChild(i);
|
|
1218
|
+
if (child) {
|
|
1219
|
+
this.visitNode(child);
|
|
1220
|
+
}
|
|
842
1221
|
}
|
|
1222
|
+
this.nodeStack.pop();
|
|
843
1223
|
}
|
|
844
|
-
this.nodeStack.pop();
|
|
845
1224
|
}
|
|
846
1225
|
/**
|
|
847
1226
|
* Extract an enum
|
|
@@ -914,22 +1293,35 @@ class TreeSitterExtractor {
|
|
|
914
1293
|
*/
|
|
915
1294
|
extractProperty(node) {
|
|
916
1295
|
if (!this.extractor)
|
|
917
|
-
return;
|
|
1296
|
+
return null;
|
|
918
1297
|
const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
|
|
919
1298
|
const visibility = this.extractor.getVisibility?.(node);
|
|
920
1299
|
const isStatic = this.extractor.isStatic?.(node) ?? false;
|
|
921
1300
|
const hookName = this.extractor.extractPropertyName?.(node, this.source);
|
|
1301
|
+
// JS `field_definition` names its key the `property` field (TS uses
|
|
1302
|
+
// `name`) — try both before the generic identifier scan (#808).
|
|
922
1303
|
const nameNode = hookName
|
|
923
1304
|
? null
|
|
924
|
-
: (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ||
|
|
1305
|
+
: (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ||
|
|
1306
|
+
(0, tree_sitter_helpers_1.getChildByField)(node, 'property') ||
|
|
1307
|
+
node.namedChildren.find(c => c.type === 'identifier');
|
|
925
1308
|
const name = hookName ?? (nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : null);
|
|
926
1309
|
if (!name)
|
|
927
|
-
return;
|
|
928
|
-
// Get property type
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1310
|
+
return null;
|
|
1311
|
+
// Get property type. TS/JS field definitions carry an explicit `type`
|
|
1312
|
+
// field (a `type_annotation`); their other named children are the name
|
|
1313
|
+
// and the initializer VALUE, which the generic finder below would
|
|
1314
|
+
// wrongly pick — so fields use the type field only (#808). Other
|
|
1315
|
+
// languages (C# property_declaration) keep the generic scan.
|
|
1316
|
+
const isTsJsField = node.type === 'public_field_definition' || node.type === 'field_definition';
|
|
1317
|
+
const typeNode = isTsJsField
|
|
1318
|
+
? (0, tree_sitter_helpers_1.getChildByField)(node, 'type')
|
|
1319
|
+
: node.namedChildren.find(c => c.type !== 'modifier' && c.type !== 'modifiers'
|
|
1320
|
+
&& c.type !== 'identifier' && c.type !== 'accessor_list'
|
|
1321
|
+
&& c.type !== 'accessors' && c.type !== 'equals_value_clause');
|
|
1322
|
+
const typeText = typeNode
|
|
1323
|
+
? (0, tree_sitter_helpers_1.getNodeText)(typeNode, this.source).replace(/^:\s*/, '')
|
|
1324
|
+
: undefined;
|
|
933
1325
|
const signature = typeText ? `${typeText} ${name}` : name;
|
|
934
1326
|
const propNode = this.createNode('property', name, node, {
|
|
935
1327
|
docstring,
|
|
@@ -946,6 +1338,7 @@ class TreeSitterExtractor {
|
|
|
946
1338
|
// `type_annotation` children; the C# branch walks the `type` field.
|
|
947
1339
|
this.extractTypeAnnotations(node, propNode.id);
|
|
948
1340
|
}
|
|
1341
|
+
return propNode;
|
|
949
1342
|
}
|
|
950
1343
|
/**
|
|
951
1344
|
* Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
|
|
@@ -1042,6 +1435,114 @@ class TreeSitterExtractor {
|
|
|
1042
1435
|
}
|
|
1043
1436
|
}
|
|
1044
1437
|
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Extract function-valued properties of an object literal as named function
|
|
1440
|
+
* nodes (named by their property key). Shared by the two object-of-functions
|
|
1441
|
+
* shapes in extractVariable: the object as a direct const value, and the
|
|
1442
|
+
* object returned by a store-initializer call. Handles both `key: () => {}` /
|
|
1443
|
+
* `key: function() {}` pairs and method shorthand `key() {}`.
|
|
1444
|
+
*/
|
|
1445
|
+
extractObjectLiteralFunctions(obj) {
|
|
1446
|
+
for (let i = 0; i < obj.namedChildCount; i++) {
|
|
1447
|
+
const member = obj.namedChild(i);
|
|
1448
|
+
if (!member)
|
|
1449
|
+
continue;
|
|
1450
|
+
if (member.type === 'pair') {
|
|
1451
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
|
|
1452
|
+
const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
1453
|
+
if (key && value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
|
1454
|
+
this.extractFunction(value, this.objectKeyName(key));
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
else if (member.type === 'method_definition') {
|
|
1458
|
+
// Method shorthand: `{ fetchUser() {...} }`. extractMethod deliberately
|
|
1459
|
+
// skips object-literal methods, so route through extractFunction with an
|
|
1460
|
+
// explicit name (method_definition exposes a `body` field, so resolveBody
|
|
1461
|
+
// falls through to it and the node spans the full method).
|
|
1462
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'name');
|
|
1463
|
+
if (key)
|
|
1464
|
+
this.extractFunction(member, this.objectKeyName(key));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/** Property-key text with surrounding quotes stripped (`'foo'` → `foo`). */
|
|
1469
|
+
objectKeyName(key) {
|
|
1470
|
+
return (0, tree_sitter_helpers_1.getNodeText)(key, this.source).replace(/^['"`]|['"`]$/g, '');
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Given a `call_expression` initializer (`create((set, get) => ({...}))`),
|
|
1474
|
+
* find the object literal RETURNED by a function argument — descending through
|
|
1475
|
+
* nested call_expression arguments so middleware wrappers are unwrapped
|
|
1476
|
+
* (`create(persist((set, get) => ({...}), {...}))`, devtools, immer,
|
|
1477
|
+
* subscribeWithSelector). Returns null when no such object is found — the
|
|
1478
|
+
* common case for ordinary call initializers — so this stays cheap and silent
|
|
1479
|
+
* rather than guessing. Keyed purely on AST shape; no library names.
|
|
1480
|
+
*/
|
|
1481
|
+
findInitializerReturnedObject(callNode, depth = 0) {
|
|
1482
|
+
if (depth > 4)
|
|
1483
|
+
return null;
|
|
1484
|
+
const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
|
|
1485
|
+
if (!args)
|
|
1486
|
+
return null;
|
|
1487
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1488
|
+
const arg = args.namedChild(i);
|
|
1489
|
+
if (!arg)
|
|
1490
|
+
continue;
|
|
1491
|
+
if (arg.type === 'arrow_function' || arg.type === 'function_expression') {
|
|
1492
|
+
const obj = this.functionReturnedObject(arg);
|
|
1493
|
+
if (obj)
|
|
1494
|
+
return obj;
|
|
1495
|
+
}
|
|
1496
|
+
else if (arg.type === 'call_expression') {
|
|
1497
|
+
const obj = this.findInitializerReturnedObject(arg, depth + 1);
|
|
1498
|
+
if (obj)
|
|
1499
|
+
return obj;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* The object literal a function expression returns — either the `=> ({...})`
|
|
1506
|
+
* arrow form (a parenthesized_expression wrapping an object) or a
|
|
1507
|
+
* `=> { return {...} }` block. Returns null for any other body shape.
|
|
1508
|
+
*/
|
|
1509
|
+
functionReturnedObject(fnNode) {
|
|
1510
|
+
const body = (0, tree_sitter_helpers_1.getChildByField)(fnNode, 'body');
|
|
1511
|
+
if (!body)
|
|
1512
|
+
return null;
|
|
1513
|
+
const asObject = (n) => {
|
|
1514
|
+
if (!n)
|
|
1515
|
+
return null;
|
|
1516
|
+
if (n.type === 'object' || n.type === 'object_expression')
|
|
1517
|
+
return n;
|
|
1518
|
+
if (n.type === 'parenthesized_expression') {
|
|
1519
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1520
|
+
const inner = asObject(n.namedChild(i));
|
|
1521
|
+
if (inner)
|
|
1522
|
+
return inner;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return null;
|
|
1526
|
+
};
|
|
1527
|
+
// `(set, get) => ({...})` — body is the (parenthesized) object directly.
|
|
1528
|
+
const direct = asObject(body);
|
|
1529
|
+
if (direct)
|
|
1530
|
+
return direct;
|
|
1531
|
+
// `(set, get) => { return {...} }` — scan top-level return statements.
|
|
1532
|
+
if (body.type === 'statement_block') {
|
|
1533
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
1534
|
+
const stmt = body.namedChild(i);
|
|
1535
|
+
if (stmt?.type !== 'return_statement')
|
|
1536
|
+
continue;
|
|
1537
|
+
for (let j = 0; j < stmt.namedChildCount; j++) {
|
|
1538
|
+
const obj = asObject(stmt.namedChild(j));
|
|
1539
|
+
if (obj)
|
|
1540
|
+
return obj;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1045
1546
|
/**
|
|
1046
1547
|
* Extract a variable declaration (const, let, var, etc.)
|
|
1047
1548
|
*
|
|
@@ -1093,29 +1594,41 @@ class TreeSitterExtractor {
|
|
|
1093
1594
|
if (varNode) {
|
|
1094
1595
|
this.extractVariableTypeAnnotation(child, varNode.id);
|
|
1095
1596
|
}
|
|
1597
|
+
// Exported const object-of-functions — extract each function-valued
|
|
1598
|
+
// property as a function named by its key + walk its body so its
|
|
1599
|
+
// calls are captured. Two shapes, both keyed on AST shape (not on any
|
|
1600
|
+
// library name):
|
|
1601
|
+
// `export const actions = { default: async () => {} }` — object is
|
|
1602
|
+
// the DIRECT value (SvelteKit form actions / handler maps / route
|
|
1603
|
+
// tables).
|
|
1604
|
+
// `export const useStore = create((set, get) => ({ fetchUser:
|
|
1605
|
+
// async () => {} }))` — object is RETURNED by an initializer call,
|
|
1606
|
+
// possibly through middleware wrappers (persist/devtools/immer).
|
|
1607
|
+
// Covers Zustand/Redux/Pinia/MobX stores generically. Without
|
|
1608
|
+
// this, store actions exist only as object-literal properties —
|
|
1609
|
+
// never nodes — so `node`/`callers` on `fetchUser` return "not
|
|
1610
|
+
// found" and the agent Reads the store to reconstruct the flow.
|
|
1611
|
+
// Scoped to EXPORTED consts to exclude inline-object noise
|
|
1612
|
+
// (`ctx.set({...})`) the object-method skip deliberately avoids.
|
|
1613
|
+
const objectOfFns = valueNode && (valueNode.type === 'object' || valueNode.type === 'object_expression')
|
|
1614
|
+
? valueNode
|
|
1615
|
+
: valueNode?.type === 'call_expression'
|
|
1616
|
+
? this.findInitializerReturnedObject(valueNode)
|
|
1617
|
+
: null;
|
|
1618
|
+
const extractObjectMethods = isExported && !!objectOfFns;
|
|
1619
|
+
// Visit the initializer body for calls — EXCEPT object literals (their
|
|
1620
|
+
// function-valued properties are extracted below) and the store-factory
|
|
1621
|
+
// call whose returned object we extract method-by-method below (walking
|
|
1622
|
+
// the whole call would re-visit those method arrows and mis-attribute
|
|
1623
|
+
// their inner calls to the file/module scope).
|
|
1096
1624
|
if (valueNode &&
|
|
1097
1625
|
valueNode.type !== 'object' &&
|
|
1098
|
-
valueNode.type !== 'object_expression'
|
|
1626
|
+
valueNode.type !== 'object_expression' &&
|
|
1627
|
+
!(extractObjectMethods && valueNode.type === 'call_expression')) {
|
|
1099
1628
|
this.visitFunctionBody(valueNode, '');
|
|
1100
1629
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
// / route tables). Extract each function-valued property as a function
|
|
1104
|
-
// named by its key + walk its body so its calls (e.g. api.post) are
|
|
1105
|
-
// captured. Scoped to EXPORTED consts to exclude the inline-object
|
|
1106
|
-
// noise (`ctx.set({...})`) the object-method skip deliberately avoids.
|
|
1107
|
-
if (isExported && valueNode &&
|
|
1108
|
-
(valueNode.type === 'object' || valueNode.type === 'object_expression')) {
|
|
1109
|
-
for (let j = 0; j < valueNode.namedChildCount; j++) {
|
|
1110
|
-
const pair = valueNode.namedChild(j);
|
|
1111
|
-
if (pair?.type !== 'pair')
|
|
1112
|
-
continue;
|
|
1113
|
-
const v = (0, tree_sitter_helpers_1.getChildByField)(pair, 'value');
|
|
1114
|
-
const k = (0, tree_sitter_helpers_1.getChildByField)(pair, 'key');
|
|
1115
|
-
if (k && v && (v.type === 'arrow_function' || v.type === 'function_expression')) {
|
|
1116
|
-
this.extractFunction(v, (0, tree_sitter_helpers_1.getNodeText)(k, this.source).replace(/^['"`]|['"`]$/g, ''));
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1630
|
+
if (extractObjectMethods && objectOfFns) {
|
|
1631
|
+
this.extractObjectLiteralFunctions(objectOfFns);
|
|
1119
1632
|
}
|
|
1120
1633
|
}
|
|
1121
1634
|
}
|
|
@@ -1143,16 +1656,34 @@ class TreeSitterExtractor {
|
|
|
1143
1656
|
const specs = node.namedChildren.filter(c => c.type === 'var_spec' || c.type === 'const_spec');
|
|
1144
1657
|
for (const spec of specs) {
|
|
1145
1658
|
const nameNode = spec.namedChild(0);
|
|
1659
|
+
let varNode = null;
|
|
1146
1660
|
if (nameNode && nameNode.type === 'identifier') {
|
|
1147
1661
|
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
1148
1662
|
const valueNode = spec.namedChildCount > 1 ? spec.namedChild(spec.namedChildCount - 1) : null;
|
|
1149
1663
|
const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
|
|
1150
1664
|
const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
|
|
1151
|
-
this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
|
|
1665
|
+
varNode = this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
|
|
1152
1666
|
docstring,
|
|
1153
1667
|
signature: initSignature,
|
|
1154
1668
|
});
|
|
1155
1669
|
}
|
|
1670
|
+
// Walk the initializer so composite literals and calls in a
|
|
1671
|
+
// package-level `var Query Binding = queryBinding{}` (a registry of
|
|
1672
|
+
// implementations) or `var c = pkg.New()` are extracted as
|
|
1673
|
+
// instantiates/calls dependencies — the body walker only covers
|
|
1674
|
+
// initializers inside functions, not these top-level declarations.
|
|
1675
|
+
// Scope the walk to the declared symbol so a call inside an anonymous
|
|
1676
|
+
// func_literal initializer — a cobra `RunE: func(){…}` handler, a
|
|
1677
|
+
// goroutine or callback closure — attributes to the var instead of
|
|
1678
|
+
// leaking to the file node (which reads as "no caller"), issue #693.
|
|
1679
|
+
const valueField = (0, tree_sitter_helpers_1.getChildByField)(spec, 'value');
|
|
1680
|
+
if (valueField) {
|
|
1681
|
+
if (varNode)
|
|
1682
|
+
this.nodeStack.push(varNode.id);
|
|
1683
|
+
this.visitFunctionBody(valueField, varNode?.id ?? '');
|
|
1684
|
+
if (varNode)
|
|
1685
|
+
this.nodeStack.pop();
|
|
1686
|
+
}
|
|
1156
1687
|
}
|
|
1157
1688
|
// Handle short_var_declaration (:=)
|
|
1158
1689
|
if (node.type === 'short_var_declaration') {
|
|
@@ -1290,6 +1821,13 @@ class TreeSitterExtractor {
|
|
|
1290
1821
|
const typeChild = (0, tree_sitter_helpers_1.getChildByField)(node, 'type');
|
|
1291
1822
|
if (typeChild)
|
|
1292
1823
|
this.extractInheritance(typeChild, interfaceNode.id);
|
|
1824
|
+
// Go: extract the interface's method specs as `method` nodes so implicit
|
|
1825
|
+
// interface satisfaction (a struct's method set ⊇ the interface's) and
|
|
1826
|
+
// impl-navigation can see the contract. Go has no `implements` keyword, so
|
|
1827
|
+
// without the interface's method set there's nothing to match against.
|
|
1828
|
+
if (this.language === 'go' && typeChild) {
|
|
1829
|
+
this.extractGoInterfaceMethods(typeChild, interfaceNode.id);
|
|
1830
|
+
}
|
|
1293
1831
|
return true;
|
|
1294
1832
|
}
|
|
1295
1833
|
const typeAliasNode = this.createNode('type_alias', name, node, {
|
|
@@ -1309,11 +1847,39 @@ class TreeSitterExtractor {
|
|
|
1309
1847
|
// an unrelated class method picked by path-proximity (#359).
|
|
1310
1848
|
if (this.language === 'typescript' || this.language === 'tsx') {
|
|
1311
1849
|
this.extractTsTypeAliasMembers(value, typeAliasNode);
|
|
1850
|
+
// `type List = [ Service<'name', Req, Resp>, … ]` — surface each
|
|
1851
|
+
// entry's string-literal name as a searchable member (issue #634).
|
|
1852
|
+
this.extractTsTupleContractNames(value, typeAliasNode);
|
|
1312
1853
|
}
|
|
1313
1854
|
}
|
|
1314
1855
|
}
|
|
1315
1856
|
return false;
|
|
1316
1857
|
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Extract the method specs of a Go `interface_type` body as `method` nodes
|
|
1860
|
+
* contained by the interface (e.g. `Marshal`, `Unmarshal` of a `Core`
|
|
1861
|
+
* interface). tree-sitter-go names these `method_elem` (newer) or
|
|
1862
|
+
* `method_spec` (older). Embedded interfaces (`Reader` inside `ReadWriter`)
|
|
1863
|
+
* are `type_identifier`s, not methods, and are left to inheritance extraction.
|
|
1864
|
+
*/
|
|
1865
|
+
extractGoInterfaceMethods(interfaceType, ifaceId) {
|
|
1866
|
+
this.nodeStack.push(ifaceId);
|
|
1867
|
+
for (let i = 0; i < interfaceType.namedChildCount; i++) {
|
|
1868
|
+
const m = interfaceType.namedChild(i);
|
|
1869
|
+
if (!m || (m.type !== 'method_elem' && m.type !== 'method_spec'))
|
|
1870
|
+
continue;
|
|
1871
|
+
const nameNode = (0, tree_sitter_helpers_1.getChildByField)(m, 'name') ?? m.namedChild(0);
|
|
1872
|
+
if (!nameNode)
|
|
1873
|
+
continue;
|
|
1874
|
+
const mname = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
1875
|
+
if (mname) {
|
|
1876
|
+
this.createNode('method', mname, m, {
|
|
1877
|
+
signature: this.extractor?.getSignature?.(m, this.source),
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
this.nodeStack.pop();
|
|
1882
|
+
}
|
|
1317
1883
|
/**
|
|
1318
1884
|
* Surface the members of a TypeScript `type X = { ... }` (or intersection
|
|
1319
1885
|
* thereof) as `property` / `method` nodes under the type-alias node. Only
|
|
@@ -1370,6 +1936,82 @@ class TreeSitterExtractor {
|
|
|
1370
1936
|
}
|
|
1371
1937
|
this.nodeStack.pop();
|
|
1372
1938
|
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Surface the string-literal "names" of a TypeScript service/contract
|
|
1941
|
+
* registry written as a tuple of generic instantiations:
|
|
1942
|
+
*
|
|
1943
|
+
* type MyServiceList = [
|
|
1944
|
+
* Service<'query_apply_record', Req, Resp>,
|
|
1945
|
+
* Service<'apply_confirm', Req, Resp>,
|
|
1946
|
+
* ];
|
|
1947
|
+
*
|
|
1948
|
+
* Each `Service<'name', …>` tags an entry with a string-literal name that a
|
|
1949
|
+
* dynamic factory (`createService<MyServiceList>()`) turns into a callable
|
|
1950
|
+
* property (`api.query_apply_record(…)`). Static extraction otherwise never
|
|
1951
|
+
* sees that name — it's a type argument, not a declaration — so
|
|
1952
|
+
* `codegraph query query_apply_record` returned nothing (issue #634). We emit
|
|
1953
|
+
* each name as a `method` node under the type alias (qualifiedName
|
|
1954
|
+
* `MyServiceList::query_apply_record`) so it's searchable and resolvable as a
|
|
1955
|
+
* symbol. (A call through the proxy, `api.query_apply_record(…)`, still
|
|
1956
|
+
* resolves to the imported `api` binding — the receiver's type isn't known —
|
|
1957
|
+
* so this fixes discoverability, not the per-method call edge.)
|
|
1958
|
+
*
|
|
1959
|
+
* Scope is deliberately narrow to avoid noise: only a string literal that is
|
|
1960
|
+
* a DIRECT type argument of a `generic_type` that is itself a DIRECT element
|
|
1961
|
+
* of a `tuple_type`. This excludes utility types (`Pick`/`Omit`/`Record` are
|
|
1962
|
+
* never written as tuples) and string args nested deeper
|
|
1963
|
+
* (`Service<'a', Pick<U, 'id'>>` yields only `a`, never `id`). Names must be
|
|
1964
|
+
* valid identifiers, which also rules out route paths / arbitrary strings.
|
|
1965
|
+
*/
|
|
1966
|
+
extractTsTupleContractNames(value, typeAliasNode) {
|
|
1967
|
+
const tuples = [];
|
|
1968
|
+
const collectTuples = (n, depth) => {
|
|
1969
|
+
if (depth > 6)
|
|
1970
|
+
return; // a type expression is shallow; cap defensively
|
|
1971
|
+
if (n.type === 'tuple_type')
|
|
1972
|
+
tuples.push(n);
|
|
1973
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1974
|
+
const c = n.namedChild(i);
|
|
1975
|
+
if (c)
|
|
1976
|
+
collectTuples(c, depth + 1);
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
collectTuples(value, 0);
|
|
1980
|
+
if (tuples.length === 0)
|
|
1981
|
+
return;
|
|
1982
|
+
this.nodeStack.push(typeAliasNode.id);
|
|
1983
|
+
for (const tuple of tuples) {
|
|
1984
|
+
for (let i = 0; i < tuple.namedChildCount; i++) {
|
|
1985
|
+
const entry = tuple.namedChild(i);
|
|
1986
|
+
if (!entry || entry.type !== 'generic_type')
|
|
1987
|
+
continue;
|
|
1988
|
+
const typeArgs = (0, tree_sitter_helpers_1.getChildByField)(entry, 'type_arguments');
|
|
1989
|
+
if (!typeArgs)
|
|
1990
|
+
continue;
|
|
1991
|
+
for (let j = 0; j < typeArgs.namedChildCount; j++) {
|
|
1992
|
+
const arg = typeArgs.namedChild(j);
|
|
1993
|
+
if (!arg || arg.type !== 'literal_type')
|
|
1994
|
+
continue;
|
|
1995
|
+
// literal_type wraps the actual literal; only a string is a name.
|
|
1996
|
+
const strNode = arg.namedChild(0);
|
|
1997
|
+
if (!strNode || strNode.type !== 'string')
|
|
1998
|
+
continue;
|
|
1999
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(strNode, this.source)
|
|
2000
|
+
.trim()
|
|
2001
|
+
.replace(/^['"`]/, '')
|
|
2002
|
+
.replace(/['"`]$/, '');
|
|
2003
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name))
|
|
2004
|
+
continue;
|
|
2005
|
+
const signature = (0, tree_sitter_helpers_1.getNodeText)(entry, this.source).replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
2006
|
+
this.createNode('method', name, entry, {
|
|
2007
|
+
signature,
|
|
2008
|
+
qualifiedName: `${typeAliasNode.name}::${name}`,
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
this.nodeStack.pop();
|
|
2014
|
+
}
|
|
1373
2015
|
/**
|
|
1374
2016
|
* `foo: () => T` → property_signature whose type_annotation contains a
|
|
1375
2017
|
* `function_type`. Treat that as a method-shaped contract member, since
|
|
@@ -1421,6 +2063,48 @@ class TreeSitterExtractor {
|
|
|
1421
2063
|
});
|
|
1422
2064
|
}
|
|
1423
2065
|
}
|
|
2066
|
+
// Link each imported binding to its definition so imported-but-not-
|
|
2067
|
+
// called/typed symbols still record a cross-file dependency (TS/JS only).
|
|
2068
|
+
if (this.language === 'typescript' || this.language === 'tsx' ||
|
|
2069
|
+
this.language === 'javascript' || this.language === 'jsx') {
|
|
2070
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2071
|
+
if (parentId)
|
|
2072
|
+
this.emitImportBindingRefs(node, parentId);
|
|
2073
|
+
}
|
|
2074
|
+
// Python `from module import X, Y` — link each imported name to its
|
|
2075
|
+
// definition (covers `__init__.py` re-export barrels, which are just
|
|
2076
|
+
// `from .sub import X`). Same recall gap as TS: a name imported and
|
|
2077
|
+
// used in a non-call position created no dependency edge.
|
|
2078
|
+
if (this.language === 'python' && node.type === 'import_from_statement') {
|
|
2079
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2080
|
+
if (parentId)
|
|
2081
|
+
this.emitPyFromImportRefs(node, parentId);
|
|
2082
|
+
}
|
|
2083
|
+
// Rust `use crate::m::Item;` / `pub use self::sub::Item;` — link each
|
|
2084
|
+
// imported leaf to its definition. Covers `pub use` re-export hubs
|
|
2085
|
+
// (a `mod.rs` re-exporting submodule items, e.g. tokio's `fs/mod.rs`)
|
|
2086
|
+
// and items imported but used in non-call/non-type positions.
|
|
2087
|
+
if (this.language === 'rust' && node.type === 'use_declaration') {
|
|
2088
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2089
|
+
if (parentId)
|
|
2090
|
+
this.emitRustUseBindingRefs(node, parentId);
|
|
2091
|
+
}
|
|
2092
|
+
// PHP `use Foo\Bar\Baz;` — link to the namespace-qualified definition so
|
|
2093
|
+
// an imported-but-DI-injected contract (Laravel's pattern) records a
|
|
2094
|
+
// cross-file dependency. Grouped imports are handled in their own branch.
|
|
2095
|
+
if (this.language === 'php' && node.type === 'namespace_use_declaration') {
|
|
2096
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2097
|
+
if (parentId)
|
|
2098
|
+
this.emitPhpUseRefs(node, parentId);
|
|
2099
|
+
}
|
|
2100
|
+
// Ruby `require "lib/foo"` / `require_relative "../foo"` — resolve to the
|
|
2101
|
+
// required FILE so a file pulled in only by `require` (config-loaded
|
|
2102
|
+
// components, gems that don't autoload) records a cross-file dependency.
|
|
2103
|
+
if (this.language === 'ruby' && node.type === 'call') {
|
|
2104
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2105
|
+
if (parentId)
|
|
2106
|
+
this.emitRubyRequireRefs(node, parentId);
|
|
2107
|
+
}
|
|
1424
2108
|
return;
|
|
1425
2109
|
}
|
|
1426
2110
|
// Hook returned null — fall through to multi-import inline handlers only
|
|
@@ -1430,12 +2114,31 @@ class TreeSitterExtractor {
|
|
|
1430
2114
|
// Multi-import cases that create multiple nodes (can't be expressed with single-return hook)
|
|
1431
2115
|
// Python import_statement: import os, sys (creates one import per module)
|
|
1432
2116
|
if (this.language === 'python' && node.type === 'import_statement') {
|
|
2117
|
+
const importParentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2118
|
+
// A bare `import a.b.c` of an internal module (the standard Django
|
|
2119
|
+
// `AppConfig.ready(): import myapp.signals` registration pattern, and any
|
|
2120
|
+
// `import pkg.mod` used for its side effects) had no edge to the module
|
|
2121
|
+
// file — only `from x import y` was linked. Push an `imports` ref (like
|
|
2122
|
+
// Go) so the resolver maps the dotted path to its file. Stdlib/external
|
|
2123
|
+
// modules naturally don't resolve (no `os.py` file node in the repo).
|
|
2124
|
+
const pushModuleRef = (dotted) => {
|
|
2125
|
+
if (!importParentId)
|
|
2126
|
+
return;
|
|
2127
|
+
this.unresolvedReferences.push({
|
|
2128
|
+
fromNodeId: importParentId,
|
|
2129
|
+
referenceName: (0, tree_sitter_helpers_1.getNodeText)(dotted, this.source),
|
|
2130
|
+
referenceKind: 'imports',
|
|
2131
|
+
line: dotted.startPosition.row + 1,
|
|
2132
|
+
column: dotted.startPosition.column,
|
|
2133
|
+
});
|
|
2134
|
+
};
|
|
1433
2135
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1434
2136
|
const child = node.namedChild(i);
|
|
1435
2137
|
if (child?.type === 'dotted_name') {
|
|
1436
2138
|
this.createNode('import', (0, tree_sitter_helpers_1.getNodeText)(child, this.source), node, {
|
|
1437
2139
|
signature: importText,
|
|
1438
2140
|
});
|
|
2141
|
+
pushModuleRef(child);
|
|
1439
2142
|
}
|
|
1440
2143
|
else if (child?.type === 'aliased_import') {
|
|
1441
2144
|
const dottedName = child.namedChildren.find(c => c.type === 'dotted_name');
|
|
@@ -1443,6 +2146,7 @@ class TreeSitterExtractor {
|
|
|
1443
2146
|
this.createNode('import', (0, tree_sitter_helpers_1.getNodeText)(dottedName, this.source), node, {
|
|
1444
2147
|
signature: importText,
|
|
1445
2148
|
});
|
|
2149
|
+
pushModuleRef(dottedName);
|
|
1446
2150
|
}
|
|
1447
2151
|
}
|
|
1448
2152
|
}
|
|
@@ -1503,6 +2207,9 @@ class TreeSitterExtractor {
|
|
|
1503
2207
|
this.createNode('import', fullPath, node, {
|
|
1504
2208
|
signature: importText,
|
|
1505
2209
|
});
|
|
2210
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2211
|
+
if (parentId)
|
|
2212
|
+
this.pushPhpUseRef(fullPath, parentId, node);
|
|
1506
2213
|
}
|
|
1507
2214
|
}
|
|
1508
2215
|
return;
|
|
@@ -1516,6 +2223,285 @@ class TreeSitterExtractor {
|
|
|
1516
2223
|
signature: importText,
|
|
1517
2224
|
});
|
|
1518
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Emit one `imports` reference per named/default import binding (TS/JS family),
|
|
2228
|
+
* attributed to the file node — so the resolver links each imported symbol to
|
|
2229
|
+
* the file that DEFINES it.
|
|
2230
|
+
*
|
|
2231
|
+
* Importing a symbol IS a dependency, but extraction only emits references for
|
|
2232
|
+
* calls, instantiations, type annotations, and inheritance. A symbol that's
|
|
2233
|
+
* imported and then only re-exported (`export { X } from './x'`), placed in a
|
|
2234
|
+
* registry array (`[expressResolver, …]`), passed as an argument, or used in
|
|
2235
|
+
* JSX produced NO cross-file edge at all — so the providing file showed a
|
|
2236
|
+
* false "0 dependents" and was invisible to blast-radius / `affected`. The
|
|
2237
|
+
* resolver maps the local name (alias-aware) to the provider's definition and
|
|
2238
|
+
* creates a cross-file `imports` edge; `getFileDependents` picks it up, while
|
|
2239
|
+
* `getImpactRadius` keeps it as a bounded leaf (the importing file node).
|
|
2240
|
+
*
|
|
2241
|
+
* Namespace imports (`import * as NS`) bind a whole module: `NS.member` calls
|
|
2242
|
+
* resolve on their own, but a namespace used ONLY via a value-member read
|
|
2243
|
+
* (`NS.SOME_CONST`) would leave no edge — so we also emit the namespace local
|
|
2244
|
+
* name, which the resolver links to the module FILE as a dependency backstop.
|
|
2245
|
+
*/
|
|
2246
|
+
emitImportBindingRefs(node, fromNodeId) {
|
|
2247
|
+
const clause = node.namedChildren.find((c) => c.type === 'import_clause');
|
|
2248
|
+
if (!clause)
|
|
2249
|
+
return; // side-effect import (`import './x'`) — no bindings
|
|
2250
|
+
const pushRef = (nameNode) => {
|
|
2251
|
+
if (!nameNode)
|
|
2252
|
+
return;
|
|
2253
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
2254
|
+
if (!name)
|
|
2255
|
+
return;
|
|
2256
|
+
this.unresolvedReferences.push({
|
|
2257
|
+
fromNodeId,
|
|
2258
|
+
referenceName: name,
|
|
2259
|
+
referenceKind: 'imports',
|
|
2260
|
+
line: nameNode.startPosition.row + 1,
|
|
2261
|
+
column: nameNode.startPosition.column,
|
|
2262
|
+
});
|
|
2263
|
+
};
|
|
2264
|
+
for (const child of clause.namedChildren) {
|
|
2265
|
+
if (child.type === 'identifier') {
|
|
2266
|
+
// default import: `import Foo from './x'`
|
|
2267
|
+
pushRef(child);
|
|
2268
|
+
}
|
|
2269
|
+
else if (child.type === 'named_imports') {
|
|
2270
|
+
// `import { A, B as C } from './x'` — link the LOCAL name (alias if any)
|
|
2271
|
+
for (const spec of child.namedChildren) {
|
|
2272
|
+
if (spec.type !== 'import_specifier')
|
|
2273
|
+
continue;
|
|
2274
|
+
pushRef((0, tree_sitter_helpers_1.getChildByField)(spec, 'alias') ?? (0, tree_sitter_helpers_1.getChildByField)(spec, 'name') ?? spec.namedChild(0));
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
else if (child.type === 'namespace_import') {
|
|
2278
|
+
// `import * as NS from './x'` — emit NS so the module-import backstop can
|
|
2279
|
+
// record the file dependency even if NS is only used by value-member read.
|
|
2280
|
+
pushRef(child.namedChildren.find((c) => c.type === 'identifier') ?? child.namedChild(0));
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Emit one `imports` reference per re-exported binding of a
|
|
2286
|
+
* `export { A, B as C } from './y'` statement, attributed to the file node —
|
|
2287
|
+
* so a barrel that re-exports from another module records a dependency on it.
|
|
2288
|
+
*
|
|
2289
|
+
* Links the SOURCE-side name (`A`, the `name` field — not the local alias
|
|
2290
|
+
* `C`), since that is what the source module defines. `export * from './y'`
|
|
2291
|
+
* has no named bindings to attribute and `export { default as X }` can't be
|
|
2292
|
+
* name-matched, so both are skipped.
|
|
2293
|
+
*/
|
|
2294
|
+
emitReExportRefs(node, fromNodeId) {
|
|
2295
|
+
const clause = node.namedChildren.find((c) => c.type === 'export_clause');
|
|
2296
|
+
if (!clause)
|
|
2297
|
+
return; // `export * from './y'` — no named bindings
|
|
2298
|
+
for (const spec of clause.namedChildren) {
|
|
2299
|
+
if (spec.type !== 'export_specifier')
|
|
2300
|
+
continue;
|
|
2301
|
+
const nameNode = (0, tree_sitter_helpers_1.getChildByField)(spec, 'name') ?? spec.namedChild(0);
|
|
2302
|
+
if (!nameNode)
|
|
2303
|
+
continue;
|
|
2304
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
2305
|
+
if (!name || name === 'default')
|
|
2306
|
+
continue;
|
|
2307
|
+
this.unresolvedReferences.push({
|
|
2308
|
+
fromNodeId,
|
|
2309
|
+
referenceName: name,
|
|
2310
|
+
referenceKind: 'imports',
|
|
2311
|
+
line: nameNode.startPosition.row + 1,
|
|
2312
|
+
column: nameNode.startPosition.column,
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Emit one `imports` reference per binding of a Rust `use` declaration —
|
|
2318
|
+
* `use crate::m::Item`, `use crate::m::{A, B as C}`, `pub use self::sub::Item`.
|
|
2319
|
+
* Emits the FULL path (e.g. `self::sub::Item`, not just `Item`) so the resolver
|
|
2320
|
+
* can resolve the module prefix to a file and find the leaf symbol there —
|
|
2321
|
+
* disambiguating common-name re-exports (`pub use self::read::read`, where the
|
|
2322
|
+
* leaf `read` collides with many same-named symbols). Falls back to name-match
|
|
2323
|
+
* on the leaf when the path can't be resolved. `use ...::*` has no leaf binding.
|
|
2324
|
+
*/
|
|
2325
|
+
emitRustUseBindingRefs(node, fromNodeId) {
|
|
2326
|
+
const paths = [];
|
|
2327
|
+
const join = (prefix, seg) => (prefix ? `${prefix}::${seg}` : seg);
|
|
2328
|
+
const collect = (n, prefix) => {
|
|
2329
|
+
switch (n.type) {
|
|
2330
|
+
case 'identifier':
|
|
2331
|
+
paths.push({ text: join(prefix, (0, tree_sitter_helpers_1.getNodeText)(n, this.source)), node: n });
|
|
2332
|
+
break;
|
|
2333
|
+
case 'scoped_identifier': {
|
|
2334
|
+
// Full scoped path (`a::b::C`); combine with any outer group prefix.
|
|
2335
|
+
const full = (0, tree_sitter_helpers_1.getNodeText)(n, this.source).trim();
|
|
2336
|
+
paths.push({ text: prefix ? `${prefix}::${full}` : full, node: n });
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
case 'scoped_use_list': {
|
|
2340
|
+
// `path::{ ... }` — the group's path becomes the prefix for each item.
|
|
2341
|
+
const pathNode = (0, tree_sitter_helpers_1.getChildByField)(n, 'path');
|
|
2342
|
+
const seg = pathNode ? (0, tree_sitter_helpers_1.getNodeText)(pathNode, this.source).trim() : '';
|
|
2343
|
+
const newPrefix = seg ? join(prefix, seg) : prefix;
|
|
2344
|
+
const list = (0, tree_sitter_helpers_1.getChildByField)(n, 'list') ?? n.namedChildren.find((c) => c.type === 'use_list');
|
|
2345
|
+
if (list)
|
|
2346
|
+
collect(list, newPrefix);
|
|
2347
|
+
break;
|
|
2348
|
+
}
|
|
2349
|
+
case 'use_list':
|
|
2350
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2351
|
+
const c = n.namedChild(i);
|
|
2352
|
+
if (c)
|
|
2353
|
+
collect(c, prefix);
|
|
2354
|
+
}
|
|
2355
|
+
break;
|
|
2356
|
+
case 'use_as_clause': {
|
|
2357
|
+
// `Path as Alias` → link the source path (the definition), not the alias.
|
|
2358
|
+
const p = (0, tree_sitter_helpers_1.getChildByField)(n, 'path') ?? n.namedChild(0);
|
|
2359
|
+
if (p)
|
|
2360
|
+
collect(p, prefix);
|
|
2361
|
+
break;
|
|
2362
|
+
}
|
|
2363
|
+
// use_wildcard → no specific binding to link.
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2367
|
+
const c = node.namedChild(i);
|
|
2368
|
+
if (c)
|
|
2369
|
+
collect(c, '');
|
|
2370
|
+
}
|
|
2371
|
+
for (const p of paths) {
|
|
2372
|
+
// The leaf must be a real name (skip a path that is only `self`/`super`/`crate`).
|
|
2373
|
+
const leaf = p.text.split('::').pop();
|
|
2374
|
+
if (!leaf || leaf === 'self' || leaf === 'super' || leaf === 'crate' || leaf === '*')
|
|
2375
|
+
continue;
|
|
2376
|
+
this.unresolvedReferences.push({
|
|
2377
|
+
fromNodeId,
|
|
2378
|
+
referenceName: p.text,
|
|
2379
|
+
referenceKind: 'imports',
|
|
2380
|
+
line: p.node.startPosition.row + 1,
|
|
2381
|
+
column: p.node.startPosition.column,
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Emit an `imports` reference for a single PHP `use Foo\Bar\Baz;` (grouped
|
|
2387
|
+
* imports `use Foo\{A, B}` are handled where their per-item nodes are created).
|
|
2388
|
+
* The reference targets the namespace-qualified `Foo\Bar::Baz` form classes are
|
|
2389
|
+
* stored under (see the PHP `namespace` capture), so it resolves to the RIGHT
|
|
2390
|
+
* definition — Laravel has many same-named contracts (`Factory`, `Dispatcher`,
|
|
2391
|
+
* `Guard`) across namespaces that a bare-name match can't disambiguate.
|
|
2392
|
+
*/
|
|
2393
|
+
emitPhpUseRefs(node, fromNodeId) {
|
|
2394
|
+
const clause = node.namedChildren.find((c) => c.type === 'namespace_use_clause');
|
|
2395
|
+
if (!clause)
|
|
2396
|
+
return;
|
|
2397
|
+
const qn = clause.namedChildren.find((c) => c.type === 'qualified_name')
|
|
2398
|
+
?? clause.namedChildren.find((c) => c.type === 'name');
|
|
2399
|
+
if (qn)
|
|
2400
|
+
this.pushPhpUseRef((0, tree_sitter_helpers_1.getNodeText)(qn, this.source), fromNodeId, node);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Ruby `require`/`require_relative` → an `imports` ref to the required FILE.
|
|
2404
|
+
* `require "sidekiq/fetch"` is load-path-relative (matched by file-path suffix
|
|
2405
|
+
* via {@link matchByFilePath}); `require_relative "../foo"` is resolved against
|
|
2406
|
+
* this file's directory. Bare gem/stdlib requires (`require "json"`, no slash)
|
|
2407
|
+
* are skipped — they're external. The path form (a `/` + `.rb`) makes the ref
|
|
2408
|
+
* resolve to the file node, so a file pulled in only by `require` — not by a
|
|
2409
|
+
* resolved constant/call — still records a cross-file dependency.
|
|
2410
|
+
*/
|
|
2411
|
+
emitRubyRequireRefs(node, fromNodeId) {
|
|
2412
|
+
const method = node.namedChildren.find((c) => c.type === 'identifier');
|
|
2413
|
+
const mname = method ? (0, tree_sitter_helpers_1.getNodeText)(method, this.source) : '';
|
|
2414
|
+
if (mname !== 'require' && mname !== 'require_relative')
|
|
2415
|
+
return;
|
|
2416
|
+
const argList = node.namedChildren.find((c) => c.type === 'argument_list');
|
|
2417
|
+
const str = argList?.namedChildren.find((c) => c.type === 'string');
|
|
2418
|
+
const content = str?.namedChildren.find((c) => c.type === 'string_content');
|
|
2419
|
+
if (!content)
|
|
2420
|
+
return;
|
|
2421
|
+
const req = (0, tree_sitter_helpers_1.getNodeText)(content, this.source).trim();
|
|
2422
|
+
if (!req)
|
|
2423
|
+
return;
|
|
2424
|
+
let refPath;
|
|
2425
|
+
if (mname === 'require_relative') {
|
|
2426
|
+
const slash = this.filePath.lastIndexOf('/');
|
|
2427
|
+
const dir = slash >= 0 ? this.filePath.slice(0, slash) : '';
|
|
2428
|
+
refPath = path.posix.normalize(dir ? `${dir}/${req}` : req);
|
|
2429
|
+
}
|
|
2430
|
+
else {
|
|
2431
|
+
refPath = req; // load-path require — suffix-matched against the file path
|
|
2432
|
+
}
|
|
2433
|
+
if (!refPath.includes('/'))
|
|
2434
|
+
return; // bare gem/stdlib require — external
|
|
2435
|
+
if (!refPath.endsWith('.rb'))
|
|
2436
|
+
refPath += '.rb';
|
|
2437
|
+
this.unresolvedReferences.push({
|
|
2438
|
+
fromNodeId,
|
|
2439
|
+
referenceName: refPath,
|
|
2440
|
+
referenceKind: 'imports',
|
|
2441
|
+
line: node.startPosition.row + 1,
|
|
2442
|
+
column: node.startPosition.column,
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
/** Convert a PHP FQN `Foo\Bar\Baz` to the stored `Foo\Bar::Baz` and emit an `imports` ref. */
|
|
2446
|
+
pushPhpUseRef(fqn, fromNodeId, node) {
|
|
2447
|
+
const clean = fqn.replace(/^\\/, '');
|
|
2448
|
+
const lastSep = clean.lastIndexOf('\\');
|
|
2449
|
+
if (lastSep < 0)
|
|
2450
|
+
return; // global-namespace class — already matches by simple name
|
|
2451
|
+
this.unresolvedReferences.push({
|
|
2452
|
+
fromNodeId,
|
|
2453
|
+
referenceName: `${clean.slice(0, lastSep)}::${clean.slice(lastSep + 1)}`,
|
|
2454
|
+
referenceKind: 'imports',
|
|
2455
|
+
line: node.startPosition.row + 1,
|
|
2456
|
+
column: node.startPosition.column,
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Emit one `imports` reference per name imported in a Python
|
|
2461
|
+
* `from module import A, B as C` statement, attributed to the file node — so
|
|
2462
|
+
* the resolver links each imported name to the module that DEFINES it.
|
|
2463
|
+
*
|
|
2464
|
+
* Same recall gap as TS: extraction only emitted references for calls,
|
|
2465
|
+
* instantiations, and inheritance, so a name imported and then used in a
|
|
2466
|
+
* non-call position (a list/dict literal, a default argument, a decorator
|
|
2467
|
+
* target, or simply re-exported through an `__init__.py` barrel) produced no
|
|
2468
|
+
* cross-file edge — the providing module showed a false "0 dependents". Links
|
|
2469
|
+
* the LOCAL name (alias when present, since that's what the resolver's import
|
|
2470
|
+
* mapping keys on); `from module import *` has no names to attribute.
|
|
2471
|
+
*/
|
|
2472
|
+
emitPyFromImportRefs(node, fromNodeId) {
|
|
2473
|
+
const moduleNameNode = (0, tree_sitter_helpers_1.getChildByField)(node, 'module_name');
|
|
2474
|
+
for (const child of node.namedChildren) {
|
|
2475
|
+
// Skip the `from <module>` part itself and `import *`.
|
|
2476
|
+
if (moduleNameNode &&
|
|
2477
|
+
child.startIndex === moduleNameNode.startIndex &&
|
|
2478
|
+
child.endIndex === moduleNameNode.endIndex)
|
|
2479
|
+
continue;
|
|
2480
|
+
if (child.type === 'wildcard_import')
|
|
2481
|
+
continue;
|
|
2482
|
+
let nameNode = null;
|
|
2483
|
+
if (child.type === 'aliased_import') {
|
|
2484
|
+
nameNode = (0, tree_sitter_helpers_1.getChildByField)(child, 'alias') ?? (0, tree_sitter_helpers_1.getChildByField)(child, 'name') ?? child.namedChild(0);
|
|
2485
|
+
}
|
|
2486
|
+
else if (child.type === 'dotted_name') {
|
|
2487
|
+
nameNode = child;
|
|
2488
|
+
}
|
|
2489
|
+
if (!nameNode)
|
|
2490
|
+
continue;
|
|
2491
|
+
const raw = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
2492
|
+
// Imported names are simple identifiers; defensively take the last segment.
|
|
2493
|
+
const local = raw.includes('.') ? raw.split('.').pop() : raw;
|
|
2494
|
+
if (!local)
|
|
2495
|
+
continue;
|
|
2496
|
+
this.unresolvedReferences.push({
|
|
2497
|
+
fromNodeId,
|
|
2498
|
+
referenceName: local,
|
|
2499
|
+
referenceKind: 'imports',
|
|
2500
|
+
line: nameNode.startPosition.row + 1,
|
|
2501
|
+
column: nameNode.startPosition.column,
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
1519
2505
|
/**
|
|
1520
2506
|
* Extract a function call
|
|
1521
2507
|
*/
|
|
@@ -1539,6 +2525,57 @@ class TreeSitterExtractor {
|
|
|
1539
2525
|
// single-dot receiver regex fails. Pull out the immediate field after `this.`
|
|
1540
2526
|
// so the receiver is the field name (`userbo`), which the resolver can then
|
|
1541
2527
|
// look up in the enclosing class's field declarations.
|
|
2528
|
+
// PHP static-factory fluent chain: `Cls::for($x)->method()` — the receiver
|
|
2529
|
+
// is itself a static call, so resolution must infer the method's class
|
|
2530
|
+
// from what `Cls::for` RETURNS (its `: self` / `: static` / `: Type`),
|
|
2531
|
+
// #608 (mirrors the C++ chain fix in #645). Encode `<Cls::factory>().<method>`;
|
|
2532
|
+
// the `().` marker lets the PHP resolver split it. The receiver text
|
|
2533
|
+
// (`Cls::for('x')`) carries the args, so without this it degrades to an
|
|
2534
|
+
// unresolvable string and the call edge is dropped.
|
|
2535
|
+
if (methodName && this.language === 'php' && objectField.type === 'scoped_call_expression') {
|
|
2536
|
+
const innerScope = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'scope');
|
|
2537
|
+
const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
|
|
2538
|
+
if (innerScope && innerName) {
|
|
2539
|
+
calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerScope, this.source)}::${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
|
|
2540
|
+
}
|
|
2541
|
+
else {
|
|
2542
|
+
calleeName = methodName;
|
|
2543
|
+
}
|
|
2544
|
+
if (calleeName) {
|
|
2545
|
+
this.unresolvedReferences.push({
|
|
2546
|
+
fromNodeId: callerId,
|
|
2547
|
+
referenceName: calleeName,
|
|
2548
|
+
referenceKind: 'calls',
|
|
2549
|
+
line: node.startPosition.row + 1,
|
|
2550
|
+
column: node.startPosition.column,
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
// Java static-factory / fluent chain: `Foo.getInstance().bar()` — the
|
|
2556
|
+
// receiver is itself a method call, so resolution must infer bar's class
|
|
2557
|
+
// from what `Foo.getInstance` RETURNS (its declared return type), the
|
|
2558
|
+
// #645/#608 mechanism. Encode `<inner-receiver>.<inner-method>().<method>`;
|
|
2559
|
+
// the `().` marker lets the Java chain resolver split it, and normalizing to
|
|
2560
|
+
// empty parens drops any factory args (`Foo.create(cfg).bar()`) that would
|
|
2561
|
+
// otherwise leave a `(cfg)` in the receiver text and break the split.
|
|
2562
|
+
if (methodName &&
|
|
2563
|
+
this.language === 'java' &&
|
|
2564
|
+
objectField.type === 'method_invocation') {
|
|
2565
|
+
const innerObj = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
|
|
2566
|
+
const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
|
|
2567
|
+
if (innerObj && innerName) {
|
|
2568
|
+
calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerObj, this.source)}.${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
|
|
2569
|
+
this.unresolvedReferences.push({
|
|
2570
|
+
fromNodeId: callerId,
|
|
2571
|
+
referenceName: calleeName,
|
|
2572
|
+
referenceKind: 'calls',
|
|
2573
|
+
line: node.startPosition.row + 1,
|
|
2574
|
+
column: node.startPosition.column,
|
|
2575
|
+
});
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
1542
2579
|
let receiverName;
|
|
1543
2580
|
if (objectField.type === 'field_access') {
|
|
1544
2581
|
const inner = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
|
|
@@ -1584,15 +2621,77 @@ class TreeSitterExtractor {
|
|
|
1584
2621
|
}
|
|
1585
2622
|
}
|
|
1586
2623
|
if (methodKeywords.length > 0) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2624
|
+
// A selector keyword takes a `:` when it has an argument. A SINGLE
|
|
2625
|
+
// keyword can be unary (`[c reset]` → `reset`) OR take one argument
|
|
2626
|
+
// (`[c storeImage:k]` → `storeImage:`) — distinguished by whether the
|
|
2627
|
+
// message has a `:` token. Without this, every single-argument message
|
|
2628
|
+
// (the most common form: `addObject:`, `storeImage:`, …) was named
|
|
2629
|
+
// without the colon and never matched its `storeImage:` method.
|
|
2630
|
+
let hasColon = false;
|
|
2631
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2632
|
+
if (node.child(i)?.type === ':') {
|
|
2633
|
+
hasColon = true;
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const methodName = hasColon
|
|
2638
|
+
? methodKeywords.map((k) => `${k}:`).join('')
|
|
2639
|
+
: methodKeywords[0];
|
|
1590
2640
|
const receiverField = (0, tree_sitter_helpers_1.getChildByField)(node, 'receiver');
|
|
1591
2641
|
const SKIP_RECEIVERS = new Set(['self', 'super']);
|
|
1592
2642
|
if (receiverField && receiverField.type !== 'message_expression') {
|
|
1593
2643
|
const receiverName = (0, tree_sitter_helpers_1.getNodeText)(receiverField, this.source);
|
|
1594
2644
|
if (receiverName && !SKIP_RECEIVERS.has(receiverName)) {
|
|
1595
2645
|
calleeName = `${receiverName}.${methodName}`;
|
|
2646
|
+
// A CLASS-message receiver (`[SDImageCache alloc]`,
|
|
2647
|
+
// `[SDImageCache sharedCache]`) is a capitalized class name. The
|
|
2648
|
+
// call resolves the method (`alloc`/`sharedCache`), but the CLASS
|
|
2649
|
+
// itself — whose @interface lives in the header — would otherwise
|
|
2650
|
+
// never be referenced. Emit a `references` edge to it so a class
|
|
2651
|
+
// used only via class messages (alloc/init, singletons, factories)
|
|
2652
|
+
// and its header record a dependent.
|
|
2653
|
+
if (/^[A-Z][A-Za-z0-9_]*$/.test(receiverName)) {
|
|
2654
|
+
this.unresolvedReferences.push({
|
|
2655
|
+
fromNodeId: callerId,
|
|
2656
|
+
referenceName: receiverName,
|
|
2657
|
+
referenceKind: 'references',
|
|
2658
|
+
line: receiverField.startPosition.row + 1,
|
|
2659
|
+
column: receiverField.startPosition.column,
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
else {
|
|
2664
|
+
calleeName = methodName;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
else if (receiverField && receiverField.type === 'message_expression' && /^\w+$/.test(methodName)) {
|
|
2668
|
+
// Chained message send `[[Foo create] doIt]` — the receiver is itself a
|
|
2669
|
+
// class message. Recover the inner `Class.selector` and encode
|
|
2670
|
+
// `Class.selector().doIt` so resolution infers doIt's class from what
|
|
2671
|
+
// `Class.selector` RETURNS (#645/#608). Only a CLASS-factory chain
|
|
2672
|
+
// (capitalized inner receiver); a unary outer selector is required
|
|
2673
|
+
// because the chain resolver's method part is `\w+` (no `:`). An
|
|
2674
|
+
// instance chain (`[[obj foo] bar]`, lowercase inner) stays bare.
|
|
2675
|
+
const innerRecv = (0, tree_sitter_helpers_1.getChildByField)(receiverField, 'receiver');
|
|
2676
|
+
const innerRecvName = innerRecv ? (0, tree_sitter_helpers_1.getNodeText)(innerRecv, this.source) : '';
|
|
2677
|
+
if (innerRecv?.type === 'identifier' && /^[A-Z]/.test(innerRecvName)) {
|
|
2678
|
+
const innerKw = [];
|
|
2679
|
+
for (let i = 0; i < receiverField.namedChildCount; i++) {
|
|
2680
|
+
if (receiverField.fieldNameForNamedChild(i) === 'method') {
|
|
2681
|
+
const kw = receiverField.namedChild(i);
|
|
2682
|
+
if (kw)
|
|
2683
|
+
innerKw.push((0, tree_sitter_helpers_1.getNodeText)(kw, this.source));
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
let innerColon = false;
|
|
2687
|
+
for (let i = 0; i < receiverField.childCount; i++) {
|
|
2688
|
+
if (receiverField.child(i)?.type === ':') {
|
|
2689
|
+
innerColon = true;
|
|
2690
|
+
break;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
const innerSelector = innerColon ? innerKw.map((k) => `${k}:`).join('') : innerKw[0];
|
|
2694
|
+
calleeName = innerSelector ? `${innerRecvName}.${innerSelector}().${methodName}` : methodName;
|
|
1596
2695
|
}
|
|
1597
2696
|
else {
|
|
1598
2697
|
calleeName = methodName;
|
|
@@ -1642,6 +2741,67 @@ class TreeSitterExtractor {
|
|
|
1642
2741
|
calleeName = methodName;
|
|
1643
2742
|
}
|
|
1644
2743
|
}
|
|
2744
|
+
else if ((this.language === 'cpp' ||
|
|
2745
|
+
this.language === 'c' ||
|
|
2746
|
+
this.language === 'kotlin' ||
|
|
2747
|
+
this.language === 'swift' ||
|
|
2748
|
+
this.language === 'rust' ||
|
|
2749
|
+
this.language === 'go' ||
|
|
2750
|
+
this.language === 'scala') &&
|
|
2751
|
+
receiver &&
|
|
2752
|
+
receiver.type === 'call_expression') {
|
|
2753
|
+
// Receiver that is itself a call — `Foo::instance().bar()`,
|
|
2754
|
+
// `openSession()->run()`, `mgr.view().render()` (C/C++),
|
|
2755
|
+
// `Foo.getInstance().bar()` (Kotlin) / `Foo.make().draw()` (Swift),
|
|
2756
|
+
// `Foo::new().bar()` (Rust), or `New().Method()` (Go). Keep the inner
|
|
2757
|
+
// call so resolution can infer bar()'s class from what the inner call
|
|
2758
|
+
// RETURNS (#645/#608). Encode as `<innerCallee>().<method>`; the `().`
|
|
2759
|
+
// marker never appears in an ordinary ref, so the resolver can detect
|
|
2760
|
+
// and split it. Other languages keep the bare-name behavior below.
|
|
2761
|
+
let innerCallee;
|
|
2762
|
+
let reencode;
|
|
2763
|
+
if (this.language === 'kotlin' || this.language === 'swift') {
|
|
2764
|
+
// tree-sitter-kotlin/swift expose the inner callee as the
|
|
2765
|
+
// call_expression's first named child (a navigation_expression
|
|
2766
|
+
// `Foo.getInstance`, or a bare identifier for a free/constructor call).
|
|
2767
|
+
const innerNav = receiver.namedChild(0);
|
|
2768
|
+
innerCallee = innerNav ? (0, tree_sitter_helpers_1.getNodeText)(innerNav, this.source).replace(/\s+/g, '') : '';
|
|
2769
|
+
// Only re-encode a CLASS / companion-factory / constructor chain,
|
|
2770
|
+
// whose receiver chain starts with a capitalized type
|
|
2771
|
+
// (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain
|
|
2772
|
+
// (`list.filter{}.map{}`) has a lowercase receiver whose type we
|
|
2773
|
+
// can't recover here — re-encoding it would only drop the edge (no
|
|
2774
|
+
// chain resolution, no bare-name fallback), regressing recall in
|
|
2775
|
+
// fluent codebases. Leave those to the bare-name path.
|
|
2776
|
+
reencode = /^[A-Z]/.test(innerCallee);
|
|
2777
|
+
}
|
|
2778
|
+
else {
|
|
2779
|
+
const innerFn = (0, tree_sitter_helpers_1.getChildByField)(receiver, 'function');
|
|
2780
|
+
innerCallee = innerFn
|
|
2781
|
+
? (0, tree_sitter_helpers_1.getNodeText)(innerFn, this.source).replace(/->/g, '.').replace(/\s+/g, '')
|
|
2782
|
+
: '';
|
|
2783
|
+
// Rust: only re-encode an associated-function chain
|
|
2784
|
+
// (`Foo::new().bar()`), whose inner callee is a path/`scoped_identifier`.
|
|
2785
|
+
// Go: only a bare package-level factory chain (`New().Method()`),
|
|
2786
|
+
// whose inner callee is an `identifier`. An instance chain
|
|
2787
|
+
// (`x.foo().bar()` Rust, `obj.Method().Other()` Go) keeps bare-name —
|
|
2788
|
+
// the resolver can't recover a variable's type, so re-encoding would
|
|
2789
|
+
// only drop the edge. C/C++ re-encode any inner.
|
|
2790
|
+
if (this.language === 'rust')
|
|
2791
|
+
reencode = innerFn?.type === 'scoped_identifier';
|
|
2792
|
+
else if (this.language === 'go')
|
|
2793
|
+
reencode = innerFn?.type === 'identifier';
|
|
2794
|
+
// Scala: only a companion-factory / case-class-apply chain whose
|
|
2795
|
+
// receiver chain starts with a capitalized type (`Foo.create().bar()`,
|
|
2796
|
+
// `Foo(args).bar()`). An instance chain (`list.map().filter()`) has a
|
|
2797
|
+
// lowercase receiver whose type we can't recover — leave it bare.
|
|
2798
|
+
else if (this.language === 'scala')
|
|
2799
|
+
reencode = /^[A-Z]/.test(innerCallee);
|
|
2800
|
+
else
|
|
2801
|
+
reencode = !!innerCallee;
|
|
2802
|
+
}
|
|
2803
|
+
calleeName = reencode ? `${innerCallee}().${methodName}` : methodName;
|
|
2804
|
+
}
|
|
1645
2805
|
else {
|
|
1646
2806
|
calleeName = methodName;
|
|
1647
2807
|
}
|
|
@@ -1651,11 +2811,39 @@ class TreeSitterExtractor {
|
|
|
1651
2811
|
// Scoped call: Module::function()
|
|
1652
2812
|
calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
|
|
1653
2813
|
}
|
|
2814
|
+
else if (this.language === 'csharp' && func.type === 'member_access_expression') {
|
|
2815
|
+
// C# member call `recv.Method(...)`. When the receiver is itself a call
|
|
2816
|
+
// — a chained factory `Foo.Create(args).Bar()` — encode `inner().Bar`
|
|
2817
|
+
// with normalized empty parens so resolution can infer Bar's class from
|
|
2818
|
+
// what `Foo.Create` RETURNS (#645/#608). A non-call receiver keeps the
|
|
2819
|
+
// full member-access text (the existing `recv.Method` behavior).
|
|
2820
|
+
const recv = (0, tree_sitter_helpers_1.getChildByField)(func, 'expression');
|
|
2821
|
+
const nameNode = (0, tree_sitter_helpers_1.getChildByField)(func, 'name');
|
|
2822
|
+
const methodName = nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : '';
|
|
2823
|
+
if (recv && recv.type === 'invocation_expression' && methodName) {
|
|
2824
|
+
const innerFunc = (0, tree_sitter_helpers_1.getChildByField)(recv, 'function');
|
|
2825
|
+
const innerCallee = innerFunc ? (0, tree_sitter_helpers_1.getNodeText)(innerFunc, this.source).replace(/\s+/g, '') : '';
|
|
2826
|
+
calleeName = innerCallee ? `${innerCallee}().${methodName}` : methodName;
|
|
2827
|
+
}
|
|
2828
|
+
else {
|
|
2829
|
+
calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
1654
2832
|
else {
|
|
1655
2833
|
calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
|
|
1656
2834
|
}
|
|
1657
2835
|
}
|
|
1658
2836
|
}
|
|
2837
|
+
// Parenthesized type conversions — Go `(*T)(x)` / `(T)(x)` (and a
|
|
2838
|
+
// parenthesized callee generally) parse as a call whose "function" is a
|
|
2839
|
+
// parenthesized type/expression, so the callee text is the un-resolvable
|
|
2840
|
+
// literal `(*T)`. Normalize to the inner name so it resolves to `T` (a real
|
|
2841
|
+
// dependency on the converted-to type) instead of dropping on the floor.
|
|
2842
|
+
if (calleeName) {
|
|
2843
|
+
const conv = calleeName.match(/^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$/);
|
|
2844
|
+
if (conv && conv[1])
|
|
2845
|
+
calleeName = conv[1];
|
|
2846
|
+
}
|
|
1659
2847
|
if (calleeName) {
|
|
1660
2848
|
this.unresolvedReferences.push({
|
|
1661
2849
|
fromNodeId: callerId,
|
|
@@ -1689,6 +2877,46 @@ class TreeSitterExtractor {
|
|
|
1689
2877
|
node.namedChild(0);
|
|
1690
2878
|
if (!ctor)
|
|
1691
2879
|
return;
|
|
2880
|
+
// Go composite literals: `Widget{...}` (same package) and `pkga.Widget{...}`
|
|
2881
|
+
// (cross-package). Only a directly-named struct type is a meaningful
|
|
2882
|
+
// instantiation target — skip slice/map/array literals (`[]T{}`,
|
|
2883
|
+
// `map[K]V{}`) whose `type` field is a composite type, not a named type.
|
|
2884
|
+
// Unlike `new ns.Foo()`, KEEP the package qualifier (`pkga.Widget`) so the
|
|
2885
|
+
// Go cross-package resolver can disambiguate it to the right package's type.
|
|
2886
|
+
if (node.type === 'composite_literal') {
|
|
2887
|
+
if (ctor.type !== 'type_identifier' && ctor.type !== 'qualified_type')
|
|
2888
|
+
return;
|
|
2889
|
+
let goType = (0, tree_sitter_helpers_1.getNodeText)(ctor, this.source).trim();
|
|
2890
|
+
const brIdx = goType.indexOf('['); // strip Go generic args: `Box[T]{}` -> `Box`
|
|
2891
|
+
if (brIdx > 0)
|
|
2892
|
+
goType = goType.slice(0, brIdx).trim();
|
|
2893
|
+
if (goType) {
|
|
2894
|
+
this.unresolvedReferences.push({
|
|
2895
|
+
fromNodeId: fromId,
|
|
2896
|
+
referenceName: goType,
|
|
2897
|
+
referenceKind: 'instantiates',
|
|
2898
|
+
line: node.startPosition.row + 1,
|
|
2899
|
+
column: node.startPosition.column,
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
// Scala: `new Monoid[Int] { ... }` — the constructor is a `generic_type`
|
|
2905
|
+
// (or qualified `stable_type_identifier`) using `[...]` type args, which the
|
|
2906
|
+
// generic `<...>` strip below misses. Unwrap to the base type name.
|
|
2907
|
+
if (node.type === 'instance_expression') {
|
|
2908
|
+
const name = scalaBaseTypeName(ctor, this.source);
|
|
2909
|
+
if (name) {
|
|
2910
|
+
this.unresolvedReferences.push({
|
|
2911
|
+
fromNodeId: fromId,
|
|
2912
|
+
referenceName: name,
|
|
2913
|
+
referenceKind: 'instantiates',
|
|
2914
|
+
line: node.startPosition.row + 1,
|
|
2915
|
+
column: node.startPosition.column,
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
1692
2920
|
let className = (0, tree_sitter_helpers_1.getNodeText)(ctor, this.source);
|
|
1693
2921
|
// Strip type-argument suffix first: `new Map<K, V>()` would
|
|
1694
2922
|
// otherwise produce className 'Map<K, V>' (the constructor
|
|
@@ -1714,6 +2942,75 @@ class TreeSitterExtractor {
|
|
|
1714
2942
|
});
|
|
1715
2943
|
}
|
|
1716
2944
|
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Static-member / value-read pass. A type/enum/class used only via a member
|
|
2947
|
+
* VALUE — `Enum.value`, `Type.CONST`, `Colors.red`, `Foo::BAR` — recorded no
|
|
2948
|
+
* edge, because the body walker only handled CALLS (`Type.method()`). So a
|
|
2949
|
+
* type referenced only by an enum value or a static field looked like nothing
|
|
2950
|
+
* depended on it (the residual frontier across Dart/Java/C#/Swift/Kotlin/PHP).
|
|
2951
|
+
* Emit a `references` edge to the capitalized receiver. Gated to languages
|
|
2952
|
+
* where types are Capitalized by convention, and skipped when the access is a
|
|
2953
|
+
* call's callee (the call extractor already links the method).
|
|
2954
|
+
*/
|
|
2955
|
+
extractStaticMemberRef(node) {
|
|
2956
|
+
if (!STATIC_MEMBER_LANGS.has(this.language))
|
|
2957
|
+
return;
|
|
2958
|
+
if (this.nodeStack.length === 0)
|
|
2959
|
+
return;
|
|
2960
|
+
const ownerId = this.nodeStack[this.nodeStack.length - 1];
|
|
2961
|
+
if (!ownerId)
|
|
2962
|
+
return;
|
|
2963
|
+
// Dart structures member access as an `identifier` + a sibling `selector`,
|
|
2964
|
+
// not a single node. A value-read selector (no `argument_part`) whose
|
|
2965
|
+
// previous sibling is a capitalized identifier is `Enum.value`.
|
|
2966
|
+
if (this.language === 'dart') {
|
|
2967
|
+
if (node.type !== 'selector')
|
|
2968
|
+
return;
|
|
2969
|
+
if (node.namedChildren.some((c) => c.type === 'argument_part'))
|
|
2970
|
+
return;
|
|
2971
|
+
const prev = node.previousNamedSibling;
|
|
2972
|
+
if (prev?.type === 'identifier' && /^[A-Z][A-Za-z0-9_]*$/.test(prev.text)) {
|
|
2973
|
+
this.pushStaticMemberRef(prev.text, ownerId, prev);
|
|
2974
|
+
}
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
if (!MEMBER_ACCESS_TYPES.has(node.type))
|
|
2978
|
+
return;
|
|
2979
|
+
// Skip `Type.method()` — the access is the callee of a call, already linked.
|
|
2980
|
+
const parent = node.parent;
|
|
2981
|
+
if (parent && this.extractor.callTypes.includes(parent.type)) {
|
|
2982
|
+
const callee = (0, tree_sitter_helpers_1.getChildByField)(parent, 'function') ??
|
|
2983
|
+
(0, tree_sitter_helpers_1.getChildByField)(parent, 'method') ??
|
|
2984
|
+
parent.namedChild(0);
|
|
2985
|
+
if (callee && callee.startIndex === node.startIndex)
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
// The receiver must be a SIMPLE capitalized identifier — `Type.X`, not the
|
|
2989
|
+
// nested `a.B.c` (whose own head member-access is visited separately) nor a
|
|
2990
|
+
// lowercase `obj.field` / `pkg.func`.
|
|
2991
|
+
const recv = (0, tree_sitter_helpers_1.getChildByField)(node, 'object') ??
|
|
2992
|
+
(0, tree_sitter_helpers_1.getChildByField)(node, 'expression') ??
|
|
2993
|
+
(0, tree_sitter_helpers_1.getChildByField)(node, 'scope') ??
|
|
2994
|
+
node.namedChild(0);
|
|
2995
|
+
if (!recv)
|
|
2996
|
+
return;
|
|
2997
|
+
const t = recv.type;
|
|
2998
|
+
if (t === 'identifier' || t === 'type_identifier' || t === 'simple_identifier' ||
|
|
2999
|
+
t === 'name' || t === 'scoped_type_identifier') {
|
|
3000
|
+
const text = (0, tree_sitter_helpers_1.getNodeText)(recv, this.source);
|
|
3001
|
+
if (/^[A-Z][A-Za-z0-9_]*$/.test(text))
|
|
3002
|
+
this.pushStaticMemberRef(text, ownerId, recv);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
pushStaticMemberRef(name, ownerId, node) {
|
|
3006
|
+
this.unresolvedReferences.push({
|
|
3007
|
+
fromNodeId: ownerId,
|
|
3008
|
+
referenceName: name,
|
|
3009
|
+
referenceKind: 'references',
|
|
3010
|
+
line: node.startPosition.row + 1,
|
|
3011
|
+
column: node.startPosition.column,
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
1717
3014
|
/**
|
|
1718
3015
|
* Find a `class_body` child of an `object_creation_expression` — the
|
|
1719
3016
|
* marker for an anonymous class (`new T() { ... }`). Returns the body
|
|
@@ -1804,11 +3101,13 @@ class TreeSitterExtractor {
|
|
|
1804
3101
|
if (!n)
|
|
1805
3102
|
return;
|
|
1806
3103
|
// `marker_annotation` is Java's grammar for arg-less annotations
|
|
1807
|
-
// (`@Override`, `@Deprecated`);
|
|
1808
|
-
//
|
|
3104
|
+
// (`@Override`, `@Deprecated`); `attribute` is Swift's grammar for
|
|
3105
|
+
// attributes and PROPERTY WRAPPERS (`@objc`, `@Argument`, `@Published`,
|
|
3106
|
+
// `@State`). Without these, those usages would be silently skipped.
|
|
1809
3107
|
if (n.type !== 'decorator' &&
|
|
1810
3108
|
n.type !== 'annotation' &&
|
|
1811
|
-
n.type !== 'marker_annotation'
|
|
3109
|
+
n.type !== 'marker_annotation' &&
|
|
3110
|
+
n.type !== 'attribute') {
|
|
1812
3111
|
return;
|
|
1813
3112
|
}
|
|
1814
3113
|
// Find the leading identifier: skip the `@` punct, unwrap
|
|
@@ -1828,7 +3127,9 @@ class TreeSitterExtractor {
|
|
|
1828
3127
|
if (child.type === 'identifier' ||
|
|
1829
3128
|
child.type === 'member_expression' ||
|
|
1830
3129
|
child.type === 'scoped_identifier' ||
|
|
1831
|
-
child.type === 'navigation_expression'
|
|
3130
|
+
child.type === 'navigation_expression' ||
|
|
3131
|
+
child.type === 'user_type' || // swift attribute → user_type (`@Argument`)
|
|
3132
|
+
child.type === 'type_identifier') {
|
|
1832
3133
|
target = child;
|
|
1833
3134
|
break;
|
|
1834
3135
|
}
|
|
@@ -1836,9 +3137,13 @@ class TreeSitterExtractor {
|
|
|
1836
3137
|
if (!target)
|
|
1837
3138
|
return;
|
|
1838
3139
|
let name = (0, tree_sitter_helpers_1.getNodeText)(target, this.source);
|
|
3140
|
+
const lt = name.indexOf('<'); // strip generic args: `@Argument<T>` → `Argument`
|
|
3141
|
+
if (lt > 0)
|
|
3142
|
+
name = name.slice(0, lt);
|
|
1839
3143
|
const lastDot = Math.max(name.lastIndexOf('.'), name.lastIndexOf('::'));
|
|
1840
3144
|
if (lastDot >= 0)
|
|
1841
3145
|
name = name.slice(lastDot + 1).replace(/^[:.]/, '');
|
|
3146
|
+
name = name.trim();
|
|
1842
3147
|
if (!name)
|
|
1843
3148
|
return;
|
|
1844
3149
|
this.unresolvedReferences.push({
|
|
@@ -1852,7 +3157,17 @@ class TreeSitterExtractor {
|
|
|
1852
3157
|
// 1. Decorators that are direct children of the declaration
|
|
1853
3158
|
// (method/property style, also some grammars for class).
|
|
1854
3159
|
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
1855
|
-
|
|
3160
|
+
const child = declNode.namedChild(i);
|
|
3161
|
+
consider(child);
|
|
3162
|
+
// Java/Kotlin/C# put annotations INSIDE a `modifiers` node
|
|
3163
|
+
// (`@MyAnno public class X` → class_declaration → modifiers → annotation),
|
|
3164
|
+
// so descend into it — otherwise every annotation usage is silently
|
|
3165
|
+
// dropped and annotation types show zero dependents.
|
|
3166
|
+
if (child && child.type === 'modifiers') {
|
|
3167
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
3168
|
+
consider(child.namedChild(j));
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
1856
3171
|
}
|
|
1857
3172
|
// 2. Decorators that are PRECEDING siblings of the declaration
|
|
1858
3173
|
// inside the parent's children (TypeScript class style).
|
|
@@ -1900,11 +3215,80 @@ class TreeSitterExtractor {
|
|
|
1900
3215
|
* tree-sitter to interpret the namespace block as a function_definition,
|
|
1901
3216
|
* hiding real class/struct/enum nodes inside the "function body".
|
|
1902
3217
|
*/
|
|
3218
|
+
/**
|
|
3219
|
+
* Rocket route-registration macros — `routes![a::b::handler, c::d::other]`
|
|
3220
|
+
* and `catchers![not_found]`. Tree-sitter leaves a macro body as a flat
|
|
3221
|
+
* `token_tree` of raw tokens (`identifier`, `::`, `,`), so the handler paths
|
|
3222
|
+
* are never seen as references and each handler fn looks like it has no caller
|
|
3223
|
+
* — it's mounted by Rocket at runtime, not called by in-repo code, so its file
|
|
3224
|
+
* shows 0 dependents. Walk the token tree, reconstruct each comma-separated
|
|
3225
|
+
* path, and emit a `references` edge; the Rust path resolver
|
|
3226
|
+
* (`resolveRustPathReference`) then links it to the handler fn. The handler
|
|
3227
|
+
* names are explicit in source, so this is precise static extraction, not a
|
|
3228
|
+
* heuristic — no false edges (resolution still validates each path).
|
|
3229
|
+
*/
|
|
3230
|
+
extractRustRouteMacro(node) {
|
|
3231
|
+
if (this.language !== 'rust')
|
|
3232
|
+
return;
|
|
3233
|
+
const macroName = node.namedChild(0);
|
|
3234
|
+
if (!macroName)
|
|
3235
|
+
return;
|
|
3236
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(macroName, this.source);
|
|
3237
|
+
if (name !== 'routes' && name !== 'catchers')
|
|
3238
|
+
return;
|
|
3239
|
+
const tokenTree = node.namedChildren.find((c) => c.type === 'token_tree');
|
|
3240
|
+
if (!tokenTree)
|
|
3241
|
+
return;
|
|
3242
|
+
const fromId = this.nodeStack[this.nodeStack.length - 1];
|
|
3243
|
+
if (!fromId)
|
|
3244
|
+
return;
|
|
3245
|
+
// The token tree is a flat stream: `[ id :: id :: id , id … ]`. Group runs
|
|
3246
|
+
// of `identifier` tokens (the `::` joiners are anonymous) into one path; a
|
|
3247
|
+
// `,` (or the closing `]`) ends a path.
|
|
3248
|
+
let parts = [];
|
|
3249
|
+
let line = 0;
|
|
3250
|
+
let column = 0;
|
|
3251
|
+
const flush = () => {
|
|
3252
|
+
if (parts.length > 0) {
|
|
3253
|
+
this.unresolvedReferences.push({
|
|
3254
|
+
fromNodeId: fromId,
|
|
3255
|
+
referenceName: parts.join('::'),
|
|
3256
|
+
referenceKind: 'references',
|
|
3257
|
+
line,
|
|
3258
|
+
column,
|
|
3259
|
+
});
|
|
3260
|
+
parts = [];
|
|
3261
|
+
}
|
|
3262
|
+
};
|
|
3263
|
+
for (let i = 0; i < tokenTree.childCount; i++) {
|
|
3264
|
+
const t = tokenTree.child(i);
|
|
3265
|
+
if (!t)
|
|
3266
|
+
continue;
|
|
3267
|
+
if (t.type === 'identifier') {
|
|
3268
|
+
if (parts.length === 0) {
|
|
3269
|
+
line = t.startPosition.row + 1;
|
|
3270
|
+
column = t.startPosition.column;
|
|
3271
|
+
}
|
|
3272
|
+
parts.push((0, tree_sitter_helpers_1.getNodeText)(t, this.source));
|
|
3273
|
+
}
|
|
3274
|
+
else if (t.type === ',') {
|
|
3275
|
+
flush();
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
flush();
|
|
3279
|
+
}
|
|
1903
3280
|
visitFunctionBody(body, _functionId) {
|
|
1904
3281
|
if (!this.extractor)
|
|
1905
3282
|
return;
|
|
1906
3283
|
const visitForCallsAndStructure = (node) => {
|
|
1907
3284
|
const nodeType = node.type;
|
|
3285
|
+
// Function-as-value capture (#756) — function bodies are walked here,
|
|
3286
|
+
// not in visitNode, so the capture hook must fire in both walkers.
|
|
3287
|
+
this.maybeCaptureFnRefs(node, nodeType);
|
|
3288
|
+
// Rocket route-registration macros (`routes![…]` / `catchers![…]`): the
|
|
3289
|
+
// handler paths live in a raw token tree the call walker can't see.
|
|
3290
|
+
if (nodeType === 'macro_invocation')
|
|
3291
|
+
this.extractRustRouteMacro(node);
|
|
1908
3292
|
if (this.extractor.callTypes.includes(nodeType)) {
|
|
1909
3293
|
this.extractCall(node);
|
|
1910
3294
|
}
|
|
@@ -1938,6 +3322,24 @@ class TreeSitterExtractor {
|
|
|
1938
3322
|
}
|
|
1939
3323
|
}
|
|
1940
3324
|
}
|
|
3325
|
+
// Static-member / value-read: `Enum.value`, `Type.CONST`, `Foo::BAR`.
|
|
3326
|
+
this.extractStaticMemberRef(node);
|
|
3327
|
+
// Local variable type annotations inside a body — `const items: Foo[] = []`,
|
|
3328
|
+
// `const x: SomeType = svc.load()`. We deliberately do NOT create nodes for
|
|
3329
|
+
// locals (that would explode the graph — the data-flow frontier we leave
|
|
3330
|
+
// uncovered), but the TYPE a local is annotated with is a real dependency of
|
|
3331
|
+
// the enclosing function, so attribute a `references` edge to it. Without
|
|
3332
|
+
// this, a function that uses a type ONLY in its body (very common — e.g. a
|
|
3333
|
+
// resolver building `const nodes: Node[] = []`) produced no edge to that
|
|
3334
|
+
// type, so impact / `affected` missed the dependency entirely. We fall
|
|
3335
|
+
// through to the default recursion below so the initializer's calls (and any
|
|
3336
|
+
// nested declarators) are still walked.
|
|
3337
|
+
if (nodeType === 'variable_declarator' &&
|
|
3338
|
+
this.TYPE_ANNOTATION_LANGUAGES.has(this.language)) {
|
|
3339
|
+
const ownerId = this.nodeStack[this.nodeStack.length - 1];
|
|
3340
|
+
if (ownerId)
|
|
3341
|
+
this.extractVariableTypeAnnotation(node, ownerId);
|
|
3342
|
+
}
|
|
1941
3343
|
// Nested NAMED functions inside a body — function declarations and named
|
|
1942
3344
|
// function expressions like `.on('mount', function onmount(){})` — become
|
|
1943
3345
|
// their own nodes so the graph can link to them (callback handlers, local
|
|
@@ -2039,6 +3441,62 @@ class TreeSitterExtractor {
|
|
|
2039
3441
|
child.type === 'base_clause' || // PHP class extends
|
|
2040
3442
|
child.type === 'extends_interfaces' // Java interface extends
|
|
2041
3443
|
) {
|
|
3444
|
+
// Scala: `extends A[X] with B with C` packs EVERY supertype into the
|
|
3445
|
+
// one extends_clause (separated by `with`), each a `generic_type` /
|
|
3446
|
+
// `type_identifier` / `stable_type_identifier`. The generic path below
|
|
3447
|
+
// takes only namedChild(0) and keeps the full text (`A[X]`), so a
|
|
3448
|
+
// parameterized supertype — every typeclass in cats/algebra — never
|
|
3449
|
+
// matched and `with`-mixed traits past the first were dropped. Iterate
|
|
3450
|
+
// all supertypes and unwrap each to its base type name.
|
|
3451
|
+
if (this.language === 'scala') {
|
|
3452
|
+
for (const target of child.namedChildren) {
|
|
3453
|
+
const name = scalaBaseTypeName(target, this.source);
|
|
3454
|
+
if (name) {
|
|
3455
|
+
this.unresolvedReferences.push({
|
|
3456
|
+
fromNodeId: classId,
|
|
3457
|
+
referenceName: name,
|
|
3458
|
+
referenceKind: 'extends',
|
|
3459
|
+
line: target.startPosition.row + 1,
|
|
3460
|
+
column: target.startPosition.column,
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
continue;
|
|
3465
|
+
}
|
|
3466
|
+
// Dart: `class C extends Base with M1, M2` — the `superclass` node holds
|
|
3467
|
+
// the extends type as a direct `type_identifier` AND a `mixins` child
|
|
3468
|
+
// listing the `with` mixins (and `class C with M` has ONLY mixins, no
|
|
3469
|
+
// extends type). The generic `namedChild(0)` path would read the
|
|
3470
|
+
// `mixins` node itself as the superclass and drop every mixin — yet
|
|
3471
|
+
// mixins are Dart's core composition mechanism (Flutter is built on
|
|
3472
|
+
// them). Emit `extends` for the base and `implements` for each mixin.
|
|
3473
|
+
if (this.language === 'dart' && child.type === 'superclass') {
|
|
3474
|
+
for (const t of child.namedChildren) {
|
|
3475
|
+
if (t.type === 'mixins') {
|
|
3476
|
+
for (const m of t.namedChildren) {
|
|
3477
|
+
if (m.type === 'type_identifier') {
|
|
3478
|
+
this.unresolvedReferences.push({
|
|
3479
|
+
fromNodeId: classId,
|
|
3480
|
+
referenceName: (0, tree_sitter_helpers_1.getNodeText)(m, this.source),
|
|
3481
|
+
referenceKind: 'implements',
|
|
3482
|
+
line: m.startPosition.row + 1,
|
|
3483
|
+
column: m.startPosition.column,
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
else if (t.type === 'type_identifier') {
|
|
3489
|
+
this.unresolvedReferences.push({
|
|
3490
|
+
fromNodeId: classId,
|
|
3491
|
+
referenceName: (0, tree_sitter_helpers_1.getNodeText)(t, this.source),
|
|
3492
|
+
referenceKind: 'extends',
|
|
3493
|
+
line: t.startPosition.row + 1,
|
|
3494
|
+
column: t.startPosition.column,
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
2042
3500
|
// Extract parent class/interface names
|
|
2043
3501
|
// Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier
|
|
2044
3502
|
const typeList = child.namedChildren.find((c) => c.type === 'type_list');
|
|
@@ -2318,7 +3776,15 @@ class TreeSitterExtractor {
|
|
|
2318
3776
|
* Languages that support type annotations (TypeScript, etc.)
|
|
2319
3777
|
*/
|
|
2320
3778
|
TYPE_ANNOTATION_LANGUAGES = new Set([
|
|
2321
|
-
'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp',
|
|
3779
|
+
'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp', 'scala', 'php',
|
|
3780
|
+
]);
|
|
3781
|
+
/**
|
|
3782
|
+
* PHP pseudo-types and `self`/`static`/`parent` that aren't project symbols.
|
|
3783
|
+
* (Scalar primitives parse as `primitive_type` and are skipped structurally.)
|
|
3784
|
+
*/
|
|
3785
|
+
PHP_PSEUDO_TYPES = new Set([
|
|
3786
|
+
'self', 'static', 'parent', 'mixed', 'object', 'iterable', 'callable', 'void',
|
|
3787
|
+
'null', 'false', 'true', 'never', 'array', 'int', 'float', 'string', 'bool',
|
|
2322
3788
|
]);
|
|
2323
3789
|
/**
|
|
2324
3790
|
* Built-in/primitive type names that shouldn't create references
|
|
@@ -2334,6 +3800,9 @@ class TreeSitterExtractor {
|
|
|
2334
3800
|
// Go
|
|
2335
3801
|
'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64',
|
|
2336
3802
|
'float32', 'float64', 'complex64', 'complex128', 'rune', 'error',
|
|
3803
|
+
// Scala (capitalized primitives + ubiquitous stdlib aliases)
|
|
3804
|
+
'Int', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Boolean', 'Char', 'Unit',
|
|
3805
|
+
'String', 'Any', 'AnyRef', 'AnyVal', 'Nothing', 'Null',
|
|
2337
3806
|
]);
|
|
2338
3807
|
/**
|
|
2339
3808
|
* Extract type references from type annotations on a function/method/field node.
|
|
@@ -2354,16 +3823,67 @@ class TreeSitterExtractor {
|
|
|
2354
3823
|
this.extractCsharpTypeRefs(node, nodeId);
|
|
2355
3824
|
return;
|
|
2356
3825
|
}
|
|
2357
|
-
//
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
3826
|
+
// PHP type-hints are `named_type`/`optional_type`/`union_type` wrapping a
|
|
3827
|
+
// `name`/`qualified_name` — never `type_identifier` — so the generic walker
|
|
3828
|
+
// below emits nothing for them. Dispatch to a PHP-aware path that walks only
|
|
3829
|
+
// type positions (parameter / return / property types), so type-hinted
|
|
3830
|
+
// dependencies (the constructor-injected contracts that dominate Laravel) are
|
|
3831
|
+
// recorded and a `variable_name` like `$events` never mis-emits as a ref.
|
|
3832
|
+
if (this.language === 'php') {
|
|
3833
|
+
this.extractPhpTypeRefs(node, nodeId);
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
// Dart: a `method_signature` wraps the real `function_signature` (where the
|
|
3837
|
+
// params and return type live), and the return type is a bare
|
|
3838
|
+
// `type_identifier` child, not a `type` field — so getChildByField below
|
|
3839
|
+
// finds neither. Walk the inner signature: param names / the method name are
|
|
3840
|
+
// `identifier` (not `type_identifier`), so only types surface.
|
|
3841
|
+
if (this.language === 'dart') {
|
|
3842
|
+
let sig = node;
|
|
3843
|
+
if (node.type === 'method_signature') {
|
|
3844
|
+
sig = node.namedChildren.find((c) => c.type === 'function_signature' ||
|
|
3845
|
+
c.type === 'getter_signature' ||
|
|
3846
|
+
c.type === 'setter_signature' ||
|
|
3847
|
+
c.type === 'constructor_signature' ||
|
|
3848
|
+
c.type === 'factory_constructor_signature') ?? node;
|
|
3849
|
+
}
|
|
3850
|
+
this.extractTypeRefsFromSubtree(sig, nodeId);
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
// Extract parameter type annotations. Scala curries — `def f(a)(implicit
|
|
3854
|
+
// M: TC)` has MULTIPLE `parameters` siblings, and the typeclass is almost
|
|
3855
|
+
// always in the trailing implicit list — so walk every parameter list, not
|
|
3856
|
+
// just getChildByField's first match.
|
|
3857
|
+
if (this.language === 'scala') {
|
|
3858
|
+
for (const pc of node.namedChildren) {
|
|
3859
|
+
if (pc.type === 'parameters')
|
|
3860
|
+
this.extractTypeRefsFromSubtree(pc, nodeId);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
else {
|
|
3864
|
+
const params = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.paramsField || 'parameters');
|
|
3865
|
+
if (params) {
|
|
3866
|
+
this.extractTypeRefsFromSubtree(params, nodeId);
|
|
3867
|
+
}
|
|
2361
3868
|
}
|
|
2362
3869
|
// Extract return type annotation
|
|
2363
3870
|
const returnType = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.returnField || 'return_type');
|
|
2364
3871
|
if (returnType) {
|
|
2365
3872
|
this.extractTypeRefsFromSubtree(returnType, nodeId);
|
|
2366
3873
|
}
|
|
3874
|
+
// Scala context bounds / type-parameter bounds: `def f[A: Monoid]`,
|
|
3875
|
+
// `[F[_]: Monad]`, `[A <: Foo]` carry the bound type inside `type_parameters`.
|
|
3876
|
+
// This is THE pervasive way a typeclass is required in Scala, yet the bound
|
|
3877
|
+
// never appears in the value parameters. Param NAMES are `identifier` (not
|
|
3878
|
+
// `type_identifier`), so only the bound types surface. Scala-only: in other
|
|
3879
|
+
// languages a `type_parameters` child holds declaration names as
|
|
3880
|
+
// `type_identifier` (TS `<T>`), which would wrongly surface as refs.
|
|
3881
|
+
if (this.language === 'scala') {
|
|
3882
|
+
const typeParams = node.namedChildren.find((c) => c.type === 'type_parameters');
|
|
3883
|
+
if (typeParams) {
|
|
3884
|
+
this.extractTypeRefsFromSubtree(typeParams, nodeId);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
2367
3887
|
// Extract direct type annotation (for class fields like `model: ITextModel`)
|
|
2368
3888
|
const typeAnnotation = node.namedChildren.find((c) => c.type === 'type_annotation');
|
|
2369
3889
|
if (typeAnnotation) {
|
|
@@ -2383,8 +3903,11 @@ class TreeSitterExtractor {
|
|
|
2383
3903
|
* `tuple_type`, …) — none of which are `type_identifier`. Closes #381.
|
|
2384
3904
|
*/
|
|
2385
3905
|
extractCsharpTypeRefs(node, nodeId) {
|
|
2386
|
-
//
|
|
2387
|
-
|
|
3906
|
+
// A property's type is under the `type` field; a method/constructor's RETURN
|
|
3907
|
+
// type is under `returns` (tree-sitter-c-sharp 0.23.x — older builds used
|
|
3908
|
+
// `type` for both). A node carries only one of the two, so checking both
|
|
3909
|
+
// covers return types and property types without conflating them.
|
|
3910
|
+
const directType = (0, tree_sitter_helpers_1.getChildByField)(node, 'type') ?? (0, tree_sitter_helpers_1.getChildByField)(node, 'returns');
|
|
2388
3911
|
if (directType)
|
|
2389
3912
|
this.walkCsharpTypePosition(directType, nodeId);
|
|
2390
3913
|
// Field declarations wrap declarators in a `variable_declaration`
|
|
@@ -2414,6 +3937,32 @@ class TreeSitterExtractor {
|
|
|
2414
3937
|
}
|
|
2415
3938
|
}
|
|
2416
3939
|
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Record the dependencies declared by a C# PRIMARY CONSTRUCTOR
|
|
3942
|
+
* (`class Svc(IRepo repo, [FromKeyedServices("k")] ICache cache) { … }`,
|
|
3943
|
+
* C# 12+). The parameter list hangs off the class/struct/record declaration
|
|
3944
|
+
* as an unnamed-field `parameter_list` child (not the `parameters` field a
|
|
3945
|
+
* method uses), so it's found by node type. Each parameter's declared type
|
|
3946
|
+
* becomes a `references` edge from the owning type — these are exactly the
|
|
3947
|
+
* services a DI-registered type depends on, so impact/blast-radius and
|
|
3948
|
+
* "who depends on this contract" now see them. No-op when there's no primary
|
|
3949
|
+
* constructor. (#237)
|
|
3950
|
+
*/
|
|
3951
|
+
extractCsharpPrimaryCtorParamRefs(node, ownerId) {
|
|
3952
|
+
if (this.language !== 'csharp')
|
|
3953
|
+
return;
|
|
3954
|
+
const paramList = node.namedChildren.find((c) => c.type === 'parameter_list');
|
|
3955
|
+
if (!paramList)
|
|
3956
|
+
return;
|
|
3957
|
+
for (let i = 0; i < paramList.namedChildCount; i++) {
|
|
3958
|
+
const param = paramList.namedChild(i);
|
|
3959
|
+
if (!param || param.type !== 'parameter')
|
|
3960
|
+
continue;
|
|
3961
|
+
const paramType = (0, tree_sitter_helpers_1.getChildByField)(param, 'type');
|
|
3962
|
+
if (paramType)
|
|
3963
|
+
this.walkCsharpTypePosition(paramType, ownerId);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
2417
3966
|
/**
|
|
2418
3967
|
* Walk a C# subtree that is KNOWN to be in a type position
|
|
2419
3968
|
* (return type, parameter type, property type, field type, generic
|
|
@@ -2476,6 +4025,65 @@ class TreeSitterExtractor {
|
|
|
2476
4025
|
this.walkCsharpTypePosition(child, fromNodeId);
|
|
2477
4026
|
}
|
|
2478
4027
|
}
|
|
4028
|
+
/**
|
|
4029
|
+
* Extract PHP type references from a method/function/property declaration.
|
|
4030
|
+
* Walks ONLY type positions: each parameter's type child (inside
|
|
4031
|
+
* `formal_parameters`), the return type, and a property's type — all
|
|
4032
|
+
* `named_type` / `optional_type` / `union_type` / … direct children. Parameter
|
|
4033
|
+
* and property NAMES are `variable_name` (`$x`), never type nodes, so they
|
|
4034
|
+
* can't be mis-emitted.
|
|
4035
|
+
*/
|
|
4036
|
+
extractPhpTypeRefs(node, nodeId) {
|
|
4037
|
+
const params = node.namedChildren.find((c) => c.type === 'formal_parameters');
|
|
4038
|
+
if (params) {
|
|
4039
|
+
for (const p of params.namedChildren) {
|
|
4040
|
+
// simple_parameter / property_promotion_parameter / variadic_parameter
|
|
4041
|
+
for (const c of p.namedChildren) {
|
|
4042
|
+
if (PHP_TYPE_NODES.has(c.type))
|
|
4043
|
+
this.walkPhpTypePosition(c, nodeId);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
// Return type (method/function) and property type are TYPE nodes that are
|
|
4048
|
+
// DIRECT children of the declaration.
|
|
4049
|
+
for (const c of node.namedChildren) {
|
|
4050
|
+
if (PHP_TYPE_NODES.has(c.type))
|
|
4051
|
+
this.walkPhpTypePosition(c, nodeId);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
/** Walk a PHP subtree KNOWN to be in a type position; emit class/interface refs. */
|
|
4055
|
+
walkPhpTypePosition(node, fromNodeId) {
|
|
4056
|
+
if (node.type === 'primitive_type')
|
|
4057
|
+
return; // int/string/void/…
|
|
4058
|
+
if (node.type === 'name') {
|
|
4059
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(node, this.source);
|
|
4060
|
+
if (name && !this.PHP_PSEUDO_TYPES.has(name)) {
|
|
4061
|
+
this.unresolvedReferences.push({
|
|
4062
|
+
fromNodeId, referenceName: name, referenceKind: 'references',
|
|
4063
|
+
line: node.startPosition.row + 1, column: node.startPosition.column,
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
if (node.type === 'qualified_name') {
|
|
4069
|
+
// `App\Contracts\Logger` → match on the trailing simple name (what the
|
|
4070
|
+
// class node is stored as, and what a `use` import brings into scope).
|
|
4071
|
+
const last = (0, tree_sitter_helpers_1.getNodeText)(node, this.source).split('\\').pop() ?? '';
|
|
4072
|
+
if (last && !this.PHP_PSEUDO_TYPES.has(last)) {
|
|
4073
|
+
this.unresolvedReferences.push({
|
|
4074
|
+
fromNodeId, referenceName: last, referenceKind: 'references',
|
|
4075
|
+
line: node.startPosition.row + 1, column: node.startPosition.column,
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
// optional_type / nullable_type / union_type / intersection_type / named_type → recurse
|
|
4081
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4082
|
+
const child = node.namedChild(i);
|
|
4083
|
+
if (child)
|
|
4084
|
+
this.walkPhpTypePosition(child, fromNodeId);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
2479
4087
|
/**
|
|
2480
4088
|
* Extract type references from a variable's type annotation.
|
|
2481
4089
|
*/
|
|
@@ -2807,9 +4415,29 @@ class TreeSitterExtractor {
|
|
|
2807
4415
|
}
|
|
2808
4416
|
}
|
|
2809
4417
|
}
|
|
2810
|
-
|
|
2811
|
-
this.methodIndex.get(shortNameKey)
|
|
2812
|
-
|
|
4418
|
+
let parentId = this.methodIndex.get(fullNameKey) ||
|
|
4419
|
+
this.methodIndex.get(shortNameKey);
|
|
4420
|
+
// No existing node? This is an implementation-only **free** procedure/function
|
|
4421
|
+
// (`procedure Helper; begin … end;` with no interface declaration and not a
|
|
4422
|
+
// class method). Create a function node so its body's calls attribute to it,
|
|
4423
|
+
// not to the enclosing file/module. A method (`TClass.Method`, a dotted name)
|
|
4424
|
+
// always has a node from its class declaration, so this only fires for free
|
|
4425
|
+
// routines — and the methodIndex lookup above already covers interface-declared
|
|
4426
|
+
// free routines, so there's no duplicate.
|
|
4427
|
+
if (!parentId && !fullName.includes('.')) {
|
|
4428
|
+
const fnNode = this.createNode('function', fullName, declProc, {
|
|
4429
|
+
signature: this.extractor?.getSignature?.(declProc, this.source),
|
|
4430
|
+
visibility: this.extractor?.getVisibility?.(declProc),
|
|
4431
|
+
});
|
|
4432
|
+
if (fnNode) {
|
|
4433
|
+
parentId = fnNode.id;
|
|
4434
|
+
this.methodIndex.set(fullNameKey, fnNode.id);
|
|
4435
|
+
if (!this.methodIndex.has(shortNameKey))
|
|
4436
|
+
this.methodIndex.set(shortNameKey, fnNode.id);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
if (!parentId)
|
|
4440
|
+
parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
2813
4441
|
if (!parentId)
|
|
2814
4442
|
return;
|
|
2815
4443
|
// Visit the block for calls
|
|
@@ -2835,10 +4463,41 @@ class TreeSitterExtractor {
|
|
|
2835
4463
|
return;
|
|
2836
4464
|
let calleeName = '';
|
|
2837
4465
|
if (firstChild.type === 'exprDot') {
|
|
2838
|
-
//
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
4466
|
+
// Chained static-factory call: `TFoo.GetInstance().DoIt()` — the exprDot's
|
|
4467
|
+
// receiver is itself an `exprCall`, so the bare identifier list would
|
|
4468
|
+
// collapse to just `DoIt` and mis-resolve to a same-named method on an
|
|
4469
|
+
// unrelated class. Encode `TFoo.GetInstance().DoIt` so resolution infers
|
|
4470
|
+
// DoIt's class from what `TFoo.GetInstance` RETURNS (#645/#608). Only a
|
|
4471
|
+
// capitalized class-factory chain; a unary outer method.
|
|
4472
|
+
const innerCall = firstChild.namedChildren.find((c) => c.type === 'exprCall');
|
|
4473
|
+
const outerId = firstChild.namedChildren.filter((c) => c.type === 'identifier').pop();
|
|
4474
|
+
const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
|
|
4475
|
+
if (innerCall && method && /^\w+$/.test(method)) {
|
|
4476
|
+
const innerFirst = innerCall.namedChild(0);
|
|
4477
|
+
let innerCallee = '';
|
|
4478
|
+
if (innerFirst?.type === 'exprDot') {
|
|
4479
|
+
innerCallee = innerFirst.namedChildren
|
|
4480
|
+
.filter((c) => c.type === 'identifier')
|
|
4481
|
+
.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
|
|
4482
|
+
.join('.');
|
|
4483
|
+
}
|
|
4484
|
+
else if (innerFirst?.type === 'identifier') {
|
|
4485
|
+
innerCallee = (0, tree_sitter_helpers_1.getNodeText)(innerFirst, this.source);
|
|
4486
|
+
}
|
|
4487
|
+
// Gate on the Delphi type-naming convention — `TFoo` classes / `IFoo`
|
|
4488
|
+
// interfaces — so a class-factory chain re-encodes but a capitalized
|
|
4489
|
+
// VARIABLE/parameter chain (Pascal capitalizes locals too: `Curve.X().Y()`,
|
|
4490
|
+
// `Self.X().Y()`) stays bare and keeps its existing bare-name resolution.
|
|
4491
|
+
calleeName = innerCallee && /^[TI][A-Z]/.test(innerCallee)
|
|
4492
|
+
? `${innerCallee}().${method}`
|
|
4493
|
+
: method;
|
|
4494
|
+
}
|
|
4495
|
+
else {
|
|
4496
|
+
// Qualified call: Obj.Method(...)
|
|
4497
|
+
const identifiers = firstChild.namedChildren.filter((c) => c.type === 'identifier');
|
|
4498
|
+
if (identifiers.length > 0) {
|
|
4499
|
+
calleeName = identifiers.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source)).join('.');
|
|
4500
|
+
}
|
|
2842
4501
|
}
|
|
2843
4502
|
}
|
|
2844
4503
|
else if (firstChild.type === 'identifier') {
|
|
@@ -2859,6 +4518,72 @@ class TreeSitterExtractor {
|
|
|
2859
4518
|
this.visitPascalBlock(args);
|
|
2860
4519
|
}
|
|
2861
4520
|
}
|
|
4521
|
+
/**
|
|
4522
|
+
* Extract a PAREN-LESS Pascal method/procedure call (`Obj.Method;`,
|
|
4523
|
+
* `TFoo.GetInstance.DoIt;`). Pascal lets a no-arg method drop its parens, so it
|
|
4524
|
+
* parses as a bare `exprDot` (not an `exprCall`). A bare `exprDot` is
|
|
4525
|
+
* syntactically identical to a field/property access, so this is only ever
|
|
4526
|
+
* called for a STATEMENT-level exprDot (caller-gated): a bare `Obj.Field;`
|
|
4527
|
+
* statement is a no-op, so a statement-level dot expression is a call. (An
|
|
4528
|
+
* exprDot in assignment LHS/RHS or a condition is left alone — there it really
|
|
4529
|
+
* can be a field/property read.)
|
|
4530
|
+
*/
|
|
4531
|
+
extractPascalParenlessCall(node) {
|
|
4532
|
+
if (this.nodeStack.length === 0)
|
|
4533
|
+
return;
|
|
4534
|
+
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
|
4535
|
+
if (!callerId)
|
|
4536
|
+
return;
|
|
4537
|
+
const receiver = node.namedChild(0);
|
|
4538
|
+
const outerId = node.namedChildren.filter((c) => c.type === 'identifier').pop();
|
|
4539
|
+
const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
|
|
4540
|
+
if (!method)
|
|
4541
|
+
return;
|
|
4542
|
+
let calleeName = '';
|
|
4543
|
+
// Chained: the receiver is itself a call — a paren-less `TFoo.GetInstance` (an
|
|
4544
|
+
// inner exprDot) or a paren'd `TFoo.GetInstance()` (an exprCall). Encode the
|
|
4545
|
+
// chain `TFoo.GetInstance().DoIt` so resolution infers DoIt's class from what
|
|
4546
|
+
// the factory RETURNS (#645/#608), gated on the Delphi `TFoo`/`IFoo` type
|
|
4547
|
+
// convention; a capitalized VARIABLE chain stays a bare method name.
|
|
4548
|
+
if ((receiver?.type === 'exprDot' || receiver?.type === 'exprCall') && /^\w+$/.test(method)) {
|
|
4549
|
+
const innerCalleeNode = receiver.type === 'exprCall' ? receiver.namedChild(0) : receiver;
|
|
4550
|
+
const innerCallee = !innerCalleeNode
|
|
4551
|
+
? ''
|
|
4552
|
+
: innerCalleeNode.type === 'identifier'
|
|
4553
|
+
? (0, tree_sitter_helpers_1.getNodeText)(innerCalleeNode, this.source)
|
|
4554
|
+
: innerCalleeNode.namedChildren
|
|
4555
|
+
.filter((c) => c.type === 'identifier')
|
|
4556
|
+
.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
|
|
4557
|
+
.join('.');
|
|
4558
|
+
if (innerCallee && /^[TI][A-Z]/.test(innerCallee)) {
|
|
4559
|
+
calleeName = `${innerCallee}().${method}`;
|
|
4560
|
+
// The T/I-prefixed inner is itself a real call — record it too.
|
|
4561
|
+
if (receiver.type === 'exprCall')
|
|
4562
|
+
this.extractPascalCall(receiver);
|
|
4563
|
+
else
|
|
4564
|
+
this.extractPascalParenlessCall(receiver);
|
|
4565
|
+
}
|
|
4566
|
+
else {
|
|
4567
|
+
calleeName = method; // non-class receiver: a bare method ref (no field-access ref)
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
else {
|
|
4571
|
+
// Simple: `Obj.Method` → the dotted name (resolves via the receiver / bare name).
|
|
4572
|
+
calleeName = node.namedChildren
|
|
4573
|
+
.filter((c) => c.type === 'identifier')
|
|
4574
|
+
.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
|
|
4575
|
+
.join('.');
|
|
4576
|
+
}
|
|
4577
|
+
if (calleeName) {
|
|
4578
|
+
this.unresolvedReferences.push({
|
|
4579
|
+
fromNodeId: callerId,
|
|
4580
|
+
referenceName: calleeName,
|
|
4581
|
+
referenceKind: 'calls',
|
|
4582
|
+
line: node.startPosition.row + 1,
|
|
4583
|
+
column: node.startPosition.column,
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
2862
4587
|
/**
|
|
2863
4588
|
* Recursively visit a Pascal block/statement tree for call expressions
|
|
2864
4589
|
*/
|
|
@@ -2867,15 +4592,32 @@ class TreeSitterExtractor {
|
|
|
2867
4592
|
const child = node.namedChild(i);
|
|
2868
4593
|
if (!child)
|
|
2869
4594
|
continue;
|
|
4595
|
+
// Function-as-value capture (#756): Pascal bodies are walked here, not
|
|
4596
|
+
// in visitNode/visitForCallsAndStructure, so the capture hook fires here
|
|
4597
|
+
// — assignment RHS is the Delphi event-wiring idiom (`OnFire := Handler`).
|
|
4598
|
+
this.maybeCaptureFnRefs(child, child.type);
|
|
2870
4599
|
if (child.type === 'exprCall') {
|
|
2871
4600
|
this.extractPascalCall(child);
|
|
4601
|
+
// The walker doesn't descend into a call's arguments — dispatch the
|
|
4602
|
+
// argument container directly (`RegisterHandler(TargetCb)` / `(@Cb)`).
|
|
4603
|
+
const args = child.namedChildren.find((c) => c.type === 'exprArgs');
|
|
4604
|
+
if (args)
|
|
4605
|
+
this.maybeCaptureFnRefs(args, 'exprArgs');
|
|
2872
4606
|
}
|
|
2873
4607
|
else if (child.type === 'exprDot') {
|
|
2874
|
-
//
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
4608
|
+
// A STATEMENT-level bare exprDot is a paren-less call (`Obj.Free;`,
|
|
4609
|
+
// `TFoo.GetInstance.DoIt;`). Anywhere else (assignment side, condition,
|
|
4610
|
+
// expression) a bare exprDot is ambiguous with a field/property access,
|
|
4611
|
+
// so there we only descend for paren'd inner calls.
|
|
4612
|
+
if (node.type === 'statement') {
|
|
4613
|
+
this.extractPascalParenlessCall(child);
|
|
4614
|
+
}
|
|
4615
|
+
else {
|
|
4616
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
4617
|
+
const grandchild = child.namedChild(j);
|
|
4618
|
+
if (grandchild?.type === 'exprCall') {
|
|
4619
|
+
this.extractPascalCall(grandchild);
|
|
4620
|
+
}
|
|
2879
4621
|
}
|
|
2880
4622
|
}
|
|
2881
4623
|
}
|
|
@@ -2907,11 +4649,21 @@ function extractFromSource(filePath, source, language, frameworkNames) {
|
|
|
2907
4649
|
const extractor = new vue_extractor_1.VueExtractor(filePath, source);
|
|
2908
4650
|
result = extractor.extract();
|
|
2909
4651
|
}
|
|
4652
|
+
else if (detectedLanguage === 'astro') {
|
|
4653
|
+
// Use custom extractor for Astro (frontmatter + template delegation)
|
|
4654
|
+
const extractor = new astro_extractor_1.AstroExtractor(filePath, source);
|
|
4655
|
+
result = extractor.extract();
|
|
4656
|
+
}
|
|
2910
4657
|
else if (detectedLanguage === 'liquid') {
|
|
2911
4658
|
// Use custom extractor for Liquid
|
|
2912
4659
|
const extractor = new liquid_extractor_1.LiquidExtractor(filePath, source);
|
|
2913
4660
|
result = extractor.extract();
|
|
2914
4661
|
}
|
|
4662
|
+
else if (detectedLanguage === 'razor') {
|
|
4663
|
+
// Use custom extractor for ASP.NET Razor (.cshtml) / Blazor (.razor) markup
|
|
4664
|
+
const extractor = new razor_extractor_1.RazorExtractor(filePath, source);
|
|
4665
|
+
result = extractor.extract();
|
|
4666
|
+
}
|
|
2915
4667
|
else if (detectedLanguage === 'xml') {
|
|
2916
4668
|
// Custom extractor for MyBatis mapper XML. Non-mapper XML returns just a
|
|
2917
4669
|
// file node so the watcher tracks it without emitting symbols.
|