@optave/codegraph 3.11.2 → 3.13.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/README.md +73 -37
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +2 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +1 -0
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +272 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/triage.js +1 -1
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/shared/options.d.ts +2 -1
- package/dist/cli/shared/options.d.ts.map +1 -1
- package/dist/cli/shared/options.js +11 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +8 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts +2 -0
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +24 -2
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +16 -10
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +251 -34
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +69 -0
- package/dist/domain/graph/builder/cha.d.ts.map +1 -0
- package/dist/domain/graph/builder/cha.js +158 -0
- package/dist/domain/graph/builder/cha.js.map +1 -0
- package/dist/domain/graph/builder/context.d.ts +3 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +2 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +25 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +178 -5
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +74 -2
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +37 -2
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +704 -34
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +4 -0
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +783 -37
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +10 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/journal.js +1 -1
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/resolver/points-to.d.ts +53 -0
- package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
- package/dist/domain/graph/resolver/points-to.js +213 -0
- package/dist/domain/graph/resolver/points-to.js.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.js +476 -0
- package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
- package/dist/domain/parser.d.ts +12 -4
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +83 -20
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +35 -2
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +34 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +15 -1
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/c.js +3 -3
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/clojure.js +1 -1
- package/dist/extractors/clojure.js.map +1 -1
- package/dist/extractors/cpp.d.ts.map +1 -1
- package/dist/extractors/cpp.js +45 -4
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/csharp.d.ts.map +1 -1
- package/dist/extractors/csharp.js +37 -8
- package/dist/extractors/csharp.js.map +1 -1
- package/dist/extractors/cuda.d.ts.map +1 -1
- package/dist/extractors/cuda.js +45 -4
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +6 -6
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/fsharp.js +1 -1
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/go.js +5 -5
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/haskell.js +1 -1
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/helpers.d.ts +11 -0
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +10 -9
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts +2 -0
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +1812 -71
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +5 -5
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/lua.js +1 -1
- package/dist/extractors/lua.js.map +1 -1
- package/dist/extractors/objc.js +3 -3
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/ocaml.js +1 -1
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/php.js +2 -2
- package/dist/extractors/php.js.map +1 -1
- package/dist/extractors/python.js +7 -7
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/ruby.js +2 -2
- package/dist/extractors/ruby.js.map +1 -1
- package/dist/extractors/scala.js +1 -1
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +1 -1
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/swift.js +4 -4
- package/dist/extractors/swift.js.map +1 -1
- package/dist/extractors/zig.js +4 -4
- package/dist/extractors/zig.js.map +1 -1
- package/dist/features/structure-query.d.ts +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +6 -6
- package/dist/features/structure-query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/config.d.ts +85 -2
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +408 -19
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/native.d.ts +11 -0
- package/dist/infrastructure/native.d.ts.map +1 -1
- package/dist/infrastructure/native.js +78 -5
- package/dist/infrastructure/native.js.map +1 -1
- package/dist/infrastructure/registry.d.ts +27 -0
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +59 -1
- package/dist/infrastructure/registry.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +5 -0
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +2 -2
- package/dist/presentation/structure.js.map +1 -1
- package/dist/types.d.ts +221 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +7 -8
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/batch.ts +1 -0
- package/src/cli/commands/build.ts +6 -1
- package/src/cli/commands/config.ts +353 -0
- package/src/cli/commands/triage.ts +1 -1
- package/src/cli/index.ts +10 -0
- package/src/cli/shared/options.ts +11 -1
- package/src/cli/types.ts +2 -0
- package/src/db/migrations.ts +8 -1
- package/src/domain/analysis/module-map.ts +29 -1
- package/src/domain/graph/builder/call-resolver.ts +263 -35
- package/src/domain/graph/builder/cha.ts +192 -0
- package/src/domain/graph/builder/context.ts +3 -0
- package/src/domain/graph/builder/helpers.ts +195 -5
- package/src/domain/graph/builder/incremental.ts +80 -1
- package/src/domain/graph/builder/pipeline.ts +49 -2
- package/src/domain/graph/builder/stages/build-edges.ts +867 -32
- package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
- package/src/domain/graph/builder/stages/finalize.ts +4 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
- package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
- package/src/domain/graph/journal.ts +1 -1
- package/src/domain/graph/resolver/points-to.ts +254 -0
- package/src/domain/graph/resolver/ts-resolver.ts +536 -0
- package/src/domain/parser.ts +86 -17
- package/src/domain/wasm-worker-entry.ts +35 -2
- package/src/domain/wasm-worker-pool.ts +22 -0
- package/src/domain/wasm-worker-protocol.ts +15 -0
- package/src/extractors/c.ts +3 -3
- package/src/extractors/clojure.ts +1 -1
- package/src/extractors/cpp.ts +47 -4
- package/src/extractors/csharp.ts +33 -9
- package/src/extractors/cuda.ts +47 -4
- package/src/extractors/elixir.ts +6 -6
- package/src/extractors/fsharp.ts +1 -1
- package/src/extractors/go.ts +5 -5
- package/src/extractors/haskell.ts +1 -1
- package/src/extractors/helpers.ts +43 -0
- package/src/extractors/java.ts +10 -9
- package/src/extractors/javascript.ts +1929 -72
- package/src/extractors/kotlin.ts +5 -5
- package/src/extractors/lua.ts +1 -1
- package/src/extractors/objc.ts +3 -3
- package/src/extractors/ocaml.ts +1 -1
- package/src/extractors/php.ts +2 -2
- package/src/extractors/python.ts +7 -7
- package/src/extractors/ruby.ts +2 -2
- package/src/extractors/scala.ts +1 -1
- package/src/extractors/solidity.ts +1 -1
- package/src/extractors/swift.ts +4 -4
- package/src/extractors/zig.ts +4 -4
- package/src/features/structure-query.ts +7 -7
- package/src/index.ts +5 -1
- package/src/infrastructure/config.ts +494 -20
- package/src/infrastructure/native.ts +87 -5
- package/src/infrastructure/registry.ts +82 -1
- package/src/presentation/queries-cli/overview.ts +15 -1
- package/src/presentation/structure.ts +3 -3
- package/src/types.ts +235 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { debug } from '../infrastructure/logger.js';
|
|
2
2
|
import type {
|
|
3
|
+
ArrayCallbackBinding,
|
|
4
|
+
ArrayElemBinding,
|
|
3
5
|
Call,
|
|
6
|
+
CallAssignment,
|
|
4
7
|
ClassRelation,
|
|
5
8
|
Definition,
|
|
6
9
|
Export,
|
|
7
10
|
ExtractorOutput,
|
|
11
|
+
FnRefBinding,
|
|
12
|
+
ForOfBinding,
|
|
8
13
|
Import,
|
|
14
|
+
ObjectPropBinding,
|
|
15
|
+
ObjectRestParamBinding,
|
|
16
|
+
ParamBinding,
|
|
17
|
+
SpreadArgBinding,
|
|
9
18
|
SubDeclaration,
|
|
19
|
+
ThisCallBinding,
|
|
10
20
|
TreeSitterNode,
|
|
11
21
|
TreeSitterQuery,
|
|
12
22
|
TreeSitterTree,
|
|
@@ -76,8 +86,17 @@ const BUILTIN_GLOBALS: Set<string> = new Set([
|
|
|
76
86
|
'Buffer',
|
|
77
87
|
'EventEmitter',
|
|
78
88
|
'Stream',
|
|
89
|
+
'process',
|
|
90
|
+
'window',
|
|
91
|
+
'document',
|
|
92
|
+
'globalThis',
|
|
79
93
|
]);
|
|
80
94
|
|
|
95
|
+
/** Maximum chain depth for inter-procedural return-type propagation (Phase 8.2). */
|
|
96
|
+
const MAX_PROPAGATION_DEPTH = 3;
|
|
97
|
+
/** Confidence penalty applied per propagation hop (1.0 → 0.9 → 0.8 → 0.7). */
|
|
98
|
+
export const PROPAGATION_HOP_PENALTY = 0.1;
|
|
99
|
+
|
|
81
100
|
/**
|
|
82
101
|
* Extract symbols from a JS/TS parsed AST.
|
|
83
102
|
* When a compiled tree-sitter Query is provided (from parser.js),
|
|
@@ -150,7 +169,19 @@ function handleClassCapture(
|
|
|
150
169
|
|
|
151
170
|
/** Handle method_definition capture. */
|
|
152
171
|
function handleMethodCapture(c: Record<string, TreeSitterNode>, definitions: Definition[]): void {
|
|
153
|
-
const
|
|
172
|
+
const methNameNode = c.meth_name!;
|
|
173
|
+
let methName: string;
|
|
174
|
+
if (methNameNode.type === 'computed_property_name') {
|
|
175
|
+
// Extract the inner string literal from `['methodName']` or `["methodName"]`.
|
|
176
|
+
// Non-string computed keys (e.g. `[Symbol.iterator]`) cannot be resolved at
|
|
177
|
+
// dot-notation call sites, so skip them entirely.
|
|
178
|
+
const inner = methNameNode.child(1); // child(0)='[', child(1)=string, child(2)=']'
|
|
179
|
+
if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) return;
|
|
180
|
+
methName = inner.text.replace(/^['"]|['"]$/g, '');
|
|
181
|
+
if (!methName) return;
|
|
182
|
+
} else {
|
|
183
|
+
methName = methNameNode.text;
|
|
184
|
+
}
|
|
154
185
|
const parentClass = findParentClass(c.meth_node!);
|
|
155
186
|
const fullName = parentClass ? `${parentClass}.${methName}` : methName;
|
|
156
187
|
const methChildren = extractParameters(c.meth_node!);
|
|
@@ -177,7 +208,9 @@ function handleExportCapture(
|
|
|
177
208
|
const declType = decl.type;
|
|
178
209
|
const kindMap: Record<string, string> = {
|
|
179
210
|
function_declaration: 'function',
|
|
211
|
+
generator_function_declaration: 'function',
|
|
180
212
|
class_declaration: 'class',
|
|
213
|
+
abstract_class_declaration: 'class',
|
|
181
214
|
interface_declaration: 'interface',
|
|
182
215
|
type_alias_declaration: 'type',
|
|
183
216
|
};
|
|
@@ -296,6 +329,7 @@ function dispatchQueryMatch(
|
|
|
296
329
|
if (callInfo) calls.push(callInfo);
|
|
297
330
|
} else if (c.assign_node) {
|
|
298
331
|
handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
|
|
332
|
+
handleFuncPropAssignment(c.assign_left!, c.assign_right!, definitions);
|
|
299
333
|
}
|
|
300
334
|
}
|
|
301
335
|
|
|
@@ -306,6 +340,17 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
|
|
|
306
340
|
const classes: ClassRelation[] = [];
|
|
307
341
|
const exps: Export[] = [];
|
|
308
342
|
const typeMap: Map<string, TypeMapEntry> = new Map();
|
|
343
|
+
const returnTypeMap: Map<string, TypeMapEntry> = new Map();
|
|
344
|
+
const callAssignments: CallAssignment[] = [];
|
|
345
|
+
const fnRefBindings: FnRefBinding[] = [];
|
|
346
|
+
const paramBindings: ParamBinding[] = [];
|
|
347
|
+
const arrayElemBindings: ArrayElemBinding[] = [];
|
|
348
|
+
const spreadArgBindings: SpreadArgBinding[] = [];
|
|
349
|
+
const forOfBindings: ForOfBinding[] = [];
|
|
350
|
+
const arrayCallbackBindings: ArrayCallbackBinding[] = [];
|
|
351
|
+
const objectRestParamBindings: ObjectRestParamBinding[] = [];
|
|
352
|
+
const objectPropBindings: ObjectPropBinding[] = [];
|
|
353
|
+
const thisCallBindings: ThisCallBinding[] = [];
|
|
309
354
|
|
|
310
355
|
const matches = query.matches(tree.rootNode);
|
|
311
356
|
|
|
@@ -319,16 +364,69 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
|
|
|
319
364
|
// Extract top-level constants via targeted walk (query patterns don't cover these)
|
|
320
365
|
extractConstantsWalk(tree.rootNode, definitions);
|
|
321
366
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
367
|
+
// Phase 8.2: Extract function return types first — runContextCollectorWalk's
|
|
368
|
+
// declarator handler reads the *complete* per-file map for inter-procedural
|
|
369
|
+
// propagation, so this cannot be folded into that pass.
|
|
370
|
+
extractReturnTypeMapWalk(tree.rootNode, returnTypeMap);
|
|
371
|
+
|
|
372
|
+
// Context-tracking collector pass: typeMap (with return-type propagation),
|
|
373
|
+
// object-rest param bindings, and spread/for-of/Array.from bindings.
|
|
374
|
+
runContextCollectorWalk(tree.rootNode, {
|
|
375
|
+
typeMap,
|
|
376
|
+
returnTypeMap,
|
|
377
|
+
callAssignments,
|
|
378
|
+
fnRefBindings,
|
|
379
|
+
objectRestParamBindings,
|
|
380
|
+
spreadArgBindings,
|
|
381
|
+
forOfBindings,
|
|
382
|
+
arrayCallbackBindings,
|
|
383
|
+
});
|
|
327
384
|
|
|
328
385
|
// Extract definitions from destructured bindings (query patterns don't match object_pattern)
|
|
329
386
|
extractDestructuredBindingsWalk(tree.rootNode, definitions);
|
|
330
387
|
|
|
331
|
-
|
|
388
|
+
// Everything without bespoke traversal semantics is collected in ONE pass:
|
|
389
|
+
// dynamic import() calls, prototype-method definitions, param bindings,
|
|
390
|
+
// array-element bindings, object-prop bindings, `new X()` names,
|
|
391
|
+
// Object.defineProperty receivers, class members (fields/static blocks,
|
|
392
|
+
// which query patterns don't capture), and this()/call/apply bindings.
|
|
393
|
+
const newExpressions: string[] = [];
|
|
394
|
+
const definePropertyReceivers: Map<string, string> = new Map();
|
|
395
|
+
runCollectorWalk(tree.rootNode, {
|
|
396
|
+
definitions,
|
|
397
|
+
typeMap,
|
|
398
|
+
paramBindings,
|
|
399
|
+
arrayElemBindings,
|
|
400
|
+
objectPropBindings,
|
|
401
|
+
newExpressions,
|
|
402
|
+
definePropertyReceivers,
|
|
403
|
+
imports,
|
|
404
|
+
calls,
|
|
405
|
+
thisCallBindings,
|
|
406
|
+
classMemberDefs: definitions,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
definitions,
|
|
411
|
+
calls,
|
|
412
|
+
imports,
|
|
413
|
+
classes,
|
|
414
|
+
exports: exps,
|
|
415
|
+
typeMap,
|
|
416
|
+
returnTypeMap,
|
|
417
|
+
callAssignments,
|
|
418
|
+
fnRefBindings,
|
|
419
|
+
paramBindings,
|
|
420
|
+
arrayElemBindings,
|
|
421
|
+
spreadArgBindings,
|
|
422
|
+
forOfBindings,
|
|
423
|
+
arrayCallbackBindings,
|
|
424
|
+
objectRestParamBindings,
|
|
425
|
+
objectPropBindings,
|
|
426
|
+
thisCallBindings,
|
|
427
|
+
newExpressions,
|
|
428
|
+
...(definePropertyReceivers.size > 0 ? { definePropertyReceivers } : {}),
|
|
429
|
+
};
|
|
332
430
|
}
|
|
333
431
|
|
|
334
432
|
/** Node types that define a function scope — constants inside these are skipped. */
|
|
@@ -384,6 +482,11 @@ function extractConstantsWalk(node: TreeSitterNode, definitions: Definition[]):
|
|
|
384
482
|
}
|
|
385
483
|
}
|
|
386
484
|
|
|
485
|
+
// Class field definitions and static initializer blocks (which query patterns
|
|
486
|
+
// don't capture) are collected inline in runCollectorWalk's field_definition /
|
|
487
|
+
// class_static_block cases when `classMemberDefs` is set. The walk-based path
|
|
488
|
+
// (extractSymbolsWalk) handles these node types via walkJavaScriptNode instead.
|
|
489
|
+
|
|
387
490
|
/**
|
|
388
491
|
* Walk the AST to find destructured const bindings (query patterns don't match object_pattern).
|
|
389
492
|
* e.g. `const { handleToken, checkPermissions } = initAuth(config)`
|
|
@@ -407,7 +510,7 @@ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Defi
|
|
|
407
510
|
) {
|
|
408
511
|
for (let j = 0; j < declNode.childCount; j++) {
|
|
409
512
|
const declarator = declNode.child(j);
|
|
410
|
-
if (
|
|
513
|
+
if (declarator?.type !== 'variable_declarator') continue;
|
|
411
514
|
const nameN = declarator.childForFieldName('name');
|
|
412
515
|
if (nameN && nameN.type === 'object_pattern') {
|
|
413
516
|
extractDestructuredBindings(
|
|
@@ -416,6 +519,15 @@ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Defi
|
|
|
416
519
|
nodeEndLine(declNode),
|
|
417
520
|
definitions,
|
|
418
521
|
);
|
|
522
|
+
} else if (nameN && nameN.type === 'array_pattern') {
|
|
523
|
+
// `const [x, y] = ...` — emit a single constant node whose name is the
|
|
524
|
+
// full array pattern text (e.g. `[x, y]`), matching native engine behaviour.
|
|
525
|
+
definitions.push({
|
|
526
|
+
name: nameN.text,
|
|
527
|
+
kind: 'constant',
|
|
528
|
+
line: nodeStartLine(declNode),
|
|
529
|
+
endLine: nodeEndLine(declNode),
|
|
530
|
+
});
|
|
419
531
|
}
|
|
420
532
|
}
|
|
421
533
|
}
|
|
@@ -434,13 +546,18 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
|
|
|
434
546
|
|
|
435
547
|
for (let j = 0; j < declNode.childCount; j++) {
|
|
436
548
|
const declarator = declNode.child(j);
|
|
437
|
-
if (
|
|
549
|
+
if (declarator?.type !== 'variable_declarator') continue;
|
|
438
550
|
const nameN = declarator.childForFieldName('name');
|
|
439
551
|
const valueN = declarator.childForFieldName('value');
|
|
440
|
-
if (
|
|
552
|
+
if (nameN?.type !== 'identifier' || !valueN) continue;
|
|
441
553
|
// Skip functions — already captured by query patterns
|
|
442
554
|
const valType = valueN.type;
|
|
443
|
-
if (
|
|
555
|
+
if (
|
|
556
|
+
valType === 'arrow_function' ||
|
|
557
|
+
valType === 'function_expression' ||
|
|
558
|
+
valType === 'function' ||
|
|
559
|
+
valType === 'generator_function'
|
|
560
|
+
)
|
|
444
561
|
continue;
|
|
445
562
|
if (isConstantValue(valueN)) {
|
|
446
563
|
definitions.push({
|
|
@@ -449,6 +566,14 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
|
|
|
449
566
|
line: nodeStartLine(declNode),
|
|
450
567
|
endLine: nodeEndLine(declNode),
|
|
451
568
|
});
|
|
569
|
+
// Phase 8.3f: extract function/arrow properties from object literals.
|
|
570
|
+
// Scope guard: extractConstDeclarators is only called from extractConstantsWalk, which
|
|
571
|
+
// already skips const declarations inside function scopes (line ~412). So these definitions
|
|
572
|
+
// are always top-level. Any new call site must add a hasFunctionScopeAncestor guard
|
|
573
|
+
// (the walk path at handleVariableDecl does this).
|
|
574
|
+
if (valueN.type === 'object') {
|
|
575
|
+
extractObjectLiteralFunctions(valueN, nameN.text, definitions);
|
|
576
|
+
}
|
|
452
577
|
}
|
|
453
578
|
}
|
|
454
579
|
}
|
|
@@ -458,34 +583,36 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
|
|
|
458
583
|
* Query patterns match call_expression with identifier/member_expression/subscript_expression
|
|
459
584
|
* functions, but import() has function type `import` which none of those patterns cover.
|
|
460
585
|
*/
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Collect a dynamic `import()` call at `node` (a call_expression).
|
|
588
|
+
* Returns true when the node *is* an import() call — the collector walk uses
|
|
589
|
+
* this to suppress dynamic-import collection inside the import's own argument
|
|
590
|
+
* subtree, preserving the former standalone walk's "don't recurse into
|
|
591
|
+
* import() children" behaviour without hiding those children from the other
|
|
592
|
+
* collectors.
|
|
593
|
+
*/
|
|
594
|
+
function collectDynamicImport(node: TreeSitterNode, imports: Import[]): boolean {
|
|
595
|
+
const fn = node.childForFieldName('function');
|
|
596
|
+
if (fn?.type !== 'import') return false;
|
|
597
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
598
|
+
if (args) {
|
|
599
|
+
const strArg = findChild(args, 'string');
|
|
600
|
+
if (strArg) {
|
|
601
|
+
const modPath = strArg.text.replace(/['"]/g, '');
|
|
602
|
+
const names = extractDynamicImportNames(node);
|
|
603
|
+
imports.push({
|
|
604
|
+
source: modPath,
|
|
605
|
+
names,
|
|
606
|
+
line: nodeStartLine(node),
|
|
607
|
+
dynamicImport: true,
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
debug(
|
|
611
|
+
`Skipping non-static dynamic import() at line ${nodeStartLine(node)} (template literal or variable)`,
|
|
612
|
+
);
|
|
484
613
|
}
|
|
485
614
|
}
|
|
486
|
-
|
|
487
|
-
extractDynamicImportsWalk(node.child(i)!, imports);
|
|
488
|
-
}
|
|
615
|
+
return true;
|
|
489
616
|
}
|
|
490
617
|
|
|
491
618
|
function handleCommonJSAssignment(
|
|
@@ -556,22 +683,77 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput {
|
|
|
556
683
|
classes: [],
|
|
557
684
|
exports: [],
|
|
558
685
|
typeMap: new Map(),
|
|
686
|
+
returnTypeMap: new Map(),
|
|
687
|
+
callAssignments: [],
|
|
688
|
+
fnRefBindings: [],
|
|
689
|
+
paramBindings: [],
|
|
690
|
+
arrayElemBindings: [],
|
|
691
|
+
spreadArgBindings: [],
|
|
692
|
+
forOfBindings: [],
|
|
693
|
+
arrayCallbackBindings: [],
|
|
694
|
+
objectRestParamBindings: [],
|
|
695
|
+
objectPropBindings: [],
|
|
696
|
+
thisCallBindings: [],
|
|
559
697
|
};
|
|
560
698
|
|
|
561
699
|
walkJavaScriptNode(tree.rootNode, ctx);
|
|
562
|
-
//
|
|
563
|
-
|
|
700
|
+
// Phase 8.2: Extract function return types first — runContextCollectorWalk's
|
|
701
|
+
// declarator handler reads the *complete* per-file map for inter-procedural
|
|
702
|
+
// propagation, so this cannot be folded into that pass.
|
|
703
|
+
extractReturnTypeMapWalk(tree.rootNode, ctx.returnTypeMap!);
|
|
704
|
+
// Context-tracking collector pass: typeMap (with return-type propagation),
|
|
705
|
+
// object-rest param bindings, and spread/for-of/Array.from bindings.
|
|
706
|
+
runContextCollectorWalk(tree.rootNode, {
|
|
707
|
+
typeMap: ctx.typeMap!,
|
|
708
|
+
returnTypeMap: ctx.returnTypeMap,
|
|
709
|
+
callAssignments: ctx.callAssignments,
|
|
710
|
+
fnRefBindings: ctx.fnRefBindings!,
|
|
711
|
+
objectRestParamBindings: ctx.objectRestParamBindings!,
|
|
712
|
+
spreadArgBindings: ctx.spreadArgBindings!,
|
|
713
|
+
forOfBindings: ctx.forOfBindings!,
|
|
714
|
+
arrayCallbackBindings: ctx.arrayCallbackBindings!,
|
|
715
|
+
});
|
|
716
|
+
// Single collector pass for everything else: prototype-method and func-prop
|
|
717
|
+
// definitions, param bindings, array-element bindings, object-prop bindings,
|
|
718
|
+
// `new X()` names, and Object.defineProperty receivers. Dynamic imports,
|
|
719
|
+
// this()/call/apply bindings, and class members are omitted here —
|
|
720
|
+
// walkJavaScriptNode already covers those node types on this path.
|
|
721
|
+
const newExpressions: string[] = [];
|
|
722
|
+
const definePropertyReceivers: Map<string, string> = new Map();
|
|
723
|
+
runCollectorWalk(tree.rootNode, {
|
|
724
|
+
definitions: ctx.definitions,
|
|
725
|
+
typeMap: ctx.typeMap!,
|
|
726
|
+
paramBindings: ctx.paramBindings!,
|
|
727
|
+
arrayElemBindings: ctx.arrayElemBindings!,
|
|
728
|
+
objectPropBindings: ctx.objectPropBindings!,
|
|
729
|
+
newExpressions,
|
|
730
|
+
definePropertyReceivers,
|
|
731
|
+
funcPropDefs: ctx.definitions,
|
|
732
|
+
});
|
|
733
|
+
ctx.newExpressions = newExpressions;
|
|
734
|
+
if (definePropertyReceivers.size > 0) ctx.definePropertyReceivers = definePropertyReceivers;
|
|
564
735
|
return ctx;
|
|
565
736
|
}
|
|
566
737
|
|
|
567
738
|
function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
568
739
|
switch (node.type) {
|
|
569
740
|
case 'function_declaration':
|
|
741
|
+
case 'generator_function_declaration':
|
|
570
742
|
handleFunctionDecl(node, ctx);
|
|
571
743
|
break;
|
|
572
744
|
case 'class_declaration':
|
|
745
|
+
case 'abstract_class_declaration':
|
|
746
|
+
// class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
|
|
747
|
+
case 'class':
|
|
573
748
|
handleClassDecl(node, ctx);
|
|
574
749
|
break;
|
|
750
|
+
case 'class_static_block':
|
|
751
|
+
handleStaticBlock(node, ctx.definitions);
|
|
752
|
+
break;
|
|
753
|
+
case 'field_definition':
|
|
754
|
+
case 'public_field_definition':
|
|
755
|
+
handleFieldDef(node, ctx.definitions);
|
|
756
|
+
break;
|
|
575
757
|
case 'method_definition':
|
|
576
758
|
handleMethodDef(node, ctx);
|
|
577
759
|
break;
|
|
@@ -655,8 +837,20 @@ function handleClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
655
837
|
function handleMethodDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
656
838
|
const nameNode = node.childForFieldName('name');
|
|
657
839
|
if (nameNode) {
|
|
840
|
+
let methName: string;
|
|
841
|
+
if (nameNode.type === 'computed_property_name') {
|
|
842
|
+
// Extract the inner string literal from `['methodName']` or `["methodName"]`.
|
|
843
|
+
// Non-string computed keys (e.g. `[Symbol.iterator]`) cannot be resolved at
|
|
844
|
+
// dot-notation call sites, so skip them entirely.
|
|
845
|
+
const inner = nameNode.child(1); // child(0)='[', child(1)=string, child(2)=']'
|
|
846
|
+
if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) return;
|
|
847
|
+
methName = inner.text.replace(/^['"]|['"]$/g, '');
|
|
848
|
+
if (!methName) return;
|
|
849
|
+
} else {
|
|
850
|
+
methName = nameNode.text;
|
|
851
|
+
}
|
|
658
852
|
const parentClass = findParentClass(node);
|
|
659
|
-
const fullName = parentClass ? `${parentClass}.${
|
|
853
|
+
const fullName = parentClass ? `${parentClass}.${methName}` : methName;
|
|
660
854
|
const methChildren = extractParameters(node);
|
|
661
855
|
const methVis = extractVisibility(node);
|
|
662
856
|
ctx.definitions.push({
|
|
@@ -670,6 +864,69 @@ function handleMethodDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
670
864
|
}
|
|
671
865
|
}
|
|
672
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Create a synthetic `ClassName.<static:L:C>` definition for a class static block
|
|
869
|
+
* so that calls inside the block can be attributed to a method-kind node and
|
|
870
|
+
* `resolveThisDispatch` can walk up to the parent class for `super.method()`.
|
|
871
|
+
*
|
|
872
|
+
* The start line and column are appended to the name to ensure uniqueness when a
|
|
873
|
+
* class has multiple `static { }` blocks (each has a distinct start position even
|
|
874
|
+
* if on the same line).
|
|
875
|
+
*
|
|
876
|
+
* Tree-sitter uses `class_static_block` (not `static_block`) for `static { ... }`.
|
|
877
|
+
*/
|
|
878
|
+
function handleStaticBlock(node: TreeSitterNode, definitions: Definition[]): void {
|
|
879
|
+
const parentClass = findParentClass(node);
|
|
880
|
+
if (!parentClass) return;
|
|
881
|
+
const line = nodeStartLine(node);
|
|
882
|
+
const col = node.startPosition.column;
|
|
883
|
+
definitions.push({
|
|
884
|
+
name: `${parentClass}.<static:${line}:${col}>`,
|
|
885
|
+
kind: 'method',
|
|
886
|
+
line,
|
|
887
|
+
endLine: nodeEndLine(node),
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Emit a `ClassName.fieldName` definition for class fields that have an initializer.
|
|
893
|
+
* This lets `findCaller` attribute calls inside field initializers (e.g. static field
|
|
894
|
+
* side-effects) to the field rather than the enclosing class.
|
|
895
|
+
*
|
|
896
|
+
* JS `field_definition` uses the `'property'` field name; TS
|
|
897
|
+
* `public_field_definition` uses `'name'`. As a third fallback (Rust/TS parity) we
|
|
898
|
+
* also check for a positional `property_identifier` child.
|
|
899
|
+
*/
|
|
900
|
+
const CALLABLE_FIELD_TYPES = new Set([
|
|
901
|
+
'arrow_function',
|
|
902
|
+
'function_expression',
|
|
903
|
+
'generator_function',
|
|
904
|
+
]);
|
|
905
|
+
|
|
906
|
+
function handleFieldDef(node: TreeSitterNode, definitions: Definition[]): void {
|
|
907
|
+
// JS field_definition uses 'property' field; TS public_field_definition uses 'name' field
|
|
908
|
+
const nameNode =
|
|
909
|
+
node.childForFieldName('name') ||
|
|
910
|
+
node.childForFieldName('property') ||
|
|
911
|
+
findChild(node, 'property_identifier');
|
|
912
|
+
const valueNode = node.childForFieldName('value');
|
|
913
|
+
if (!nameNode || !valueNode) return;
|
|
914
|
+
if (nameNode.type === 'computed_property_name') return;
|
|
915
|
+
// Only emit a callable definition when the initializer is a function/arrow expression.
|
|
916
|
+
// Scalar fields like `static x = 42` should not appear as method-kind nodes.
|
|
917
|
+
if (!CALLABLE_FIELD_TYPES.has(valueNode.type)) return;
|
|
918
|
+
const fieldName = nameNode.text;
|
|
919
|
+
if (!fieldName) return;
|
|
920
|
+
const parentClass = findParentClass(node);
|
|
921
|
+
if (!parentClass) return;
|
|
922
|
+
definitions.push({
|
|
923
|
+
name: `${parentClass}.${fieldName}`,
|
|
924
|
+
kind: 'method',
|
|
925
|
+
line: nodeStartLine(node),
|
|
926
|
+
endLine: nodeEndLine(node),
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
673
930
|
function handleInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
674
931
|
const nameNode = node.childForFieldName('name');
|
|
675
932
|
if (!nameNode) return;
|
|
@@ -746,7 +1003,8 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
746
1003
|
if (
|
|
747
1004
|
valType === 'arrow_function' ||
|
|
748
1005
|
valType === 'function_expression' ||
|
|
749
|
-
valType === 'function'
|
|
1006
|
+
valType === 'function' ||
|
|
1007
|
+
valType === 'generator_function'
|
|
750
1008
|
) {
|
|
751
1009
|
const varFnChildren = extractParameters(valueN);
|
|
752
1010
|
ctx.definitions.push({
|
|
@@ -756,13 +1014,28 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
756
1014
|
endLine: nodeEndLine(valueN),
|
|
757
1015
|
children: varFnChildren.length > 0 ? varFnChildren : undefined,
|
|
758
1016
|
});
|
|
759
|
-
} else if (
|
|
1017
|
+
} else if (
|
|
1018
|
+
isConst &&
|
|
1019
|
+
nameN.type === 'identifier' &&
|
|
1020
|
+
isConstantValue(valueN) &&
|
|
1021
|
+
!hasFunctionScopeAncestor(node)
|
|
1022
|
+
) {
|
|
760
1023
|
ctx.definitions.push({
|
|
761
1024
|
name: nameN.text,
|
|
762
1025
|
kind: 'constant',
|
|
763
1026
|
line: nodeStartLine(node),
|
|
764
1027
|
endLine: nodeEndLine(node),
|
|
765
1028
|
});
|
|
1029
|
+
// Phase 8.3f: extract function/arrow properties from object literals so that
|
|
1030
|
+
// this.method() calls inside Object.defineProperty accessors can resolve them.
|
|
1031
|
+
// Scope guard: hasFunctionScopeAncestor mirrors the Rust path's find_parent_of_types
|
|
1032
|
+
// check and the sibling destructured-binding branch below — skips object literals
|
|
1033
|
+
// inside function bodies to avoid polluting the global definition index with
|
|
1034
|
+
// local variable properties (e.g. `localObj.fn` from `const localObj = { fn: ... }`
|
|
1035
|
+
// inside a function).
|
|
1036
|
+
if (valueN.type === 'object') {
|
|
1037
|
+
extractObjectLiteralFunctions(valueN, nameN.text, ctx.definitions);
|
|
1038
|
+
}
|
|
766
1039
|
} else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
|
|
767
1040
|
// Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
|
|
768
1041
|
// Each destructured property becomes a function definition so it can be
|
|
@@ -777,7 +1050,81 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
777
1050
|
nodeEndLine(node),
|
|
778
1051
|
ctx.definitions,
|
|
779
1052
|
);
|
|
1053
|
+
} else if (isConst && nameN.type === 'array_pattern' && !hasFunctionScopeAncestor(node)) {
|
|
1054
|
+
// Array destructuring: `const [x, y] = ...` — emit a single constant node
|
|
1055
|
+
// whose name is the full array pattern text (e.g. `[x, y]`), matching
|
|
1056
|
+
// native engine behaviour. Scope guard mirrors the object_pattern branch above.
|
|
1057
|
+
ctx.definitions.push({
|
|
1058
|
+
name: nameN.text,
|
|
1059
|
+
kind: 'constant',
|
|
1060
|
+
line: nodeStartLine(node),
|
|
1061
|
+
endLine: nodeEndLine(node),
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Phase 8.3f: extract function/arrow function properties from an object literal as standalone
|
|
1071
|
+
* definitions so that `this.method()` calls inside Object.defineProperty accessor functions can
|
|
1072
|
+
* resolve them via the same-file definition lookup.
|
|
1073
|
+
*
|
|
1074
|
+
* Definitions are emitted as qualified names (`obj.baz` rather than bare `baz`) to avoid
|
|
1075
|
+
* polluting the global definition index with common property names like `init`, `run`, or
|
|
1076
|
+
* `render`. The typeMap value stored by the caller also uses the qualified name so the resolver
|
|
1077
|
+
* looks up `lookup.byName('obj.baz')` rather than `lookup.byName('baz')`.
|
|
1078
|
+
*
|
|
1079
|
+
* `const obj = { baz: () => {} }` → emits Definition { name: 'obj.baz', kind: 'function' }
|
|
1080
|
+
*/
|
|
1081
|
+
function extractObjectLiteralFunctions(
|
|
1082
|
+
objNode: TreeSitterNode,
|
|
1083
|
+
varName: string,
|
|
1084
|
+
definitions: Definition[],
|
|
1085
|
+
): void {
|
|
1086
|
+
for (let i = 0; i < objNode.childCount; i++) {
|
|
1087
|
+
const child = objNode.child(i);
|
|
1088
|
+
if (!child) continue;
|
|
1089
|
+
if (child.type === 'pair') {
|
|
1090
|
+
const keyNode = child.childForFieldName('key');
|
|
1091
|
+
const valueNode = child.childForFieldName('value');
|
|
1092
|
+
if (!keyNode || !valueNode) continue;
|
|
1093
|
+
const keyName =
|
|
1094
|
+
keyNode.type === 'string' ? keyNode.text.replace(/^['"]|['"]$/g, '') : keyNode.text;
|
|
1095
|
+
if (!keyName) continue;
|
|
1096
|
+
if (
|
|
1097
|
+
valueNode.type === 'arrow_function' ||
|
|
1098
|
+
valueNode.type === 'function_expression' ||
|
|
1099
|
+
valueNode.type === 'function'
|
|
1100
|
+
) {
|
|
1101
|
+
definitions.push({
|
|
1102
|
+
name: `${varName}.${keyName}`,
|
|
1103
|
+
kind: 'function',
|
|
1104
|
+
line: nodeStartLine(child),
|
|
1105
|
+
endLine: nodeEndLine(valueNode),
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
} else if (child.type === 'method_definition') {
|
|
1109
|
+
const nameNode = child.childForFieldName('name');
|
|
1110
|
+
if (nameNode) {
|
|
1111
|
+
let methodName: string;
|
|
1112
|
+
if (nameNode.type === 'computed_property_name') {
|
|
1113
|
+
// Strip brackets+quotes from `['methodName']` to get a resolvable name.
|
|
1114
|
+
// Skip non-string computed keys (e.g. [Symbol.iterator]).
|
|
1115
|
+
const inner = nameNode.child(1);
|
|
1116
|
+
if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) continue;
|
|
1117
|
+
methodName = inner.text.replace(/^['"]|['"]$/g, '');
|
|
1118
|
+
if (!methodName) continue;
|
|
1119
|
+
} else {
|
|
1120
|
+
methodName = nameNode.text;
|
|
780
1121
|
}
|
|
1122
|
+
definitions.push({
|
|
1123
|
+
name: `${varName}.${methodName}`,
|
|
1124
|
+
kind: 'function',
|
|
1125
|
+
line: nodeStartLine(child),
|
|
1126
|
+
endLine: nodeEndLine(child),
|
|
1127
|
+
});
|
|
781
1128
|
}
|
|
782
1129
|
}
|
|
783
1130
|
}
|
|
@@ -819,11 +1166,44 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
819
1166
|
if (fn.type === 'import') {
|
|
820
1167
|
handleDynamicImportCall(node, ctx.imports);
|
|
821
1168
|
} else {
|
|
1169
|
+
// this() calls: `this` used as a function (not as a receiver).
|
|
1170
|
+
if (fn.type === 'this') {
|
|
1171
|
+
ctx.calls.push({ name: 'this', line: nodeStartLine(node) });
|
|
1172
|
+
return; // no further processing needed for this()-style calls
|
|
1173
|
+
}
|
|
822
1174
|
const callInfo = extractCallInfo(fn, node);
|
|
823
1175
|
if (callInfo) ctx.calls.push(callInfo);
|
|
824
1176
|
if (fn.type === 'member_expression') {
|
|
825
1177
|
const cbDef = extractCallbackDefinition(node, fn);
|
|
826
1178
|
if (cbDef) ctx.definitions.push(cbDef);
|
|
1179
|
+
// this-call bindings: `fn.call(namedCtx, ...)` / `fn.apply(namedCtx, ...)`
|
|
1180
|
+
const obj = fn.childForFieldName('object');
|
|
1181
|
+
const prop = fn.childForFieldName('property');
|
|
1182
|
+
if (
|
|
1183
|
+
obj?.type === 'identifier' &&
|
|
1184
|
+
prop &&
|
|
1185
|
+
(prop.text === 'call' || prop.text === 'apply') &&
|
|
1186
|
+
!BUILTIN_GLOBALS.has(obj.text)
|
|
1187
|
+
) {
|
|
1188
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
1189
|
+
if (args) {
|
|
1190
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
1191
|
+
const child = args.child(i);
|
|
1192
|
+
if (!child) continue;
|
|
1193
|
+
const t = child.type;
|
|
1194
|
+
if (t === '(' || t === ')' || t === ',') continue;
|
|
1195
|
+
if (
|
|
1196
|
+
t === 'identifier' &&
|
|
1197
|
+
!BUILTIN_GLOBALS.has(child.text) &&
|
|
1198
|
+
child.text !== 'undefined' &&
|
|
1199
|
+
child.text !== 'null'
|
|
1200
|
+
) {
|
|
1201
|
+
ctx.thisCallBindings!.push({ callee: obj.text, thisArg: child.text });
|
|
1202
|
+
}
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
827
1207
|
}
|
|
828
1208
|
ctx.calls.push(...extractCallbackReferenceCalls(node));
|
|
829
1209
|
}
|
|
@@ -878,7 +1258,9 @@ function handleExportStmt(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
878
1258
|
const declType = decl.type;
|
|
879
1259
|
const kindMap: Record<string, string> = {
|
|
880
1260
|
function_declaration: 'function',
|
|
1261
|
+
generator_function_declaration: 'function',
|
|
881
1262
|
class_declaration: 'class',
|
|
1263
|
+
abstract_class_declaration: 'class',
|
|
882
1264
|
interface_declaration: 'interface',
|
|
883
1265
|
type_alias_declaration: 'type',
|
|
884
1266
|
};
|
|
@@ -1103,7 +1485,7 @@ function extractSimpleTypeName(typeAnnotationNode: TreeSitterNode): string | nul
|
|
|
1103
1485
|
}
|
|
1104
1486
|
|
|
1105
1487
|
function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null {
|
|
1106
|
-
if (
|
|
1488
|
+
if (newExprNode?.type !== 'new_expression') return null;
|
|
1107
1489
|
const ctor = newExprNode.childForFieldName('constructor') || newExprNode.child(1);
|
|
1108
1490
|
if (!ctor) return null;
|
|
1109
1491
|
if (ctor.type === 'identifier') return ctor.text;
|
|
@@ -1114,43 +1496,529 @@ function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null {
|
|
|
1114
1496
|
return null;
|
|
1115
1497
|
}
|
|
1116
1498
|
|
|
1499
|
+
// ── Phase 8.2: Inter-Procedural Return Type Propagation ─────────────────────
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Walk the AST and record the return type of every function/method definition.
|
|
1503
|
+
*
|
|
1504
|
+
* Keys: plain name (e.g. "createUser") or "ClassName.methodName" for methods.
|
|
1505
|
+
* Confidence:
|
|
1506
|
+
* - 1.0: explicit TypeScript return type annotation
|
|
1507
|
+
* - 0.85: inferred from the first `return new Constructor()` in the body
|
|
1508
|
+
*/
|
|
1509
|
+
function extractReturnTypeMapWalk(
|
|
1510
|
+
rootNode: TreeSitterNode,
|
|
1511
|
+
returnTypeMap: Map<string, TypeMapEntry>,
|
|
1512
|
+
): void {
|
|
1513
|
+
function walk(node: TreeSitterNode, depth: number, currentClass: string | null): void {
|
|
1514
|
+
if (depth >= MAX_WALK_DEPTH) return;
|
|
1515
|
+
const t = node.type;
|
|
1516
|
+
|
|
1517
|
+
if (t === 'class_declaration' || t === 'abstract_class_declaration' || t === 'class') {
|
|
1518
|
+
const nameNode = node.childForFieldName('name');
|
|
1519
|
+
const className = nameNode?.text ?? null;
|
|
1520
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1521
|
+
walk(node.child(i)!, depth + 1, className);
|
|
1522
|
+
}
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (t === 'function_declaration' || t === 'generator_function_declaration') {
|
|
1527
|
+
const nameNode = node.childForFieldName('name');
|
|
1528
|
+
if (nameNode?.type === 'identifier' && nameNode.text !== 'constructor') {
|
|
1529
|
+
const fnName = currentClass ? `${currentClass}.${nameNode.text}` : nameNode.text;
|
|
1530
|
+
storeReturnType(node, fnName, returnTypeMap);
|
|
1531
|
+
}
|
|
1532
|
+
// Recurse into the function body with null currentClass so nested
|
|
1533
|
+
// function declarations are not stored under the enclosing class name.
|
|
1534
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1535
|
+
walk(node.child(i)!, depth + 1, null);
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
} else if (t === 'method_definition') {
|
|
1539
|
+
const nameNode = node.childForFieldName('name');
|
|
1540
|
+
if (nameNode && currentClass && nameNode.text !== 'constructor') {
|
|
1541
|
+
storeReturnType(node, `${currentClass}.${nameNode.text}`, returnTypeMap);
|
|
1542
|
+
}
|
|
1543
|
+
// Recurse into the method body with null currentClass so nested
|
|
1544
|
+
// function declarations are not stored under the enclosing class name.
|
|
1545
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1546
|
+
walk(node.child(i)!, depth + 1, null);
|
|
1547
|
+
}
|
|
1548
|
+
return;
|
|
1549
|
+
} else if (t === 'variable_declarator') {
|
|
1550
|
+
// const foo = (): ReturnType => … or const foo = function(): ReturnType { … }
|
|
1551
|
+
const nameN = node.childForFieldName('name');
|
|
1552
|
+
const valueN = node.childForFieldName('value');
|
|
1553
|
+
if (nameN?.type === 'identifier' && valueN) {
|
|
1554
|
+
const vt = valueN.type;
|
|
1555
|
+
if (
|
|
1556
|
+
vt === 'arrow_function' ||
|
|
1557
|
+
vt === 'function_expression' ||
|
|
1558
|
+
vt === 'generator_function'
|
|
1559
|
+
) {
|
|
1560
|
+
const fnName = currentClass ? `${currentClass}.${nameN.text}` : nameN.text;
|
|
1561
|
+
storeReturnType(valueN, fnName, returnTypeMap);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1567
|
+
walk(node.child(i)!, depth + 1, currentClass);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
walk(rootNode, 0, null);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/** Extract the return type of a function node and store it in the returnTypeMap. */
|
|
1574
|
+
function storeReturnType(
|
|
1575
|
+
fnNode: TreeSitterNode,
|
|
1576
|
+
fnName: string,
|
|
1577
|
+
returnTypeMap: Map<string, TypeMapEntry>,
|
|
1578
|
+
): void {
|
|
1579
|
+
const returnTypeNode = fnNode.childForFieldName('return_type');
|
|
1580
|
+
if (returnTypeNode) {
|
|
1581
|
+
const typeName = extractSimpleTypeName(returnTypeNode);
|
|
1582
|
+
if (typeName) {
|
|
1583
|
+
const existing = returnTypeMap.get(fnName);
|
|
1584
|
+
if (!existing || existing.confidence < 1.0)
|
|
1585
|
+
returnTypeMap.set(fnName, { type: typeName, confidence: 1.0 });
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
// Infer from first `return new Constructor()` in the function body
|
|
1590
|
+
const body = fnNode.childForFieldName('body');
|
|
1591
|
+
if (body) {
|
|
1592
|
+
const inferred = findReturnNewExprType(body);
|
|
1593
|
+
if (inferred) {
|
|
1594
|
+
const existing = returnTypeMap.get(fnName);
|
|
1595
|
+
if (!existing || 0.85 > existing.confidence)
|
|
1596
|
+
returnTypeMap.set(fnName, { type: inferred, confidence: 0.85 });
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** Return the constructor name from the first `return new Constructor()` in a body, or null. */
|
|
1602
|
+
function findReturnNewExprType(bodyNode: TreeSitterNode): string | null {
|
|
1603
|
+
for (let i = 0; i < bodyNode.childCount; i++) {
|
|
1604
|
+
const child = bodyNode.child(i);
|
|
1605
|
+
if (child?.type !== 'return_statement') continue;
|
|
1606
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
1607
|
+
const expr = child.child(j);
|
|
1608
|
+
if (expr?.type === 'new_expression') return extractNewExprTypeName(expr);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* Resolve the return type of a call_expression node using returnTypeMap.
|
|
1616
|
+
* Handles: createUser() (identifier), service.getRepo() (member), and
|
|
1617
|
+
* getService().getRepo() (chained call) up to MAX_PROPAGATION_DEPTH hops.
|
|
1618
|
+
*
|
|
1619
|
+
* `depth` tracks total chain hops consumed so far. Each call boundary — both
|
|
1620
|
+
* resolving the receiver and resolving the final return type — costs one hop.
|
|
1621
|
+
* Confidence = annotated return type confidence − 0.1 × (depth + 1).
|
|
1622
|
+
*
|
|
1623
|
+
* Examples (annotated sources → confidence 1.0):
|
|
1624
|
+
* createUser() depth=0 → 1.0 − 0.1 = 0.9 (1 hop)
|
|
1625
|
+
* svc.getUser() depth=0 → 1.0 − 0.1 = 0.9 (1 hop; receiver from typeMap)
|
|
1626
|
+
* getService().getRepo() depth=0 → inner resolved at depth=1, outer at depth+1 → 0.8 (2 hops)
|
|
1627
|
+
*/
|
|
1628
|
+
function resolveCallExprReturnType(
|
|
1629
|
+
callNode: TreeSitterNode,
|
|
1630
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
1631
|
+
returnTypeMap: Map<string, TypeMapEntry>,
|
|
1632
|
+
depth: number,
|
|
1633
|
+
): TypeMapEntry | null {
|
|
1634
|
+
if (depth >= MAX_PROPAGATION_DEPTH) return null;
|
|
1635
|
+
|
|
1636
|
+
const fn = callNode.childForFieldName('function');
|
|
1637
|
+
if (!fn) return null;
|
|
1638
|
+
|
|
1639
|
+
if (fn.type === 'identifier') {
|
|
1640
|
+
const entry = returnTypeMap.get(fn.text);
|
|
1641
|
+
if (!entry) return null;
|
|
1642
|
+
const confidence = entry.confidence - PROPAGATION_HOP_PENALTY * (depth + 1);
|
|
1643
|
+
return confidence > 0 ? { type: entry.type, confidence } : null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (fn.type === 'member_expression') {
|
|
1647
|
+
const obj = fn.childForFieldName('object');
|
|
1648
|
+
const prop = fn.childForFieldName('property');
|
|
1649
|
+
if (!obj || !prop) return null;
|
|
1650
|
+
|
|
1651
|
+
let receiverType: string | null = null;
|
|
1652
|
+
// effectiveDepth tracks the depth at which THIS call's return type is charged.
|
|
1653
|
+
// When the receiver is itself a call expression (chain), we've already consumed
|
|
1654
|
+
// a hop resolving it, so charge this call at depth+1.
|
|
1655
|
+
let effectiveDepth = depth;
|
|
1656
|
+
|
|
1657
|
+
if (obj.type === 'identifier') {
|
|
1658
|
+
const typeEntry = typeMap.get(obj.text);
|
|
1659
|
+
receiverType = typeEntry ? typeEntry.type : null;
|
|
1660
|
+
} else if (obj.type === 'call_expression') {
|
|
1661
|
+
// Each link in a call chain costs an extra hop.
|
|
1662
|
+
const innerResult = resolveCallExprReturnType(obj, typeMap, returnTypeMap, depth + 1);
|
|
1663
|
+
receiverType = innerResult ? innerResult.type : null;
|
|
1664
|
+
effectiveDepth = depth + 1;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (receiverType) {
|
|
1668
|
+
const entry = returnTypeMap.get(`${receiverType}.${prop.text}`);
|
|
1669
|
+
if (entry) {
|
|
1670
|
+
const confidence = entry.confidence - PROPAGATION_HOP_PENALTY * (effectiveDepth + 1);
|
|
1671
|
+
return confidence > 0 ? { type: entry.type, confidence } : null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Record a call assignment into callAssignments for cross-file propagation.
|
|
1681
|
+
* Only records cases where the callee is a simple identifier or a method call
|
|
1682
|
+
* on a known-typed variable — chain expressions are skipped (handled locally).
|
|
1683
|
+
*/
|
|
1684
|
+
function recordCallAssignment(
|
|
1685
|
+
callNode: TreeSitterNode,
|
|
1686
|
+
varName: string,
|
|
1687
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
1688
|
+
callAssignments: CallAssignment[],
|
|
1689
|
+
): void {
|
|
1690
|
+
const fn = callNode.childForFieldName('function');
|
|
1691
|
+
if (!fn) return;
|
|
1692
|
+
if (fn.type === 'identifier') {
|
|
1693
|
+
callAssignments.push({ varName, calleeName: fn.text });
|
|
1694
|
+
} else if (fn.type === 'member_expression') {
|
|
1695
|
+
const obj = fn.childForFieldName('object');
|
|
1696
|
+
const prop = fn.childForFieldName('property');
|
|
1697
|
+
if (obj?.type === 'identifier' && prop) {
|
|
1698
|
+
const receiverEntry = typeMap.get(obj.text);
|
|
1699
|
+
callAssignments.push({
|
|
1700
|
+
varName,
|
|
1701
|
+
calleeName: prop.text,
|
|
1702
|
+
receiverTypeName: receiverEntry?.type,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Phase 8.5 (RTA): collect all constructor names from `new X()` expressions
|
|
1710
|
+
* in the file. Captures both assigned (`const x = new Foo()`) and unassigned
|
|
1711
|
+
* (`doSomething(new Foo())`) usages that the typeMap-based approach would miss.
|
|
1712
|
+
*/
|
|
1713
|
+
// `new X()` constructor-name collection (Phase 8.5 RTA instantiation tracking)
|
|
1714
|
+
// happens inline in runCollectorWalk's new_expression case.
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Walk the AST to find `Object.defineProperty(obj, "bar", { get: getter })` patterns
|
|
1718
|
+
* and record which functions are used as getter/setter accessors for which objects.
|
|
1719
|
+
*
|
|
1720
|
+
* Result is stored in the provided map as `funcName → receiverVarName`.
|
|
1721
|
+
*/
|
|
1722
|
+
function collectDefinePropertyReceiver(node: TreeSitterNode, out: Map<string, string>): void {
|
|
1723
|
+
const fn = node.childForFieldName('function');
|
|
1724
|
+
// Match `Object.defineProperty`
|
|
1725
|
+
if (fn?.type !== 'member_expression') return;
|
|
1726
|
+
const obj = fn.childForFieldName('object');
|
|
1727
|
+
const prop = fn.childForFieldName('property');
|
|
1728
|
+
if (obj?.type !== 'identifier' || obj.text !== 'Object' || prop?.text !== 'defineProperty') {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const argsNode = node.childForFieldName('arguments') ?? findChild(node, 'arguments');
|
|
1732
|
+
if (!argsNode) return;
|
|
1733
|
+
// Collect non-punctuation children: arg0 (target obj), arg1 (prop name string), arg2 (descriptor)
|
|
1734
|
+
const argChildren: TreeSitterNode[] = [];
|
|
1735
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1736
|
+
const c = argsNode.child(i);
|
|
1737
|
+
if (!c) continue;
|
|
1738
|
+
if (c.type === ',' || c.type === '(' || c.type === ')') continue;
|
|
1739
|
+
argChildren.push(c);
|
|
1740
|
+
}
|
|
1741
|
+
if (argChildren.length < 3) return;
|
|
1742
|
+
const targetObj = argChildren[0];
|
|
1743
|
+
const descriptor = argChildren[2];
|
|
1744
|
+
if (targetObj?.type !== 'identifier' || descriptor?.type !== 'object') return;
|
|
1745
|
+
const targetName = targetObj.text;
|
|
1746
|
+
// Walk the descriptor object's pair children looking for get/set
|
|
1747
|
+
for (let i = 0; i < descriptor.childCount; i++) {
|
|
1748
|
+
const pair = descriptor.child(i);
|
|
1749
|
+
if (pair?.type !== 'pair') continue;
|
|
1750
|
+
const key = pair.childForFieldName('key');
|
|
1751
|
+
const val = pair.childForFieldName('value');
|
|
1752
|
+
if (
|
|
1753
|
+
key &&
|
|
1754
|
+
(key.text === 'get' || key.text === 'set') &&
|
|
1755
|
+
val?.type === 'identifier' &&
|
|
1756
|
+
!BUILTIN_GLOBALS.has(val.text)
|
|
1757
|
+
) {
|
|
1758
|
+
// Known limitation: if the same function is registered as an
|
|
1759
|
+
// accessor on multiple objects, last-write-wins — only the
|
|
1760
|
+
// last target object is retained. This is an unusual pattern
|
|
1761
|
+
// (sharing one function across multiple defineProperty calls)
|
|
1762
|
+
// and covering it would require Map<string, string[]> which
|
|
1763
|
+
// changes the consumer API. Tracked as a known edge case.
|
|
1764
|
+
out.set(val.text, targetName);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/** Outputs for {@link runContextCollectorWalk}. */
|
|
1770
|
+
interface ContextCollectorOutputs {
|
|
1771
|
+
typeMap: Map<string, TypeMapEntry>;
|
|
1772
|
+
returnTypeMap?: Map<string, TypeMapEntry>;
|
|
1773
|
+
callAssignments?: CallAssignment[];
|
|
1774
|
+
fnRefBindings: FnRefBinding[];
|
|
1775
|
+
objectRestParamBindings: ObjectRestParamBinding[];
|
|
1776
|
+
spreadArgBindings: SpreadArgBinding[];
|
|
1777
|
+
forOfBindings: ForOfBinding[];
|
|
1778
|
+
arrayCallbackBindings: ArrayCallbackBinding[];
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1117
1781
|
/**
|
|
1118
|
-
*
|
|
1782
|
+
* Single context-tracking pass combining what were three separate full-tree
|
|
1783
|
+
* walks (typeMap, object-rest params, spread/for-of) — see runCollectorWalk
|
|
1784
|
+
* for why traversal count dominates extraction cost on WASM trees.
|
|
1785
|
+
*
|
|
1786
|
+
* Each concern keeps its own enclosing-class register because their reset
|
|
1787
|
+
* rules intentionally differ:
|
|
1788
|
+
*
|
|
1789
|
+
* - typeMap (`typeMapClass`): extracts variable-to-type assignments.
|
|
1790
|
+
* Values are `{ type: string, confidence: number }`:
|
|
1791
|
+
* - 1.0: explicit constructor (`new Foo()`)
|
|
1792
|
+
* - 0.9: type annotation (`: Foo`) or typed parameter
|
|
1793
|
+
* - 0.85: property write (`obj.prop = fn` — Phase 8.3d pts tracking)
|
|
1794
|
+
* - 0.7–0.9: inter-procedural propagation from return-type map (Phase 8.2)
|
|
1795
|
+
* - 0.7: factory method call (`Foo.create()` — uppercase-first heuristic)
|
|
1796
|
+
* Higher-confidence entries take priority when the same variable is seen
|
|
1797
|
+
* twice. Class declarations propagate their name into the subtree; class
|
|
1798
|
+
* *expressions* (`const Foo = class Bar { … }`) propagate null because the
|
|
1799
|
+
* expression-internal name is never visible to the resolver, preserving the
|
|
1800
|
+
* `this.prop` fallback in resolveByMethodOrGlobal. No reset at function
|
|
1801
|
+
* boundaries.
|
|
1119
1802
|
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1803
|
+
* - object-rest params (`objectRestClass`, Phase 8.3f): context flows only
|
|
1804
|
+
* class_declaration/class → class_body → method_definition so methods are
|
|
1805
|
+
* keyed "ClassName.method"; every other node type resets to null, and
|
|
1806
|
+
* function/method bodies recurse with null so nested declarations don't
|
|
1807
|
+
* inherit the class context.
|
|
1124
1808
|
*
|
|
1125
|
-
*
|
|
1809
|
+
* - spread/for-of (`funcStack`/`classStack`, Phase 8.3e): tracks the
|
|
1810
|
+
* enclosing *function* (not just class) via push/pop so for-of bindings
|
|
1811
|
+
* record the qualified enclosing callable (e.g. 'Foo.bar', 'obj.method',
|
|
1812
|
+
* or '<module>' at top level).
|
|
1813
|
+
*
|
|
1814
|
+
* NOTE: returnTypeMap population stays a separate, earlier pass
|
|
1815
|
+
* (extractReturnTypeMapWalk) — handleVarDeclaratorTypeMap reads it for
|
|
1816
|
+
* inter-procedural propagation, so it must be complete for the whole file
|
|
1817
|
+
* before any declarator is processed (a function declared *after* its first
|
|
1818
|
+
* use would otherwise be missed).
|
|
1126
1819
|
*/
|
|
1127
|
-
function
|
|
1128
|
-
|
|
1820
|
+
function runContextCollectorWalk(rootNode: TreeSitterNode, out: ContextCollectorOutputs): void {
|
|
1821
|
+
const funcStack: string[] = [];
|
|
1822
|
+
const classStack: string[] = [];
|
|
1823
|
+
|
|
1824
|
+
const walk = (
|
|
1825
|
+
node: TreeSitterNode,
|
|
1826
|
+
depth: number,
|
|
1827
|
+
typeMapClass: string | null,
|
|
1828
|
+
objectRestClass: string | null,
|
|
1829
|
+
): void => {
|
|
1129
1830
|
if (depth >= MAX_WALK_DEPTH) return;
|
|
1130
1831
|
const t = node.type;
|
|
1832
|
+
|
|
1833
|
+
const isClassDecl = t === 'class_declaration' || t === 'abstract_class_declaration';
|
|
1834
|
+
const isClassExpr = t === 'class';
|
|
1835
|
+
const isFnDecl = t === 'function_declaration' || t === 'generator_function_declaration';
|
|
1836
|
+
|
|
1837
|
+
// Class name read once, shared by every concern that needs it below.
|
|
1838
|
+
let className: string | null = null;
|
|
1839
|
+
let classNameIsIdentifier = false;
|
|
1840
|
+
if (isClassDecl || isClassExpr) {
|
|
1841
|
+
const nameNode = node.childForFieldName('name');
|
|
1842
|
+
className = nameNode?.text ?? null;
|
|
1843
|
+
classNameIsIdentifier = nameNode?.type === 'identifier';
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// ── spread/for-of enclosing-context stacks (push on enter, pop after children) ──
|
|
1847
|
+
let pushedFunc = false;
|
|
1848
|
+
let pushedClass = false;
|
|
1849
|
+
if (isClassDecl || isClassExpr) {
|
|
1850
|
+
// The stack push keeps the original walk's `identifier`-only check (TS
|
|
1851
|
+
// class names parse as type_identifier and were never pushed), while
|
|
1852
|
+
// typeMapClass/objectRestClass below use the bare text like their
|
|
1853
|
+
// original walks did.
|
|
1854
|
+
if (className && classNameIsIdentifier) {
|
|
1855
|
+
classStack.push(className);
|
|
1856
|
+
pushedClass = true;
|
|
1857
|
+
}
|
|
1858
|
+
} else if (isFnDecl) {
|
|
1859
|
+
const nameNode = node.childForFieldName('name');
|
|
1860
|
+
if (nameNode?.type === 'identifier') {
|
|
1861
|
+
funcStack.push(nameNode.text);
|
|
1862
|
+
pushedFunc = true;
|
|
1863
|
+
}
|
|
1864
|
+
} else if (t === 'method_definition') {
|
|
1865
|
+
const nameNode = node.childForFieldName('name');
|
|
1866
|
+
if (nameNode) {
|
|
1867
|
+
// Qualify with the enclosing class name so the PTS key matches
|
|
1868
|
+
// callerName from findCaller (which uses def.name = 'ClassName.method').
|
|
1869
|
+
const enclosingClass = classStack.length > 0 ? classStack[classStack.length - 1] : null;
|
|
1870
|
+
let rawName: string;
|
|
1871
|
+
if (nameNode.type === 'computed_property_name') {
|
|
1872
|
+
const inner = nameNode.child(1);
|
|
1873
|
+
if (!inner || (inner.type !== 'string' && inner.type !== 'string_fragment')) {
|
|
1874
|
+
// Non-string computed key — skip adding to funcStack (no resolvable name).
|
|
1875
|
+
rawName = '';
|
|
1876
|
+
} else {
|
|
1877
|
+
rawName = inner.text.replace(/^['"]|['"]$/g, '');
|
|
1878
|
+
}
|
|
1879
|
+
} else {
|
|
1880
|
+
rawName = nameNode.text;
|
|
1881
|
+
}
|
|
1882
|
+
if (rawName) {
|
|
1883
|
+
const qualifiedName = enclosingClass ? `${enclosingClass}.${rawName}` : rawName;
|
|
1884
|
+
funcStack.push(qualifiedName);
|
|
1885
|
+
pushedFunc = true;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
} else if (t === 'variable_declarator') {
|
|
1889
|
+
// `const process = (arr) => { ... }` — arrow/expression functions assigned
|
|
1890
|
+
// to a variable have no `name` field on the function node itself.
|
|
1891
|
+
const nameNode = node.childForFieldName('name');
|
|
1892
|
+
const valueNode = node.childForFieldName('value');
|
|
1893
|
+
if (
|
|
1894
|
+
nameNode?.type === 'identifier' &&
|
|
1895
|
+
(valueNode?.type === 'arrow_function' || valueNode?.type === 'function_expression')
|
|
1896
|
+
) {
|
|
1897
|
+
funcStack.push(nameNode.text);
|
|
1898
|
+
pushedFunc = true;
|
|
1899
|
+
}
|
|
1900
|
+
} else if (t === 'assignment_expression') {
|
|
1901
|
+
// `obj.method = function() { ... }` — func-prop assignment.
|
|
1902
|
+
// Mirror handleFuncPropAssignment's logic so for-of loops inside the
|
|
1903
|
+
// body get the correct enclosingFunc (e.g. 'obj.method') instead of
|
|
1904
|
+
// '<module>' or the wrong outer function name.
|
|
1905
|
+
const lhs = node.childForFieldName('left');
|
|
1906
|
+
const rhs = node.childForFieldName('right');
|
|
1907
|
+
if (
|
|
1908
|
+
lhs?.type === 'member_expression' &&
|
|
1909
|
+
(rhs?.type === 'function_expression' || rhs?.type === 'arrow_function')
|
|
1910
|
+
) {
|
|
1911
|
+
const obj = lhs.childForFieldName('object');
|
|
1912
|
+
const prop = lhs.childForFieldName('property');
|
|
1913
|
+
if (
|
|
1914
|
+
obj?.type === 'identifier' &&
|
|
1915
|
+
(prop?.type === 'property_identifier' || prop?.type === 'identifier') &&
|
|
1916
|
+
!BUILTIN_GLOBALS.has(obj.text) &&
|
|
1917
|
+
prop.text !== 'prototype'
|
|
1918
|
+
) {
|
|
1919
|
+
funcStack.push(`${obj.text}.${prop.text}`);
|
|
1920
|
+
pushedFunc = true;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// ── per-node collectors (class nodes match none of these types) ──
|
|
1131
1926
|
if (t === 'variable_declarator') {
|
|
1132
|
-
handleVarDeclaratorTypeMap(
|
|
1927
|
+
handleVarDeclaratorTypeMap(
|
|
1928
|
+
node,
|
|
1929
|
+
out.typeMap,
|
|
1930
|
+
out.returnTypeMap,
|
|
1931
|
+
out.callAssignments,
|
|
1932
|
+
out.fnRefBindings,
|
|
1933
|
+
);
|
|
1934
|
+
collectCollectionWrapBinding(node, out.fnRefBindings);
|
|
1133
1935
|
} else if (t === 'required_parameter' || t === 'optional_parameter') {
|
|
1134
|
-
handleParamTypeMap(node, typeMap);
|
|
1936
|
+
handleParamTypeMap(node, out.typeMap);
|
|
1937
|
+
} else if (t === 'public_field_definition' || t === 'field_definition') {
|
|
1938
|
+
handleFieldDefTypeMap(node, out.typeMap, typeMapClass);
|
|
1939
|
+
} else if (t === 'assignment_expression') {
|
|
1940
|
+
handlePropWriteTypeMap(node, out.typeMap, typeMapClass);
|
|
1941
|
+
} else if (t === 'call_expression') {
|
|
1942
|
+
handleDefinePropertyTypeMap(node, out.typeMap);
|
|
1943
|
+
collectSpreadAndArrayFromBindings(node, out.spreadArgBindings, out.arrayCallbackBindings);
|
|
1944
|
+
} else if (t === 'for_in_statement') {
|
|
1945
|
+
const enclosingFunc = funcStack.length > 0 ? funcStack[funcStack.length - 1]! : '<module>';
|
|
1946
|
+
collectForOfBinding(node, enclosingFunc, out.forOfBindings);
|
|
1135
1947
|
}
|
|
1948
|
+
collectObjectRestParams(node, t, objectRestClass, out.objectRestParamBindings);
|
|
1949
|
+
|
|
1950
|
+
// ── child context per concern ──
|
|
1951
|
+
const childTypeMapClass = isClassDecl ? className : isClassExpr ? null : typeMapClass;
|
|
1952
|
+
let childObjectRestClass: string | null = null;
|
|
1953
|
+
if (t === 'class_declaration' || t === 'class') {
|
|
1954
|
+
childObjectRestClass = className;
|
|
1955
|
+
} else if (t === 'class_body') {
|
|
1956
|
+
childObjectRestClass = objectRestClass;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1136
1959
|
for (let i = 0; i < node.childCount; i++) {
|
|
1137
|
-
walk(node.child(i)!, depth + 1);
|
|
1960
|
+
walk(node.child(i)!, depth + 1, childTypeMapClass, childObjectRestClass);
|
|
1138
1961
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1962
|
+
|
|
1963
|
+
if (pushedFunc) funcStack.pop();
|
|
1964
|
+
if (pushedClass) classStack.pop();
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
walk(rootNode, 0, null, null);
|
|
1141
1968
|
}
|
|
1142
1969
|
|
|
1143
1970
|
/** Extract type info from a variable_declarator: type annotation, constructor, or factory. */
|
|
1144
1971
|
function handleVarDeclaratorTypeMap(
|
|
1145
1972
|
node: TreeSitterNode,
|
|
1146
1973
|
typeMap: Map<string, TypeMapEntry>,
|
|
1974
|
+
returnTypeMap?: Map<string, TypeMapEntry>,
|
|
1975
|
+
callAssignments?: CallAssignment[],
|
|
1976
|
+
fnRefBindings?: FnRefBinding[],
|
|
1147
1977
|
): void {
|
|
1148
1978
|
const nameN = node.childForFieldName('name');
|
|
1149
|
-
if (
|
|
1979
|
+
if (nameN?.type !== 'identifier') return;
|
|
1150
1980
|
|
|
1151
1981
|
const typeAnno = findChild(node, 'type_annotation');
|
|
1152
1982
|
const valueN = node.childForFieldName('value');
|
|
1153
1983
|
|
|
1984
|
+
// Phase 8.3: record function-reference bindings before any type-analysis early returns.
|
|
1985
|
+
// Captures `const fn = handler` (identifier) and `const fn = obj.method` (member_expression).
|
|
1986
|
+
// Also handles `const f = fn.bind(ctx)` — bind returns a new function aliasing fn.
|
|
1987
|
+
if (fnRefBindings && valueN) {
|
|
1988
|
+
if (valueN.type === 'identifier' && !BUILTIN_GLOBALS.has(valueN.text)) {
|
|
1989
|
+
fnRefBindings.push({ lhs: nameN.text, rhs: valueN.text });
|
|
1990
|
+
} else if (valueN.type === 'member_expression') {
|
|
1991
|
+
const prop = valueN.childForFieldName('property');
|
|
1992
|
+
const obj = valueN.childForFieldName('object');
|
|
1993
|
+
// Guard: only static property access (property_identifier or identifier), not
|
|
1994
|
+
// computed subscript expressions like obj[expr] where prop.text would be the
|
|
1995
|
+
// full expression rather than a simple name — those can never match pts keys.
|
|
1996
|
+
if (
|
|
1997
|
+
prop &&
|
|
1998
|
+
(prop.type === 'property_identifier' || prop.type === 'identifier') &&
|
|
1999
|
+
obj?.type === 'identifier' &&
|
|
2000
|
+
!BUILTIN_GLOBALS.has(obj.text)
|
|
2001
|
+
) {
|
|
2002
|
+
fnRefBindings.push({ lhs: nameN.text, rhs: prop.text, rhsReceiver: obj.text });
|
|
2003
|
+
}
|
|
2004
|
+
} else if (valueN.type === 'call_expression') {
|
|
2005
|
+
// `const f = fn.bind(ctx)` — bind returns a bound copy of fn; track f → fn so
|
|
2006
|
+
// pts(f) ⊇ pts(fn) and subsequent `f(args)` calls resolve to fn.
|
|
2007
|
+
// Note: only flat-identifier binds (fn.bind) are tracked here; method-receiver
|
|
2008
|
+
// binds like `obj.method.bind(ctx)` are not captured (boundFn must be an identifier).
|
|
2009
|
+
const callFn = valueN.childForFieldName('function');
|
|
2010
|
+
if (callFn?.type === 'member_expression') {
|
|
2011
|
+
const bindProp = callFn.childForFieldName('property');
|
|
2012
|
+
if (bindProp?.text === 'bind') {
|
|
2013
|
+
const boundFn = callFn.childForFieldName('object');
|
|
2014
|
+
if (boundFn?.type === 'identifier' && !BUILTIN_GLOBALS.has(boundFn.text)) {
|
|
2015
|
+
fnRefBindings.push({ lhs: nameN.text, rhs: boundFn.text });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1154
2022
|
// Constructor on the same declaration wins over annotation: the runtime type is
|
|
1155
2023
|
// what matters for call resolution (e.g. `const x: Base = new Derived()` should
|
|
1156
2024
|
// resolve `x.render()` to `Derived.render`, not `Base.render`).
|
|
@@ -1173,15 +2041,50 @@ function handleVarDeclaratorTypeMap(
|
|
|
1173
2041
|
}
|
|
1174
2042
|
|
|
1175
2043
|
if (!valueN) return;
|
|
1176
|
-
|
|
1177
|
-
// Constructor already handled above — only factory path remains.
|
|
1178
2044
|
if (valueN.type === 'new_expression') return;
|
|
1179
|
-
|
|
1180
|
-
|
|
2045
|
+
|
|
2046
|
+
if (valueN.type === 'call_expression') {
|
|
2047
|
+
// Phase 8.3e: Object.create({ f1, f2 }) — seed composite pts keys obj.f1 → f1, etc.
|
|
2048
|
+
const createFn = valueN.childForFieldName('function');
|
|
2049
|
+
if (createFn?.type === 'member_expression') {
|
|
2050
|
+
const createObj = createFn.childForFieldName('object');
|
|
2051
|
+
const createProp = createFn.childForFieldName('property');
|
|
2052
|
+
if (createObj?.text === 'Object' && createProp?.text === 'create') {
|
|
2053
|
+
const createArgs = valueN.childForFieldName('arguments') || findChild(valueN, 'arguments');
|
|
2054
|
+
if (createArgs) {
|
|
2055
|
+
let proto: TreeSitterNode | null = null;
|
|
2056
|
+
for (let i = 0; i < createArgs.childCount; i++) {
|
|
2057
|
+
const n = createArgs.child(i);
|
|
2058
|
+
if (n && n.type !== '(' && n.type !== ')' && n.type !== ',') {
|
|
2059
|
+
proto = n;
|
|
2060
|
+
break;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (proto?.type === 'object') {
|
|
2064
|
+
seedProtoProperties(nameN.text, proto, typeMap);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
// Phase 8.2: inter-procedural propagation — try to resolve return type from
|
|
2071
|
+
// the local returnTypeMap before falling back to factory heuristics.
|
|
2072
|
+
if (returnTypeMap) {
|
|
2073
|
+
const result = resolveCallExprReturnType(valueN, typeMap, returnTypeMap, 0);
|
|
2074
|
+
if (result) {
|
|
2075
|
+
setTypeMapEntry(typeMap, nameN.text, result.type, result.confidence);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
// Record for cross-file resolution in build-edges.ts (imported functions)
|
|
2080
|
+
if (callAssignments) {
|
|
2081
|
+
recordCallAssignment(valueN, nameN.text, typeMap, callAssignments);
|
|
2082
|
+
}
|
|
2083
|
+
// Factory method heuristic: const x = Foo.create() → type Foo, confidence 0.7
|
|
1181
2084
|
const fn = valueN.childForFieldName('function');
|
|
1182
|
-
if (fn
|
|
2085
|
+
if (fn?.type === 'member_expression') {
|
|
1183
2086
|
const obj = fn.childForFieldName('object');
|
|
1184
|
-
if (obj
|
|
2087
|
+
if (obj?.type === 'identifier') {
|
|
1185
2088
|
const objName = obj.text;
|
|
1186
2089
|
if (
|
|
1187
2090
|
objName[0] &&
|
|
@@ -1193,13 +2096,63 @@ function handleVarDeclaratorTypeMap(
|
|
|
1193
2096
|
}
|
|
1194
2097
|
}
|
|
1195
2098
|
}
|
|
1196
|
-
}
|
|
1197
2099
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const
|
|
2100
|
+
// Phase 8.3f: seed composite pts keys for object literal properties.
|
|
2101
|
+
// `const obj = { baz: () => {} }` → typeMap['obj.baz'] = 'obj.baz'
|
|
2102
|
+
// `const obj = { baz }` (shorthand) → typeMap['obj.baz'] = 'baz' (bare identifier target)
|
|
2103
|
+
// `const obj = { baz: otherFn }` → typeMap['obj.baz'] = 'otherFn' (identifier alias)
|
|
2104
|
+
//
|
|
2105
|
+
// For function/arrow values, the value is the qualified name ('obj.baz') because
|
|
2106
|
+
// extractObjectLiteralFunctions now registers definitions under that qualified name to avoid
|
|
2107
|
+
// polluting the global index with bare property names like 'init', 'run', or 'render'.
|
|
2108
|
+
// Enables accessor this-dispatch: when typeMap['getter:this'] = 'obj',
|
|
2109
|
+
// resolving this.baz() inside getter → typeMap['obj.baz'] → 'obj.baz' → lookup.byName('obj.baz').
|
|
2110
|
+
//
|
|
2111
|
+
// Scope guard: mirrors Rust handle_var_decl's find_parent_of_types check — skip object literals
|
|
2112
|
+
// inside function bodies so function-scoped `const localObj = { fn: ... }` never seeds
|
|
2113
|
+
// the typeMap (which would shadow a module-level `const obj` with the same property names).
|
|
2114
|
+
if (valueN.type === 'object' && !hasFunctionScopeAncestor(node)) {
|
|
2115
|
+
for (let i = 0; i < valueN.childCount; i++) {
|
|
2116
|
+
const child = valueN.child(i);
|
|
2117
|
+
if (!child) continue;
|
|
2118
|
+
if (child.type === 'shorthand_property_identifier') {
|
|
2119
|
+
setTypeMapEntry(typeMap, `${nameN.text}.${child.text}`, child.text, 0.85);
|
|
2120
|
+
} else if (child.type === 'pair') {
|
|
2121
|
+
const keyNode = child.childForFieldName('key');
|
|
2122
|
+
const valNode = child.childForFieldName('value');
|
|
2123
|
+
if (!keyNode || !valNode) continue;
|
|
2124
|
+
const keyName =
|
|
2125
|
+
keyNode.type === 'string' ? keyNode.text.replace(/^['"]|['"]$/g, '') : keyNode.text;
|
|
2126
|
+
if (!keyName) continue;
|
|
2127
|
+
const qualifiedKey = `${nameN.text}.${keyName}`;
|
|
2128
|
+
if (
|
|
2129
|
+
valNode.type === 'arrow_function' ||
|
|
2130
|
+
valNode.type === 'function_expression' ||
|
|
2131
|
+
valNode.type === 'function'
|
|
2132
|
+
) {
|
|
2133
|
+
// Store the qualified name so the resolver finds the qualified definition.
|
|
2134
|
+
setTypeMapEntry(typeMap, qualifiedKey, qualifiedKey, 0.85);
|
|
2135
|
+
} else if (valNode.type === 'identifier') {
|
|
2136
|
+
setTypeMapEntry(typeMap, qualifiedKey, valNode.text, 0.85);
|
|
2137
|
+
}
|
|
2138
|
+
} else if (child.type === 'method_definition') {
|
|
2139
|
+
// Method shorthand: `const obj = { baz() {} }` → typeMap['obj.baz'] = 'obj.baz'
|
|
2140
|
+
// extractObjectLiteralFunctions registers a definition under the qualified name;
|
|
2141
|
+
// seed the matching typeMap entry so the two-step accessor dispatch finds it.
|
|
2142
|
+
const nameNode = child.childForFieldName('name');
|
|
2143
|
+
if (!nameNode) continue;
|
|
2144
|
+
const qualifiedKey = `${nameN.text}.${nameNode.text}`;
|
|
2145
|
+
setTypeMapEntry(typeMap, qualifiedKey, qualifiedKey, 0.85);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/** Extract type info from a required_parameter or optional_parameter. */
|
|
2152
|
+
function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEntry>): void {
|
|
2153
|
+
const nameNode =
|
|
1201
2154
|
node.childForFieldName('pattern') || node.childForFieldName('left') || node.child(0);
|
|
1202
|
-
if (
|
|
2155
|
+
if (nameNode?.type !== 'identifier') return;
|
|
1203
2156
|
const typeAnno = findChild(node, 'type_annotation');
|
|
1204
2157
|
if (typeAnno) {
|
|
1205
2158
|
const typeName = extractSimpleTypeName(typeAnno);
|
|
@@ -1207,10 +2160,577 @@ function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEn
|
|
|
1207
2160
|
}
|
|
1208
2161
|
}
|
|
1209
2162
|
|
|
2163
|
+
/**
|
|
2164
|
+
* Extract type info from a class field declaration: `private repo: Repository<User>`.
|
|
2165
|
+
*
|
|
2166
|
+
* Seeds a class-scoped key `ClassName.field` (confidence 0.9) as the primary entry
|
|
2167
|
+
* so that two classes with identically-named fields don't overwrite each other's
|
|
2168
|
+
* typeMap entry (issue #1458). The resolver's `CallerClass.X` fallback (call-resolver.ts
|
|
2169
|
+
* line 110) looks up exactly this key.
|
|
2170
|
+
*
|
|
2171
|
+
* Bare `field` and `this.field` keys are kept at lower confidence (0.6) as fallbacks
|
|
2172
|
+
* for single-class files where the resolver may not have a callerClass context.
|
|
2173
|
+
*
|
|
2174
|
+
* Mirrors the field_definition branch of match_js_type_map in
|
|
2175
|
+
* crates/codegraph-core/src/extractors/javascript.rs.
|
|
2176
|
+
*/
|
|
2177
|
+
function handleFieldDefTypeMap(
|
|
2178
|
+
node: TreeSitterNode,
|
|
2179
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
2180
|
+
currentClass: string | null,
|
|
2181
|
+
): void {
|
|
2182
|
+
const nameNode =
|
|
2183
|
+
node.childForFieldName('name') ||
|
|
2184
|
+
node.childForFieldName('property') ||
|
|
2185
|
+
findChild(node, 'property_identifier');
|
|
2186
|
+
if (!nameNode) return;
|
|
2187
|
+
const kind = nameNode.type;
|
|
2188
|
+
if (
|
|
2189
|
+
kind !== 'property_identifier' &&
|
|
2190
|
+
kind !== 'identifier' &&
|
|
2191
|
+
kind !== 'private_property_identifier'
|
|
2192
|
+
)
|
|
2193
|
+
return;
|
|
2194
|
+
const typeAnno = findChild(node, 'type_annotation');
|
|
2195
|
+
if (!typeAnno) return;
|
|
2196
|
+
const typeName = extractSimpleTypeName(typeAnno);
|
|
2197
|
+
if (!typeName) return;
|
|
2198
|
+
if (currentClass) {
|
|
2199
|
+
// Primary: class-scoped key prevents cross-class collision (issue #1458).
|
|
2200
|
+
setTypeMapEntry(typeMap, `${currentClass}.${nameNode.text}`, typeName, 0.9);
|
|
2201
|
+
// Fallback: bare keys at lower confidence for single-class files or when
|
|
2202
|
+
// the resolver does not have a callerClass in scope.
|
|
2203
|
+
setTypeMapEntry(typeMap, nameNode.text, typeName, 0.6);
|
|
2204
|
+
setTypeMapEntry(typeMap, `this.${nameNode.text}`, typeName, 0.6);
|
|
2205
|
+
} else {
|
|
2206
|
+
// No enclosing class declaration (e.g. class expression) — use bare keys only.
|
|
2207
|
+
setTypeMapEntry(typeMap, nameNode.text, typeName, 0.9);
|
|
2208
|
+
setTypeMapEntry(typeMap, `this.${nameNode.text}`, typeName, 0.9);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
/**
|
|
2213
|
+
* Phase 8.3d: seed the pts map from object property writes.
|
|
2214
|
+
*
|
|
2215
|
+
* `handlers.auth = authMiddleware` → typeMap.set('handlers.auth', { type: 'authMiddleware', confidence: 0.85 })
|
|
2216
|
+
* `this.logger = new Logger(...)` → typeMap.set('UserService.logger', { type: 'Logger', confidence: 1.0 })
|
|
2217
|
+
* (keyed as ClassName.prop when currentClass is known, to avoid collisions across classes)
|
|
2218
|
+
*
|
|
2219
|
+
* Only simple `obj.prop = identifier` and `this.prop = new Ctor()` writes are tracked
|
|
2220
|
+
* (not chained `a.b.c = x`). BUILTIN_GLOBALS are skipped (e.g. `console.log = fn`).
|
|
2221
|
+
*/
|
|
2222
|
+
function handlePropWriteTypeMap(
|
|
2223
|
+
node: TreeSitterNode,
|
|
2224
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
2225
|
+
currentClass: string | null,
|
|
2226
|
+
): void {
|
|
2227
|
+
const lhsN = node.childForFieldName('left');
|
|
2228
|
+
const rhsN = node.childForFieldName('right');
|
|
2229
|
+
if (!lhsN || !rhsN) return;
|
|
2230
|
+
if (lhsN.type !== 'member_expression') return;
|
|
2231
|
+
|
|
2232
|
+
const obj = lhsN.childForFieldName('object');
|
|
2233
|
+
const prop = lhsN.childForFieldName('property');
|
|
2234
|
+
if (!obj || !prop) return;
|
|
2235
|
+
// Guard: only static property access (property_identifier or identifier), not
|
|
2236
|
+
// computed subscript expressions — consistent with the adjacent fnRefBindings block.
|
|
2237
|
+
if (prop.type !== 'property_identifier' && prop.type !== 'identifier') return;
|
|
2238
|
+
|
|
2239
|
+
// this.prop = new ClassName(...) — constructor-assigned property type.
|
|
2240
|
+
// Key as ClassName.prop (class-scoped) so two classes with identically-named
|
|
2241
|
+
// properties don't overwrite each other's typeMap entry.
|
|
2242
|
+
if (obj.type === 'this' && rhsN.type === 'new_expression') {
|
|
2243
|
+
const ctorType = extractNewExprTypeName(rhsN);
|
|
2244
|
+
if (ctorType) {
|
|
2245
|
+
const key = currentClass ? `${currentClass}.${prop.text}` : `this.${prop.text}`;
|
|
2246
|
+
setTypeMapEntry(typeMap, key, ctorType, 1.0);
|
|
2247
|
+
}
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// obj.prop = identifier — existing behaviour (skip chained a.b.c = x and builtins)
|
|
2252
|
+
if (rhsN.type !== 'identifier') return;
|
|
2253
|
+
if (obj.type !== 'identifier') return;
|
|
2254
|
+
const objName = obj.text;
|
|
2255
|
+
if (BUILTIN_GLOBALS.has(objName)) return;
|
|
2256
|
+
setTypeMapEntry(typeMap, `${objName}.${prop.text}`, rhsN.text, 0.85);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/**
|
|
2260
|
+
* Phase 8.3e/8.3f: seed composite pts keys from Object.defineProperty / defineProperties.
|
|
2261
|
+
*
|
|
2262
|
+
* `Object.defineProperty(obj, "key", { value: fn })` → typeMap.set('obj.key', fn, 0.85)
|
|
2263
|
+
* `Object.defineProperties(obj, { "k1": { value: v1 } })` → typeMap.set('obj.k1', v1, 0.85)
|
|
2264
|
+
* `Object.defineProperty(obj, "key", { get: getter })` → typeMap.set('getter:this', obj, 0.85)
|
|
2265
|
+
*/
|
|
2266
|
+
function handleDefinePropertyTypeMap(
|
|
2267
|
+
node: TreeSitterNode,
|
|
2268
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
2269
|
+
): void {
|
|
2270
|
+
const fn = node.childForFieldName('function');
|
|
2271
|
+
if (fn?.type !== 'member_expression') return;
|
|
2272
|
+
const fnObj = fn.childForFieldName('object');
|
|
2273
|
+
const fnProp = fn.childForFieldName('property');
|
|
2274
|
+
if (fnObj?.text !== 'Object') return;
|
|
2275
|
+
const method = fnProp?.text;
|
|
2276
|
+
if (method !== 'defineProperty' && method !== 'defineProperties') return;
|
|
2277
|
+
|
|
2278
|
+
const argsNode = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
2279
|
+
if (!argsNode) return;
|
|
2280
|
+
|
|
2281
|
+
const args: TreeSitterNode[] = [];
|
|
2282
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
2283
|
+
const n = argsNode.child(i);
|
|
2284
|
+
if (n && n.type !== '(' && n.type !== ')' && n.type !== ',') args.push(n);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
if (method === 'defineProperty') {
|
|
2288
|
+
if (args.length < 3) return;
|
|
2289
|
+
const arg0 = args[0]!,
|
|
2290
|
+
arg1 = args[1]!,
|
|
2291
|
+
arg2 = args[2]!;
|
|
2292
|
+
if (arg0.type !== 'identifier') return;
|
|
2293
|
+
if (arg1.type !== 'string') return;
|
|
2294
|
+
const key = arg1.text.replace(/^['"]|['"]$/g, '');
|
|
2295
|
+
if (!key) return;
|
|
2296
|
+
// Phase 8.3e: { value: fn } → obj.key pts to fn
|
|
2297
|
+
const target = findDescriptorValue(arg2);
|
|
2298
|
+
if (target) {
|
|
2299
|
+
setTypeMapEntry(typeMap, `${arg0.text}.${key}`, target, 0.85);
|
|
2300
|
+
}
|
|
2301
|
+
// Phase 8.3f: { get: getter } and/or { set: setter } → this inside each accessor is arg0 (obj)
|
|
2302
|
+
// Key format: '<accessorName>:this' — colon is a reserved separator used only by this phase.
|
|
2303
|
+
// JS identifiers cannot contain ':', so this key never collides with real variable names.
|
|
2304
|
+
for (const accessor of findDescriptorAccessors(arg2)) {
|
|
2305
|
+
setTypeMapEntry(typeMap, `${accessor}:this`, arg0.text, 0.85);
|
|
2306
|
+
}
|
|
2307
|
+
} else {
|
|
2308
|
+
// defineProperties
|
|
2309
|
+
if (args.length < 2) return;
|
|
2310
|
+
const arg0 = args[0]!,
|
|
2311
|
+
arg1 = args[1]!;
|
|
2312
|
+
if (arg0.type !== 'identifier') return;
|
|
2313
|
+
if (arg1.type !== 'object') return;
|
|
2314
|
+
for (let i = 0; i < arg1.childCount; i++) {
|
|
2315
|
+
const pair = arg1.child(i);
|
|
2316
|
+
if (pair?.type !== 'pair') continue;
|
|
2317
|
+
const keyN = pair.childForFieldName('key');
|
|
2318
|
+
const valN = pair.childForFieldName('value');
|
|
2319
|
+
if (!keyN || !valN) continue;
|
|
2320
|
+
const key = keyN.type === 'string' ? keyN.text.replace(/^['"]|['"]$/g, '') : keyN.text;
|
|
2321
|
+
const target = findDescriptorValue(valN);
|
|
2322
|
+
if (!target) continue;
|
|
2323
|
+
setTypeMapEntry(typeMap, `${arg0.text}.${key}`, target, 0.85);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
/** Return the identifier text of the `value` field in a property descriptor object. */
|
|
2329
|
+
function findDescriptorValue(desc: TreeSitterNode): string | undefined {
|
|
2330
|
+
if (desc.type !== 'object') return undefined;
|
|
2331
|
+
for (let i = 0; i < desc.childCount; i++) {
|
|
2332
|
+
const pair = desc.child(i);
|
|
2333
|
+
if (pair?.type !== 'pair') continue;
|
|
2334
|
+
const key = pair.childForFieldName('key');
|
|
2335
|
+
const val = pair.childForFieldName('value');
|
|
2336
|
+
if (key?.text === 'value' && val?.type === 'identifier') return val.text;
|
|
2337
|
+
}
|
|
2338
|
+
return undefined;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Phase 8.3f: return the identifier texts of all `get` and `set` accessors in a property
|
|
2343
|
+
* descriptor. `{ get: getter, set: setter }` → ['getter', 'setter'].
|
|
2344
|
+
* Returns all accessors so that each one gets a `callerName:this = obj` typeMap entry.
|
|
2345
|
+
*/
|
|
2346
|
+
function findDescriptorAccessors(desc: TreeSitterNode): string[] {
|
|
2347
|
+
if (desc.type !== 'object') return [];
|
|
2348
|
+
const result: string[] = [];
|
|
2349
|
+
for (let i = 0; i < desc.childCount; i++) {
|
|
2350
|
+
const pair = desc.child(i);
|
|
2351
|
+
if (pair?.type !== 'pair') continue;
|
|
2352
|
+
const key = pair.childForFieldName('key');
|
|
2353
|
+
const val = pair.childForFieldName('value');
|
|
2354
|
+
if ((key?.text === 'get' || key?.text === 'set') && val?.type === 'identifier') {
|
|
2355
|
+
result.push(val.text);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return result;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/** Seed composite pts keys for each property in a prototype object literal. */
|
|
2362
|
+
function seedProtoProperties(
|
|
2363
|
+
varName: string,
|
|
2364
|
+
proto: TreeSitterNode,
|
|
2365
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
2366
|
+
): void {
|
|
2367
|
+
for (let i = 0; i < proto.childCount; i++) {
|
|
2368
|
+
const child = proto.child(i);
|
|
2369
|
+
if (!child) continue;
|
|
2370
|
+
if (child.type === 'shorthand_property_identifier') {
|
|
2371
|
+
setTypeMapEntry(typeMap, `${varName}.${child.text}`, child.text, 0.85);
|
|
2372
|
+
} else if (child.type === 'pair') {
|
|
2373
|
+
const keyN = child.childForFieldName('key');
|
|
2374
|
+
const valN = child.childForFieldName('value');
|
|
2375
|
+
if (!keyN || !valN || valN.type !== 'identifier') continue;
|
|
2376
|
+
const key = keyN.type === 'string' ? keyN.text.replace(/^['"]|['"]$/g, '') : keyN.text;
|
|
2377
|
+
setTypeMapEntry(typeMap, `${varName}.${key}`, valN.text, 0.85);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
/**
|
|
2383
|
+
* Phase 8.3c: record argument-to-parameter bindings at call sites.
|
|
2384
|
+
*
|
|
2385
|
+
* For each `f(x, y)` where the callee is a simple identifier and an argument
|
|
2386
|
+
* is a simple identifier, emits a ParamBinding so the pts solver can add
|
|
2387
|
+
* constraint: pts(param_i_of_f) ⊇ pts(arg_i). The solver uses the
|
|
2388
|
+
* definitionParams map to resolve the actual parameter names.
|
|
2389
|
+
*
|
|
2390
|
+
* Scope: intra-module only (the solver only materialises constraints for
|
|
2391
|
+
* locally-defined callees, so cross-module calls produce no spurious flow).
|
|
2392
|
+
*/
|
|
2393
|
+
function collectParamBindings(node: TreeSitterNode, paramBindings: ParamBinding[]): void {
|
|
2394
|
+
const fn = node.childForFieldName('function');
|
|
2395
|
+
const args = node.childForFieldName('arguments') ?? findChild(node, 'arguments');
|
|
2396
|
+
if (fn?.type === 'identifier' && !BUILTIN_GLOBALS.has(fn.text) && args) {
|
|
2397
|
+
let argIdx = 0;
|
|
2398
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
2399
|
+
const child = args.child(i);
|
|
2400
|
+
if (!child) continue;
|
|
2401
|
+
const ct = child.type;
|
|
2402
|
+
if (ct === ',' || ct === '(' || ct === ')') continue;
|
|
2403
|
+
if (ct === 'identifier' && !BUILTIN_GLOBALS.has(child.text)) {
|
|
2404
|
+
paramBindings.push({ callee: fn.text, argIndex: argIdx, argName: child.text });
|
|
2405
|
+
} else if (ct === 'spread_element') {
|
|
2406
|
+
// f(...[a, b]) — inline array literal: expand each element as a direct param binding.
|
|
2407
|
+
const inner =
|
|
2408
|
+
child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
|
|
2409
|
+
if (inner?.type === 'array') {
|
|
2410
|
+
let elemCount = 0;
|
|
2411
|
+
for (let j = 0; j < inner.childCount; j++) {
|
|
2412
|
+
const elem = inner.child(j);
|
|
2413
|
+
if (!elem) continue;
|
|
2414
|
+
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
|
|
2415
|
+
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
|
|
2416
|
+
paramBindings.push({
|
|
2417
|
+
callee: fn.text,
|
|
2418
|
+
argIndex: argIdx + elemCount,
|
|
2419
|
+
argName: elem.text,
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
elemCount++;
|
|
2423
|
+
}
|
|
2424
|
+
// Advance by the exact number of slots this spread occupies and skip
|
|
2425
|
+
// the unconditional argIdx++ below so that zero-element spreads (...[])
|
|
2426
|
+
// do not shift subsequent argument indices.
|
|
2427
|
+
argIdx += elemCount;
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
argIdx++;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/** Collection constructors whose argument is treated as an element source. */
|
|
2437
|
+
const COLLECTION_CTOR_SET = new Set(['Set', 'Map']);
|
|
2438
|
+
|
|
2439
|
+
/**
|
|
2440
|
+
* Phase 8.3e: Extract array-element bindings from `const arr = [fn1, fn2]` patterns.
|
|
2441
|
+
* Emits an ArrayElemBinding for each identifier element in an array literal assigned
|
|
2442
|
+
* to a variable.
|
|
2443
|
+
*/
|
|
2444
|
+
function collectArrayElemBindings(
|
|
2445
|
+
node: TreeSitterNode,
|
|
2446
|
+
arrayElemBindings: ArrayElemBinding[],
|
|
2447
|
+
): void {
|
|
2448
|
+
const nameN = node.childForFieldName('name');
|
|
2449
|
+
const valueN = node.childForFieldName('value');
|
|
2450
|
+
if (nameN?.type === 'identifier' && valueN?.type === 'array') {
|
|
2451
|
+
let idx = 0;
|
|
2452
|
+
for (let i = 0; i < valueN.childCount; i++) {
|
|
2453
|
+
const elem = valueN.child(i);
|
|
2454
|
+
if (!elem) continue;
|
|
2455
|
+
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
|
|
2456
|
+
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
|
|
2457
|
+
arrayElemBindings.push({ arrayName: nameN.text, index: idx, elemName: elem.text });
|
|
2458
|
+
}
|
|
2459
|
+
idx++;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Phase 8.3e collectors (spread-argument, Array.from, collection-wrap, for-of
|
|
2466
|
+
* bindings), invoked from runContextCollectorWalk:
|
|
2467
|
+
*
|
|
2468
|
+
* - Spread: `f(...arr)` → SpreadArgBinding
|
|
2469
|
+
* - Array.from: `Array.from(src, cb)` → ArrayCallbackBinding
|
|
2470
|
+
* - Collection wrap: `new Set(arr)` / `new Map(arr)` → FnRefBinding lhs=s[*] rhs=arr[*]
|
|
2471
|
+
* - For-of: `for (const x of arr)` → ForOfBinding
|
|
2472
|
+
*/
|
|
2473
|
+
function collectSpreadAndArrayFromBindings(
|
|
2474
|
+
node: TreeSitterNode,
|
|
2475
|
+
spreadArgBindings: SpreadArgBinding[],
|
|
2476
|
+
arrayCallbackBindings: ArrayCallbackBinding[],
|
|
2477
|
+
): void {
|
|
2478
|
+
const fn = node.childForFieldName('function');
|
|
2479
|
+
const argsNode = node.childForFieldName('arguments') ?? findChild(node, 'arguments');
|
|
2480
|
+
|
|
2481
|
+
// Spread: f(...arr)
|
|
2482
|
+
if (fn?.type === 'identifier' && !BUILTIN_GLOBALS.has(fn.text) && argsNode) {
|
|
2483
|
+
let argIdx = 0;
|
|
2484
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
2485
|
+
const child = argsNode.child(i);
|
|
2486
|
+
if (!child) continue;
|
|
2487
|
+
if (child.type === ',' || child.type === '(' || child.type === ')') continue;
|
|
2488
|
+
if (child.type === 'spread_element') {
|
|
2489
|
+
const spreadTarget =
|
|
2490
|
+
child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
|
|
2491
|
+
if (spreadTarget?.type === 'identifier' && !BUILTIN_GLOBALS.has(spreadTarget.text)) {
|
|
2492
|
+
spreadArgBindings.push({
|
|
2493
|
+
callee: fn.text,
|
|
2494
|
+
arrayName: spreadTarget.text,
|
|
2495
|
+
startIndex: argIdx,
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
argIdx++;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// Array.from(source, cb)
|
|
2504
|
+
if (fn?.type === 'member_expression' && argsNode) {
|
|
2505
|
+
const obj = fn.childForFieldName('object');
|
|
2506
|
+
const prop = fn.childForFieldName('property');
|
|
2507
|
+
if (obj?.text === 'Array' && prop?.text === 'from') {
|
|
2508
|
+
const fnArgs: TreeSitterNode[] = [];
|
|
2509
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
2510
|
+
const child = argsNode.child(i);
|
|
2511
|
+
if (!child) continue;
|
|
2512
|
+
if (child.type === ',' || child.type === '(' || child.type === ')') continue;
|
|
2513
|
+
fnArgs.push(child);
|
|
2514
|
+
}
|
|
2515
|
+
if (fnArgs.length >= 2) {
|
|
2516
|
+
const srcArg = fnArgs[0]!;
|
|
2517
|
+
const cbArg = fnArgs[1]!;
|
|
2518
|
+
if (
|
|
2519
|
+
srcArg.type === 'identifier' &&
|
|
2520
|
+
!BUILTIN_GLOBALS.has(srcArg.text) &&
|
|
2521
|
+
cbArg.type === 'identifier' &&
|
|
2522
|
+
!BUILTIN_GLOBALS.has(cbArg.text)
|
|
2523
|
+
) {
|
|
2524
|
+
arrayCallbackBindings.push({ sourceName: srcArg.text, calleeName: cbArg.text });
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
/** Collection wrap: `const s = new Set(arr)` or `new Map(arr)` (variable_declarator). */
|
|
2532
|
+
function collectCollectionWrapBinding(node: TreeSitterNode, fnRefBindings: FnRefBinding[]): void {
|
|
2533
|
+
const nameN = node.childForFieldName('name');
|
|
2534
|
+
const valueN = node.childForFieldName('value');
|
|
2535
|
+
if (nameN?.type === 'identifier' && valueN?.type === 'new_expression') {
|
|
2536
|
+
const ctor = valueN.childForFieldName('constructor');
|
|
2537
|
+
const args = valueN.childForFieldName('arguments');
|
|
2538
|
+
if (ctor && COLLECTION_CTOR_SET.has(ctor.text) && args) {
|
|
2539
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
2540
|
+
const arg = args.child(i);
|
|
2541
|
+
if (!arg || arg.type === '(' || arg.type === ')') continue;
|
|
2542
|
+
if (arg.type === 'identifier' && !BUILTIN_GLOBALS.has(arg.text)) {
|
|
2543
|
+
fnRefBindings.push({ lhs: `${nameN.text}[*]`, rhs: `${arg.text}[*]` });
|
|
2544
|
+
break;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/** For-of: `for (const x of arr)` (for_in_statement with an `of` keyword). */
|
|
2552
|
+
function collectForOfBinding(
|
|
2553
|
+
node: TreeSitterNode,
|
|
2554
|
+
enclosingFunc: string,
|
|
2555
|
+
forOfBindings: ForOfBinding[],
|
|
2556
|
+
): void {
|
|
2557
|
+
let isForOf = false;
|
|
2558
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2559
|
+
if (node.child(i)?.text === 'of') {
|
|
2560
|
+
isForOf = true;
|
|
2561
|
+
break;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
if (!isForOf) return;
|
|
2565
|
+
const right = node.childForFieldName('right');
|
|
2566
|
+
if (right?.type !== 'identifier' || BUILTIN_GLOBALS.has(right.text)) return;
|
|
2567
|
+
const left = node.childForFieldName('left');
|
|
2568
|
+
let varName: string | null = null;
|
|
2569
|
+
if (left?.type === 'identifier') {
|
|
2570
|
+
varName = left.text;
|
|
2571
|
+
} else if (left) {
|
|
2572
|
+
for (let i = 0; i < left.childCount; i++) {
|
|
2573
|
+
const lc = left.child(i);
|
|
2574
|
+
if (lc?.type === 'variable_declarator') {
|
|
2575
|
+
const nc = lc.childForFieldName('name');
|
|
2576
|
+
if (nc?.type === 'identifier') {
|
|
2577
|
+
varName = nc.text;
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2580
|
+
} else if (
|
|
2581
|
+
lc?.type === 'identifier' &&
|
|
2582
|
+
lc.text !== 'const' &&
|
|
2583
|
+
lc.text !== 'let' &&
|
|
2584
|
+
lc.text !== 'var'
|
|
2585
|
+
) {
|
|
2586
|
+
varName = lc.text;
|
|
2587
|
+
break;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
if (varName && !BUILTIN_GLOBALS.has(varName)) {
|
|
2592
|
+
forOfBindings.push({ varName, sourceName: right.text, enclosingFunc });
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/**
|
|
2597
|
+
* Phase 8.3f: record object-destructuring rest-parameter bindings from function definitions.
|
|
2598
|
+
*
|
|
2599
|
+
* For each `function f({ a, ...rest })` (or arrow/function-expression equivalent),
|
|
2600
|
+
* records { callee: 'f', restName: 'rest', argIndex: N }. Also covers class methods
|
|
2601
|
+
* (`callee: 'ClassName.method'`) and object-literal methods (`callee: 'method'`).
|
|
2602
|
+
* The edge builder uses these to seed typeMap[rest] = { type: argName } when f(obj)
|
|
2603
|
+
* is called with an identifier, enabling `rest.method()` calls to resolve.
|
|
2604
|
+
*/
|
|
2605
|
+
function collectObjectRestParams(
|
|
2606
|
+
node: TreeSitterNode,
|
|
2607
|
+
t: string,
|
|
2608
|
+
currentClass: string | null,
|
|
2609
|
+
bindings: ObjectRestParamBinding[],
|
|
2610
|
+
): void {
|
|
2611
|
+
let fnName: string | null = null;
|
|
2612
|
+
let paramsNode: TreeSitterNode | null = null;
|
|
2613
|
+
|
|
2614
|
+
if (t === 'function_declaration' || t === 'generator_function_declaration') {
|
|
2615
|
+
const nameN = node.childForFieldName('name');
|
|
2616
|
+
if (nameN?.type === 'identifier') fnName = nameN.text;
|
|
2617
|
+
paramsNode = node.childForFieldName('parameters') ?? findChild(node, 'formal_parameters');
|
|
2618
|
+
} else if (t === 'variable_declarator') {
|
|
2619
|
+
const nameN = node.childForFieldName('name');
|
|
2620
|
+
const valueN = node.childForFieldName('value');
|
|
2621
|
+
if (nameN?.type === 'identifier' && valueN) {
|
|
2622
|
+
const vt = valueN.type;
|
|
2623
|
+
if (vt === 'arrow_function' || vt === 'function_expression' || vt === 'generator_function') {
|
|
2624
|
+
fnName = nameN.text;
|
|
2625
|
+
paramsNode =
|
|
2626
|
+
valueN.childForFieldName('parameters') ?? findChild(valueN, 'formal_parameters');
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
} else if (t === 'method_definition') {
|
|
2630
|
+
// class method: `class Foo { bar({ a, ...rest }) {} }`
|
|
2631
|
+
// object-literal shorthand method: `{ bar({ a, ...rest }) {} }`
|
|
2632
|
+
const nameN = node.childForFieldName('name');
|
|
2633
|
+
if (nameN) {
|
|
2634
|
+
fnName = currentClass ? `${currentClass}.${nameN.text}` : nameN.text;
|
|
2635
|
+
paramsNode = node.childForFieldName('parameters') ?? findChild(node, 'formal_parameters');
|
|
2636
|
+
}
|
|
2637
|
+
} else if (t === 'pair') {
|
|
2638
|
+
// object-literal method: `{ bar: function({ a, ...rest }) {} }`
|
|
2639
|
+
// Skip computed property keys (e.g. `{ [Symbol.iterator]: function({ ...rest }) {} }`)
|
|
2640
|
+
// because `callee: '[Symbol.iterator]'` can never match a paramBinding callee.
|
|
2641
|
+
const keyN = node.childForFieldName('key');
|
|
2642
|
+
const valueN = node.childForFieldName('value');
|
|
2643
|
+
if (keyN && valueN && keyN.type !== 'computed_property_name') {
|
|
2644
|
+
const vt = valueN.type;
|
|
2645
|
+
if (vt === 'arrow_function' || vt === 'function_expression' || vt === 'generator_function') {
|
|
2646
|
+
fnName = keyN.type === 'string' ? keyN.text.slice(1, -1) : keyN.text;
|
|
2647
|
+
paramsNode =
|
|
2648
|
+
valueN.childForFieldName('parameters') ?? findChild(valueN, 'formal_parameters');
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
if (fnName && paramsNode) {
|
|
2654
|
+
let paramIdx = 0;
|
|
2655
|
+
for (let i = 0; i < paramsNode.childCount; i++) {
|
|
2656
|
+
const child = paramsNode.child(i);
|
|
2657
|
+
if (!child) continue;
|
|
2658
|
+
const ct = child.type;
|
|
2659
|
+
if (ct === ',' || ct === '(' || ct === ')') continue;
|
|
2660
|
+
if (ct === 'object_pattern') {
|
|
2661
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
2662
|
+
const inner = child.child(j);
|
|
2663
|
+
if (!inner) continue;
|
|
2664
|
+
if (inner.type === 'rest_pattern' || inner.type === 'rest_element') {
|
|
2665
|
+
// rest_pattern node: `...identifier` — the identifier is at child index 1
|
|
2666
|
+
const restId = inner.child(1) ?? inner.childForFieldName('name');
|
|
2667
|
+
if (restId?.type === 'identifier') {
|
|
2668
|
+
bindings.push({ callee: fnName, restName: restId.text, argIndex: paramIdx });
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
paramIdx++;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/**
|
|
2679
|
+
* Phase 8.3f: collect object-property bindings from object literals.
|
|
2680
|
+
*
|
|
2681
|
+
* `const obj = { e4 }` → `{ objectName: "obj", propName: "e4", valueName: "e4" }`
|
|
2682
|
+
* `const obj = { e1: fn }` → `{ objectName: "obj", propName: "e1", valueName: "fn" }`
|
|
2683
|
+
*
|
|
2684
|
+
* Only tracks shorthand and `key: identifier` pairs; skips function literals.
|
|
2685
|
+
*/
|
|
2686
|
+
function collectObjectPropBindings(node: TreeSitterNode, bindings: ObjectPropBinding[]): void {
|
|
2687
|
+
const nameN = node.childForFieldName('name');
|
|
2688
|
+
const valueN = node.childForFieldName('value');
|
|
2689
|
+
if (nameN?.type === 'identifier' && valueN?.type === 'object') {
|
|
2690
|
+
const objectName = nameN.text;
|
|
2691
|
+
for (let i = 0; i < valueN.childCount; i++) {
|
|
2692
|
+
const child = valueN.child(i);
|
|
2693
|
+
if (!child) continue;
|
|
2694
|
+
if (child.type === 'shorthand_property_identifier') {
|
|
2695
|
+
bindings.push({ objectName, propName: child.text, valueName: child.text });
|
|
2696
|
+
} else if (child.type === 'pair') {
|
|
2697
|
+
const keyN = child.childForFieldName('key');
|
|
2698
|
+
const valN = child.childForFieldName('value');
|
|
2699
|
+
if (
|
|
2700
|
+
keyN?.type === 'property_identifier' &&
|
|
2701
|
+
valN?.type === 'identifier' &&
|
|
2702
|
+
!BUILTIN_GLOBALS.has(valN.text)
|
|
2703
|
+
) {
|
|
2704
|
+
bindings.push({ objectName, propName: keyN.text, valueName: valN.text });
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
1210
2711
|
function extractReceiverName(objNode: TreeSitterNode | null): string | undefined {
|
|
1211
2712
|
if (!objNode) return undefined;
|
|
1212
2713
|
const t = objNode.type;
|
|
1213
2714
|
if (t === 'identifier' || t === 'this' || t === 'super') return objNode.text;
|
|
2715
|
+
// `(new Foo(...)).method()` — extract the constructor name so the resolver can
|
|
2716
|
+
// look up `Foo.method` directly without relying on a text-based regex heuristic.
|
|
2717
|
+
if (t === 'new_expression') {
|
|
2718
|
+
const name = extractNewExprTypeName(objNode);
|
|
2719
|
+
if (name) return name;
|
|
2720
|
+
}
|
|
2721
|
+
if (t === 'parenthesized_expression') {
|
|
2722
|
+
// Only one level of parentheses is unwrapped here. Doubly-nested parens
|
|
2723
|
+
// (e.g. `((new Dog())).bark()`) and cast expressions inside parens
|
|
2724
|
+
// (e.g. `(new Dog() as Animal).bark()`) fall through to raw-text handling
|
|
2725
|
+
// below and are caught by the regex fallback in call-resolver.ts.
|
|
2726
|
+
for (let i = 0; i < objNode.childCount; i++) {
|
|
2727
|
+
const child = objNode.child(i);
|
|
2728
|
+
if (child?.type === 'new_expression') {
|
|
2729
|
+
const name = extractNewExprTypeName(child);
|
|
2730
|
+
if (name) return name;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
1214
2734
|
return objNode.text;
|
|
1215
2735
|
}
|
|
1216
2736
|
|
|
@@ -1421,12 +2941,23 @@ function firstArgIsStringLiteral(argsNode: TreeSitterNode): boolean {
|
|
|
1421
2941
|
* member-expr args are only emitted when the first argument is a string
|
|
1422
2942
|
* literal route path — matching Express/router shape and skipping
|
|
1423
2943
|
* `cache.get(user.id)`-style calls.
|
|
2944
|
+
*
|
|
2945
|
+
* `.call()` / `.apply()` / `.bind()` — the first arg is the `this` context (not a callback of
|
|
2946
|
+
* the enclosing function) and subsequent args flow into the delegated function's parameters.
|
|
2947
|
+
* Emitting them here would produce false-positive edges from the *calling* function.
|
|
2948
|
+
* This-rebinding (fn::this → ctx) is handled separately by extractThisCallBindingsWalk.
|
|
1424
2949
|
*/
|
|
1425
2950
|
function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
|
|
1426
2951
|
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
|
|
1427
2952
|
if (!args) return [];
|
|
1428
2953
|
|
|
1429
2954
|
const calleeName = extractCalleeName(callNode);
|
|
2955
|
+
// .call() / .apply() / .bind() — the first arg is the `this` context (not a callback of
|
|
2956
|
+
// the enclosing function) and subsequent args flow into the delegated function's parameters.
|
|
2957
|
+
// Emitting them here would produce false-positive edges from the *calling* function.
|
|
2958
|
+
// This-rebinding (fn::this → ctx) is handled separately by extractThisCallBindingsWalk.
|
|
2959
|
+
if (calleeName === 'call' || calleeName === 'apply' || calleeName === 'bind') return [];
|
|
2960
|
+
|
|
1430
2961
|
let memberExprArgsAllowed = calleeName !== null && CALLBACK_ACCEPTING_CALLEES.has(calleeName);
|
|
1431
2962
|
if (memberExprArgsAllowed && calleeName !== null && HTTP_VERB_CALLEES.has(calleeName)) {
|
|
1432
2963
|
// HTTP verbs require a string-literal route path to be treated as a
|
|
@@ -1457,6 +2988,151 @@ function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
|
|
|
1457
2988
|
return result;
|
|
1458
2989
|
}
|
|
1459
2990
|
|
|
2991
|
+
/**
|
|
2992
|
+
* Collect, from a call_expression node:
|
|
2993
|
+
* - `this(args)` call expressions → `{name: 'this', ...}` entries in `calls`
|
|
2994
|
+
* (where `this` is used as a function, not as a receiver)
|
|
2995
|
+
* - `fn.call(namedCtx, ...)` / `fn.apply(namedCtx, ...)` bindings →
|
|
2996
|
+
* `{ callee: 'fn', thisArg: 'namedCtx' }` entries in `thisCallBindings`
|
|
2997
|
+
*/
|
|
2998
|
+
function collectThisCallAndBindings(
|
|
2999
|
+
node: TreeSitterNode,
|
|
3000
|
+
calls: Call[],
|
|
3001
|
+
thisCallBindings: ThisCallBinding[],
|
|
3002
|
+
): void {
|
|
3003
|
+
const fn = node.childForFieldName('function');
|
|
3004
|
+
if (fn?.type === 'this') {
|
|
3005
|
+
calls.push({ name: 'this', line: nodeStartLine(node) });
|
|
3006
|
+
} else if (fn?.type === 'member_expression') {
|
|
3007
|
+
const obj = fn.childForFieldName('object');
|
|
3008
|
+
const prop = fn.childForFieldName('property');
|
|
3009
|
+
if (
|
|
3010
|
+
obj?.type === 'identifier' &&
|
|
3011
|
+
prop &&
|
|
3012
|
+
(prop.text === 'call' || prop.text === 'apply') &&
|
|
3013
|
+
!BUILTIN_GLOBALS.has(obj.text)
|
|
3014
|
+
) {
|
|
3015
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
3016
|
+
if (args) {
|
|
3017
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
3018
|
+
const child = args.child(i);
|
|
3019
|
+
if (!child) continue;
|
|
3020
|
+
const t = child.type;
|
|
3021
|
+
if (t === '(' || t === ')' || t === ',') continue;
|
|
3022
|
+
// First real argument: only bind if it's a plain identifier
|
|
3023
|
+
if (
|
|
3024
|
+
t === 'identifier' &&
|
|
3025
|
+
!BUILTIN_GLOBALS.has(child.text) &&
|
|
3026
|
+
child.text !== 'undefined' &&
|
|
3027
|
+
child.text !== 'null'
|
|
3028
|
+
) {
|
|
3029
|
+
thisCallBindings.push({ callee: obj.text, thisArg: child.text });
|
|
3030
|
+
}
|
|
3031
|
+
break;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
/**
|
|
3039
|
+
* Outputs for {@link runCollectorWalk}. Required targets are collected on both
|
|
3040
|
+
* extraction paths; optional targets are path-specific:
|
|
3041
|
+
* - `imports` / `calls`+`thisCallBindings` / `classMemberDefs` — query path only
|
|
3042
|
+
* (the walk path's walkJavaScriptNode covers those node types itself).
|
|
3043
|
+
* - `funcPropDefs` — walk path only (the query path captures `fn.method = …`
|
|
3044
|
+
* assignments via the `assign_left`/`assign_right` query pattern).
|
|
3045
|
+
*/
|
|
3046
|
+
interface CollectorWalkTargets {
|
|
3047
|
+
definitions: Definition[];
|
|
3048
|
+
typeMap: Map<string, TypeMapEntry>;
|
|
3049
|
+
paramBindings: ParamBinding[];
|
|
3050
|
+
arrayElemBindings: ArrayElemBinding[];
|
|
3051
|
+
objectPropBindings: ObjectPropBinding[];
|
|
3052
|
+
newExpressions: string[];
|
|
3053
|
+
definePropertyReceivers: Map<string, string>;
|
|
3054
|
+
imports?: Import[];
|
|
3055
|
+
calls?: Call[];
|
|
3056
|
+
thisCallBindings?: ThisCallBinding[];
|
|
3057
|
+
classMemberDefs?: Definition[];
|
|
3058
|
+
funcPropDefs?: Definition[];
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
/**
|
|
3062
|
+
* Single-pass collector walk: one DFS that dispatches each node to every
|
|
3063
|
+
* collector interested in its type.
|
|
3064
|
+
*
|
|
3065
|
+
* This replaces what had grown to ten independent full-tree traversals (one
|
|
3066
|
+
* per collector). On WASM trees every node access (`child(i)`, `.type`,
|
|
3067
|
+
* `childForFieldName`) marshals through the JS↔WASM boundary, so traversal
|
|
3068
|
+
* count — not collector work — dominated extraction cost: the accumulated
|
|
3069
|
+
* per-collector walks made extraction ~2.4× slower between v3.11.2 and
|
|
3070
|
+
* v3.12.0 (7.5 → 17.7 ms/file on codegraph's own corpus).
|
|
3071
|
+
*
|
|
3072
|
+
* Collectors with bespoke traversal semantics stay separate:
|
|
3073
|
+
* - extractConstantsWalk / extractDestructuredBindingsWalk prune function
|
|
3074
|
+
* scopes and unwrap export statements on the way down;
|
|
3075
|
+
* - extractReturnTypeMapWalk / extractTypeMapWalk / extractSpreadForOfWalk /
|
|
3076
|
+
* extractObjectRestParamBindingsWalk thread enclosing-class context with
|
|
3077
|
+
* per-walk reset rules that intentionally differ (see each walk's comments).
|
|
3078
|
+
*/
|
|
3079
|
+
function runCollectorWalk(rootNode: TreeSitterNode, targets: CollectorWalkTargets): void {
|
|
3080
|
+
const walk = (node: TreeSitterNode, depth: number, inDynamicImport: boolean): void => {
|
|
3081
|
+
if (depth >= MAX_WALK_DEPTH) return;
|
|
3082
|
+
let childInDynamicImport = inDynamicImport;
|
|
3083
|
+
switch (node.type) {
|
|
3084
|
+
case 'call_expression': {
|
|
3085
|
+
// Matched import() calls suppress *dynamic-import* collection in their
|
|
3086
|
+
// argument subtree (mirrors the old walk's early return) while leaving
|
|
3087
|
+
// the subtree visible to every other collector. The !inDynamicImport
|
|
3088
|
+
// check runs first so nested import() calls are neither collected nor
|
|
3089
|
+
// re-matched.
|
|
3090
|
+
if (targets.imports && !inDynamicImport && collectDynamicImport(node, targets.imports)) {
|
|
3091
|
+
childInDynamicImport = true;
|
|
3092
|
+
}
|
|
3093
|
+
if (targets.calls && targets.thisCallBindings) {
|
|
3094
|
+
collectThisCallAndBindings(node, targets.calls, targets.thisCallBindings);
|
|
3095
|
+
}
|
|
3096
|
+
collectParamBindings(node, targets.paramBindings);
|
|
3097
|
+
collectDefinePropertyReceiver(node, targets.definePropertyReceivers);
|
|
3098
|
+
break;
|
|
3099
|
+
}
|
|
3100
|
+
case 'variable_declarator':
|
|
3101
|
+
collectArrayElemBindings(node, targets.arrayElemBindings);
|
|
3102
|
+
collectObjectPropBindings(node, targets.objectPropBindings);
|
|
3103
|
+
break;
|
|
3104
|
+
case 'expression_statement': {
|
|
3105
|
+
const expr = node.child(0);
|
|
3106
|
+
if (expr?.type === 'assignment_expression') {
|
|
3107
|
+
const lhs = expr.childForFieldName('left');
|
|
3108
|
+
const rhs = expr.childForFieldName('right');
|
|
3109
|
+
if (lhs && rhs) {
|
|
3110
|
+
handlePrototypeAssignment(lhs, rhs, targets.definitions, targets.typeMap);
|
|
3111
|
+
if (targets.funcPropDefs) handleFuncPropAssignment(lhs, rhs, targets.funcPropDefs);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
break;
|
|
3115
|
+
}
|
|
3116
|
+
case 'new_expression': {
|
|
3117
|
+
const name = extractNewExprTypeName(node);
|
|
3118
|
+
if (name) targets.newExpressions.push(name);
|
|
3119
|
+
break;
|
|
3120
|
+
}
|
|
3121
|
+
case 'field_definition':
|
|
3122
|
+
case 'public_field_definition':
|
|
3123
|
+
if (targets.classMemberDefs) handleFieldDef(node, targets.classMemberDefs);
|
|
3124
|
+
break;
|
|
3125
|
+
case 'class_static_block':
|
|
3126
|
+
if (targets.classMemberDefs) handleStaticBlock(node, targets.classMemberDefs);
|
|
3127
|
+
break;
|
|
3128
|
+
}
|
|
3129
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3130
|
+
walk(node.child(i)!, depth + 1, childInDynamicImport);
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
3133
|
+
walk(rootNode, 0, false);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
1460
3136
|
function findAnonymousCallback(argsNode: TreeSitterNode): TreeSitterNode | null {
|
|
1461
3137
|
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1462
3138
|
const child = argsNode.child(i);
|
|
@@ -1517,7 +3193,7 @@ function extractCallbackDefinition(
|
|
|
1517
3193
|
fn?: TreeSitterNode | null,
|
|
1518
3194
|
): Definition | null {
|
|
1519
3195
|
if (!fn) fn = callNode.childForFieldName('function');
|
|
1520
|
-
if (
|
|
3196
|
+
if (fn?.type !== 'member_expression') return null;
|
|
1521
3197
|
|
|
1522
3198
|
const prop = fn.childForFieldName('property');
|
|
1523
3199
|
if (!prop) return null;
|
|
@@ -1549,7 +3225,7 @@ function extractCallbackDefinition(
|
|
|
1549
3225
|
// Express: app.get('/path', callback)
|
|
1550
3226
|
if (EXPRESS_METHODS.has(method)) {
|
|
1551
3227
|
const strArg = findFirstStringArg(args);
|
|
1552
|
-
if (!strArg
|
|
3228
|
+
if (!strArg?.startsWith('/')) return null;
|
|
1553
3229
|
const cb = findAnonymousCallback(args);
|
|
1554
3230
|
if (!cb) return null;
|
|
1555
3231
|
return {
|
|
@@ -1588,7 +3264,7 @@ function extractSuperclass(heritage: TreeSitterNode): string | null {
|
|
|
1588
3264
|
return null;
|
|
1589
3265
|
}
|
|
1590
3266
|
|
|
1591
|
-
const JS_CLASS_TYPES = ['class_declaration', 'class'] as const;
|
|
3267
|
+
const JS_CLASS_TYPES = ['class_declaration', 'abstract_class_declaration', 'class'] as const;
|
|
1592
3268
|
function findParentClass(node: TreeSitterNode): string | null {
|
|
1593
3269
|
return findParentNode(node, JS_CLASS_TYPES);
|
|
1594
3270
|
}
|
|
@@ -1628,7 +3304,7 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
|
|
|
1628
3304
|
// Skip await_expression wrapper if present
|
|
1629
3305
|
if (current && current.type === 'await_expression') current = current.parent;
|
|
1630
3306
|
// We should now be at a variable_declarator (or not, if standalone import())
|
|
1631
|
-
if (
|
|
3307
|
+
if (current?.type !== 'variable_declarator') return [];
|
|
1632
3308
|
|
|
1633
3309
|
const nameNode = current.childForFieldName('name');
|
|
1634
3310
|
if (!nameNode) return [];
|
|
@@ -1671,3 +3347,184 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
|
|
|
1671
3347
|
|
|
1672
3348
|
return [];
|
|
1673
3349
|
}
|
|
3350
|
+
|
|
3351
|
+
// ── Phase 8.X: Prototype-based method extraction ────────────────────────────
|
|
3352
|
+
|
|
3353
|
+
/**
|
|
3354
|
+
* Walk the AST and extract prototype-based method definitions and aliases.
|
|
3355
|
+
*
|
|
3356
|
+
* Handles three patterns:
|
|
3357
|
+
* 1. `Foo.prototype.bar = function(){...}` — emits Foo.bar as method definition
|
|
3358
|
+
* 2. `Foo.prototype.bar = identifier` — sets typeMap['Foo.bar'] = { type: identifier }
|
|
3359
|
+
* 3. `Foo.prototype = { bar: fn, ... }` — emits defs and typeMap entries per property
|
|
3360
|
+
*
|
|
3361
|
+
* Emitting definitions under the canonical `ClassName.methodName` name lets the
|
|
3362
|
+
* existing typeMap-based call resolver find them when a typed receiver dispatches
|
|
3363
|
+
* `instance.method()` (lookup.byName('C.foo') in resolveByMethodOrGlobal).
|
|
3364
|
+
*
|
|
3365
|
+
* typeMap entries for identifier aliases (`Foo.bar → { type: 'someId' }`) are
|
|
3366
|
+
* consumed by the prototype-alias fallback added to resolveByMethodOrGlobal.
|
|
3367
|
+
*/
|
|
3368
|
+
// Prototype-method assignments (`Foo.prototype.bar = fn`) are collected inline
|
|
3369
|
+
// in runCollectorWalk's expression_statement case via handlePrototypeAssignment.
|
|
3370
|
+
|
|
3371
|
+
/**
|
|
3372
|
+
* Handle an assignment_expression that may be a prototype assignment.
|
|
3373
|
+
*
|
|
3374
|
+
* Matches:
|
|
3375
|
+
* - `Foo.prototype.bar = rhs` (lhs ends in .prototype.bar)
|
|
3376
|
+
* - `Foo.prototype = { ... }` (lhs ends in .prototype, rhs is object literal)
|
|
3377
|
+
*/
|
|
3378
|
+
function handlePrototypeAssignment(
|
|
3379
|
+
lhs: TreeSitterNode,
|
|
3380
|
+
rhs: TreeSitterNode,
|
|
3381
|
+
definitions: Definition[],
|
|
3382
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
3383
|
+
): void {
|
|
3384
|
+
if (lhs.type !== 'member_expression') return;
|
|
3385
|
+
|
|
3386
|
+
const lhsObj = lhs.childForFieldName('object');
|
|
3387
|
+
const lhsProp = lhs.childForFieldName('property');
|
|
3388
|
+
if (!lhsObj || !lhsProp) return;
|
|
3389
|
+
|
|
3390
|
+
// Pattern 1: `Foo.prototype.bar = rhs`
|
|
3391
|
+
// lhs.object is `Foo.prototype` (member_expression), lhs.property is `bar`
|
|
3392
|
+
if (
|
|
3393
|
+
lhsObj.type === 'member_expression' &&
|
|
3394
|
+
(lhsProp.type === 'property_identifier' || lhsProp.type === 'identifier')
|
|
3395
|
+
) {
|
|
3396
|
+
const protoObj = lhsObj.childForFieldName('object');
|
|
3397
|
+
const protoProp = lhsObj.childForFieldName('property');
|
|
3398
|
+
if (
|
|
3399
|
+
protoObj?.type === 'identifier' &&
|
|
3400
|
+
protoProp?.text === 'prototype' &&
|
|
3401
|
+
!BUILTIN_GLOBALS.has(protoObj.text)
|
|
3402
|
+
) {
|
|
3403
|
+
emitPrototypeMethod(protoObj.text, lhsProp.text, rhs, definitions, typeMap);
|
|
3404
|
+
}
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// Pattern 2: `Foo.prototype = { bar: fn, ... }`
|
|
3409
|
+
// lhs.object is `Foo` (identifier), lhs.property is `prototype`
|
|
3410
|
+
if (
|
|
3411
|
+
lhsObj.type === 'identifier' &&
|
|
3412
|
+
lhsProp.text === 'prototype' &&
|
|
3413
|
+
!BUILTIN_GLOBALS.has(lhsObj.text) &&
|
|
3414
|
+
rhs.type === 'object'
|
|
3415
|
+
) {
|
|
3416
|
+
extractPrototypeObjectLiteral(lhsObj.text, rhs, definitions, typeMap);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
/** Emit one prototype method definition or typeMap alias for `ClassName.methodName = rhs`. */
|
|
3421
|
+
function emitPrototypeMethod(
|
|
3422
|
+
className: string,
|
|
3423
|
+
methodName: string,
|
|
3424
|
+
rhs: TreeSitterNode,
|
|
3425
|
+
definitions: Definition[],
|
|
3426
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
3427
|
+
): void {
|
|
3428
|
+
const fullName = `${className}.${methodName}`;
|
|
3429
|
+
if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
|
|
3430
|
+
const params = extractParameters(rhs);
|
|
3431
|
+
definitions.push({
|
|
3432
|
+
name: fullName,
|
|
3433
|
+
kind: 'method',
|
|
3434
|
+
line: nodeStartLine(rhs),
|
|
3435
|
+
endLine: nodeEndLine(rhs),
|
|
3436
|
+
children: params.length > 0 ? params : undefined,
|
|
3437
|
+
});
|
|
3438
|
+
} else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
|
|
3439
|
+
// Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
|
|
3440
|
+
// Consumed by the prototype-alias fallback in resolveByMethodOrGlobal.
|
|
3441
|
+
setTypeMapEntry(typeMap, fullName, rhs.text, 0.9);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
/**
|
|
3446
|
+
* Extract function-as-object property method definitions.
|
|
3447
|
+
*
|
|
3448
|
+
* Handles `fn.method = function() {}` and `fn.method = () => {}` patterns.
|
|
3449
|
+
* Emits a `method` definition named `fn.method` so that:
|
|
3450
|
+
* 1. `findCaller` attributes calls inside the body to `fn.method`
|
|
3451
|
+
* 2. `resolveByMethodOrGlobal` resolves `this.other()` inside `fn.method` to `fn.other`
|
|
3452
|
+
*
|
|
3453
|
+
* Excludes BUILTIN_GLOBALS objects and `.prototype` (handled by extractPrototypeMethodsWalk).
|
|
3454
|
+
*/
|
|
3455
|
+
// Function-as-object-property assignments (`fn.method = function(){}`) are
|
|
3456
|
+
// collected inline in runCollectorWalk's expression_statement case (walk path
|
|
3457
|
+
// only — the query path captures them via the `assign_left`/`assign_right`
|
|
3458
|
+
// query pattern in dispatchQueryMatch).
|
|
3459
|
+
|
|
3460
|
+
function handleFuncPropAssignment(
|
|
3461
|
+
lhs: TreeSitterNode,
|
|
3462
|
+
rhs: TreeSitterNode,
|
|
3463
|
+
definitions: Definition[],
|
|
3464
|
+
): void {
|
|
3465
|
+
if (lhs.type !== 'member_expression') return;
|
|
3466
|
+
if (rhs.type !== 'function_expression' && rhs.type !== 'arrow_function') return;
|
|
3467
|
+
|
|
3468
|
+
const obj = lhs.childForFieldName('object');
|
|
3469
|
+
const prop = lhs.childForFieldName('property');
|
|
3470
|
+
if (!obj || !prop) return;
|
|
3471
|
+
if (obj.type !== 'identifier') return;
|
|
3472
|
+
if (prop.type !== 'property_identifier' && prop.type !== 'identifier') return;
|
|
3473
|
+
if (BUILTIN_GLOBALS.has(obj.text)) return;
|
|
3474
|
+
if (prop.text === 'prototype') return;
|
|
3475
|
+
|
|
3476
|
+
const params = extractParameters(rhs);
|
|
3477
|
+
definitions.push({
|
|
3478
|
+
name: `${obj.text}.${prop.text}`,
|
|
3479
|
+
kind: 'method',
|
|
3480
|
+
line: nodeStartLine(rhs),
|
|
3481
|
+
endLine: nodeEndLine(rhs),
|
|
3482
|
+
children: params.length > 0 ? params : undefined,
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
/** Iterate over an object literal assigned to `Foo.prototype` and emit defs/aliases. */
|
|
3487
|
+
function extractPrototypeObjectLiteral(
|
|
3488
|
+
className: string,
|
|
3489
|
+
objNode: TreeSitterNode,
|
|
3490
|
+
definitions: Definition[],
|
|
3491
|
+
typeMap: Map<string, TypeMapEntry>,
|
|
3492
|
+
): void {
|
|
3493
|
+
for (let i = 0; i < objNode.childCount; i++) {
|
|
3494
|
+
const child = objNode.child(i);
|
|
3495
|
+
if (!child) continue;
|
|
3496
|
+
|
|
3497
|
+
if (child.type === 'method_definition') {
|
|
3498
|
+
// Shorthand method: `Foo.prototype = { bar() {} }`
|
|
3499
|
+
const nameNode = child.childForFieldName('name');
|
|
3500
|
+
if (nameNode) {
|
|
3501
|
+
definitions.push({
|
|
3502
|
+
name: `${className}.${nameNode.text}`,
|
|
3503
|
+
kind: 'method',
|
|
3504
|
+
line: nodeStartLine(child),
|
|
3505
|
+
endLine: nodeEndLine(child),
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
if (child.type === 'shorthand_property_identifier') {
|
|
3512
|
+
// ES6 shorthand: `Foo.prototype = { bar }` → alias typeMap['Foo.bar'] = { type: 'bar' }
|
|
3513
|
+
if (!BUILTIN_GLOBALS.has(child.text)) {
|
|
3514
|
+
setTypeMapEntry(typeMap, `${className}.${child.text}`, child.text, 0.9);
|
|
3515
|
+
}
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
if (child.type !== 'pair') continue;
|
|
3520
|
+
|
|
3521
|
+
const keyNode = child.childForFieldName('key');
|
|
3522
|
+
const valueNode = child.childForFieldName('value');
|
|
3523
|
+
if (!keyNode || !valueNode) continue;
|
|
3524
|
+
|
|
3525
|
+
const methodName = keyNode.type === 'string' ? keyNode.text.replace(/['"]/g, '') : keyNode.text;
|
|
3526
|
+
if (!methodName) continue;
|
|
3527
|
+
|
|
3528
|
+
emitPrototypeMethod(className, methodName, valueNode, definitions, typeMap);
|
|
3529
|
+
}
|
|
3530
|
+
}
|