@veewo/gitnexus 1.3.11 → 1.4.6-rc
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 +37 -80
- package/dist/benchmark/agent-context/tool-runner.js +2 -2
- package/dist/benchmark/neonspark-candidates.js +3 -3
- package/dist/benchmark/tool-runner.js +2 -2
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +16 -12
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +68 -48
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +8 -1
- package/dist/cli/eval-server.js +30 -13
- package/dist/cli/index.js +28 -82
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/mcp.js +3 -1
- package/dist/cli/setup.js +87 -48
- package/dist/cli/setup.test.js +18 -13
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +50 -16
- package/dist/cli/wiki.js +8 -4
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +4 -1
- package/dist/config/supported-languages.js +3 -2
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +7 -2
- package/dist/core/ingestion/ast-cache.js +3 -2
- package/dist/core/ingestion/call-processor.d.ts +8 -6
- package/dist/core/ingestion/call-processor.js +468 -206
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
- package/dist/core/ingestion/entry-point-scoring.js +116 -23
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.d.ts +19 -4
- package/dist/core/ingestion/framework-detection.js +182 -6
- package/dist/core/ingestion/heritage-processor.d.ts +13 -5
- package/dist/core/ingestion/heritage-processor.js +109 -55
- package/dist/core/ingestion/import-processor.d.ts +16 -20
- package/dist/core/ingestion/import-processor.js +199 -579
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +4 -1
- package/dist/core/ingestion/parsing-processor.js +107 -109
- package/dist/core/ingestion/pipeline.d.ts +6 -3
- package/dist/core/ingestion/pipeline.js +208 -114
- package/dist/core/ingestion/process-processor.js +8 -2
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/symbol-table.d.ts +21 -1
- package/dist/core/ingestion/symbol-table.js +40 -12
- package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
- package/dist/core/ingestion/tree-sitter-queries.js +297 -7
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +611 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +406 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +449 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
- package/dist/core/ingestion/type-extractors/shared.js +703 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +103 -0
- package/dist/core/ingestion/utils.js +1085 -4
- package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
- package/dist/core/ingestion/workers/parse-worker.js +634 -222
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
- package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
- package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
- package/dist/core/{kuzu → lbug}/schema.js +23 -22
- package/dist/core/lbug/schema.test.d.ts +1 -0
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +12 -11
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +19 -0
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +6 -6
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
- package/dist/mcp/core/lbug-adapter.js +327 -0
- package/dist/mcp/local/local-backend.d.ts +21 -16
- package/dist/mcp/local/local-backend.js +306 -706
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
- package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
- package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
- package/dist/mcp/resources.js +2 -2
- package/dist/mcp/server.js +28 -13
- package/dist/mcp/staleness.js +2 -2
- package/dist/mcp/tools.js +12 -3
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +74 -4
- package/dist/types/pipeline.d.ts +1 -1
- package/hooks/claude/gitnexus-hook.cjs +149 -46
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +20 -4
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/skills/gitnexus-cli.md +8 -8
- package/skills/gitnexus-debugging.md +1 -1
- package/skills/gitnexus-exploring.md +1 -1
- package/skills/gitnexus-guide.md +1 -1
- package/skills/gitnexus-impact-analysis.md +1 -1
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/mcp/core/kuzu-adapter.js +0 -231
- /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
- /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
- /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
|
@@ -1,151 +1,114 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
-
import {
|
|
2
|
+
import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
3
|
+
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
3
4
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
4
5
|
import { generateId } from '../../lib/utils.js';
|
|
5
|
-
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const FUNCTION_NODE_TYPES = new Set([
|
|
11
|
-
// TypeScript/JavaScript
|
|
12
|
-
'function_declaration',
|
|
13
|
-
'arrow_function',
|
|
14
|
-
'function_expression',
|
|
15
|
-
'method_definition',
|
|
16
|
-
'generator_function_declaration',
|
|
17
|
-
// Python
|
|
18
|
-
'function_definition',
|
|
19
|
-
// Common async variants
|
|
20
|
-
'async_function_declaration',
|
|
21
|
-
'async_arrow_function',
|
|
22
|
-
// Java
|
|
23
|
-
'method_declaration',
|
|
24
|
-
'constructor_declaration',
|
|
25
|
-
// C/C++
|
|
26
|
-
// 'function_definition' already included above
|
|
27
|
-
// Go
|
|
28
|
-
// 'method_declaration' already included from Java
|
|
29
|
-
// C#
|
|
30
|
-
'local_function_statement',
|
|
31
|
-
// Rust
|
|
32
|
-
'function_item',
|
|
33
|
-
'impl_item', // Methods inside impl blocks
|
|
34
|
-
]);
|
|
6
|
+
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
|
|
7
|
+
import { buildTypeEnv } from './type-env.js';
|
|
8
|
+
import { getTreeSitterBufferSize } from './constants.js';
|
|
9
|
+
import { callRouters } from './call-routing.js';
|
|
10
|
+
import { extractReturnTypeName } from './type-extractors/shared.js';
|
|
35
11
|
/**
|
|
36
12
|
* Walk up the AST from a node to find the enclosing function/method.
|
|
37
13
|
* Returns null if the call is at module/file level (top-level code).
|
|
38
14
|
*/
|
|
39
|
-
const findEnclosingFunction = (node, filePath,
|
|
15
|
+
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
40
16
|
let current = node.parent;
|
|
41
17
|
while (current) {
|
|
42
18
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
current.type === 'function_definition' ||
|
|
49
|
-
current.type === 'async_function_declaration' ||
|
|
50
|
-
current.type === 'generator_function_declaration' ||
|
|
51
|
-
current.type === 'function_item') { // Rust function
|
|
52
|
-
// Named function: function foo() {}
|
|
53
|
-
const nameNode = current.childForFieldName?.('name') ||
|
|
54
|
-
current.children?.find((c) => c.type === 'identifier' || c.type === 'property_identifier');
|
|
55
|
-
funcName = nameNode?.text;
|
|
56
|
-
}
|
|
57
|
-
else if (current.type === 'impl_item') {
|
|
58
|
-
// Rust method inside impl block: wrapper around function_item or const_item
|
|
59
|
-
// We need to look inside for the function_item
|
|
60
|
-
const funcItem = current.children?.find((c) => c.type === 'function_item');
|
|
61
|
-
if (funcItem) {
|
|
62
|
-
const nameNode = funcItem.childForFieldName?.('name') ||
|
|
63
|
-
funcItem.children?.find((c) => c.type === 'identifier');
|
|
64
|
-
funcName = nameNode?.text;
|
|
65
|
-
label = 'Method';
|
|
19
|
+
const { funcName, label } = extractFunctionName(current);
|
|
20
|
+
if (funcName) {
|
|
21
|
+
const resolved = ctx.resolve(funcName, filePath);
|
|
22
|
+
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
23
|
+
return resolved.candidates[0].nodeId;
|
|
66
24
|
}
|
|
25
|
+
return generateId(label, `${filePath}:${funcName}`);
|
|
67
26
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
27
|
+
}
|
|
28
|
+
current = current.parent;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Verify constructor bindings against SymbolTable and infer receiver types.
|
|
34
|
+
* Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths.
|
|
35
|
+
*/
|
|
36
|
+
const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
37
|
+
const verified = new Map();
|
|
38
|
+
for (const { scope, varName, calleeName, receiverClassName } of bindings) {
|
|
39
|
+
const tiered = ctx.resolve(calleeName, filePath);
|
|
40
|
+
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
|
|
41
|
+
if (isClass) {
|
|
42
|
+
verified.set(receiverKey(scope, varName), calleeName);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
let callableDefs = tiered?.candidates.filter(d => d.type === 'Function' || d.type === 'Method');
|
|
46
|
+
// When receiver class is known (e.g. $this->method() in PHP), narrow
|
|
47
|
+
// candidates to methods owned by that class to avoid false disambiguation failures.
|
|
48
|
+
if (callableDefs && callableDefs.length > 1 && receiverClassName) {
|
|
49
|
+
if (graph) {
|
|
50
|
+
// Worker path: use graph.getNode (fast, already in-memory)
|
|
51
|
+
const narrowed = callableDefs.filter(d => {
|
|
52
|
+
if (!d.ownerId)
|
|
53
|
+
return false;
|
|
54
|
+
const owner = graph.getNode(d.ownerId);
|
|
55
|
+
return owner?.properties.name === receiverClassName;
|
|
56
|
+
});
|
|
57
|
+
if (narrowed.length > 0)
|
|
58
|
+
callableDefs = narrowed;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Sequential path: use ctx.resolve (no graph available)
|
|
62
|
+
const classResolved = ctx.resolve(receiverClassName, filePath);
|
|
63
|
+
if (classResolved && classResolved.candidates.length > 0) {
|
|
64
|
+
const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
|
|
65
|
+
const narrowed = callableDefs.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
|
66
|
+
if (narrowed.length > 0)
|
|
67
|
+
callableDefs = narrowed;
|
|
68
|
+
}
|
|
96
69
|
}
|
|
97
70
|
}
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return nodeId;
|
|
104
|
-
// Try construct ID manually if lookup fails (common for non-exported internal functions)
|
|
105
|
-
// Format should match what parsing-processor generates: "Function:path/to/file:funcName"
|
|
106
|
-
// Check if we already have a node with this ID in the symbol table to be safe
|
|
107
|
-
const generatedId = generateId(label, `${filePath}:${funcName}`);
|
|
108
|
-
// Ideally we should verify this ID exists, but strictly speaking if we are inside it,
|
|
109
|
-
// it SHOULD exist. Returning it is better than falling back to File.
|
|
110
|
-
return generatedId;
|
|
71
|
+
if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) {
|
|
72
|
+
const typeName = extractReturnTypeName(callableDefs[0].returnType);
|
|
73
|
+
if (typeName) {
|
|
74
|
+
verified.set(receiverKey(scope, varName), typeName);
|
|
75
|
+
}
|
|
111
76
|
}
|
|
112
|
-
// Couldn't determine function name - try parent (might be nested)
|
|
113
77
|
}
|
|
114
|
-
current = current.parent;
|
|
115
78
|
}
|
|
116
|
-
return
|
|
79
|
+
return verified;
|
|
117
80
|
};
|
|
118
|
-
export const processCalls = async (graph, files, astCache,
|
|
81
|
+
export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
119
82
|
const parser = await loadParser();
|
|
83
|
+
const collectedHeritage = [];
|
|
84
|
+
const logSkipped = isVerboseIngestionEnabled();
|
|
85
|
+
const skippedByLang = logSkipped ? new Map() : null;
|
|
120
86
|
for (let i = 0; i < files.length; i++) {
|
|
121
87
|
const file = files[i];
|
|
122
88
|
onProgress?.(i + 1, files.length);
|
|
123
89
|
if (i % 20 === 0)
|
|
124
90
|
await yieldToEventLoop();
|
|
125
|
-
// 1. Check language support first
|
|
126
91
|
const language = getLanguageFromFilename(file.path);
|
|
127
92
|
if (!language)
|
|
128
93
|
continue;
|
|
94
|
+
if (!isLanguageAvailable(language)) {
|
|
95
|
+
if (skippedByLang) {
|
|
96
|
+
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
129
100
|
const queryStr = LANGUAGE_QUERIES[language];
|
|
130
101
|
if (!queryStr)
|
|
131
102
|
continue;
|
|
132
|
-
// 2. ALWAYS load the language before querying (parser is stateful)
|
|
133
103
|
await loadLanguage(language, file.path);
|
|
134
|
-
// 3. Get AST (Try Cache First)
|
|
135
104
|
let tree = astCache.get(file.path);
|
|
136
|
-
let wasReparsed = false;
|
|
137
105
|
if (!tree) {
|
|
138
|
-
// Cache Miss: Re-parse
|
|
139
|
-
// Use larger bufferSize for files > 32KB
|
|
140
106
|
try {
|
|
141
|
-
tree = parser.parse(file.content, undefined, { bufferSize:
|
|
107
|
+
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
142
108
|
}
|
|
143
109
|
catch (parseError) {
|
|
144
|
-
// Skip files that can't be parsed
|
|
145
110
|
continue;
|
|
146
111
|
}
|
|
147
|
-
wasReparsed = true;
|
|
148
|
-
// Cache re-parsed tree so heritage phase gets hits
|
|
149
112
|
astCache.set(file.path, tree);
|
|
150
113
|
}
|
|
151
114
|
let query;
|
|
@@ -159,28 +122,130 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
159
122
|
console.warn(`Query error for ${file.path}:`, queryError);
|
|
160
123
|
continue;
|
|
161
124
|
}
|
|
162
|
-
|
|
125
|
+
const lang = getLanguageFromFilename(file.path);
|
|
126
|
+
const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
|
|
127
|
+
const callRouter = callRouters[language];
|
|
128
|
+
const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
|
|
129
|
+
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
|
130
|
+
: new Map();
|
|
131
|
+
ctx.enableCache(file.path);
|
|
163
132
|
matches.forEach(match => {
|
|
164
133
|
const captureMap = {};
|
|
165
134
|
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
166
|
-
// Only process @call captures
|
|
167
135
|
if (!captureMap['call'])
|
|
168
136
|
return;
|
|
169
137
|
const nameNode = captureMap['call.name'];
|
|
170
138
|
if (!nameNode)
|
|
171
139
|
return;
|
|
172
140
|
const calledName = nameNode.text;
|
|
173
|
-
|
|
141
|
+
const routed = callRouter(calledName, captureMap['call']);
|
|
142
|
+
if (routed) {
|
|
143
|
+
switch (routed.kind) {
|
|
144
|
+
case 'skip':
|
|
145
|
+
case 'import':
|
|
146
|
+
return;
|
|
147
|
+
case 'heritage':
|
|
148
|
+
for (const item of routed.items) {
|
|
149
|
+
collectedHeritage.push({
|
|
150
|
+
filePath: file.path,
|
|
151
|
+
className: item.enclosingClass,
|
|
152
|
+
parentName: item.mixinName,
|
|
153
|
+
kind: item.heritageKind,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
case 'properties': {
|
|
158
|
+
const fileId = generateId('File', file.path);
|
|
159
|
+
const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
|
|
160
|
+
for (const item of routed.items) {
|
|
161
|
+
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
|
162
|
+
graph.addNode({
|
|
163
|
+
id: nodeId,
|
|
164
|
+
label: 'Property',
|
|
165
|
+
properties: {
|
|
166
|
+
name: item.propName, filePath: file.path,
|
|
167
|
+
startLine: item.startLine, endLine: item.endLine,
|
|
168
|
+
language, isExported: true,
|
|
169
|
+
description: item.accessorType,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
ctx.symbols.add(file.path, item.propName, nodeId, 'Property', propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
|
|
173
|
+
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
174
|
+
graph.addRelationship({
|
|
175
|
+
id: relId, sourceId: fileId, targetId: nodeId,
|
|
176
|
+
type: 'DEFINES', confidence: 1.0, reason: '',
|
|
177
|
+
});
|
|
178
|
+
if (propEnclosingClassId) {
|
|
179
|
+
graph.addRelationship({
|
|
180
|
+
id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
|
|
181
|
+
sourceId: propEnclosingClassId, targetId: nodeId,
|
|
182
|
+
type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
case 'call':
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
174
192
|
if (isBuiltInOrNoise(calledName))
|
|
175
193
|
return;
|
|
176
|
-
|
|
177
|
-
const
|
|
194
|
+
const callNode = captureMap['call'];
|
|
195
|
+
const callForm = inferCallForm(callNode, nameNode);
|
|
196
|
+
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
197
|
+
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
198
|
+
// Fall back to verified constructor bindings for return type inference
|
|
199
|
+
if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
|
|
200
|
+
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
201
|
+
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
202
|
+
receiverTypeName = lookupReceiverType(verifiedReceivers, funcName, receiverName);
|
|
203
|
+
}
|
|
204
|
+
// Fall back to class-as-receiver for static method calls (e.g. UserService.find_user()).
|
|
205
|
+
// When the receiver name is not a variable in TypeEnv but resolves to a Class/Struct/Interface
|
|
206
|
+
// through the standard tiered resolution, use it directly as the receiver type.
|
|
207
|
+
if (!receiverTypeName && receiverName && callForm === 'member') {
|
|
208
|
+
const typeResolved = ctx.resolve(receiverName, file.path);
|
|
209
|
+
if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
|
|
210
|
+
receiverTypeName = receiverName;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Fall back to chained call resolution when the receiver is a call expression
|
|
214
|
+
// (e.g. svc.getUser().save() — receiver of save() is getUser(), not a simple identifier).
|
|
215
|
+
if (callForm === 'member' && !receiverTypeName && !receiverName) {
|
|
216
|
+
const receiverNode = extractReceiverNode(nameNode);
|
|
217
|
+
if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
|
|
218
|
+
const extracted = extractCallChain(receiverNode);
|
|
219
|
+
if (extracted) {
|
|
220
|
+
// Resolve the base receiver type if possible
|
|
221
|
+
let baseType = extracted.baseReceiverName && typeEnv
|
|
222
|
+
? typeEnv.lookup(extracted.baseReceiverName, callNode)
|
|
223
|
+
: undefined;
|
|
224
|
+
if (!baseType && extracted.baseReceiverName && verifiedReceivers.size > 0) {
|
|
225
|
+
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
226
|
+
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
227
|
+
baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName);
|
|
228
|
+
}
|
|
229
|
+
// Class-as-receiver for chain base (e.g. UserService.find_user().save())
|
|
230
|
+
if (!baseType && extracted.baseReceiverName) {
|
|
231
|
+
const cr = ctx.resolve(extracted.baseReceiverName, file.path);
|
|
232
|
+
if (cr?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
|
|
233
|
+
baseType = extracted.baseReceiverName;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
receiverTypeName = resolveChainedReceiver(extracted.chain, baseType, file.path, ctx);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const resolved = resolveCallTarget({
|
|
241
|
+
calledName,
|
|
242
|
+
argCount: countCallArguments(callNode),
|
|
243
|
+
callForm,
|
|
244
|
+
receiverTypeName,
|
|
245
|
+
}, file.path, ctx);
|
|
178
246
|
if (!resolved)
|
|
179
247
|
return;
|
|
180
|
-
|
|
181
|
-
const callNode = captureMap['call'];
|
|
182
|
-
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
|
|
183
|
-
// Use enclosing function as source, fallback to file for top-level calls
|
|
248
|
+
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
184
249
|
const sourceId = enclosingFuncId || generateId('File', file.path);
|
|
185
250
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
186
251
|
graph.addRelationship({
|
|
@@ -192,105 +257,216 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
192
257
|
reason: resolved.reason,
|
|
193
258
|
});
|
|
194
259
|
});
|
|
195
|
-
|
|
260
|
+
ctx.clearCache();
|
|
196
261
|
}
|
|
262
|
+
if (skippedByLang && skippedByLang.size > 0) {
|
|
263
|
+
for (const [lang, count] of skippedByLang.entries()) {
|
|
264
|
+
console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in call processing — ${lang} parser not available.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return collectedHeritage;
|
|
268
|
+
};
|
|
269
|
+
const CALLABLE_SYMBOL_TYPES = new Set([
|
|
270
|
+
'Function',
|
|
271
|
+
'Method',
|
|
272
|
+
'Constructor',
|
|
273
|
+
'Macro',
|
|
274
|
+
'Delegate',
|
|
275
|
+
]);
|
|
276
|
+
const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
|
|
277
|
+
const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
278
|
+
let kindFiltered;
|
|
279
|
+
if (callForm === 'constructor') {
|
|
280
|
+
const constructors = candidates.filter(c => c.type === 'Constructor');
|
|
281
|
+
if (constructors.length > 0) {
|
|
282
|
+
kindFiltered = constructors;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const types = candidates.filter(c => CONSTRUCTOR_TARGET_TYPES.has(c.type));
|
|
286
|
+
kindFiltered = types.length > 0 ? types : candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
kindFiltered = candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
|
|
291
|
+
}
|
|
292
|
+
if (kindFiltered.length === 0)
|
|
293
|
+
return [];
|
|
294
|
+
if (argCount === undefined)
|
|
295
|
+
return kindFiltered;
|
|
296
|
+
const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
|
|
297
|
+
if (!hasParameterMetadata)
|
|
298
|
+
return kindFiltered;
|
|
299
|
+
return kindFiltered.filter(candidate => candidate.parameterCount === undefined || candidate.parameterCount === argCount);
|
|
197
300
|
};
|
|
301
|
+
const toResolveResult = (definition, tier) => ({
|
|
302
|
+
nodeId: definition.nodeId,
|
|
303
|
+
confidence: TIER_CONFIDENCE[tier],
|
|
304
|
+
reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
|
|
305
|
+
});
|
|
198
306
|
/**
|
|
199
|
-
* Resolve a
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* C. Fuzzy global search (lowest confidence)
|
|
307
|
+
* Resolve a chain of intermediate method calls to find the receiver type for a
|
|
308
|
+
* final member call. Called when the receiver of a call is itself a call
|
|
309
|
+
* expression (e.g. `svc.getUser().save()`).
|
|
203
310
|
*
|
|
204
|
-
*
|
|
311
|
+
* @param chainNames Ordered list of method names from outermost to innermost
|
|
312
|
+
* intermediate call (e.g. ['getUser'] for `svc.getUser().save()`).
|
|
313
|
+
* @param baseReceiverTypeName The already-resolved type of the base receiver
|
|
314
|
+
* (e.g. 'UserService' for `svc`), or undefined.
|
|
315
|
+
* @param currentFile The file path for resolution context.
|
|
316
|
+
* @param ctx The resolution context for symbol lookup.
|
|
317
|
+
* @returns The type name of the final intermediate call's return type, or undefined
|
|
318
|
+
* if resolution fails at any step.
|
|
205
319
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
320
|
+
function resolveChainedReceiver(chainNames, baseReceiverTypeName, currentFile, ctx) {
|
|
321
|
+
let currentType = baseReceiverTypeName;
|
|
322
|
+
for (const name of chainNames) {
|
|
323
|
+
const resolved = resolveCallTarget({ calledName: name, callForm: 'member', receiverTypeName: currentType }, currentFile, ctx);
|
|
324
|
+
if (!resolved)
|
|
325
|
+
return undefined;
|
|
326
|
+
const candidates = ctx.symbols.lookupFuzzy(name);
|
|
327
|
+
const symDef = candidates.find(c => c.nodeId === resolved.nodeId);
|
|
328
|
+
if (!symDef?.returnType)
|
|
329
|
+
return undefined;
|
|
330
|
+
const returnTypeName = extractReturnTypeName(symDef.returnType);
|
|
331
|
+
if (!returnTypeName)
|
|
332
|
+
return undefined;
|
|
333
|
+
currentType = returnTypeName;
|
|
211
334
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
335
|
+
return currentType;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Resolve a function call to its target node ID using priority strategy:
|
|
339
|
+
* A. Narrow candidates by scope tier via ctx.resolve()
|
|
340
|
+
* B. Filter to callable symbol kinds (constructor-aware when callForm is set)
|
|
341
|
+
* C. Apply arity filtering when parameter metadata is available
|
|
342
|
+
* D. Apply receiver-type filtering for member calls with typed receivers
|
|
343
|
+
*
|
|
344
|
+
* If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
|
|
345
|
+
*/
|
|
346
|
+
const resolveCallTarget = (call, currentFile, ctx) => {
|
|
347
|
+
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
348
|
+
if (!tiered)
|
|
349
|
+
return null;
|
|
350
|
+
const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
|
351
|
+
// D. Receiver-type filtering: for member calls with a known receiver type,
|
|
352
|
+
// resolve the type through the same tiered import infrastructure, then
|
|
353
|
+
// filter method candidates to the type's defining file. Fall back to
|
|
354
|
+
// fuzzy ownerId matching only when file-based narrowing is inconclusive.
|
|
355
|
+
//
|
|
356
|
+
// Applied regardless of candidate count — the sole same-file candidate may
|
|
357
|
+
// belong to the wrong class (e.g. super.save() should hit the parent's save,
|
|
358
|
+
// not the child's own save method in the same file).
|
|
359
|
+
if (call.callForm === 'member' && call.receiverTypeName) {
|
|
360
|
+
// D1. Resolve the receiver type
|
|
361
|
+
const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
|
|
362
|
+
if (typeResolved && typeResolved.candidates.length > 0) {
|
|
363
|
+
const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
|
|
364
|
+
const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
|
|
365
|
+
// D2. Widen candidates: same-file tier may miss the parent's method when
|
|
366
|
+
// it lives in another file. Query the symbol table directly for all
|
|
367
|
+
// global methods with this name, then apply arity/kind filtering.
|
|
368
|
+
const methodPool = filteredCandidates.length <= 1
|
|
369
|
+
? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
|
|
370
|
+
: filteredCandidates;
|
|
371
|
+
// D3. File-based: prefer candidates whose filePath matches the resolved type's file
|
|
372
|
+
const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
|
|
373
|
+
if (fileFiltered.length === 1) {
|
|
374
|
+
return toResolveResult(fileFiltered[0], tiered.tier);
|
|
375
|
+
}
|
|
376
|
+
// D4. ownerId fallback: narrow by ownerId matching the type's nodeId
|
|
377
|
+
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
|
|
378
|
+
const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
379
|
+
if (ownerFiltered.length === 1) {
|
|
380
|
+
return toResolveResult(ownerFiltered[0], tiered.tier);
|
|
223
381
|
}
|
|
382
|
+
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
383
|
+
return null;
|
|
224
384
|
}
|
|
225
|
-
// Strategy C: Fuzzy global (no import match found)
|
|
226
|
-
const confidence = allDefs.length === 1 ? 0.5 : 0.3;
|
|
227
|
-
return { nodeId: allDefs[0].nodeId, confidence, reason: 'fuzzy-global' };
|
|
228
385
|
}
|
|
229
|
-
|
|
386
|
+
if (filteredCandidates.length !== 1)
|
|
387
|
+
return null;
|
|
388
|
+
return toResolveResult(filteredCandidates[0], tiered.tier);
|
|
389
|
+
};
|
|
390
|
+
// ── Scope key helpers ────────────────────────────────────────────────────
|
|
391
|
+
// Scope keys use the format "funcName@startIndex" (produced by type-env.ts).
|
|
392
|
+
// Source IDs use "Label:filepath:funcName" (produced by parse-worker.ts).
|
|
393
|
+
// NUL (\0) is used as a composite-key separator because it cannot appear
|
|
394
|
+
// in source-code identifiers, preventing ambiguous concatenation.
|
|
395
|
+
//
|
|
396
|
+
// receiverKey stores the FULL scope (funcName@startIndex) to prevent
|
|
397
|
+
// collisions between overloaded methods with the same name in different
|
|
398
|
+
// classes (e.g. User.save@100 and Repo.save@200 are distinct keys).
|
|
399
|
+
// Lookup uses a secondary funcName-only index built in lookupReceiverType.
|
|
400
|
+
/** Extract the function name from a scope key ("funcName@startIndex" → "funcName"). */
|
|
401
|
+
const extractFuncNameFromScope = (scope) => scope.slice(0, scope.indexOf('@'));
|
|
402
|
+
/** Extract the trailing function name from a sourceId ("Function:filepath:funcName" → "funcName"). */
|
|
403
|
+
const extractFuncNameFromSourceId = (sourceId) => {
|
|
404
|
+
const lastColon = sourceId.lastIndexOf(':');
|
|
405
|
+
return lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
|
|
230
406
|
};
|
|
231
407
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
408
|
+
* Build a composite key for receiver type storage.
|
|
409
|
+
* Uses the full scope string (e.g. "save@100") to distinguish overloaded
|
|
410
|
+
* methods with the same name in different classes.
|
|
234
411
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe',
|
|
277
|
-
'min', 'max', 'clamp', 'abs', 'swap',
|
|
278
|
-
'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg',
|
|
279
|
-
'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg',
|
|
280
|
-
'GFP_KERNEL', 'GFP_ATOMIC',
|
|
281
|
-
'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore',
|
|
282
|
-
'mutex_lock', 'mutex_unlock', 'mutex_init',
|
|
283
|
-
'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree',
|
|
284
|
-
'get', 'put',
|
|
285
|
-
]);
|
|
286
|
-
const isBuiltInOrNoise = (name) => BUILT_IN_NAMES.has(name);
|
|
412
|
+
const receiverKey = (scope, varName) => `${scope}\0${varName}`;
|
|
413
|
+
/**
|
|
414
|
+
* Look up a receiver type from a verified receiver map.
|
|
415
|
+
* The map is keyed by `scope\0varName` (full scope with @startIndex).
|
|
416
|
+
* Since the lookup side only has `funcName` (no startIndex), we scan for
|
|
417
|
+
* all entries whose key starts with `funcName@` and has the matching varName.
|
|
418
|
+
* If exactly one unique type is found, return it. If multiple distinct types
|
|
419
|
+
* exist (true overload collision), return undefined (refuse to guess).
|
|
420
|
+
* Falls back to the file-level scope key `\0varName` (empty funcName).
|
|
421
|
+
*/
|
|
422
|
+
const lookupReceiverType = (map, funcName, varName) => {
|
|
423
|
+
// Fast path: file-level scope (empty funcName — used as fallback)
|
|
424
|
+
const fileLevelKey = receiverKey('', varName);
|
|
425
|
+
const prefix = `${funcName}@`;
|
|
426
|
+
const suffix = `\0${varName}`;
|
|
427
|
+
let found;
|
|
428
|
+
let ambiguous = false;
|
|
429
|
+
for (const [key, value] of map) {
|
|
430
|
+
if (key === fileLevelKey)
|
|
431
|
+
continue; // handled separately below
|
|
432
|
+
if (key.startsWith(prefix) && key.endsWith(suffix)) {
|
|
433
|
+
// Verify the key is exactly "funcName@<digits>\0varName" with no extra chars.
|
|
434
|
+
// The part between prefix and suffix should be the startIndex (digits only),
|
|
435
|
+
// but we accept any non-empty segment to be forward-compatible.
|
|
436
|
+
const middle = key.slice(prefix.length, key.length - suffix.length);
|
|
437
|
+
if (middle.length === 0)
|
|
438
|
+
continue; // malformed key — skip
|
|
439
|
+
if (found === undefined) {
|
|
440
|
+
found = value;
|
|
441
|
+
}
|
|
442
|
+
else if (found !== value) {
|
|
443
|
+
ambiguous = true;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!ambiguous && found !== undefined)
|
|
449
|
+
return found;
|
|
450
|
+
// Fallback: file-level scope (bindings outside any function)
|
|
451
|
+
return map.get(fileLevelKey);
|
|
452
|
+
};
|
|
287
453
|
/**
|
|
288
454
|
* Fast path: resolve pre-extracted call sites from workers.
|
|
289
455
|
* No AST parsing — workers already extracted calledName + sourceId.
|
|
290
|
-
* This function only does symbol table lookups + graph mutations.
|
|
291
456
|
*/
|
|
292
|
-
export const processCallsFromExtracted = async (graph, extractedCalls,
|
|
293
|
-
//
|
|
457
|
+
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
|
|
458
|
+
// Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
|
|
459
|
+
// The scope dimension prevents collisions when two functions in the same file
|
|
460
|
+
// have same-named locals pointing to different constructor types.
|
|
461
|
+
const fileReceiverTypes = new Map();
|
|
462
|
+
if (constructorBindings) {
|
|
463
|
+
for (const { filePath, bindings } of constructorBindings) {
|
|
464
|
+
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
|
|
465
|
+
if (verified.size > 0) {
|
|
466
|
+
fileReceiverTypes.set(filePath, verified);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
294
470
|
const byFile = new Map();
|
|
295
471
|
for (const call of extractedCalls) {
|
|
296
472
|
let list = byFile.get(call.filePath);
|
|
@@ -302,26 +478,112 @@ export const processCallsFromExtracted = async (graph, extractedCalls, symbolTab
|
|
|
302
478
|
}
|
|
303
479
|
const totalFiles = byFile.size;
|
|
304
480
|
let filesProcessed = 0;
|
|
305
|
-
for (const [
|
|
481
|
+
for (const [filePath, calls] of byFile) {
|
|
306
482
|
filesProcessed++;
|
|
307
483
|
if (filesProcessed % 100 === 0) {
|
|
308
484
|
onProgress?.(filesProcessed, totalFiles);
|
|
309
485
|
await yieldToEventLoop();
|
|
310
486
|
}
|
|
487
|
+
ctx.enableCache(filePath);
|
|
488
|
+
const receiverMap = fileReceiverTypes.get(filePath);
|
|
311
489
|
for (const call of calls) {
|
|
312
|
-
|
|
490
|
+
let effectiveCall = call;
|
|
491
|
+
// Step 1: resolve receiver type from constructor bindings
|
|
492
|
+
if (!call.receiverTypeName && call.receiverName && receiverMap) {
|
|
493
|
+
const callFuncName = extractFuncNameFromSourceId(call.sourceId);
|
|
494
|
+
const resolvedType = lookupReceiverType(receiverMap, callFuncName, call.receiverName);
|
|
495
|
+
if (resolvedType) {
|
|
496
|
+
effectiveCall = { ...call, receiverTypeName: resolvedType };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Step 1b: class-as-receiver for static method calls (e.g. UserService.find_user())
|
|
500
|
+
if (!effectiveCall.receiverTypeName && effectiveCall.receiverName && effectiveCall.callForm === 'member') {
|
|
501
|
+
const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
|
|
502
|
+
if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
|
|
503
|
+
effectiveCall = { ...effectiveCall, receiverTypeName: effectiveCall.receiverName };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Step 2: if the call has a receiver call chain (e.g. svc.getUser().save()),
|
|
507
|
+
// resolve the chain to determine the final receiver type.
|
|
508
|
+
// This runs whenever receiverCallChain is present — even when Step 1 set a
|
|
509
|
+
// receiverTypeName, that type is the BASE receiver (e.g. UserService for svc),
|
|
510
|
+
// and the chain must be walked to produce the FINAL receiver (e.g. User from
|
|
511
|
+
// getUser() : User).
|
|
512
|
+
if (effectiveCall.receiverCallChain?.length) {
|
|
513
|
+
// Step 1 may have resolved the base receiver type (e.g. svc → UserService).
|
|
514
|
+
// Use it as the starting point for chain resolution.
|
|
515
|
+
let baseType = effectiveCall.receiverTypeName;
|
|
516
|
+
// If Step 1 didn't resolve it, try the receiver map directly.
|
|
517
|
+
if (!baseType && effectiveCall.receiverName && receiverMap) {
|
|
518
|
+
const callFuncName = extractFuncNameFromSourceId(effectiveCall.sourceId);
|
|
519
|
+
baseType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName);
|
|
520
|
+
}
|
|
521
|
+
const chainedType = resolveChainedReceiver(effectiveCall.receiverCallChain, baseType, effectiveCall.filePath, ctx);
|
|
522
|
+
if (chainedType) {
|
|
523
|
+
effectiveCall = { ...effectiveCall, receiverTypeName: chainedType };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
313
527
|
if (!resolved)
|
|
314
528
|
continue;
|
|
315
|
-
const relId = generateId('CALLS', `${
|
|
529
|
+
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
316
530
|
graph.addRelationship({
|
|
317
531
|
id: relId,
|
|
318
|
-
sourceId:
|
|
532
|
+
sourceId: effectiveCall.sourceId,
|
|
319
533
|
targetId: resolved.nodeId,
|
|
320
534
|
type: 'CALLS',
|
|
321
535
|
confidence: resolved.confidence,
|
|
322
536
|
reason: resolved.reason,
|
|
323
537
|
});
|
|
324
538
|
}
|
|
539
|
+
ctx.clearCache();
|
|
325
540
|
}
|
|
326
541
|
onProgress?.(totalFiles, totalFiles);
|
|
327
542
|
};
|
|
543
|
+
/**
|
|
544
|
+
* Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
|
|
545
|
+
*/
|
|
546
|
+
export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
|
|
547
|
+
for (let i = 0; i < extractedRoutes.length; i++) {
|
|
548
|
+
const route = extractedRoutes[i];
|
|
549
|
+
if (i % 50 === 0) {
|
|
550
|
+
onProgress?.(i, extractedRoutes.length);
|
|
551
|
+
await yieldToEventLoop();
|
|
552
|
+
}
|
|
553
|
+
if (!route.controllerName || !route.methodName)
|
|
554
|
+
continue;
|
|
555
|
+
const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
|
|
556
|
+
if (!controllerResolved || controllerResolved.candidates.length === 0)
|
|
557
|
+
continue;
|
|
558
|
+
if (controllerResolved.tier === 'global' && controllerResolved.candidates.length > 1)
|
|
559
|
+
continue;
|
|
560
|
+
const controllerDef = controllerResolved.candidates[0];
|
|
561
|
+
const confidence = TIER_CONFIDENCE[controllerResolved.tier];
|
|
562
|
+
const methodResolved = ctx.resolve(route.methodName, controllerDef.filePath);
|
|
563
|
+
const methodId = methodResolved?.tier === 'same-file' ? methodResolved.candidates[0]?.nodeId : undefined;
|
|
564
|
+
const sourceId = generateId('File', route.filePath);
|
|
565
|
+
if (!methodId) {
|
|
566
|
+
const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
|
|
567
|
+
const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
|
|
568
|
+
graph.addRelationship({
|
|
569
|
+
id: relId,
|
|
570
|
+
sourceId,
|
|
571
|
+
targetId: guessedId,
|
|
572
|
+
type: 'CALLS',
|
|
573
|
+
confidence: confidence * 0.8,
|
|
574
|
+
reason: 'laravel-route',
|
|
575
|
+
});
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const relId = generateId('CALLS', `${sourceId}:route->${methodId}`);
|
|
579
|
+
graph.addRelationship({
|
|
580
|
+
id: relId,
|
|
581
|
+
sourceId,
|
|
582
|
+
targetId: methodId,
|
|
583
|
+
type: 'CALLS',
|
|
584
|
+
confidence,
|
|
585
|
+
reason: 'laravel-route',
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
onProgress?.(extractedRoutes.length, extractedRoutes.length);
|
|
589
|
+
};
|