gitnexus 1.4.1 → 1.4.6
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 +215 -194
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +117 -90
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +57 -30
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +1 -1
- package/dist/cli/eval-server.js +14 -6
- package/dist/cli/index.js +18 -25
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/setup.js +42 -32
- 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 +48 -13
- package/dist/cli/wiki.js +2 -2
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -1
- package/dist/core/augmentation/engine.js +99 -72
- 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 +74 -47
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +5 -2
- package/dist/core/ingestion/ast-cache.js +3 -2
- package/dist/core/ingestion/call-processor.d.ts +5 -7
- package/dist/core/ingestion/call-processor.js +430 -283
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- 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 +94 -24
- 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 +5 -1
- package/dist/core/ingestion/framework-detection.js +48 -8
- 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 +202 -696
- 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 +3 -11
- package/dist/core/ingestion/parsing-processor.js +85 -181
- package/dist/core/ingestion/pipeline.d.ts +5 -1
- package/dist/core/ingestion/pipeline.js +192 -116
- package/dist/core/ingestion/process-processor.js +2 -1
- 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 +12 -11
- package/dist/core/ingestion/tree-sitter-queries.js +642 -485
- 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/types.js +1 -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 +98 -0
- package/dist/core/ingestion/utils.js +1064 -9
- package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
- package/dist/core/ingestion/workers/parse-worker.js +251 -359
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
- package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
- package/dist/core/{kuzu → lbug}/schema.js +304 -289
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +17 -16
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +9 -9
- package/dist/core/tree-sitter/parser-loader.js +9 -2
- package/dist/core/wiki/generator.d.ts +4 -52
- package/dist/core/wiki/generator.js +53 -552
- package/dist/core/wiki/graph-queries.d.ts +4 -46
- package/dist/core/wiki/graph-queries.js +103 -282
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +11 -73
- package/dist/core/wiki/prompts.d.ts +8 -52
- package/dist/core/wiki/prompts.js +86 -200
- 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} +7 -9
- package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
- package/dist/mcp/local/local-backend.d.ts +7 -6
- package/dist/mcp/local/local-backend.js +176 -147
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +18 -19
- package/dist/mcp/tools.js +103 -104
- 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/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +55 -1
- package/dist/types/pipeline.d.ts +1 -1
- package/hooks/claude/gitnexus-hook.cjs +238 -155
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +99 -96
- package/scripts/patch-tree-sitter-swift.cjs +74 -74
- package/skills/gitnexus-cli.md +82 -82
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-impact-analysis.md +97 -97
- package/skills/gitnexus-pr-review.md +163 -163
- package/skills/gitnexus-refactoring.md +121 -121
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
- package/dist/core/wiki/diagrams.d.ts +0 -27
- package/dist/core/wiki/diagrams.js +0 -163
|
@@ -1,161 +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
|
-
// Kotlin (function_declaration already included above via JS/TS)
|
|
35
|
-
'anonymous_function',
|
|
36
|
-
'lambda_literal',
|
|
37
|
-
// PHP — no additional node types needed
|
|
38
|
-
// Swift
|
|
39
|
-
'init_declaration',
|
|
40
|
-
'deinit_declaration',
|
|
41
|
-
]);
|
|
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';
|
|
42
11
|
/**
|
|
43
12
|
* Walk up the AST from a node to find the enclosing function/method.
|
|
44
13
|
* Returns null if the call is at module/file level (top-level code).
|
|
45
14
|
*/
|
|
46
|
-
const findEnclosingFunction = (node, filePath,
|
|
15
|
+
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
47
16
|
let current = node.parent;
|
|
48
17
|
while (current) {
|
|
49
18
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (current.type === 'init_declaration' || current.type === 'deinit_declaration') {
|
|
56
|
-
const funcName = current.type === 'init_declaration' ? 'init' : 'deinit';
|
|
57
|
-
const startLine = current.startPosition?.row ?? 0;
|
|
58
|
-
return generateId('Constructor', `${filePath}:${funcName}:${startLine}`);
|
|
59
|
-
}
|
|
60
|
-
if (current.type === 'function_declaration' ||
|
|
61
|
-
current.type === 'function_definition' ||
|
|
62
|
-
current.type === 'async_function_declaration' ||
|
|
63
|
-
current.type === 'generator_function_declaration' ||
|
|
64
|
-
current.type === 'function_item') { // Rust function
|
|
65
|
-
// Named function: function foo() {}
|
|
66
|
-
const nameNode = current.childForFieldName?.('name') ||
|
|
67
|
-
current.children?.find((c) => c.type === 'identifier' || c.type === 'property_identifier');
|
|
68
|
-
funcName = nameNode?.text;
|
|
69
|
-
}
|
|
70
|
-
else if (current.type === 'impl_item') {
|
|
71
|
-
// Rust method inside impl block: wrapper around function_item or const_item
|
|
72
|
-
// We need to look inside for the function_item
|
|
73
|
-
const funcItem = current.children?.find((c) => c.type === 'function_item');
|
|
74
|
-
if (funcItem) {
|
|
75
|
-
const nameNode = funcItem.childForFieldName?.('name') ||
|
|
76
|
-
funcItem.children?.find((c) => c.type === 'identifier');
|
|
77
|
-
funcName = nameNode?.text;
|
|
78
|
-
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;
|
|
79
24
|
}
|
|
25
|
+
return generateId(label, `${filePath}:${funcName}`);
|
|
80
26
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
69
|
}
|
|
110
70
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return nodeId;
|
|
117
|
-
// Try construct ID manually if lookup fails (common for non-exported internal functions)
|
|
118
|
-
// Format must match parsing-processor: "Label:path/to/file:funcName:startLine"
|
|
119
|
-
const startLine = current.startPosition?.row ?? 0;
|
|
120
|
-
return generateId(label, `${filePath}:${funcName}:${startLine}`);
|
|
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
|
+
}
|
|
121
76
|
}
|
|
122
|
-
// Couldn't determine function name - try parent (might be nested)
|
|
123
77
|
}
|
|
124
|
-
current = current.parent;
|
|
125
78
|
}
|
|
126
|
-
return
|
|
79
|
+
return verified;
|
|
127
80
|
};
|
|
128
|
-
export const processCalls = async (graph, files, astCache,
|
|
81
|
+
export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
129
82
|
const parser = await loadParser();
|
|
83
|
+
const collectedHeritage = [];
|
|
84
|
+
const logSkipped = isVerboseIngestionEnabled();
|
|
85
|
+
const skippedByLang = logSkipped ? new Map() : null;
|
|
130
86
|
for (let i = 0; i < files.length; i++) {
|
|
131
87
|
const file = files[i];
|
|
132
88
|
onProgress?.(i + 1, files.length);
|
|
133
89
|
if (i % 20 === 0)
|
|
134
90
|
await yieldToEventLoop();
|
|
135
|
-
// 1. Check language support first
|
|
136
91
|
const language = getLanguageFromFilename(file.path);
|
|
137
92
|
if (!language)
|
|
138
93
|
continue;
|
|
94
|
+
if (!isLanguageAvailable(language)) {
|
|
95
|
+
if (skippedByLang) {
|
|
96
|
+
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
139
100
|
const queryStr = LANGUAGE_QUERIES[language];
|
|
140
101
|
if (!queryStr)
|
|
141
102
|
continue;
|
|
142
|
-
// 2. ALWAYS load the language before querying (parser is stateful)
|
|
143
103
|
await loadLanguage(language, file.path);
|
|
144
|
-
// 3. Get AST (Try Cache First)
|
|
145
104
|
let tree = astCache.get(file.path);
|
|
146
|
-
let wasReparsed = false;
|
|
147
105
|
if (!tree) {
|
|
148
|
-
// Cache Miss: Re-parse
|
|
149
|
-
// Use larger bufferSize for files > 32KB
|
|
150
106
|
try {
|
|
151
|
-
tree = parser.parse(file.content, undefined, { bufferSize:
|
|
107
|
+
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
152
108
|
}
|
|
153
109
|
catch (parseError) {
|
|
154
|
-
// Skip files that can't be parsed
|
|
155
110
|
continue;
|
|
156
111
|
}
|
|
157
|
-
wasReparsed = true;
|
|
158
|
-
// Cache re-parsed tree so heritage phase gets hits
|
|
159
112
|
astCache.set(file.path, tree);
|
|
160
113
|
}
|
|
161
114
|
let query;
|
|
@@ -169,28 +122,130 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
169
122
|
console.warn(`Query error for ${file.path}:`, queryError);
|
|
170
123
|
continue;
|
|
171
124
|
}
|
|
172
|
-
|
|
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);
|
|
173
132
|
matches.forEach(match => {
|
|
174
133
|
const captureMap = {};
|
|
175
134
|
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
176
|
-
// Only process @call captures
|
|
177
135
|
if (!captureMap['call'])
|
|
178
136
|
return;
|
|
179
137
|
const nameNode = captureMap['call.name'];
|
|
180
138
|
if (!nameNode)
|
|
181
139
|
return;
|
|
182
140
|
const calledName = nameNode.text;
|
|
183
|
-
|
|
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
|
+
}
|
|
184
192
|
if (isBuiltInOrNoise(calledName))
|
|
185
193
|
return;
|
|
186
|
-
|
|
187
|
-
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);
|
|
188
246
|
if (!resolved)
|
|
189
247
|
return;
|
|
190
|
-
|
|
191
|
-
const callNode = captureMap['call'];
|
|
192
|
-
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
|
|
193
|
-
// Use enclosing function as source, fallback to file for top-level calls
|
|
248
|
+
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
194
249
|
const sourceId = enclosingFuncId || generateId('File', file.path);
|
|
195
250
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
196
251
|
graph.addRelationship({
|
|
@@ -202,152 +257,216 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
202
257
|
reason: resolved.reason,
|
|
203
258
|
});
|
|
204
259
|
});
|
|
205
|
-
|
|
260
|
+
ctx.clearCache();
|
|
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));
|
|
206
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);
|
|
207
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
|
+
});
|
|
208
306
|
/**
|
|
209
|
-
* Resolve a
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* 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()`).
|
|
213
310
|
*
|
|
214
|
-
*
|
|
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.
|
|
215
319
|
*/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
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;
|
|
221
334
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
|
233
381
|
}
|
|
382
|
+
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
383
|
+
return null;
|
|
234
384
|
}
|
|
235
|
-
// Strategy C: Fuzzy global (no import match found)
|
|
236
|
-
const confidence = allDefs.length === 1 ? 0.5 : 0.3;
|
|
237
|
-
return { nodeId: allDefs[0].nodeId, confidence, reason: 'fuzzy-global' };
|
|
238
385
|
}
|
|
239
|
-
|
|
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) : '';
|
|
240
406
|
};
|
|
241
407
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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.
|
|
244
411
|
*/
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
'flatMapLatest', 'flatMapMerge', 'combine',
|
|
287
|
-
'stateIn', 'shareIn', 'launchIn',
|
|
288
|
-
// Kotlin infix stdlib functions
|
|
289
|
-
'to', 'until', 'downTo', 'step',
|
|
290
|
-
// C/C++ standard library and common kernel helpers
|
|
291
|
-
'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
|
|
292
|
-
'scanf', 'fscanf', 'sscanf',
|
|
293
|
-
'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp',
|
|
294
|
-
'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr',
|
|
295
|
-
'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod',
|
|
296
|
-
'sizeof', 'offsetof', 'typeof',
|
|
297
|
-
'assert', 'abort', 'exit', '_exit',
|
|
298
|
-
'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs',
|
|
299
|
-
// Linux kernel common macros/helpers (not real call targets)
|
|
300
|
-
'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE',
|
|
301
|
-
'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL',
|
|
302
|
-
'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe',
|
|
303
|
-
'min', 'max', 'clamp', 'abs', 'swap',
|
|
304
|
-
'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg',
|
|
305
|
-
'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg',
|
|
306
|
-
'GFP_KERNEL', 'GFP_ATOMIC',
|
|
307
|
-
'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore',
|
|
308
|
-
'mutex_lock', 'mutex_unlock', 'mutex_init',
|
|
309
|
-
'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree',
|
|
310
|
-
'get', 'put',
|
|
311
|
-
// Swift/iOS built-ins and standard library
|
|
312
|
-
'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure',
|
|
313
|
-
'assert', 'assertionFailure', 'NSLog',
|
|
314
|
-
'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement',
|
|
315
|
-
'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes',
|
|
316
|
-
'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast',
|
|
317
|
-
'type', 'MemoryLayout',
|
|
318
|
-
// Swift collection/string methods (common noise)
|
|
319
|
-
'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains',
|
|
320
|
-
'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast',
|
|
321
|
-
'sorted', 'reversed', 'enumerated', 'joined', 'split',
|
|
322
|
-
'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast',
|
|
323
|
-
'isEmpty', 'count', 'index', 'startIndex', 'endIndex',
|
|
324
|
-
// UIKit/Foundation common methods (noise in call graph)
|
|
325
|
-
'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout',
|
|
326
|
-
'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize',
|
|
327
|
-
'addTarget', 'removeTarget', 'addGestureRecognizer',
|
|
328
|
-
'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints',
|
|
329
|
-
'NSLocalizedString', 'Bundle',
|
|
330
|
-
'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates',
|
|
331
|
-
'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView',
|
|
332
|
-
'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections',
|
|
333
|
-
'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController',
|
|
334
|
-
'performSegue', 'prepare',
|
|
335
|
-
// GCD / async
|
|
336
|
-
'DispatchQueue', 'async', 'sync', 'asyncAfter',
|
|
337
|
-
'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation',
|
|
338
|
-
// Combine
|
|
339
|
-
'sink', 'store', 'assign', 'receive', 'subscribe',
|
|
340
|
-
// Notification / KVO
|
|
341
|
-
'addObserver', 'removeObserver', 'post', 'NotificationCenter',
|
|
342
|
-
]);
|
|
343
|
-
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
|
+
};
|
|
344
453
|
/**
|
|
345
454
|
* Fast path: resolve pre-extracted call sites from workers.
|
|
346
455
|
* No AST parsing — workers already extracted calledName + sourceId.
|
|
347
|
-
* This function only does symbol table lookups + graph mutations.
|
|
348
456
|
*/
|
|
349
|
-
export const processCallsFromExtracted = async (graph, extractedCalls,
|
|
350
|
-
//
|
|
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
|
+
}
|
|
351
470
|
const byFile = new Map();
|
|
352
471
|
for (const call of extractedCalls) {
|
|
353
472
|
let list = byFile.get(call.filePath);
|
|
@@ -359,33 +478,72 @@ export const processCallsFromExtracted = async (graph, extractedCalls, symbolTab
|
|
|
359
478
|
}
|
|
360
479
|
const totalFiles = byFile.size;
|
|
361
480
|
let filesProcessed = 0;
|
|
362
|
-
for (const [
|
|
481
|
+
for (const [filePath, calls] of byFile) {
|
|
363
482
|
filesProcessed++;
|
|
364
483
|
if (filesProcessed % 100 === 0) {
|
|
365
484
|
onProgress?.(filesProcessed, totalFiles);
|
|
366
485
|
await yieldToEventLoop();
|
|
367
486
|
}
|
|
487
|
+
ctx.enableCache(filePath);
|
|
488
|
+
const receiverMap = fileReceiverTypes.get(filePath);
|
|
368
489
|
for (const call of calls) {
|
|
369
|
-
|
|
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);
|
|
370
527
|
if (!resolved)
|
|
371
528
|
continue;
|
|
372
|
-
const relId = generateId('CALLS', `${
|
|
529
|
+
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
373
530
|
graph.addRelationship({
|
|
374
531
|
id: relId,
|
|
375
|
-
sourceId:
|
|
532
|
+
sourceId: effectiveCall.sourceId,
|
|
376
533
|
targetId: resolved.nodeId,
|
|
377
534
|
type: 'CALLS',
|
|
378
535
|
confidence: resolved.confidence,
|
|
379
536
|
reason: resolved.reason,
|
|
380
537
|
});
|
|
381
538
|
}
|
|
539
|
+
ctx.clearCache();
|
|
382
540
|
}
|
|
383
541
|
onProgress?.(totalFiles, totalFiles);
|
|
384
542
|
};
|
|
385
543
|
/**
|
|
386
544
|
* Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
|
|
387
545
|
*/
|
|
388
|
-
export const processRoutesFromExtracted = async (graph, extractedRoutes,
|
|
546
|
+
export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
|
|
389
547
|
for (let i = 0; i < extractedRoutes.length; i++) {
|
|
390
548
|
const route = extractedRoutes[i];
|
|
391
549
|
if (i % 50 === 0) {
|
|
@@ -394,28 +552,17 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, symbolT
|
|
|
394
552
|
}
|
|
395
553
|
if (!route.controllerName || !route.methodName)
|
|
396
554
|
continue;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (controllerDefs.length === 0)
|
|
555
|
+
const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
|
|
556
|
+
if (!controllerResolved || controllerResolved.candidates.length === 0)
|
|
400
557
|
continue;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (importedFiles.has(def.filePath)) {
|
|
408
|
-
controllerDef = def;
|
|
409
|
-
confidence = 0.9;
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// Find the method on the controller
|
|
415
|
-
const methodId = symbolTable.lookupExact(controllerDef.filePath, route.methodName);
|
|
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;
|
|
416
564
|
const sourceId = generateId('File', route.filePath);
|
|
417
565
|
if (!methodId) {
|
|
418
|
-
// Construct method ID manually
|
|
419
566
|
const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
|
|
420
567
|
const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
|
|
421
568
|
graph.addRelationship({
|