graph-indexer 2.1.0 → 3.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/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +31 -105
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
package/parse/metadata.mjs
DELETED
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file parse/metadata.mjs
|
|
3
|
-
* @description Chunk metadata extractors: params, return type, class context,
|
|
4
|
-
* decorators, heritage (extends/implements), type annotations,
|
|
5
|
-
* call sites, and the legacy call list.
|
|
6
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
7
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
8
|
-
* @license MIT
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// ─── Enrichment helpers (param names, return type, class context) ─────────────
|
|
12
|
-
|
|
13
|
-
export function extractParams(chunkNode, ext) {
|
|
14
|
-
const params = [];
|
|
15
|
-
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
16
|
-
const paramTypes = JS_LIKE.includes(ext)
|
|
17
|
-
? ['required_parameter', 'optional_parameter', 'formal_parameters', 'identifier']
|
|
18
|
-
: ['parameter', 'formal_parameter', 'identifier'];
|
|
19
|
-
|
|
20
|
-
function walkParams(node) {
|
|
21
|
-
if (node.type === 'formal_parameters' || node.type === 'parameters' || node.type === 'parameter_list') {
|
|
22
|
-
for (const child of node.children) {
|
|
23
|
-
if (child.type === 'required_parameter' || child.type === 'optional_parameter' || child.type === 'formal_parameter') {
|
|
24
|
-
const id = child.childForFieldName?.('pattern') || child.childForFieldName?.('name') ||
|
|
25
|
-
child.children.find(c => c.type === 'identifier');
|
|
26
|
-
if (id) params.push(id.text);
|
|
27
|
-
const typeAnnotation = child.childForFieldName?.('type');
|
|
28
|
-
if (typeAnnotation) {
|
|
29
|
-
const typeText = typeAnnotation.text.replace(/^:\s*/, '').trim();
|
|
30
|
-
if (typeText) params.push(typeText);
|
|
31
|
-
}
|
|
32
|
-
} else if (child.type === 'identifier') {
|
|
33
|
-
params.push(child.text);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
for (const child of node.children) walkParams(child);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Avoid deep recursion into the function body by walking only the params node.
|
|
41
|
-
const paramsNode = chunkNode.childForFieldName?.('parameters') || chunkNode.childForFieldName?.('formal_parameters');
|
|
42
|
-
if (paramsNode) walkParams(paramsNode);
|
|
43
|
-
return [...new Set(params)].filter(p => p && p.length > 1).slice(0, 15);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function extractReturnType(chunkNode, ext) {
|
|
47
|
-
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
48
|
-
if (JS_LIKE.includes(ext)) {
|
|
49
|
-
const retTypeNode = chunkNode.childForFieldName?.('return_type');
|
|
50
|
-
if (retTypeNode) return retTypeNode.text.replace(/^:\s*/, '').trim().slice(0, 80);
|
|
51
|
-
}
|
|
52
|
-
return '';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function extractClassContext(chunkNode) {
|
|
56
|
-
let parent = chunkNode.parent;
|
|
57
|
-
while (parent) {
|
|
58
|
-
if (parent.type === 'class_declaration' || parent.type === 'class_definition' ||
|
|
59
|
-
parent.type === 'class_body' || parent.type === 'impl_item') {
|
|
60
|
-
const nameNode = parent.childForFieldName?.('name');
|
|
61
|
-
if (nameNode) return nameNode.text;
|
|
62
|
-
}
|
|
63
|
-
parent = parent.parent;
|
|
64
|
-
}
|
|
65
|
-
return '';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Extract decorator / annotation names applied to a chunk (and, for class chunks,
|
|
70
|
-
* to the methods inside it). Decorators encode what a symbol *is* in modern
|
|
71
|
-
* frameworks — `@Controller`, `@Injectable`, `@Get`, `@Entity` (TS: NestJS,
|
|
72
|
-
* Angular, TypeORM) and `@app.route`, `@pytest.fixture`, `@dataclass`,
|
|
73
|
-
* `@property` (Python) — yet as raw snippet text they are diluted to a single
|
|
74
|
-
* low-weight token inside a large class body. Surfacing them as a dedicated field
|
|
75
|
-
* lets a class annotated `@Controller` be retrieved by "controller" and a method
|
|
76
|
-
* annotated `@Get` by "get/route", independent of language.
|
|
77
|
-
*
|
|
78
|
-
* Generalises by node type only (the tree-sitter `decorator` node is shared across
|
|
79
|
-
* TS/JS/Python grammars) — no framework-specific names are hardcoded. Callee
|
|
80
|
-
* arguments are stripped: `@Controller('cats')` -> 'Controller',
|
|
81
|
-
* `@app.route('/x')` -> 'app.route', `@UseGuards(AuthGuard)` -> 'UseGuards'.
|
|
82
|
-
*
|
|
83
|
-
* @returns {string[]} unique decorator callee names (max 24)
|
|
84
|
-
*/
|
|
85
|
-
export function extractDecorators(chunkNode) {
|
|
86
|
-
const names = new Set();
|
|
87
|
-
const addDecorator = (decoNode) => {
|
|
88
|
-
let t = (decoNode.text || '').trim().replace(/^@/, '');
|
|
89
|
-
t = t.split('(')[0]; // drop call arguments: @Get(':id') -> Get
|
|
90
|
-
t = t.split(/[\s\n{]/)[0].trim(); // first token only
|
|
91
|
-
if (t && t.length <= 64) names.add(t);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// (1) Decorators that PRECEDE the chunk as siblings. Python wraps a decorated
|
|
95
|
-
// symbol in `decorated_definition` ([decorator…, def]); some grammars place
|
|
96
|
-
// class decorators as leading siblings rather than children.
|
|
97
|
-
let prev = chunkNode.previousSibling;
|
|
98
|
-
while (prev) {
|
|
99
|
-
if (prev.type === 'decorator') addDecorator(prev);
|
|
100
|
-
else if (prev.isNamed && prev.type !== 'comment') break;
|
|
101
|
-
prev = prev.previousSibling;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// (2) Decorators within the chunk's subtree. A captured TS class chunk (the
|
|
105
|
-
// enclosing export_statement / class_declaration) carries its own class
|
|
106
|
-
// decorators plus the @Get/@Post/@Inject decorators on its methods.
|
|
107
|
-
// Bounded traversal so a large class body cannot inflate indexing time.
|
|
108
|
-
let budget = 800;
|
|
109
|
-
const stack = [chunkNode];
|
|
110
|
-
while (stack.length && budget-- > 0) {
|
|
111
|
-
const n = stack.pop();
|
|
112
|
-
for (let i = 0; i < n.namedChildCount; i++) {
|
|
113
|
-
const child = n.namedChild(i);
|
|
114
|
-
if (child.type === 'decorator') addDecorator(child);
|
|
115
|
-
else stack.push(child);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return Array.from(names).slice(0, 24);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Extract the base classes and implemented interfaces of a class chunk — the
|
|
124
|
-
* inheritance edge that links a concept to its implementations
|
|
125
|
-
* (`class ValidationPipe extends BasePipe implements PipeTransform`). Surfacing
|
|
126
|
-
* this lets an agent move from an abstract type to the concrete classes that
|
|
127
|
-
* realise it, and feeds the semantic embedding so an implementation is retrievable
|
|
128
|
-
* by the interface it fulfils.
|
|
129
|
-
*
|
|
130
|
-
* Generalises across `extends`/`implements` (TS/JS) and base-class argument lists
|
|
131
|
-
* (Python) by node type. Returns parent type names, e.g.
|
|
132
|
-
* ['BasePipe', 'PipeTransform', 'OnInit'].
|
|
133
|
-
*
|
|
134
|
-
* @returns {string[]} base/interface names (max 12)
|
|
135
|
-
*/
|
|
136
|
-
export function extractHeritage(chunkNode, ext) {
|
|
137
|
-
const bases = new Set();
|
|
138
|
-
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
139
|
-
|
|
140
|
-
const addTypeNames = (node) => {
|
|
141
|
-
const stack = [node];
|
|
142
|
-
let budget = 200;
|
|
143
|
-
while (stack.length && budget-- > 0) {
|
|
144
|
-
const n = stack.pop();
|
|
145
|
-
if ((n.type === 'type_identifier' || n.type === 'identifier') && /^[A-Za-z_$]/.test(n.text)) {
|
|
146
|
-
bases.add(n.text);
|
|
147
|
-
}
|
|
148
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
if (JS_LIKE.includes(ext)) {
|
|
153
|
-
// Walk the chunk subtree for extends/implements clauses, but never descend
|
|
154
|
-
// into class_body — base types of the class only, not its method internals.
|
|
155
|
-
const stack = [chunkNode];
|
|
156
|
-
let budget = 400;
|
|
157
|
-
while (stack.length && budget-- > 0) {
|
|
158
|
-
const n = stack.pop();
|
|
159
|
-
if (n.type === 'extends_clause' || n.type === 'implements_clause') { addTypeNames(n); continue; }
|
|
160
|
-
for (let i = 0; i < n.namedChildCount; i++) {
|
|
161
|
-
const c = n.namedChild(i);
|
|
162
|
-
if (c.type !== 'class_body' && c.type !== 'statement_block') stack.push(c);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} else if (ext === '.py') {
|
|
166
|
-
const sc = chunkNode.childForFieldName?.('superclasses');
|
|
167
|
-
if (sc) {
|
|
168
|
-
for (let i = 0; i < sc.namedChildCount; i++) {
|
|
169
|
-
const c = sc.namedChild(i);
|
|
170
|
-
// skip keyword args like metaclass=… (keyword_argument node)
|
|
171
|
-
if (c.type === 'identifier' || c.type === 'attribute') bases.add(c.text);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
// Other indexed languages: each grammar exposes the base class / implemented
|
|
176
|
-
// interface / supertrait names under a small set of "heritage clause" node
|
|
177
|
-
// types (discovered per grammar). We walk the chunk for those clause nodes
|
|
178
|
-
// and collect the capitalized type names inside — cheap, no type inference.
|
|
179
|
-
// A grammar we don't have a clause for simply yields [] (today's behaviour).
|
|
180
|
-
const CLAUSES = HERITAGE_CLAUSES[ext];
|
|
181
|
-
if (CLAUSES) {
|
|
182
|
-
const stack = [chunkNode];
|
|
183
|
-
let budget = 400;
|
|
184
|
-
while (stack.length && budget-- > 0) {
|
|
185
|
-
const n = stack.pop();
|
|
186
|
-
if (CLAUSES.has(n.type)) { collectHeritageNames(n, bases); continue; }
|
|
187
|
-
for (let i = 0; i < n.namedChildCount; i++) {
|
|
188
|
-
const c = n.namedChild(i);
|
|
189
|
-
if (!HERITAGE_BODY_TYPES.has(c.type)) stack.push(c); // base types sit before the body
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Go has no inheritance keyword — embedding (an anonymous field whose name IS
|
|
194
|
-
// a type) is the closest "is-a/has-a" edge, so surface embedded type names.
|
|
195
|
-
if (ext === '.go') collectGoEmbedding(chunkNode, bases);
|
|
196
|
-
}
|
|
197
|
-
return Array.from(bases).slice(0, 12);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Per-grammar node types whose subtree holds base/interface/supertrait names.
|
|
201
|
-
const HERITAGE_CLAUSES = {
|
|
202
|
-
'.java': new Set(['superclass', 'super_interfaces']),
|
|
203
|
-
'.cs': new Set(['base_list']),
|
|
204
|
-
'.php': new Set(['base_clause', 'class_interface_clause']),
|
|
205
|
-
'.kt': new Set(['delegation_specifier']),
|
|
206
|
-
'.swift': new Set(['inheritance_specifier']),
|
|
207
|
-
'.rb': new Set(['superclass']),
|
|
208
|
-
'.rs': new Set(['trait_bounds']), // supertrait bounds on a trait_item
|
|
209
|
-
};
|
|
210
|
-
// Member-body nodes the heritage walk must not descend into (base types precede them).
|
|
211
|
-
const HERITAGE_BODY_TYPES = new Set([
|
|
212
|
-
'class_body', 'declaration_list', 'enum_body', 'statement_block', 'block',
|
|
213
|
-
'function_body', 'field_declaration_list', 'interface_body', 'struct_body', 'class_body_declaration',
|
|
214
|
-
]);
|
|
215
|
-
|
|
216
|
-
/** Collect capitalized type names anywhere under a heritage-clause node. */
|
|
217
|
-
function collectHeritageNames(node, set) {
|
|
218
|
-
const stack = [node];
|
|
219
|
-
let budget = 200;
|
|
220
|
-
while (stack.length && budget-- > 0) {
|
|
221
|
-
const n = stack.pop();
|
|
222
|
-
if (HERITAGE_NAME_TYPES.has(n.type) && /^[A-Z]/.test(n.text || '')) set.add(n.text);
|
|
223
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const HERITAGE_NAME_TYPES = new Set([
|
|
227
|
-
'type_identifier', 'identifier', 'constant', 'simple_identifier', 'name', 'scoped_type_identifier',
|
|
228
|
-
]);
|
|
229
|
-
|
|
230
|
-
/** Go struct/interface embedding: a field/spec whose name IS a bare type. */
|
|
231
|
-
function collectGoEmbedding(chunkNode, set) {
|
|
232
|
-
const stack = [chunkNode];
|
|
233
|
-
let budget = 400;
|
|
234
|
-
while (stack.length && budget-- > 0) {
|
|
235
|
-
const n = stack.pop();
|
|
236
|
-
// An embedded field_declaration has a type but no field name; the type node
|
|
237
|
-
// is the embed. (A named field `leash Leash` has a `name` child.)
|
|
238
|
-
if (n.type === 'field_declaration' && !n.childForFieldName?.('name')) {
|
|
239
|
-
for (let i = 0; i < n.namedChildCount; i++) {
|
|
240
|
-
const c = n.namedChild(i);
|
|
241
|
-
if ((c.type === 'type_identifier' || c.type === 'qualified_type') && /^[A-Z]/.test(c.text || '')) {
|
|
242
|
-
set.add(c.text.split('.').pop());
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// C#: C# spells type names as bare `identifier` / `generic_name` (NOT the
|
|
251
|
-
// `type_identifier` node the cross-language branch keys on), so the generic walk
|
|
252
|
-
// yields [] for C#. These constants drive a `.cs`-gated branch that reads types
|
|
253
|
-
// only from the field-precise positions where an identifier IS a type, so we never
|
|
254
|
-
// mistake a variable/member name for a type.
|
|
255
|
-
const CS_TYPE_HOST = new Set(['parameter', 'variable_declaration', 'property_declaration', 'method_declaration', 'base_list']);
|
|
256
|
-
// Generic-constraint / contextual keywords that occupy type position but aren't types.
|
|
257
|
-
const CS_TYPE_KEYWORDS = new Set(['where', 'new', 'class', 'struct', 'unmanaged', 'notnull', 'var']);
|
|
258
|
-
|
|
259
|
-
/** Collect simple C# type names from a type-position node (descending through
|
|
260
|
-
* generics / arrays / nullables / tuples). Skips predefined primitives and `var`,
|
|
261
|
-
* keeps only PascalCase names, and reduces a qualified name to its last segment. */
|
|
262
|
-
function _csTypeNamesFrom(node, out) {
|
|
263
|
-
if (!node) return;
|
|
264
|
-
const stack = [node];
|
|
265
|
-
let budget = 200;
|
|
266
|
-
while (stack.length && budget-- > 0) {
|
|
267
|
-
const n = stack.pop();
|
|
268
|
-
const t = n.type;
|
|
269
|
-
if (t === 'predefined_type' || t === 'implicit_type') continue; // int/string/void/var…
|
|
270
|
-
if (t === 'identifier' || t === 'type_identifier') {
|
|
271
|
-
const nm = n.text;
|
|
272
|
-
if (nm && /^[A-Z]/.test(nm) && !CS_TYPE_KEYWORDS.has(nm)) out.add(nm);
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (t === 'qualified_name') {
|
|
276
|
-
// The type is the LAST segment; it may itself be a generic_name
|
|
277
|
-
// (System.Collections.Generic.List<Order>). Descend into just that
|
|
278
|
-
// segment → "List" + "Order" (not a malformed "List<Order>"), while
|
|
279
|
-
// never visiting the namespace identifiers (System/Collections/Generic).
|
|
280
|
-
const last = n.namedChild(n.namedChildCount - 1);
|
|
281
|
-
if (last && (last.type === 'generic_name' || last.type === 'identifier' || last.type === 'type_identifier')) {
|
|
282
|
-
stack.push(last);
|
|
283
|
-
} else {
|
|
284
|
-
const seg = (n.text || '').split('.').pop();
|
|
285
|
-
if (seg && /^[A-Z]/.test(seg) && !CS_TYPE_KEYWORDS.has(seg)) out.add(seg);
|
|
286
|
-
}
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
// generic_name / array_type / nullable_type / tuple_type / type_argument_list /
|
|
290
|
-
// pointer_type … → descend to reach the inner type names.
|
|
291
|
-
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function extractTypeAnnotations(chunkNode, ext) {
|
|
296
|
-
const types = new Set();
|
|
297
|
-
// JS/TS + Python use the precise annotation branches below. Every other indexed
|
|
298
|
-
// language is covered by the shared `type_identifier` branch (that node appears
|
|
299
|
-
// ONLY in type position across Go/Rust/Java/Kotlin/Swift/C) plus a `named_type`
|
|
300
|
-
// branch for PHP — so the type-user dimension of find_references works in those
|
|
301
|
-
// languages too, with no early-out and no type inference. C# is the exception:
|
|
302
|
-
// it spells types as bare `identifier`/`generic_name`, so it gets a dedicated
|
|
303
|
-
// field-precise branch below (gated on `.cs`). Ruby (dynamically typed) still
|
|
304
|
-
// carries no cheap type signal and naturally yields [].
|
|
305
|
-
|
|
306
|
-
function walk(node) {
|
|
307
|
-
if (node.type === 'type_annotation') {
|
|
308
|
-
const typeText = node.text.replace(/^:\s*/, '').trim();
|
|
309
|
-
const PRIMITIVES = new Set(['string', 'number', 'boolean', 'void', 'any', 'unknown', 'never', 'null', 'undefined', 'object', 'symbol', 'bigint']);
|
|
310
|
-
for (const match of typeText.matchAll(/\b([A-Z][A-Za-z0-9]*)\b/g)) {
|
|
311
|
-
if (!PRIMITIVES.has(match[1].toLowerCase())) types.add(match[1]);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// type_identifier / generic_type: a named type in type position. This is the
|
|
315
|
-
// cross-language branch — type_identifier is how Go/Rust/Java/Kotlin/Swift/C
|
|
316
|
-
// (and TS) all spell a referenced type name.
|
|
317
|
-
else if (node.type === 'type_identifier' || node.type === 'generic_type') {
|
|
318
|
-
const name = node.children[0]?.text || node.text;
|
|
319
|
-
if (name && /^[A-Z]/.test(name)) types.add(name);
|
|
320
|
-
}
|
|
321
|
-
// PHP: a typed parameter / return uses `named_type` (e.g. `Owner`, `\App\User`).
|
|
322
|
-
else if (node.type === 'named_type') {
|
|
323
|
-
const seg = (node.text || '').replace(/^\?/, '').split('\\').pop().trim();
|
|
324
|
-
if (seg && /^[A-Z]/.test(seg)) types.add(seg);
|
|
325
|
-
}
|
|
326
|
-
else if (node.type === 'annotation' && ext === '.py') {
|
|
327
|
-
const typeText = node.text.replace(/^->\s*|^:\s*/, '').trim();
|
|
328
|
-
for (const match of typeText.matchAll(/\b([A-Z][A-Za-z0-9]*)\b/g)) {
|
|
329
|
-
types.add(match[1]);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// C#: types live as bare identifiers in field-precise positions — a
|
|
333
|
-
// parameter/field/property/local's `type`, a method's `returns`, and the
|
|
334
|
-
// class `base_list`. Additive and `.cs`-only, so all other languages are
|
|
335
|
-
// byte-identical to before.
|
|
336
|
-
else if (ext === '.cs' && CS_TYPE_HOST.has(node.type)) {
|
|
337
|
-
if (node.type === 'base_list') {
|
|
338
|
-
_csTypeNamesFrom(node, types);
|
|
339
|
-
} else {
|
|
340
|
-
_csTypeNamesFrom(node.childForFieldName?.('type') || node.childForFieldName?.('returns') || null, types);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
for (const child of node.children) walk(child);
|
|
344
|
-
}
|
|
345
|
-
walk(chunkNode);
|
|
346
|
-
return Array.from(types).slice(0, 20);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Call names that are framework/stdlib noise rather than project call edges.
|
|
350
|
-
const CALL_NOISE = new Set(['require', 'console', 'log', 'expect', 'test', 'it', 'describe', 'setTimeout', 'print', 'println!']);
|
|
351
|
-
function _validCallName(c) { return Boolean(c) && c.length > 2 && !CALL_NOISE.has(c); }
|
|
352
|
-
|
|
353
|
-
// Shell builtins / ubiquitous coreutils — emitting these as Bash call edges would
|
|
354
|
-
// bury the project's own function-to-function calls in noise (they never resolve to
|
|
355
|
-
// an indexed symbol anyway). Project functions and notable tools (docker, git, npm,
|
|
356
|
-
// kubectl…) are kept.
|
|
357
|
-
const BASH_BUILTINS = new Set([
|
|
358
|
-
'echo', 'cd', 'ls', 'cat', 'rm', 'cp', 'mv', 'mkdir', 'rmdir', 'touch', 'ln',
|
|
359
|
-
'export', 'local', 'readonly', 'declare', 'unset', 'shift', 'read', 'printf',
|
|
360
|
-
'exit', 'return', 'eval', 'exec', 'trap', 'wait', 'sleep', 'pwd', 'set', 'shopt',
|
|
361
|
-
'source', 'test', 'true', 'false', 'kill', 'jobs', 'type', 'command', 'getopts',
|
|
362
|
-
'grep', 'sed', 'awk', 'cut', 'tr', 'sort', 'uniq', 'head', 'tail', 'find', 'xargs',
|
|
363
|
-
'chmod', 'chown', 'tee', 'wc', 'basename', 'dirname', 'tar', 'curl', 'wget', 'env',
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Collapse a call's receiver expression into a compact disambiguation hint:
|
|
368
|
-
* • '' — unqualified call (`foo()`): a free function or in-scope name.
|
|
369
|
-
* • 'this' — `this.`/`self.`-rooted: dispatch on the SAME instance/class.
|
|
370
|
-
* • '<ident>' — the last identifier of the receiver expression (`db.save()` → 'db',
|
|
371
|
-
* `UserService.find()` → 'UserService'), the only cheap type signal
|
|
372
|
-
* available without full inference.
|
|
373
|
-
* This is what lets get_call_graph separate the real callers of `OrderService.save`
|
|
374
|
-
* from every unrelated `save()` in the repo (see mcp/topology.mjs classifyCallers).
|
|
375
|
-
*/
|
|
376
|
-
function _receiverHint(objNode) {
|
|
377
|
-
if (!objNode) return '';
|
|
378
|
-
const t = objNode.text || '';
|
|
379
|
-
if (/^(this|self)\b/.test(t)) return 'this';
|
|
380
|
-
const segs = t.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
|
|
381
|
-
return segs.length ? segs[segs.length - 1] : '';
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* C#: the invoked-name node is either a plain `identifier` or a `generic_name`
|
|
386
|
-
* (`Method<T>`, children = identifier + type_argument_list) whose leading
|
|
387
|
-
* `identifier` is the real method name. Strip the type-argument list so the
|
|
388
|
-
* recorded callee is `Method`, not `Method<T>`.
|
|
389
|
-
*/
|
|
390
|
-
function _csInvokedName(node) {
|
|
391
|
-
if (!node) return '';
|
|
392
|
-
if (node.type === 'identifier') return node.text;
|
|
393
|
-
const id = node.children?.find(c => c.type === 'identifier');
|
|
394
|
-
return id ? id.text : (node.text || '').split('<')[0];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ─── Scope-aware receiver type inference (intra-procedural, best-effort) ──────────
|
|
398
|
-
// `db.s.save()` and `const s = getStore(); s.save()` both record a receiver hint of
|
|
399
|
-
// `s` (a variable, not a type), so classifyCallers can only name-match them and they
|
|
400
|
-
// leak into every `save()` in the repo. A cheap pass over the SAME chunk subtree
|
|
401
|
-
// resolves the simple local bindings the AST already makes explicit — `new Repo()`,
|
|
402
|
-
// a constructor/factory call, and TS/Python type annotations — and tags the call
|
|
403
|
-
// site with `recv_type` (resolved here) or `recv_via_call` (a factory whose return
|
|
404
|
-
// type is resolved at query time from the index). This is strictly additive: a call
|
|
405
|
-
// site that cannot be resolved keeps exactly its old `{ name, recv }` shape and stays
|
|
406
|
-
// name-only. Only languages whose type is recoverable from the AST participate
|
|
407
|
-
// (TS/JS via `new`/annotations, Python via annotations/obvious constructors, plus the
|
|
408
|
-
// trivially-safe Java/C# `new Foo()`); receiver-less languages are untouched.
|
|
409
|
-
|
|
410
|
-
/** Clean a type-position node's text to a type string, or null if it carries no
|
|
411
|
-
* capitalized identifier (drops primitives like `string`/`number[]`). Generics and
|
|
412
|
-
* namespaces are preserved verbatim — classifyCallers token-matches against them. */
|
|
413
|
-
function _bindingTypeName(typeText) {
|
|
414
|
-
if (!typeText) return null;
|
|
415
|
-
const cleaned = typeText.replace(/^->\s*/, '').replace(/^[:\s]+/, '').trim().slice(0, 64);
|
|
416
|
-
return /[A-Z]/.test(cleaned) ? cleaned : null;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** Peel await/parenthesized wrappers off a value expression (TS `await getStore()`,
|
|
420
|
-
* Python `await get_store()`). Bounded to avoid pathological nesting. */
|
|
421
|
-
function _unwrapValueNode(node) {
|
|
422
|
-
let n = node, guard = 0;
|
|
423
|
-
while (n && guard++ < 6) {
|
|
424
|
-
if (n.type === 'await_expression' || n.type === 'await') { n = n.namedChildren?.[0]; continue; }
|
|
425
|
-
if (n.type === 'parenthesized_expression') { n = n.namedChildren?.[0]; continue; }
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
return n;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Classify a right-hand-side value into the type of the binding it produces:
|
|
433
|
-
* • { type } — directly recoverable now (`new Repo()`, `(x as Repo)`,
|
|
434
|
-
* a Python `Repo()` constructor, Java/C# `new Repo()`).
|
|
435
|
-
* • { viaCall } — a factory call whose return type is resolved later from the
|
|
436
|
-
* callee's recorded return_type (`getStore()`).
|
|
437
|
-
* • null — nothing safe to infer.
|
|
438
|
-
*/
|
|
439
|
-
function _classifyValueNode(node) {
|
|
440
|
-
node = _unwrapValueNode(node);
|
|
441
|
-
if (!node) return null;
|
|
442
|
-
const t = node.type;
|
|
443
|
-
if (t === 'new_expression') { // TS/JS: new Repo()
|
|
444
|
-
const ctor = node.childForFieldName?.('constructor') || node.namedChildren?.[0];
|
|
445
|
-
const tn = _bindingTypeName(ctor?.text);
|
|
446
|
-
return tn ? { type: tn } : null;
|
|
447
|
-
}
|
|
448
|
-
if (t === 'object_creation_expression') { // Java / C#: new Repo() (trivially safe)
|
|
449
|
-
const ty = node.childForFieldName?.('type') || node.namedChildren?.find(c => /type|name|identifier/.test(c.type));
|
|
450
|
-
const tn = _bindingTypeName(ty?.text);
|
|
451
|
-
return tn ? { type: tn } : null;
|
|
452
|
-
}
|
|
453
|
-
if (t === 'as_expression' || t === 'satisfies_expression') { // TS: const s = x as Repo
|
|
454
|
-
const ty = node.childForFieldName?.('type') || node.namedChildren?.[node.namedChildren.length - 1];
|
|
455
|
-
const tn = _bindingTypeName(ty?.text);
|
|
456
|
-
if (tn) return { type: tn };
|
|
457
|
-
return _classifyValueNode(node.childForFieldName?.('expression') || node.namedChildren?.[0]);
|
|
458
|
-
}
|
|
459
|
-
if (t === 'call') { // Python: Repo() is a constructor by convention; get_store() is a factory
|
|
460
|
-
const fn = node.childForFieldName?.('function') || node.children?.[0];
|
|
461
|
-
if (fn?.type === 'identifier') {
|
|
462
|
-
return /^[A-Z]/.test(fn.text) ? { type: fn.text } : (_validCallName(fn.text) ? { viaCall: fn.text } : null);
|
|
463
|
-
}
|
|
464
|
-
if (fn?.type === 'attribute') {
|
|
465
|
-
const last = fn.childForFieldName?.('attribute')?.text; // mod.Repo()
|
|
466
|
-
if (last && /^[A-Z]/.test(last)) return { type: last };
|
|
467
|
-
}
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
470
|
-
if (t === 'call_expression') { // TS/JS/Swift/Go/Rust/C: factory call — defer to return_type
|
|
471
|
-
const fn = node.childForFieldName?.('function') || node.children?.[0];
|
|
472
|
-
if (fn && (fn.type === 'identifier' || fn.type === 'simple_identifier') && _validCallName(fn.text)) return { viaCall: fn.text };
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Collect simple local variable → binding-type for one chunk subtree. Handles the
|
|
480
|
-
* common, unambiguous forms only; a variable bound to two different things is dropped
|
|
481
|
-
* (conflict) rather than guessed. `this`/`self` are excluded (handled by the `this`
|
|
482
|
-
* receiver bucket). One extra subtree pass per chunk — O(chunk size), no whole-program
|
|
483
|
-
* analysis — justified by the precision win on dynamically-typed receivers.
|
|
484
|
-
*/
|
|
485
|
-
function _inferLocalBindings(rootNode) {
|
|
486
|
-
const bindings = new Map();
|
|
487
|
-
const set = (name, val) => {
|
|
488
|
-
if (!name || name === 'this' || name === 'self' || !val) return;
|
|
489
|
-
const prev = bindings.get(name);
|
|
490
|
-
if (prev === undefined) { bindings.set(name, val); return; }
|
|
491
|
-
if (prev.conflict) return;
|
|
492
|
-
if (prev.type !== val.type || prev.viaCall !== val.viaCall) bindings.set(name, { conflict: true });
|
|
493
|
-
};
|
|
494
|
-
function walk(node) {
|
|
495
|
-
const t = node.type;
|
|
496
|
-
if (t === 'variable_declarator') {
|
|
497
|
-
const nameNode = node.childForFieldName?.('name');
|
|
498
|
-
if (nameNode && nameNode.type === 'identifier') {
|
|
499
|
-
const val = _classifyValueNode(node.childForFieldName?.('value'));
|
|
500
|
-
if (val) set(nameNode.text, val);
|
|
501
|
-
}
|
|
502
|
-
} else if (t === 'required_parameter' || t === 'optional_parameter') {
|
|
503
|
-
const id = node.childForFieldName?.('pattern');
|
|
504
|
-
const ty = node.childForFieldName?.('type');
|
|
505
|
-
if (id && id.type === 'identifier' && ty) { const tn = _bindingTypeName(ty.text); if (tn) set(id.text, { type: tn }); }
|
|
506
|
-
} else if (t === 'assignment') {
|
|
507
|
-
const left = node.childForFieldName?.('left');
|
|
508
|
-
const ty = node.childForFieldName?.('type');
|
|
509
|
-
if (left && left.type === 'identifier') {
|
|
510
|
-
if (ty) { const tn = _bindingTypeName(ty.text); if (tn) set(left.text, { type: tn }); }
|
|
511
|
-
else { const val = _classifyValueNode(node.childForFieldName?.('right')); if (val) set(left.text, val); }
|
|
512
|
-
}
|
|
513
|
-
} else if (t === 'typed_parameter' || t === 'typed_default_parameter') {
|
|
514
|
-
const id = node.childForFieldName?.('name') || node.children?.find(c => c.type === 'identifier');
|
|
515
|
-
const ty = node.childForFieldName?.('type');
|
|
516
|
-
if (id && ty) { const tn = _bindingTypeName(ty.text); if (tn) set(id.text, { type: tn }); }
|
|
517
|
-
}
|
|
518
|
-
node.children.forEach(walk);
|
|
519
|
-
}
|
|
520
|
-
walk(rootNode);
|
|
521
|
-
return bindings;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/** Resolve a call site's receiver object node to a binding ({type}|{viaCall}|null):
|
|
525
|
-
* a simple variable via the binding map, or an inline `new Repo()`/`getStore()`. */
|
|
526
|
-
function _inferReceiverType(objNode, bindings) {
|
|
527
|
-
if (!objNode) return null;
|
|
528
|
-
if (objNode.type === 'identifier' || objNode.type === 'simple_identifier') {
|
|
529
|
-
const v = objNode.text;
|
|
530
|
-
if (v === 'this' || v === 'self') return null;
|
|
531
|
-
const b = bindings.get(v);
|
|
532
|
-
return (b && !b.conflict) ? b : null;
|
|
533
|
-
}
|
|
534
|
-
return _classifyValueNode(objNode); // inline: new Repo().save(), getStore().save()
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const _FN_NODE_TYPES = new Set([
|
|
538
|
-
'function_declaration', 'function_definition', 'method_definition', 'method_declaration',
|
|
539
|
-
'arrow_function', 'function', 'function_expression', 'lambda', 'method', 'singleton_method',
|
|
540
|
-
'generator_function', 'generator_function_declaration',
|
|
541
|
-
]);
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Classify what a function chunk RETURNS, for the opt-in inter-procedural fixpoint
|
|
545
|
-
* (parse/interprocedural.mjs). Looks at this function's own return expression(s):
|
|
546
|
-
* • { type } — returns `new Repo()` (a concrete type, no annotation needed).
|
|
547
|
-
* • { viaCall } — returns a bare factory call `makeRepo()` whose type is resolved
|
|
548
|
-
* later by propagating along the call graph.
|
|
549
|
-
* • null — nothing safe (no return, mixed returns, opaque expression).
|
|
550
|
-
* Does NOT descend into nested functions/lambdas (only THIS function's returns).
|
|
551
|
-
* This is a transient signal (attached as `_return_via`) consumed at index time and
|
|
552
|
-
* stripped before serialization — it never persists in the index.
|
|
553
|
-
*/
|
|
554
|
-
export function extractReturnVia(chunkNode) {
|
|
555
|
-
const found = [];
|
|
556
|
-
const consider = (expr) => { const v = _classifyValueNode(expr); if (v) found.push(v); };
|
|
557
|
-
|
|
558
|
-
// Expression-body arrow / lambda: `() => new Repo()` has no return_statement.
|
|
559
|
-
if (chunkNode.type === 'arrow_function' || chunkNode.type === 'lambda') {
|
|
560
|
-
const body = chunkNode.childForFieldName?.('body');
|
|
561
|
-
if (body && body.type !== 'statement_block' && body.type !== 'block') consider(body);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
(function walk(node, depth) {
|
|
565
|
-
for (const child of node.children) {
|
|
566
|
-
if (depth > 0 && _FN_NODE_TYPES.has(child.type)) continue; // skip nested functions
|
|
567
|
-
if (child.type === 'return_statement') {
|
|
568
|
-
consider(child.namedChildren?.[0] || child.childForFieldName?.('argument'));
|
|
569
|
-
}
|
|
570
|
-
walk(child, depth + 1);
|
|
571
|
-
}
|
|
572
|
-
})(chunkNode, 0);
|
|
573
|
-
|
|
574
|
-
if (found.length === 0) return null;
|
|
575
|
-
const types = new Set(found.filter(f => f.type).map(f => f.type));
|
|
576
|
-
const vias = new Set(found.filter(f => f.viaCall).map(f => f.viaCall));
|
|
577
|
-
// Conservative: only a single unambiguous classification is usable.
|
|
578
|
-
if (types.size === 1 && vias.size === 0) return { type: [...types][0] };
|
|
579
|
-
if (vias.size === 1 && types.size === 0) return { viaCall: [...vias][0] };
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Walk a subtree and collect every call site as { name, recv } (receiver hint), plus
|
|
585
|
-
* an optional `recv_type` / `recv_via_call` when the receiver's type is recoverable
|
|
586
|
-
* intra-procedurally (see the inference helpers above). Deduplicated by (name, recv).
|
|
587
|
-
* Cross-language: call_expression (JS/TS/Go/Rust/C and Swift via simple_identifier/
|
|
588
|
-
* navigation_expression), call (Python), macro_invocation (Rust), method_invocation
|
|
589
|
-
* (Java/C#), method_call (Ruby), command (Bash). The receiver is the precision half of
|
|
590
|
-
* the call graph — extractCalls() derives the legacy name-only list from these sites.
|
|
591
|
-
*/
|
|
592
|
-
export function extractCallSites(rootNode) {
|
|
593
|
-
const sites = [];
|
|
594
|
-
const bindings = _inferLocalBindings(rootNode);
|
|
595
|
-
const seen = new Set();
|
|
596
|
-
const add = (name, recv, objNode = null) => {
|
|
597
|
-
if (!name) return;
|
|
598
|
-
const key = name + ' ' + recv;
|
|
599
|
-
if (seen.has(key)) return;
|
|
600
|
-
seen.add(key);
|
|
601
|
-
const site = { name, recv };
|
|
602
|
-
const inferred = objNode ? _inferReceiverType(objNode, bindings) : null;
|
|
603
|
-
if (inferred?.type) site.recv_type = inferred.type;
|
|
604
|
-
else if (inferred?.viaCall) site.recv_via_call = inferred.viaCall;
|
|
605
|
-
sites.push(site);
|
|
606
|
-
};
|
|
607
|
-
function walk(node) {
|
|
608
|
-
const t = node.type;
|
|
609
|
-
if (t === 'call_expression') {
|
|
610
|
-
const funcNode = node.childForFieldName?.('function') || node.children[0];
|
|
611
|
-
if (funcNode) {
|
|
612
|
-
if (funcNode.type === 'identifier') add(funcNode.text, '');
|
|
613
|
-
else if (funcNode.type === 'simple_identifier') add(funcNode.text, '');
|
|
614
|
-
else if (funcNode.type === 'member_expression' || funcNode.type === 'property_identifier') {
|
|
615
|
-
const prop = funcNode.childForFieldName?.('property');
|
|
616
|
-
const obj = funcNode.childForFieldName?.('object');
|
|
617
|
-
if (prop) add(prop.text, _receiverHint(obj), obj);
|
|
618
|
-
else add(funcNode.text.split('.').pop(), '');
|
|
619
|
-
} else if (funcNode.type === 'navigation_expression') { // Swift obj.method / self.method
|
|
620
|
-
const suffix = funcNode.childForFieldName?.('suffix');
|
|
621
|
-
const m = suffix?.namedChildren?.find(c => c.type === 'simple_identifier')?.text
|
|
622
|
-
|| suffix?.text?.replace(/^\./, '');
|
|
623
|
-
const target = funcNode.childForFieldName?.('target');
|
|
624
|
-
if (m) add(m, _receiverHint(target), target);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
} else if (t === 'command') {
|
|
628
|
-
const cmd = (node.childForFieldName?.('name')?.text || '').trim();
|
|
629
|
-
// bare identifiers only — skip paths, env-prefixed assignments, builtins.
|
|
630
|
-
if (cmd && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(cmd) && !BASH_BUILTINS.has(cmd)) add(cmd, '');
|
|
631
|
-
} else if (t === 'call') {
|
|
632
|
-
const funcNode = node.childForFieldName?.('function') || node.children[0];
|
|
633
|
-
if (funcNode) {
|
|
634
|
-
if (funcNode.type === 'identifier') add(funcNode.text, '');
|
|
635
|
-
else if (funcNode.type === 'attribute') {
|
|
636
|
-
const attr = funcNode.childForFieldName?.('attribute');
|
|
637
|
-
const obj = funcNode.childForFieldName?.('object');
|
|
638
|
-
if (attr) add(attr.text, _receiverHint(obj), obj);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
} else if (t === 'macro_invocation') {
|
|
642
|
-
const macroNode = node.childForFieldName?.('macro') || node.children[0];
|
|
643
|
-
if (macroNode && macroNode.type === 'identifier') add(macroNode.text + '!', '');
|
|
644
|
-
} else if (t === 'method_invocation') {
|
|
645
|
-
const nameNode = node.childForFieldName?.('name') || node.children.find(c => c.type === 'identifier');
|
|
646
|
-
const obj = node.childForFieldName?.('object');
|
|
647
|
-
if (nameNode) add(nameNode.text, _receiverHint(obj), obj);
|
|
648
|
-
} else if (t === 'invocation_expression') { // C# — the grammar has NO method_invocation; a call is
|
|
649
|
-
// invocation_expression(function, arguments). The function is a bare identifier (same-class /
|
|
650
|
-
// using-static call) or a member_access_expression (obj.Method() / this.Method()).
|
|
651
|
-
const fn = node.childForFieldName?.('function');
|
|
652
|
-
if (fn) {
|
|
653
|
-
if (fn.type === 'member_access_expression') {
|
|
654
|
-
const nm = _csInvokedName(fn.childForFieldName?.('name'));
|
|
655
|
-
const expr = fn.childForFieldName?.('expression');
|
|
656
|
-
if (nm) add(nm, _receiverHint(expr), expr);
|
|
657
|
-
} else if (fn.type === 'identifier') {
|
|
658
|
-
add(fn.text, '');
|
|
659
|
-
} else if (fn.type === 'generic_name') {
|
|
660
|
-
const nm = _csInvokedName(fn);
|
|
661
|
-
if (nm) add(nm, '');
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
} else if (t === 'method_call') {
|
|
665
|
-
const method = node.childForFieldName?.('method') || node.children.find(c => c.type === 'identifier');
|
|
666
|
-
const receiver = node.childForFieldName?.('receiver');
|
|
667
|
-
if (method && method.type === 'identifier') add(method.text, _receiverHint(receiver), receiver);
|
|
668
|
-
} else if (t === 'function_call_expression') {
|
|
669
|
-
const fn = node.childForFieldName?.('function');
|
|
670
|
-
if (fn && (fn.type === 'name' || fn.type === 'qualified_name')) {
|
|
671
|
-
const nm = (fn.text || '').split('\\').filter(Boolean).pop();
|
|
672
|
-
if (nm) add(nm, '');
|
|
673
|
-
}
|
|
674
|
-
} else if (t === 'member_call_expression' || t === 'nullsafe_member_call_expression') {
|
|
675
|
-
const nameNode = node.childForFieldName?.('name');
|
|
676
|
-
const obj = node.childForFieldName?.('object');
|
|
677
|
-
if (nameNode && nameNode.type === 'name') add(nameNode.text, _receiverHint(obj), obj);
|
|
678
|
-
} else if (t === 'scoped_call_expression') {
|
|
679
|
-
const nameNode = node.childForFieldName?.('name');
|
|
680
|
-
const scope = node.childForFieldName?.('scope');
|
|
681
|
-
if (nameNode && nameNode.type === 'name') add(nameNode.text, _receiverHint(scope), scope);
|
|
682
|
-
}
|
|
683
|
-
node.children.forEach(walk);
|
|
684
|
-
}
|
|
685
|
-
walk(rootNode);
|
|
686
|
-
return sites.filter(s => _validCallName(s.name));
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/** Legacy name-only outgoing-call list (unique callee names). Derived from
|
|
690
|
-
* extractCallSites so the two never diverge; preserved for the BM25 document
|
|
691
|
-
* and the back-compat findCallers contract. */
|
|
692
|
-
export function extractCalls(rootNode) {
|
|
693
|
-
const seen = new Set();
|
|
694
|
-
const out = [];
|
|
695
|
-
for (const { name } of extractCallSites(rootNode)) {
|
|
696
|
-
if (seen.has(name)) continue;
|
|
697
|
-
seen.add(name);
|
|
698
|
-
out.push(name);
|
|
699
|
-
}
|
|
700
|
-
return out;
|
|
701
|
-
}
|