gitnexus 1.4.7 → 1.4.8
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 +22 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup.js +78 -1
- package/dist/config/supported-languages.d.ts +30 -0
- package/dist/config/supported-languages.js +30 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +2 -1
- package/dist/core/ingestion/ast-helpers.d.ts +80 -0
- package/dist/core/ingestion/ast-helpers.js +738 -0
- package/dist/core/ingestion/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/call-analysis.js +490 -0
- package/dist/core/ingestion/call-processor.d.ts +48 -1
- package/dist/core/ingestion/call-processor.js +368 -7
- package/dist/core/ingestion/call-routing.d.ts +6 -0
- package/dist/core/ingestion/entry-point-scoring.js +36 -26
- package/dist/core/ingestion/framework-detection.d.ts +10 -2
- package/dist/core/ingestion/framework-detection.js +49 -12
- package/dist/core/ingestion/heritage-processor.js +47 -49
- package/dist/core/ingestion/import-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +103 -194
- package/dist/core/ingestion/import-resolution.d.ts +101 -0
- package/dist/core/ingestion/import-resolution.js +251 -0
- package/dist/core/ingestion/language-config.d.ts +3 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +8 -3
- package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
- package/dist/core/ingestion/named-binding-extraction.js +89 -79
- package/dist/core/ingestion/parsing-processor.d.ts +2 -2
- package/dist/core/ingestion/parsing-processor.js +14 -73
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +421 -4
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +7 -4
- package/dist/core/ingestion/resolvers/index.d.ts +1 -1
- package/dist/core/ingestion/resolvers/index.js +1 -1
- package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
- package/dist/core/ingestion/resolvers/jvm.js +25 -9
- package/dist/core/ingestion/resolvers/php.d.ts +14 -0
- package/dist/core/ingestion/resolvers/php.js +43 -3
- package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
- package/dist/core/ingestion/resolvers/utils.js +16 -0
- package/dist/core/ingestion/symbol-table.d.ts +16 -0
- package/dist/core/ingestion/symbol-table.js +20 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
- package/dist/core/ingestion/tree-sitter-queries.js +43 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +419 -96
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
- package/dist/core/ingestion/type-extractors/csharp.js +149 -16
- package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/index.js +1 -1
- package/dist/core/ingestion/type-extractors/jvm.js +169 -66
- package/dist/core/ingestion/type-extractors/rust.js +35 -1
- package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/shared.js +5 -10
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
- package/dist/core/ingestion/type-extractors/typescript.js +141 -9
- package/dist/core/ingestion/utils.d.ts +2 -120
- package/dist/core/ingestion/utils.js +3 -1051
- package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
- package/dist/core/ingestion/workers/parse-worker.js +66 -87
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +10 -0
- package/dist/core/lbug/lbug-adapter.js +69 -4
- package/dist/core/lbug/schema.d.ts +5 -3
- package/dist/core/lbug/schema.js +26 -2
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.js +12 -1
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +133 -29
- package/dist/mcp/resources.js +2 -0
- package/dist/mcp/tools.js +2 -2
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- package/package.json +10 -2
|
@@ -4,10 +4,136 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
|
|
|
4
4
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
5
5
|
import { generateId } from '../../lib/utils.js';
|
|
6
6
|
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, extractMixedChain, } from './utils.js';
|
|
7
|
-
import { buildTypeEnv } from './type-env.js';
|
|
7
|
+
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
|
8
8
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
9
9
|
import { callRouters } from './call-routing.js';
|
|
10
10
|
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
|
11
|
+
import { typeConfigs } from './type-extractors/index.js';
|
|
12
|
+
const MAX_EXPORTS_PER_FILE = 500;
|
|
13
|
+
const MAX_TYPE_NAME_LENGTH = 256;
|
|
14
|
+
/** Build a map of imported callee names → return types for cross-file call-result binding.
|
|
15
|
+
* Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle). */
|
|
16
|
+
export function buildImportedReturnTypes(filePath, namedImportMap, symbolTable) {
|
|
17
|
+
const result = new Map();
|
|
18
|
+
const fileImports = namedImportMap.get(filePath);
|
|
19
|
+
if (!fileImports)
|
|
20
|
+
return result;
|
|
21
|
+
for (const [localName, binding] of fileImports) {
|
|
22
|
+
const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
|
|
23
|
+
if (!def?.returnType)
|
|
24
|
+
continue;
|
|
25
|
+
const simpleReturn = extractReturnTypeName(def.returnType);
|
|
26
|
+
if (simpleReturn)
|
|
27
|
+
result.set(localName, simpleReturn);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/** Build cross-file RAW return types for imported callables.
|
|
32
|
+
* Unlike buildImportedReturnTypes (which stores extractReturnTypeName output),
|
|
33
|
+
* this stores the raw declared return type string (e.g., 'User[]', 'List<User>').
|
|
34
|
+
* Used by lookupRawReturnType for for-loop element extraction via extractElementTypeFromString. */
|
|
35
|
+
export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTable) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
const fileImports = namedImportMap.get(filePath);
|
|
38
|
+
if (!fileImports)
|
|
39
|
+
return result;
|
|
40
|
+
for (const [localName, binding] of fileImports) {
|
|
41
|
+
const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
|
|
42
|
+
if (!def?.returnType)
|
|
43
|
+
continue;
|
|
44
|
+
result.set(localName, def.returnType);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/** Collect resolved type bindings for exported file-scope symbols.
|
|
49
|
+
* Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. */
|
|
50
|
+
function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
|
|
51
|
+
const fileScope = typeEnv.env.get('');
|
|
52
|
+
if (!fileScope || fileScope.size === 0)
|
|
53
|
+
return null;
|
|
54
|
+
const exported = new Map();
|
|
55
|
+
for (const [varName, typeName] of fileScope) {
|
|
56
|
+
if (exported.size >= MAX_EXPORTS_PER_FILE)
|
|
57
|
+
break;
|
|
58
|
+
if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
|
|
59
|
+
continue;
|
|
60
|
+
const nodeId = symbolTable.lookupExact(filePath, varName);
|
|
61
|
+
if (!nodeId)
|
|
62
|
+
continue;
|
|
63
|
+
const node = graph.getNode(nodeId);
|
|
64
|
+
if (node?.properties?.isExported) {
|
|
65
|
+
exported.set(varName, typeName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return exported.size > 0 ? exported : null;
|
|
69
|
+
}
|
|
70
|
+
/** Build ExportedTypeMap from graph nodes — used for worker path where TypeEnv
|
|
71
|
+
* is not available in the main thread. Collects returnType/declaredType from
|
|
72
|
+
* exported symbols that have callables with known return types. */
|
|
73
|
+
export function buildExportedTypeMapFromGraph(graph, symbolTable) {
|
|
74
|
+
const result = new Map();
|
|
75
|
+
graph.forEachNode(node => {
|
|
76
|
+
if (!node.properties?.isExported)
|
|
77
|
+
return;
|
|
78
|
+
if (!node.properties?.filePath || !node.properties?.name)
|
|
79
|
+
return;
|
|
80
|
+
const filePath = node.properties.filePath;
|
|
81
|
+
const name = node.properties.name;
|
|
82
|
+
if (!name || name.length > MAX_TYPE_NAME_LENGTH)
|
|
83
|
+
return;
|
|
84
|
+
// For callable symbols, use returnType; for properties/variables, use declaredType
|
|
85
|
+
const def = symbolTable.lookupExactFull(filePath, name);
|
|
86
|
+
if (!def)
|
|
87
|
+
return;
|
|
88
|
+
const typeName = def.returnType ?? def.declaredType;
|
|
89
|
+
if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
|
|
90
|
+
return;
|
|
91
|
+
// Extract simple type name (strip Promise<>, etc.) — reuse shared utility
|
|
92
|
+
const simpleType = extractReturnTypeName(typeName) ?? typeName;
|
|
93
|
+
if (!simpleType)
|
|
94
|
+
return;
|
|
95
|
+
let fileExports = result.get(filePath);
|
|
96
|
+
if (!fileExports) {
|
|
97
|
+
fileExports = new Map();
|
|
98
|
+
result.set(filePath, fileExports);
|
|
99
|
+
}
|
|
100
|
+
if (fileExports.size < MAX_EXPORTS_PER_FILE) {
|
|
101
|
+
fileExports.set(name, simpleType);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
/** Seed cross-file receiver types into pre-extracted call records.
|
|
107
|
+
* Fills missing receiverTypeName for single-hop imported variables
|
|
108
|
+
* using ExportedTypeMap + namedImportMap — zero disk I/O, zero AST re-parsing.
|
|
109
|
+
* Mutates calls in-place. Runs BEFORE processCallsFromExtracted. */
|
|
110
|
+
export function seedCrossFileReceiverTypes(calls, namedImportMap, exportedTypeMap) {
|
|
111
|
+
if (namedImportMap.size === 0 || exportedTypeMap.size === 0) {
|
|
112
|
+
return { enrichedCount: 0 };
|
|
113
|
+
}
|
|
114
|
+
let enrichedCount = 0;
|
|
115
|
+
for (const call of calls) {
|
|
116
|
+
if (call.receiverTypeName || !call.receiverName)
|
|
117
|
+
continue;
|
|
118
|
+
if (call.callForm !== 'member')
|
|
119
|
+
continue;
|
|
120
|
+
const fileImports = namedImportMap.get(call.filePath);
|
|
121
|
+
if (!fileImports)
|
|
122
|
+
continue;
|
|
123
|
+
const binding = fileImports.get(call.receiverName);
|
|
124
|
+
if (!binding)
|
|
125
|
+
continue;
|
|
126
|
+
const upstream = exportedTypeMap.get(binding.sourcePath);
|
|
127
|
+
if (!upstream)
|
|
128
|
+
continue;
|
|
129
|
+
const type = upstream.get(binding.exportedName);
|
|
130
|
+
if (type) {
|
|
131
|
+
call.receiverTypeName = type;
|
|
132
|
+
enrichedCount++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { enrichedCount };
|
|
136
|
+
}
|
|
11
137
|
// Stdlib methods that preserve the receiver's type identity. When TypeEnv already
|
|
12
138
|
// strips nullable wrappers (Option<User> → User), these chain steps are no-ops
|
|
13
139
|
// for type resolution — the current type passes through unchanged.
|
|
@@ -87,10 +213,23 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
87
213
|
}
|
|
88
214
|
return verified;
|
|
89
215
|
};
|
|
90
|
-
export const processCalls = async (graph, files, astCache, ctx, onProgress
|
|
216
|
+
export const processCalls = async (graph, files, astCache, ctx, onProgress, exportedTypeMap,
|
|
217
|
+
/** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map<localName, typeName>. */
|
|
218
|
+
importedBindingsMap,
|
|
219
|
+
/** Phase 14 E3: cross-file return types for imported callables. Keyed by filePath → Map<calleeName, returnType>.
|
|
220
|
+
* Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
|
|
221
|
+
importedReturnTypesMap,
|
|
222
|
+
/** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
|
|
223
|
+
importedRawReturnTypesMap) => {
|
|
91
224
|
const parser = await loadParser();
|
|
92
225
|
const collectedHeritage = [];
|
|
93
226
|
const pendingWrites = [];
|
|
227
|
+
// Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf.
|
|
228
|
+
// Used as a secondary check when per-file parentMap lacks the relationship — helps
|
|
229
|
+
// when the heritage-declaring file is processed before the call site file.
|
|
230
|
+
// For remaining cases (reverse file order), the SymbolTable class-type fallback applies.
|
|
231
|
+
const globalParentMap = new Map();
|
|
232
|
+
const globalParentSeen = new Map();
|
|
94
233
|
const logSkipped = isVerboseIngestionEnabled();
|
|
95
234
|
const skippedByLang = logSkipped ? new Map() : null;
|
|
96
235
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -133,7 +272,58 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
133
272
|
continue;
|
|
134
273
|
}
|
|
135
274
|
const lang = getLanguageFromFilename(file.path);
|
|
136
|
-
|
|
275
|
+
// Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
|
|
276
|
+
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
|
277
|
+
const fileParentMap = new Map();
|
|
278
|
+
for (const match of matches) {
|
|
279
|
+
const captureMap = {};
|
|
280
|
+
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
281
|
+
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
|
282
|
+
const className = captureMap['heritage.class'].text;
|
|
283
|
+
const parentName = captureMap['heritage.extends'].text;
|
|
284
|
+
const extendsNode = captureMap['heritage.extends'];
|
|
285
|
+
const fieldDecl = extendsNode.parent;
|
|
286
|
+
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
|
|
287
|
+
continue;
|
|
288
|
+
let parents = fileParentMap.get(className);
|
|
289
|
+
if (!parents) {
|
|
290
|
+
parents = [];
|
|
291
|
+
fileParentMap.set(className, parents);
|
|
292
|
+
}
|
|
293
|
+
if (!parents.includes(parentName))
|
|
294
|
+
parents.push(parentName);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const parentMap = fileParentMap;
|
|
298
|
+
// Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
|
|
299
|
+
// Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
|
|
300
|
+
for (const [cls, parents] of fileParentMap) {
|
|
301
|
+
let global = globalParentMap.get(cls);
|
|
302
|
+
let seen = globalParentSeen.get(cls);
|
|
303
|
+
if (!global) {
|
|
304
|
+
global = [];
|
|
305
|
+
globalParentMap.set(cls, global);
|
|
306
|
+
}
|
|
307
|
+
if (!seen) {
|
|
308
|
+
seen = new Set();
|
|
309
|
+
globalParentSeen.set(cls, seen);
|
|
310
|
+
}
|
|
311
|
+
for (const p of parents) {
|
|
312
|
+
if (!seen.has(p)) {
|
|
313
|
+
seen.add(p);
|
|
314
|
+
global.push(p);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const importedBindings = importedBindingsMap?.get(file.path);
|
|
319
|
+
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
|
320
|
+
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
|
321
|
+
const typeEnv = lang ? buildTypeEnv(tree, lang, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes }) : null;
|
|
322
|
+
if (typeEnv && exportedTypeMap) {
|
|
323
|
+
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
|
324
|
+
if (fileExports)
|
|
325
|
+
exportedTypeMap.set(file.path, fileExports);
|
|
326
|
+
}
|
|
137
327
|
const callRouter = callRouters[language];
|
|
138
328
|
const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
|
|
139
329
|
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
|
@@ -245,6 +435,44 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
245
435
|
const callForm = inferCallForm(callNode, nameNode);
|
|
246
436
|
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
247
437
|
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
438
|
+
// Phase P: virtual dispatch override — when the declared type is a base class but
|
|
439
|
+
// the constructor created a known subclass, prefer the more specific type.
|
|
440
|
+
// Checks per-file parentMap first, then falls back to globalParentMap for
|
|
441
|
+
// cross-file heritage (e.g. Dog extends Animal declared in a different file).
|
|
442
|
+
// Reconstructs the exact scope key (funcName@startIndex\0varName) from the
|
|
443
|
+
// enclosing function AST node for a correct, O(1) map lookup.
|
|
444
|
+
if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) {
|
|
445
|
+
// Reconstruct scope key to match constructorTypeMap's scope\0varName format
|
|
446
|
+
let scope = '';
|
|
447
|
+
let p = callNode.parent;
|
|
448
|
+
while (p) {
|
|
449
|
+
if (FUNCTION_NODE_TYPES.has(p.type)) {
|
|
450
|
+
const { funcName } = extractFunctionName(p);
|
|
451
|
+
if (funcName) {
|
|
452
|
+
scope = `${funcName}@${p.startIndex}`;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
p = p.parent;
|
|
457
|
+
}
|
|
458
|
+
const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`);
|
|
459
|
+
if (ctorType && ctorType !== receiverTypeName) {
|
|
460
|
+
// Verify subclass relationship: per-file parentMap first, then cross-file
|
|
461
|
+
// globalParentMap, then fall back to SymbolTable class verification.
|
|
462
|
+
// The SymbolTable fallback handles cross-file cases where heritage is declared
|
|
463
|
+
// in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when
|
|
464
|
+
// processing services/App.kt). Since constructorTypeMap only records entries
|
|
465
|
+
// when a type annotation AND constructor are both present (val x: Base = Sub()),
|
|
466
|
+
// confirming both are class-like types is sufficient — the original code would
|
|
467
|
+
// not compile if Sub didn't extend Base.
|
|
468
|
+
if (isSubclassOf(ctorType, receiverTypeName, parentMap)
|
|
469
|
+
|| isSubclassOf(ctorType, receiverTypeName, globalParentMap)
|
|
470
|
+
|| (ctx.symbols.lookupFuzzy(ctorType).some(d => d.type === 'Class' || d.type === 'Struct')
|
|
471
|
+
&& ctx.symbols.lookupFuzzy(receiverTypeName).some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
|
|
472
|
+
receiverTypeName = ctorType;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
248
476
|
// Fall back to verified constructor bindings for return type inference
|
|
249
477
|
if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
|
|
250
478
|
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
@@ -290,12 +518,19 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
290
518
|
}
|
|
291
519
|
}
|
|
292
520
|
}
|
|
521
|
+
// Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
|
|
522
|
+
// Only used when multiple candidates survive arity filtering — ~1-3% of calls.
|
|
523
|
+
const langConfig = lang ? typeConfigs[lang] : undefined;
|
|
524
|
+
const hints = langConfig?.inferLiteralType
|
|
525
|
+
? { callNode, inferLiteralType: langConfig.inferLiteralType }
|
|
526
|
+
: undefined;
|
|
293
527
|
const resolved = resolveCallTarget({
|
|
294
528
|
calledName,
|
|
295
529
|
argCount: countCallArguments(callNode),
|
|
296
530
|
callForm,
|
|
297
531
|
receiverTypeName,
|
|
298
|
-
|
|
532
|
+
receiverName,
|
|
533
|
+
}, file.path, ctx, hints);
|
|
299
534
|
if (!resolved)
|
|
300
535
|
return;
|
|
301
536
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
@@ -362,7 +597,9 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
|
362
597
|
const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
|
|
363
598
|
if (!hasParameterMetadata)
|
|
364
599
|
return kindFiltered;
|
|
365
|
-
return kindFiltered.filter(candidate => candidate.parameterCount === undefined
|
|
600
|
+
return kindFiltered.filter(candidate => candidate.parameterCount === undefined
|
|
601
|
+
|| (argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount)
|
|
602
|
+
&& argCount <= candidate.parameterCount));
|
|
366
603
|
};
|
|
367
604
|
const toResolveResult = (definition, tier) => ({
|
|
368
605
|
nodeId: definition.nodeId,
|
|
@@ -370,20 +607,129 @@ const toResolveResult = (definition, tier) => ({
|
|
|
370
607
|
reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
|
|
371
608
|
returnType: definition.returnType,
|
|
372
609
|
});
|
|
610
|
+
/**
|
|
611
|
+
* Kotlin (and JVM in general) uses boxed type names in parameter declarations
|
|
612
|
+
* (e.g. `Int`, `Long`, `Boolean`) while inferJvmLiteralType returns unboxed
|
|
613
|
+
* primitives (`int`, `long`, `boolean`). Normalise both sides to lowercase so
|
|
614
|
+
* that the comparison `'Int' === 'int'` does not fail.
|
|
615
|
+
*
|
|
616
|
+
* Only applied to single-word identifiers that look like a JVM primitive alias;
|
|
617
|
+
* multi-word or qualified names are left untouched.
|
|
618
|
+
*/
|
|
619
|
+
const KOTLIN_BOXED_TO_PRIMITIVE = {
|
|
620
|
+
Int: 'int',
|
|
621
|
+
Long: 'long',
|
|
622
|
+
Short: 'short',
|
|
623
|
+
Byte: 'byte',
|
|
624
|
+
Float: 'float',
|
|
625
|
+
Double: 'double',
|
|
626
|
+
Boolean: 'boolean',
|
|
627
|
+
Char: 'char',
|
|
628
|
+
};
|
|
629
|
+
const normalizeJvmTypeName = (name) => KOTLIN_BOXED_TO_PRIMITIVE[name] ?? name;
|
|
630
|
+
/**
|
|
631
|
+
* Try to disambiguate overloaded candidates using argument literal types.
|
|
632
|
+
* Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
|
|
633
|
+
* Returns the single matching candidate, or null if ambiguous/inconclusive.
|
|
634
|
+
*/
|
|
635
|
+
const tryOverloadDisambiguation = (candidates, hints) => {
|
|
636
|
+
if (!candidates.some(c => c.parameterTypes))
|
|
637
|
+
return null;
|
|
638
|
+
// Find the argument list node in the call expression.
|
|
639
|
+
// Kotlin wraps value_arguments inside a call_suffix child, so we must also
|
|
640
|
+
// search one level deeper when a direct match is not found.
|
|
641
|
+
let argList = hints.callNode.childForFieldName?.('arguments')
|
|
642
|
+
?? hints.callNode.children.find((c) => c.type === 'arguments' || c.type === 'argument_list' || c.type === 'value_arguments');
|
|
643
|
+
if (!argList) {
|
|
644
|
+
// Kotlin: call_expression → call_suffix → value_arguments
|
|
645
|
+
const callSuffix = hints.callNode.children.find((c) => c.type === 'call_suffix');
|
|
646
|
+
if (callSuffix) {
|
|
647
|
+
argList = callSuffix.children.find((c) => c.type === 'value_arguments');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!argList)
|
|
651
|
+
return null;
|
|
652
|
+
const argTypes = [];
|
|
653
|
+
for (const arg of argList.namedChildren) {
|
|
654
|
+
if (arg.type === 'comment')
|
|
655
|
+
continue;
|
|
656
|
+
// Unwrap argument wrapper nodes before passing to inferLiteralType:
|
|
657
|
+
// - Kotlin value_argument: has 'value' field containing the literal
|
|
658
|
+
// - C# argument: has 'expression' field (handles named args like `name: "alice"`
|
|
659
|
+
// where firstNamedChild would return name_colon instead of the value)
|
|
660
|
+
// - Java/others: arg IS the literal directly (no unwrapping needed)
|
|
661
|
+
const valueNode = arg.childForFieldName?.('value')
|
|
662
|
+
?? arg.childForFieldName?.('expression')
|
|
663
|
+
?? (arg.type === 'argument' || arg.type === 'value_argument'
|
|
664
|
+
? arg.firstNamedChild ?? arg
|
|
665
|
+
: arg);
|
|
666
|
+
argTypes.push(hints.inferLiteralType(valueNode));
|
|
667
|
+
}
|
|
668
|
+
// If no literal types could be inferred, can't disambiguate
|
|
669
|
+
if (argTypes.every(t => t === undefined))
|
|
670
|
+
return null;
|
|
671
|
+
const matched = candidates.filter(c => {
|
|
672
|
+
// Keep candidates without type info — conservative: partially-annotated codebases
|
|
673
|
+
// (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
|
|
674
|
+
// If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
|
|
675
|
+
if (!c.parameterTypes)
|
|
676
|
+
return true;
|
|
677
|
+
return c.parameterTypes.every((pType, i) => {
|
|
678
|
+
if (i >= argTypes.length || !argTypes[i])
|
|
679
|
+
return true;
|
|
680
|
+
// Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
|
|
681
|
+
// that the stored declaration type matches the inferred literal type.
|
|
682
|
+
return normalizeJvmTypeName(pType) === argTypes[i];
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
if (matched.length === 1)
|
|
686
|
+
return matched[0];
|
|
687
|
+
// Multiple survivors may share the same nodeId (e.g. TypeScript overload signatures +
|
|
688
|
+
// implementation body all collide via generateId). Deduplicate by nodeId — if all
|
|
689
|
+
// matched candidates resolve to the same graph node, disambiguation succeeded.
|
|
690
|
+
if (matched.length > 1) {
|
|
691
|
+
const uniqueIds = new Set(matched.map(c => c.nodeId));
|
|
692
|
+
if (uniqueIds.size === 1)
|
|
693
|
+
return matched[0];
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
};
|
|
373
697
|
/**
|
|
374
698
|
* Resolve a function call to its target node ID using priority strategy:
|
|
375
699
|
* A. Narrow candidates by scope tier via ctx.resolve()
|
|
376
700
|
* B. Filter to callable symbol kinds (constructor-aware when callForm is set)
|
|
377
701
|
* C. Apply arity filtering when parameter metadata is available
|
|
378
702
|
* D. Apply receiver-type filtering for member calls with typed receivers
|
|
703
|
+
* E. Apply overload disambiguation via argument literal types (when available)
|
|
379
704
|
*
|
|
380
705
|
* If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
|
|
381
706
|
*/
|
|
382
|
-
const resolveCallTarget = (call, currentFile, ctx) => {
|
|
707
|
+
const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
|
|
383
708
|
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
384
709
|
if (!tiered)
|
|
385
710
|
return null;
|
|
386
|
-
|
|
711
|
+
let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
|
712
|
+
// Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
|
|
713
|
+
// The attribute access gives callForm='member', but the callee may be a Class — a valid
|
|
714
|
+
// constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
|
|
715
|
+
// emits a CALLS edge to the class node.
|
|
716
|
+
if (filteredCandidates.length === 0 && call.callForm === 'member') {
|
|
717
|
+
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
718
|
+
}
|
|
719
|
+
// Module-alias disambiguation: Python `import auth; auth.User()` — when both models.py and
|
|
720
|
+
// auth.py export User, receiverName='auth' selects auth.py via moduleAliasMap.
|
|
721
|
+
// Runs when multiple candidates survive filtering and the receiver is a known module alias.
|
|
722
|
+
if (filteredCandidates.length > 1 && call.callForm === 'member' && call.receiverName) {
|
|
723
|
+
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
|
724
|
+
if (aliasMap) {
|
|
725
|
+
const moduleFile = aliasMap.get(call.receiverName);
|
|
726
|
+
if (moduleFile) {
|
|
727
|
+
const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
|
|
728
|
+
if (aliasFiltered.length > 0)
|
|
729
|
+
filteredCandidates = aliasFiltered;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
387
733
|
// D. Receiver-type filtering: for member calls with a known receiver type,
|
|
388
734
|
// resolve the type through the same tiered import infrastructure, then
|
|
389
735
|
// filter method candidates to the type's defining file. Fall back to
|
|
@@ -415,10 +761,25 @@ const resolveCallTarget = (call, currentFile, ctx) => {
|
|
|
415
761
|
if (ownerFiltered.length === 1) {
|
|
416
762
|
return toResolveResult(ownerFiltered[0], tiered.tier);
|
|
417
763
|
}
|
|
764
|
+
// E. Try overload disambiguation on the narrowed pool
|
|
765
|
+
if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
|
|
766
|
+
const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
|
|
767
|
+
const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
|
|
768
|
+
if (disambiguated)
|
|
769
|
+
return toResolveResult(disambiguated, tiered.tier);
|
|
770
|
+
}
|
|
418
771
|
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
419
772
|
return null;
|
|
420
773
|
}
|
|
421
774
|
}
|
|
775
|
+
// E. Overload disambiguation: when multiple candidates survive arity + receiver filtering,
|
|
776
|
+
// try matching argument literal types against parameter types (Phase P).
|
|
777
|
+
// Only available on sequential path (has AST); worker path falls through gracefully.
|
|
778
|
+
if (filteredCandidates.length > 1 && overloadHints) {
|
|
779
|
+
const disambiguated = tryOverloadDisambiguation(filteredCandidates, overloadHints);
|
|
780
|
+
if (disambiguated)
|
|
781
|
+
return toResolveResult(disambiguated, tiered.tier);
|
|
782
|
+
}
|
|
422
783
|
if (filteredCandidates.length !== 1)
|
|
423
784
|
return null;
|
|
424
785
|
return toResolveResult(filteredCandidates[0], tiered.tier);
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
/** null = this call was not routed; fall through to default call handling */
|
|
14
14
|
export type CallRoutingResult = RubyCallRouting | null;
|
|
15
|
+
/**
|
|
16
|
+
* Per-language call router.
|
|
17
|
+
* IMPORTANT: Call-routed imports bypass preprocessImportPath(), so any router that
|
|
18
|
+
* returns an importPath MUST validate it independently (length cap, control-char
|
|
19
|
+
* rejection). See routeRubyCall for the reference implementation.
|
|
20
|
+
*/
|
|
15
21
|
export type CallRouter = (calledName: string, callNode: any) => CallRoutingResult;
|
|
16
22
|
/** Per-language call routing. noRouting = no special routing (normal call processing) */
|
|
17
23
|
export declare const callRouters: {
|
|
@@ -12,28 +12,31 @@
|
|
|
12
12
|
import { detectFrameworkFromPath } from './framework-detection.js';
|
|
13
13
|
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
14
14
|
// ============================================================================
|
|
15
|
-
// NAME PATTERNS - All
|
|
15
|
+
// NAME PATTERNS - All 13 supported languages
|
|
16
16
|
// ============================================================================
|
|
17
17
|
/**
|
|
18
|
-
* Common entry point naming patterns by language
|
|
19
|
-
* These patterns indicate functions that are likely feature entry points
|
|
18
|
+
* Common entry point naming patterns by language.
|
|
19
|
+
* These patterns indicate functions that are likely feature entry points.
|
|
20
|
+
*
|
|
21
|
+
* Universal patterns are separated from per-language patterns so the per-language
|
|
22
|
+
* table can use `satisfies Record<SupportedLanguages, RegExp[]>` for compile-time
|
|
23
|
+
* exhaustiveness — the compiler catches any missing language entry.
|
|
20
24
|
*/
|
|
25
|
+
const UNIVERSAL_ENTRY_POINT_PATTERNS = [
|
|
26
|
+
/^(main|init|bootstrap|start|run|setup|configure)$/i,
|
|
27
|
+
/^handle[A-Z]/, // handleLogin, handleSubmit
|
|
28
|
+
/^on[A-Z]/, // onClick, onSubmit
|
|
29
|
+
/Handler$/, // RequestHandler
|
|
30
|
+
/Controller$/, // UserController
|
|
31
|
+
/^process[A-Z]/, // processPayment
|
|
32
|
+
/^execute[A-Z]/, // executeQuery
|
|
33
|
+
/^perform[A-Z]/, // performAction
|
|
34
|
+
/^dispatch[A-Z]/, // dispatchEvent
|
|
35
|
+
/^trigger[A-Z]/, // triggerAction
|
|
36
|
+
/^fire[A-Z]/, // fireEvent
|
|
37
|
+
/^emit[A-Z]/, // emitEvent
|
|
38
|
+
];
|
|
21
39
|
const ENTRY_POINT_PATTERNS = {
|
|
22
|
-
// Universal patterns (apply to all languages)
|
|
23
|
-
'*': [
|
|
24
|
-
/^(main|init|bootstrap|start|run|setup|configure)$/i,
|
|
25
|
-
/^handle[A-Z]/, // handleLogin, handleSubmit
|
|
26
|
-
/^on[A-Z]/, // onClick, onSubmit
|
|
27
|
-
/Handler$/, // RequestHandler
|
|
28
|
-
/Controller$/, // UserController
|
|
29
|
-
/^process[A-Z]/, // processPayment
|
|
30
|
-
/^execute[A-Z]/, // executeQuery
|
|
31
|
-
/^perform[A-Z]/, // performAction
|
|
32
|
-
/^dispatch[A-Z]/, // dispatchEvent
|
|
33
|
-
/^trigger[A-Z]/, // triggerAction
|
|
34
|
-
/^fire[A-Z]/, // fireEvent
|
|
35
|
-
/^emit[A-Z]/, // emitEvent
|
|
36
|
-
],
|
|
37
40
|
// JavaScript/TypeScript
|
|
38
41
|
[SupportedLanguages.JavaScript]: [
|
|
39
42
|
/^use[A-Z]/, // React hooks (useEffect, etc.)
|
|
@@ -55,6 +58,16 @@ const ENTRY_POINT_PATTERNS = {
|
|
|
55
58
|
/^build[A-Z]/, // Builder patterns
|
|
56
59
|
/Service$/, // UserService
|
|
57
60
|
],
|
|
61
|
+
// Kotlin
|
|
62
|
+
[SupportedLanguages.Kotlin]: [
|
|
63
|
+
/^on(Create|Start|Resume|Pause|Stop|Destroy)$/, // Android lifecycle
|
|
64
|
+
/^do[A-Z]/, // doGet, doPost (shared JVM Servlet pattern)
|
|
65
|
+
/^create[A-Z]/, // Factory patterns
|
|
66
|
+
/^build[A-Z]/, // Builder patterns
|
|
67
|
+
/ViewModel$/, // MVVM pattern (Android)
|
|
68
|
+
/^module$/, // Ktor module entry point
|
|
69
|
+
/Service$/, // Service classes
|
|
70
|
+
],
|
|
58
71
|
// C#
|
|
59
72
|
[SupportedLanguages.CSharp]: [
|
|
60
73
|
/^(Get|Post|Put|Delete|Patch)/, // ASP.NET action methods
|
|
@@ -186,13 +199,10 @@ const ENTRY_POINT_PATTERNS = {
|
|
|
186
199
|
],
|
|
187
200
|
};
|
|
188
201
|
/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
|
|
189
|
-
const MERGED_ENTRY_POINT_PATTERNS =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
continue;
|
|
194
|
-
MERGED_ENTRY_POINT_PATTERNS[lang] = [...UNIVERSAL_PATTERNS, ...patterns];
|
|
195
|
-
}
|
|
202
|
+
const MERGED_ENTRY_POINT_PATTERNS = Object.fromEntries(Object.keys(ENTRY_POINT_PATTERNS).map(lang => [
|
|
203
|
+
lang,
|
|
204
|
+
[...UNIVERSAL_ENTRY_POINT_PATTERNS, ...ENTRY_POINT_PATTERNS[lang]],
|
|
205
|
+
]));
|
|
196
206
|
// ============================================================================
|
|
197
207
|
// UTILITY PATTERNS - Functions that should be penalized
|
|
198
208
|
// ============================================================================
|
|
@@ -258,7 +268,7 @@ export function calculateEntryPointScore(name, language, isExported, callerCount
|
|
|
258
268
|
}
|
|
259
269
|
else {
|
|
260
270
|
// Check positive patterns
|
|
261
|
-
const allPatterns = MERGED_ENTRY_POINT_PATTERNS[language]
|
|
271
|
+
const allPatterns = MERGED_ENTRY_POINT_PATTERNS[language];
|
|
262
272
|
if (allPatterns.some(p => p.test(name))) {
|
|
263
273
|
nameMultiplier = 1.5; // Bonus for matching entry point pattern
|
|
264
274
|
reasons.push('entry-pattern');
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* DESIGN: Returns null for unknown frameworks, which causes a 1.0 multiplier
|
|
10
10
|
* (no bonus, no penalty) - same behavior as before this feature.
|
|
11
11
|
*/
|
|
12
|
+
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
12
13
|
export interface FrameworkHint {
|
|
13
14
|
framework: string;
|
|
14
15
|
entryPointMultiplier: number;
|
|
@@ -37,15 +38,22 @@ export declare const FRAMEWORK_AST_PATTERNS: {
|
|
|
37
38
|
blazor: string[];
|
|
38
39
|
efcore: string[];
|
|
39
40
|
'go-http': string[];
|
|
41
|
+
gin: string[];
|
|
42
|
+
echo: string[];
|
|
43
|
+
fiber: string[];
|
|
44
|
+
'go-grpc': string[];
|
|
40
45
|
laravel: string[];
|
|
41
46
|
actix: string[];
|
|
42
47
|
axum: string[];
|
|
43
48
|
rocket: string[];
|
|
49
|
+
tokio: string[];
|
|
50
|
+
qt: string[];
|
|
44
51
|
uikit: string[];
|
|
45
52
|
swiftui: string[];
|
|
46
|
-
|
|
53
|
+
vapor: string[];
|
|
54
|
+
rails: string[];
|
|
55
|
+
sinatra: string[];
|
|
47
56
|
};
|
|
48
|
-
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
49
57
|
/**
|
|
50
58
|
* Detect framework entry points from AST definition text (decorators/annotations/attributes).
|
|
51
59
|
* Returns null if no known pattern is found.
|