gitnexus 1.4.10 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/cli/ai-context.d.ts +4 -1
- package/dist/cli/ai-context.js +19 -11
- package/dist/cli/analyze.d.ts +6 -0
- package/dist/cli/analyze.js +105 -251
- package/dist/cli/eval-server.js +20 -11
- package/dist/cli/index-repo.js +20 -22
- package/dist/cli/index.js +8 -7
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/serve.js +29 -1
- package/dist/cli/setup.js +9 -9
- package/dist/cli/skill-gen.js +15 -9
- package/dist/cli/wiki.d.ts +2 -0
- package/dist/cli/wiki.js +141 -26
- package/dist/config/ignore-service.js +102 -22
- package/dist/config/supported-languages.d.ts +8 -42
- package/dist/config/supported-languages.js +8 -43
- package/dist/core/augmentation/engine.js +19 -7
- package/dist/core/embeddings/embedder.js +19 -15
- package/dist/core/embeddings/embedding-pipeline.js +6 -6
- package/dist/core/embeddings/http-client.js +3 -3
- package/dist/core/embeddings/text-generator.js +9 -24
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/embeddings/types.js +1 -7
- package/dist/core/graph/graph.js +6 -2
- package/dist/core/graph/types.d.ts +9 -59
- package/dist/core/ingestion/ast-cache.js +3 -3
- package/dist/core/ingestion/call-processor.d.ts +20 -2
- package/dist/core/ingestion/call-processor.js +347 -144
- package/dist/core/ingestion/call-routing.js +10 -4
- package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
- package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
- package/dist/core/ingestion/call-sites/java.d.ts +9 -0
- package/dist/core/ingestion/call-sites/java.js +30 -0
- package/dist/core/ingestion/cluster-enricher.js +6 -8
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
- package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
- package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
- package/dist/core/ingestion/cobol-processor.js +102 -56
- package/dist/core/ingestion/community-processor.js +21 -15
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
- package/dist/core/ingestion/entry-point-scoring.js +5 -6
- package/dist/core/ingestion/export-detection.js +32 -9
- package/dist/core/ingestion/field-extractor.d.ts +1 -1
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
- package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
- package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
- package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
- package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
- package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
- package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
- package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
- package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
- package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
- package/dist/core/ingestion/field-extractors/generic.js +6 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
- package/dist/core/ingestion/field-extractors/typescript.js +1 -1
- package/dist/core/ingestion/field-types.d.ts +4 -2
- package/dist/core/ingestion/filesystem-walker.js +3 -3
- package/dist/core/ingestion/framework-detection.d.ts +1 -1
- package/dist/core/ingestion/framework-detection.js +355 -85
- package/dist/core/ingestion/heritage-processor.d.ts +24 -0
- package/dist/core/ingestion/heritage-processor.js +99 -8
- package/dist/core/ingestion/import-processor.js +44 -15
- package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
- package/dist/core/ingestion/import-resolvers/dart.js +1 -1
- package/dist/core/ingestion/import-resolvers/go.js +4 -2
- package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
- package/dist/core/ingestion/import-resolvers/php.js +4 -4
- package/dist/core/ingestion/import-resolvers/python.js +1 -1
- package/dist/core/ingestion/import-resolvers/rust.js +9 -3
- package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/standard.js +6 -5
- package/dist/core/ingestion/import-resolvers/swift.js +2 -1
- package/dist/core/ingestion/import-resolvers/utils.js +26 -7
- package/dist/core/ingestion/language-config.js +5 -4
- package/dist/core/ingestion/language-provider.d.ts +7 -2
- package/dist/core/ingestion/languages/c-cpp.js +106 -21
- package/dist/core/ingestion/languages/cobol.js +1 -1
- package/dist/core/ingestion/languages/csharp.js +96 -19
- package/dist/core/ingestion/languages/dart.js +23 -7
- package/dist/core/ingestion/languages/go.js +1 -1
- package/dist/core/ingestion/languages/index.d.ts +1 -1
- package/dist/core/ingestion/languages/index.js +2 -3
- package/dist/core/ingestion/languages/java.js +4 -1
- package/dist/core/ingestion/languages/kotlin.js +60 -13
- package/dist/core/ingestion/languages/php.js +102 -25
- package/dist/core/ingestion/languages/python.js +28 -5
- package/dist/core/ingestion/languages/ruby.js +56 -14
- package/dist/core/ingestion/languages/rust.js +55 -11
- package/dist/core/ingestion/languages/swift.js +112 -27
- package/dist/core/ingestion/languages/typescript.js +95 -19
- package/dist/core/ingestion/markdown-processor.js +5 -5
- package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
- package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
- package/dist/core/ingestion/method-extractors/generic.js +137 -0
- package/dist/core/ingestion/method-types.d.ts +61 -0
- package/dist/core/ingestion/method-types.js +2 -0
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +12 -8
- package/dist/core/ingestion/named-binding-processor.js +2 -2
- package/dist/core/ingestion/named-bindings/rust.js +3 -1
- package/dist/core/ingestion/parsing-processor.js +74 -24
- package/dist/core/ingestion/pipeline.d.ts +2 -1
- package/dist/core/ingestion/pipeline.js +208 -102
- package/dist/core/ingestion/process-processor.js +12 -10
- package/dist/core/ingestion/resolution-context.js +3 -3
- package/dist/core/ingestion/route-extractors/middleware.js +31 -7
- package/dist/core/ingestion/route-extractors/php.js +2 -1
- package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
- package/dist/core/ingestion/structure-processor.d.ts +1 -1
- package/dist/core/ingestion/structure-processor.js +4 -4
- package/dist/core/ingestion/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/symbol-table.js +22 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +1 -1
- package/dist/core/ingestion/type-env.d.ts +2 -2
- package/dist/core/ingestion/type-env.js +75 -50
- package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
- package/dist/core/ingestion/type-extractors/csharp.js +24 -14
- package/dist/core/ingestion/type-extractors/dart.js +6 -8
- package/dist/core/ingestion/type-extractors/go.js +7 -6
- package/dist/core/ingestion/type-extractors/jvm.js +10 -21
- package/dist/core/ingestion/type-extractors/php.js +26 -13
- package/dist/core/ingestion/type-extractors/python.js +11 -15
- package/dist/core/ingestion/type-extractors/ruby.js +8 -3
- package/dist/core/ingestion/type-extractors/rust.js +6 -8
- package/dist/core/ingestion/type-extractors/shared.js +134 -50
- package/dist/core/ingestion/type-extractors/swift.js +16 -13
- package/dist/core/ingestion/type-extractors/typescript.js +23 -15
- package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
- package/dist/core/ingestion/utils/ast-helpers.js +72 -35
- package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
- package/dist/core/ingestion/utils/call-analysis.js +96 -49
- package/dist/core/ingestion/utils/event-loop.js +1 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
- package/dist/core/ingestion/workers/parse-worker.js +364 -84
- package/dist/core/ingestion/workers/worker-pool.js +5 -10
- package/dist/core/lbug/csv-generator.js +54 -15
- package/dist/core/lbug/lbug-adapter.d.ts +5 -0
- package/dist/core/lbug/lbug-adapter.js +86 -23
- package/dist/core/lbug/schema.d.ts +3 -6
- package/dist/core/lbug/schema.js +6 -30
- package/dist/core/run-analyze.d.ts +49 -0
- package/dist/core/run-analyze.js +257 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -1
- package/dist/core/wiki/cursor-client.js +2 -7
- package/dist/core/wiki/generator.js +38 -23
- package/dist/core/wiki/graph-queries.js +10 -10
- package/dist/core/wiki/html-viewer.js +7 -3
- package/dist/core/wiki/llm-client.d.ts +23 -2
- package/dist/core/wiki/llm-client.js +96 -26
- package/dist/core/wiki/prompts.js +7 -6
- package/dist/mcp/core/embedder.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +4 -1
- package/dist/mcp/core/lbug-adapter.js +17 -7
- package/dist/mcp/local/local-backend.js +247 -95
- package/dist/mcp/resources.js +14 -6
- package/dist/mcp/server.js +13 -5
- package/dist/mcp/staleness.js +5 -1
- package/dist/mcp/tools.js +100 -23
- package/dist/server/analyze-job.d.ts +53 -0
- package/dist/server/analyze-job.js +146 -0
- package/dist/server/analyze-worker.d.ts +13 -0
- package/dist/server/analyze-worker.js +59 -0
- package/dist/server/api.js +795 -44
- package/dist/server/git-clone.d.ts +25 -0
- package/dist/server/git-clone.js +91 -0
- package/dist/storage/git.js +1 -3
- package/dist/storage/repo-manager.d.ts +5 -2
- package/dist/storage/repo-manager.js +4 -4
- package/dist/types/pipeline.d.ts +1 -21
- package/dist/types/pipeline.js +1 -18
- package/hooks/claude/gitnexus-hook.cjs +52 -22
- package/package.json +3 -2
- package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
- package/dist/core/ingestion/utils/language-detection.js +0 -70
|
@@ -3,12 +3,13 @@ import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
|
3
3
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
4
4
|
import { getProvider } from './languages/index.js';
|
|
5
5
|
import { generateId } from '../../lib/utils.js';
|
|
6
|
-
import { getLanguageFromFilename } from '
|
|
6
|
+
import { getLanguageFromFilename } from 'gitnexus-shared';
|
|
7
7
|
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
8
8
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
9
|
-
import { FUNCTION_NODE_TYPES, extractFunctionName, findEnclosingClassId } from './utils/ast-helpers.js';
|
|
10
|
-
import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from './utils/call-analysis.js';
|
|
9
|
+
import { FUNCTION_NODE_TYPES, extractFunctionName, findEnclosingClassId, } from './utils/ast-helpers.js';
|
|
10
|
+
import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, extractCallArgTypes, } from './utils/call-analysis.js';
|
|
11
11
|
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
|
12
|
+
import { resolveExtendsType } from './heritage-processor.js';
|
|
12
13
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
13
14
|
import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
14
15
|
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
|
@@ -75,7 +76,7 @@ function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
|
|
|
75
76
|
* exported symbols that have callables with known return types. */
|
|
76
77
|
export function buildExportedTypeMapFromGraph(graph, symbolTable) {
|
|
77
78
|
const result = new Map();
|
|
78
|
-
graph.forEachNode(node => {
|
|
79
|
+
graph.forEachNode((node) => {
|
|
79
80
|
if (!node.properties?.isExported)
|
|
80
81
|
return;
|
|
81
82
|
if (!node.properties?.filePath || !node.properties?.name)
|
|
@@ -141,8 +142,17 @@ export function seedCrossFileReceiverTypes(calls, namedImportMap, exportedTypeMa
|
|
|
141
142
|
// strips nullable wrappers (Option<User> → User), these chain steps are no-ops
|
|
142
143
|
// for type resolution — the current type passes through unchanged.
|
|
143
144
|
const TYPE_PRESERVING_METHODS = new Set([
|
|
144
|
-
'unwrap',
|
|
145
|
-
'
|
|
145
|
+
'unwrap',
|
|
146
|
+
'expect',
|
|
147
|
+
'unwrap_or',
|
|
148
|
+
'unwrap_or_default',
|
|
149
|
+
'unwrap_or_else', // Rust Option/Result
|
|
150
|
+
'clone',
|
|
151
|
+
'to_owned',
|
|
152
|
+
'as_ref',
|
|
153
|
+
'as_mut',
|
|
154
|
+
'borrow',
|
|
155
|
+
'borrow_mut', // Rust clone/borrow
|
|
146
156
|
'get', // Kotlin/Java Optional.get()
|
|
147
157
|
'orElseThrow', // Java Optional
|
|
148
158
|
]);
|
|
@@ -201,18 +211,18 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
201
211
|
const verified = new Map();
|
|
202
212
|
for (const { scope, varName, calleeName, receiverClassName } of bindings) {
|
|
203
213
|
const tiered = ctx.resolve(calleeName, filePath);
|
|
204
|
-
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
|
|
214
|
+
const isClass = tiered?.candidates.some((def) => def.type === 'Class') ?? false;
|
|
205
215
|
if (isClass) {
|
|
206
216
|
verified.set(receiverKey(scope, varName), calleeName);
|
|
207
217
|
}
|
|
208
218
|
else {
|
|
209
|
-
let callableDefs = tiered?.candidates.filter(d => d.type === 'Function' || d.type === 'Method');
|
|
219
|
+
let callableDefs = tiered?.candidates.filter((d) => d.type === 'Function' || d.type === 'Method');
|
|
210
220
|
// When receiver class is known (e.g. $this->method() in PHP), narrow
|
|
211
221
|
// candidates to methods owned by that class to avoid false disambiguation failures.
|
|
212
222
|
if (callableDefs && callableDefs.length > 1 && receiverClassName) {
|
|
213
223
|
if (graph) {
|
|
214
224
|
// Worker path: use graph.getNode (fast, already in-memory)
|
|
215
|
-
const narrowed = callableDefs.filter(d => {
|
|
225
|
+
const narrowed = callableDefs.filter((d) => {
|
|
216
226
|
if (!d.ownerId)
|
|
217
227
|
return false;
|
|
218
228
|
const owner = graph.getNode(d.ownerId);
|
|
@@ -225,8 +235,8 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
225
235
|
// Sequential path: use ctx.resolve (no graph available)
|
|
226
236
|
const classResolved = ctx.resolve(receiverClassName, filePath);
|
|
227
237
|
if (classResolved && classResolved.candidates.length > 0) {
|
|
228
|
-
const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
|
|
229
|
-
const narrowed = callableDefs.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
|
238
|
+
const classNodeIds = new Set(classResolved.candidates.map((c) => c.nodeId));
|
|
239
|
+
const narrowed = callableDefs.filter((d) => d.ownerId && classNodeIds.has(d.ownerId));
|
|
230
240
|
if (narrowed.length > 0)
|
|
231
241
|
callableDefs = narrowed;
|
|
232
242
|
}
|
|
@@ -242,6 +252,85 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
242
252
|
}
|
|
243
253
|
return verified;
|
|
244
254
|
};
|
|
255
|
+
/**
|
|
256
|
+
* Build an ImplementorMap from extracted heritage data.
|
|
257
|
+
* Only direct `implements` relationships are tracked (transitive not needed for
|
|
258
|
+
* the common Java/Kotlin/C# interface dispatch pattern).
|
|
259
|
+
* `extends` is ignored — dispatch keyed on abstract class bases is not modeled here.
|
|
260
|
+
*/
|
|
261
|
+
/**
|
|
262
|
+
* Maps interface name → file paths of classes that implement it (direct only).
|
|
263
|
+
* When `ctx` is set, `kind: 'extends'` rows are classified like heritage-processor
|
|
264
|
+
* (C#/Java base_list: class vs interface parents share one capture name).
|
|
265
|
+
*/
|
|
266
|
+
export const buildImplementorMap = (heritage, ctx) => {
|
|
267
|
+
const map = new Map();
|
|
268
|
+
for (const h of heritage) {
|
|
269
|
+
let record = false;
|
|
270
|
+
if (h.kind === 'implements') {
|
|
271
|
+
record = true;
|
|
272
|
+
}
|
|
273
|
+
else if (h.kind === 'extends' && ctx) {
|
|
274
|
+
const lang = getLanguageFromFilename(h.filePath);
|
|
275
|
+
if (lang) {
|
|
276
|
+
const { type } = resolveExtendsType(h.parentName, h.filePath, ctx, lang);
|
|
277
|
+
record = type === 'IMPLEMENTS';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (record) {
|
|
281
|
+
let files = map.get(h.parentName);
|
|
282
|
+
if (!files) {
|
|
283
|
+
files = new Set();
|
|
284
|
+
map.set(h.parentName, files);
|
|
285
|
+
}
|
|
286
|
+
files.add(h.filePath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return map;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* Merge a chunk's implementor map into the global accumulator.
|
|
293
|
+
*/
|
|
294
|
+
export const mergeImplementorMaps = (target, source) => {
|
|
295
|
+
for (const [name, files] of source) {
|
|
296
|
+
let existing = target.get(name);
|
|
297
|
+
if (!existing) {
|
|
298
|
+
existing = new Set();
|
|
299
|
+
target.set(name, existing);
|
|
300
|
+
}
|
|
301
|
+
for (const f of files)
|
|
302
|
+
existing.add(f);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* After resolving a call to an interface method, find additional targets
|
|
307
|
+
* in classes implementing that interface. Returns implementation method
|
|
308
|
+
* results with lower confidence ('interface-dispatch').
|
|
309
|
+
*/
|
|
310
|
+
function findInterfaceDispatchTargets(calledName, receiverTypeName, currentFile, ctx, implementorMap, primaryNodeId) {
|
|
311
|
+
const implFiles = implementorMap.get(receiverTypeName);
|
|
312
|
+
if (!implFiles || implFiles.size === 0)
|
|
313
|
+
return [];
|
|
314
|
+
const typeResolved = ctx.resolve(receiverTypeName, currentFile);
|
|
315
|
+
if (!typeResolved)
|
|
316
|
+
return [];
|
|
317
|
+
if (!typeResolved.candidates.some((c) => c.type === 'Interface'))
|
|
318
|
+
return [];
|
|
319
|
+
const results = [];
|
|
320
|
+
for (const implFile of implFiles) {
|
|
321
|
+
const methods = ctx.symbols.lookupExactAll(implFile, calledName);
|
|
322
|
+
for (const method of methods) {
|
|
323
|
+
if (method.nodeId !== primaryNodeId) {
|
|
324
|
+
results.push({
|
|
325
|
+
nodeId: method.nodeId,
|
|
326
|
+
confidence: 0.7,
|
|
327
|
+
reason: 'interface-dispatch',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
245
334
|
export const processCalls = async (graph, files, astCache, ctx, onProgress, exportedTypeMap,
|
|
246
335
|
/** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map<localName, typeName>. */
|
|
247
336
|
importedBindingsMap,
|
|
@@ -249,7 +338,7 @@ importedBindingsMap,
|
|
|
249
338
|
* Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
|
|
250
339
|
importedReturnTypesMap,
|
|
251
340
|
/** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
|
|
252
|
-
importedRawReturnTypesMap) => {
|
|
341
|
+
importedRawReturnTypesMap, implementorMap) => {
|
|
253
342
|
const parser = await loadParser();
|
|
254
343
|
const collectedHeritage = [];
|
|
255
344
|
const pendingWrites = [];
|
|
@@ -283,7 +372,9 @@ importedRawReturnTypesMap) => {
|
|
|
283
372
|
let tree = astCache.get(file.path);
|
|
284
373
|
if (!tree) {
|
|
285
374
|
try {
|
|
286
|
-
tree = parser.parse(file.content, undefined, {
|
|
375
|
+
tree = parser.parse(file.content, undefined, {
|
|
376
|
+
bufferSize: getTreeSitterBufferSize(file.content.length),
|
|
377
|
+
});
|
|
287
378
|
}
|
|
288
379
|
catch (parseError) {
|
|
289
380
|
continue;
|
|
@@ -306,7 +397,7 @@ importedRawReturnTypesMap) => {
|
|
|
306
397
|
const fileParentMap = new Map();
|
|
307
398
|
for (const match of matches) {
|
|
308
399
|
const captureMap = {};
|
|
309
|
-
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
400
|
+
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
|
310
401
|
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
|
311
402
|
const className = captureMap['heritage.class'].text;
|
|
312
403
|
const parentName = captureMap['heritage.extends'].text;
|
|
@@ -347,7 +438,14 @@ importedRawReturnTypesMap) => {
|
|
|
347
438
|
const importedBindings = importedBindingsMap?.get(file.path);
|
|
348
439
|
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
|
349
440
|
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
|
350
|
-
const typeEnv = buildTypeEnv(tree, language, {
|
|
441
|
+
const typeEnv = buildTypeEnv(tree, language, {
|
|
442
|
+
symbolTable: ctx.symbols,
|
|
443
|
+
parentMap,
|
|
444
|
+
importedBindings,
|
|
445
|
+
importedReturnTypes,
|
|
446
|
+
importedRawReturnTypes,
|
|
447
|
+
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
|
448
|
+
});
|
|
351
449
|
if (typeEnv && exportedTypeMap) {
|
|
352
450
|
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
|
353
451
|
if (fileExports)
|
|
@@ -360,11 +458,13 @@ importedRawReturnTypesMap) => {
|
|
|
360
458
|
const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
|
|
361
459
|
ctx.enableCache(file.path);
|
|
362
460
|
const widenCache = new Map();
|
|
363
|
-
matches.forEach(match => {
|
|
461
|
+
matches.forEach((match) => {
|
|
364
462
|
const captureMap = {};
|
|
365
|
-
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
463
|
+
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
|
366
464
|
// ── Write access: emit ACCESSES {reason: 'write'} for assignments to member fields ──
|
|
367
|
-
if (captureMap['assignment'] &&
|
|
465
|
+
if (captureMap['assignment'] &&
|
|
466
|
+
captureMap['assignment.receiver'] &&
|
|
467
|
+
captureMap['assignment.property']) {
|
|
368
468
|
const receiverNode = captureMap['assignment.receiver'];
|
|
369
469
|
const propertyName = captureMap['assignment.property'].text;
|
|
370
470
|
// Resolve receiver type: simple identifier → TypeEnv lookup or class resolution
|
|
@@ -381,8 +481,12 @@ importedRawReturnTypesMap) => {
|
|
|
381
481
|
}
|
|
382
482
|
if (!receiverTypeName && receiverText) {
|
|
383
483
|
const resolved = ctx.resolve(receiverText, file.path);
|
|
384
|
-
if (resolved?.candidates.some(d => d.type === 'Class' ||
|
|
385
|
-
|
|
484
|
+
if (resolved?.candidates.some((d) => d.type === 'Class' ||
|
|
485
|
+
d.type === 'Struct' ||
|
|
486
|
+
d.type === 'Interface' ||
|
|
487
|
+
d.type === 'Enum' ||
|
|
488
|
+
d.type === 'Record' ||
|
|
489
|
+
d.type === 'Impl')) {
|
|
386
490
|
receiverTypeName = receiverText;
|
|
387
491
|
}
|
|
388
492
|
}
|
|
@@ -430,9 +534,12 @@ importedRawReturnTypesMap) => {
|
|
|
430
534
|
id: nodeId,
|
|
431
535
|
label: 'Property',
|
|
432
536
|
properties: {
|
|
433
|
-
name: item.propName,
|
|
434
|
-
|
|
435
|
-
|
|
537
|
+
name: item.propName,
|
|
538
|
+
filePath: file.path,
|
|
539
|
+
startLine: item.startLine,
|
|
540
|
+
endLine: item.endLine,
|
|
541
|
+
language,
|
|
542
|
+
isExported: true,
|
|
436
543
|
description: item.accessorType,
|
|
437
544
|
},
|
|
438
545
|
});
|
|
@@ -442,14 +549,21 @@ importedRawReturnTypesMap) => {
|
|
|
442
549
|
});
|
|
443
550
|
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
444
551
|
graph.addRelationship({
|
|
445
|
-
id: relId,
|
|
446
|
-
|
|
552
|
+
id: relId,
|
|
553
|
+
sourceId: fileId,
|
|
554
|
+
targetId: nodeId,
|
|
555
|
+
type: 'DEFINES',
|
|
556
|
+
confidence: 1.0,
|
|
557
|
+
reason: '',
|
|
447
558
|
});
|
|
448
559
|
if (propEnclosingClassId) {
|
|
449
560
|
graph.addRelationship({
|
|
450
561
|
id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
|
|
451
|
-
sourceId: propEnclosingClassId,
|
|
452
|
-
|
|
562
|
+
sourceId: propEnclosingClassId,
|
|
563
|
+
targetId: nodeId,
|
|
564
|
+
type: 'HAS_PROPERTY',
|
|
565
|
+
confidence: 1.0,
|
|
566
|
+
reason: '',
|
|
453
567
|
});
|
|
454
568
|
}
|
|
455
569
|
}
|
|
@@ -495,10 +609,14 @@ importedRawReturnTypesMap) => {
|
|
|
495
609
|
// when a type annotation AND constructor are both present (val x: Base = Sub()),
|
|
496
610
|
// confirming both are class-like types is sufficient — the original code would
|
|
497
611
|
// not compile if Sub didn't extend Base.
|
|
498
|
-
if (isSubclassOf(ctorType, receiverTypeName, parentMap)
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
612
|
+
if (isSubclassOf(ctorType, receiverTypeName, parentMap) ||
|
|
613
|
+
isSubclassOf(ctorType, receiverTypeName, globalParentMap) ||
|
|
614
|
+
(ctx.symbols
|
|
615
|
+
.lookupFuzzy(ctorType)
|
|
616
|
+
.some((d) => d.type === 'Class' || d.type === 'Struct') &&
|
|
617
|
+
ctx.symbols
|
|
618
|
+
.lookupFuzzy(receiverTypeName)
|
|
619
|
+
.some((d) => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
|
|
502
620
|
receiverTypeName = ctorType;
|
|
503
621
|
}
|
|
504
622
|
}
|
|
@@ -514,7 +632,11 @@ importedRawReturnTypesMap) => {
|
|
|
514
632
|
// through the standard tiered resolution, use it directly as the receiver type.
|
|
515
633
|
if (!receiverTypeName && receiverName && callForm === 'member') {
|
|
516
634
|
const typeResolved = ctx.resolve(receiverName, file.path);
|
|
517
|
-
if (typeResolved &&
|
|
635
|
+
if (typeResolved &&
|
|
636
|
+
typeResolved.candidates.some((d) => d.type === 'Class' ||
|
|
637
|
+
d.type === 'Interface' ||
|
|
638
|
+
d.type === 'Struct' ||
|
|
639
|
+
d.type === 'Enum')) {
|
|
518
640
|
receiverTypeName = receiverName;
|
|
519
641
|
}
|
|
520
642
|
}
|
|
@@ -538,7 +660,10 @@ importedRawReturnTypesMap) => {
|
|
|
538
660
|
}
|
|
539
661
|
if (!currentType && extracted.baseReceiverName) {
|
|
540
662
|
const cr = ctx.resolve(extracted.baseReceiverName, file.path);
|
|
541
|
-
if (cr?.candidates.some(d => d.type === 'Class' ||
|
|
663
|
+
if (cr?.candidates.some((d) => d.type === 'Class' ||
|
|
664
|
+
d.type === 'Interface' ||
|
|
665
|
+
d.type === 'Struct' ||
|
|
666
|
+
d.type === 'Enum')) {
|
|
542
667
|
currentType = extracted.baseReceiverName;
|
|
543
668
|
}
|
|
544
669
|
}
|
|
@@ -552,7 +677,7 @@ importedRawReturnTypesMap) => {
|
|
|
552
677
|
// Only used when multiple candidates survive arity filtering — ~1-3% of calls.
|
|
553
678
|
const langConfig = provider.typeConfig;
|
|
554
679
|
const hints = langConfig?.inferLiteralType
|
|
555
|
-
? { callNode, inferLiteralType: langConfig.inferLiteralType }
|
|
680
|
+
? { callNode, inferLiteralType: langConfig.inferLiteralType, typeEnv }
|
|
556
681
|
: undefined;
|
|
557
682
|
const resolved = resolveCallTarget({
|
|
558
683
|
calledName,
|
|
@@ -572,6 +697,19 @@ importedRawReturnTypesMap) => {
|
|
|
572
697
|
confidence: resolved.confidence,
|
|
573
698
|
reason: resolved.reason,
|
|
574
699
|
});
|
|
700
|
+
if (implementorMap && callForm === 'member' && receiverTypeName) {
|
|
701
|
+
const implTargets = findInterfaceDispatchTargets(calledName, receiverTypeName, file.path, ctx, implementorMap, resolved.nodeId);
|
|
702
|
+
for (const impl of implTargets) {
|
|
703
|
+
graph.addRelationship({
|
|
704
|
+
id: generateId('CALLS', `${sourceId}:${calledName}->${impl.nodeId}`),
|
|
705
|
+
sourceId,
|
|
706
|
+
targetId: impl.nodeId,
|
|
707
|
+
type: 'CALLS',
|
|
708
|
+
confidence: impl.confidence,
|
|
709
|
+
reason: impl.reason,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
575
713
|
});
|
|
576
714
|
ctx.clearCache();
|
|
577
715
|
}
|
|
@@ -597,39 +735,34 @@ importedRawReturnTypesMap) => {
|
|
|
597
735
|
}
|
|
598
736
|
return collectedHeritage;
|
|
599
737
|
};
|
|
600
|
-
const CALLABLE_SYMBOL_TYPES = new Set([
|
|
601
|
-
'Function',
|
|
602
|
-
'Method',
|
|
603
|
-
'Constructor',
|
|
604
|
-
'Macro',
|
|
605
|
-
'Delegate',
|
|
606
|
-
]);
|
|
738
|
+
const CALLABLE_SYMBOL_TYPES = new Set(['Function', 'Method', 'Constructor', 'Macro', 'Delegate']);
|
|
607
739
|
const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
|
|
608
740
|
const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
609
741
|
let kindFiltered;
|
|
610
742
|
if (callForm === 'constructor') {
|
|
611
|
-
const constructors = candidates.filter(c => c.type === 'Constructor');
|
|
743
|
+
const constructors = candidates.filter((c) => c.type === 'Constructor');
|
|
612
744
|
if (constructors.length > 0) {
|
|
613
745
|
kindFiltered = constructors;
|
|
614
746
|
}
|
|
615
747
|
else {
|
|
616
|
-
const types = candidates.filter(c => CONSTRUCTOR_TARGET_TYPES.has(c.type));
|
|
617
|
-
kindFiltered =
|
|
748
|
+
const types = candidates.filter((c) => CONSTRUCTOR_TARGET_TYPES.has(c.type));
|
|
749
|
+
kindFiltered =
|
|
750
|
+
types.length > 0 ? types : candidates.filter((c) => CALLABLE_SYMBOL_TYPES.has(c.type));
|
|
618
751
|
}
|
|
619
752
|
}
|
|
620
753
|
else {
|
|
621
|
-
kindFiltered = candidates.filter(c => CALLABLE_SYMBOL_TYPES.has(c.type));
|
|
754
|
+
kindFiltered = candidates.filter((c) => CALLABLE_SYMBOL_TYPES.has(c.type));
|
|
622
755
|
}
|
|
623
756
|
if (kindFiltered.length === 0)
|
|
624
757
|
return [];
|
|
625
758
|
if (argCount === undefined)
|
|
626
759
|
return kindFiltered;
|
|
627
|
-
const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
|
|
760
|
+
const hasParameterMetadata = kindFiltered.some((candidate) => candidate.parameterCount !== undefined);
|
|
628
761
|
if (!hasParameterMetadata)
|
|
629
762
|
return kindFiltered;
|
|
630
|
-
return kindFiltered.filter(candidate => candidate.parameterCount === undefined
|
|
631
|
-
|
|
632
|
-
|
|
763
|
+
return kindFiltered.filter((candidate) => candidate.parameterCount === undefined ||
|
|
764
|
+
(argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount) &&
|
|
765
|
+
argCount <= candidate.parameterCount));
|
|
633
766
|
};
|
|
634
767
|
const toResolveResult = (definition, tier) => ({
|
|
635
768
|
nodeId: definition.nodeId,
|
|
@@ -638,13 +771,10 @@ const toResolveResult = (definition, tier) => ({
|
|
|
638
771
|
returnType: definition.returnType,
|
|
639
772
|
});
|
|
640
773
|
/**
|
|
641
|
-
* Kotlin
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* Only applied to single-word identifiers that look like a JVM primitive alias;
|
|
647
|
-
* multi-word or qualified names are left untouched.
|
|
774
|
+
* Kotlin often declares parameters with boxed names (`Int`, `Boolean`, …) while
|
|
775
|
+
* literal inference yields JVM primitives (`int`, `boolean`). This map aligns
|
|
776
|
+
* those for overload matching. Java parameter text is usually already primitive
|
|
777
|
+
* spellings, so lookups here are typically unchanged.
|
|
648
778
|
*/
|
|
649
779
|
const KOTLIN_BOXED_TO_PRIMITIVE = {
|
|
650
780
|
Int: 'int',
|
|
@@ -657,48 +787,10 @@ const KOTLIN_BOXED_TO_PRIMITIVE = {
|
|
|
657
787
|
Char: 'char',
|
|
658
788
|
};
|
|
659
789
|
const normalizeJvmTypeName = (name) => KOTLIN_BOXED_TO_PRIMITIVE[name] ?? name;
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
* Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
|
|
663
|
-
* Returns the single matching candidate, or null if ambiguous/inconclusive.
|
|
664
|
-
*/
|
|
665
|
-
const tryOverloadDisambiguation = (candidates, hints) => {
|
|
666
|
-
if (!candidates.some(c => c.parameterTypes))
|
|
790
|
+
const matchCandidatesByArgTypes = (candidates, argTypes) => {
|
|
791
|
+
if (!candidates.some((c) => c.parameterTypes))
|
|
667
792
|
return null;
|
|
668
|
-
|
|
669
|
-
// Kotlin wraps value_arguments inside a call_suffix child, so we must also
|
|
670
|
-
// search one level deeper when a direct match is not found.
|
|
671
|
-
let argList = hints.callNode.childForFieldName?.('arguments')
|
|
672
|
-
?? hints.callNode.children.find((c) => c.type === 'arguments' || c.type === 'argument_list' || c.type === 'value_arguments');
|
|
673
|
-
if (!argList) {
|
|
674
|
-
// Kotlin: call_expression → call_suffix → value_arguments
|
|
675
|
-
const callSuffix = hints.callNode.children.find((c) => c.type === 'call_suffix');
|
|
676
|
-
if (callSuffix) {
|
|
677
|
-
argList = callSuffix.children.find((c) => c.type === 'value_arguments');
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (!argList)
|
|
681
|
-
return null;
|
|
682
|
-
const argTypes = [];
|
|
683
|
-
for (const arg of argList.namedChildren) {
|
|
684
|
-
if (arg.type === 'comment')
|
|
685
|
-
continue;
|
|
686
|
-
// Unwrap argument wrapper nodes before passing to inferLiteralType:
|
|
687
|
-
// - Kotlin value_argument: has 'value' field containing the literal
|
|
688
|
-
// - C# argument: has 'expression' field (handles named args like `name: "alice"`
|
|
689
|
-
// where firstNamedChild would return name_colon instead of the value)
|
|
690
|
-
// - Java/others: arg IS the literal directly (no unwrapping needed)
|
|
691
|
-
const valueNode = arg.childForFieldName?.('value')
|
|
692
|
-
?? arg.childForFieldName?.('expression')
|
|
693
|
-
?? (arg.type === 'argument' || arg.type === 'value_argument'
|
|
694
|
-
? arg.firstNamedChild ?? arg
|
|
695
|
-
: arg);
|
|
696
|
-
argTypes.push(hints.inferLiteralType(valueNode));
|
|
697
|
-
}
|
|
698
|
-
// If no literal types could be inferred, can't disambiguate
|
|
699
|
-
if (argTypes.every(t => t === undefined))
|
|
700
|
-
return null;
|
|
701
|
-
const matched = candidates.filter(c => {
|
|
793
|
+
const matched = candidates.filter((c) => {
|
|
702
794
|
// Keep candidates without type info — conservative: partially-annotated codebases
|
|
703
795
|
// (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
|
|
704
796
|
// If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
|
|
@@ -718,13 +810,24 @@ const tryOverloadDisambiguation = (candidates, hints) => {
|
|
|
718
810
|
// implementation body all collide via generateId). Deduplicate by nodeId — if all
|
|
719
811
|
// matched candidates resolve to the same graph node, disambiguation succeeded.
|
|
720
812
|
if (matched.length > 1) {
|
|
721
|
-
const uniqueIds = new Set(matched.map(c => c.nodeId));
|
|
813
|
+
const uniqueIds = new Set(matched.map((c) => c.nodeId));
|
|
722
814
|
if (uniqueIds.size === 1)
|
|
723
815
|
return matched[0];
|
|
724
816
|
}
|
|
725
817
|
return null;
|
|
726
818
|
};
|
|
727
|
-
|
|
819
|
+
/**
|
|
820
|
+
* Try to disambiguate overloaded candidates using argument literal types.
|
|
821
|
+
* Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
|
|
822
|
+
* Returns the single matching candidate, or null if ambiguous/inconclusive.
|
|
823
|
+
*/
|
|
824
|
+
const tryOverloadDisambiguation = (candidates, hints) => {
|
|
825
|
+
const argTypes = extractCallArgTypes(hints.callNode, hints.inferLiteralType, hints.typeEnv ? (varName, cn) => hints.typeEnv.lookup(varName, cn) : undefined);
|
|
826
|
+
if (!argTypes)
|
|
827
|
+
return null;
|
|
828
|
+
return matchCandidatesByArgTypes(candidates, argTypes);
|
|
829
|
+
};
|
|
830
|
+
const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes) => {
|
|
728
831
|
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
729
832
|
if (!tiered)
|
|
730
833
|
return null;
|
|
@@ -733,7 +836,7 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
|
|
|
733
836
|
// If free-form filtering found no callable candidates but the symbol resolves to a
|
|
734
837
|
// Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
|
|
735
838
|
if (filteredCandidates.length === 0 && call.callForm === 'free') {
|
|
736
|
-
const hasTypeTarget = tiered.candidates.some(c => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
|
|
839
|
+
const hasTypeTarget = tiered.candidates.some((c) => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
|
|
737
840
|
if (hasTypeTarget) {
|
|
738
841
|
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
739
842
|
}
|
|
@@ -754,7 +857,7 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
|
|
|
754
857
|
if (aliasMap) {
|
|
755
858
|
const moduleFile = aliasMap.get(call.receiverName);
|
|
756
859
|
if (moduleFile) {
|
|
757
|
-
const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
|
|
860
|
+
const aliasFiltered = filteredCandidates.filter((c) => c.filePath === moduleFile);
|
|
758
861
|
if (aliasFiltered.length > 0) {
|
|
759
862
|
filteredCandidates = aliasFiltered;
|
|
760
863
|
}
|
|
@@ -769,8 +872,7 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
|
|
|
769
872
|
fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
|
|
770
873
|
widenCache?.set(cacheKey, fuzzyDefs);
|
|
771
874
|
}
|
|
772
|
-
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm)
|
|
773
|
-
.filter(c => c.filePath === moduleFile);
|
|
875
|
+
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm).filter((c) => c.filePath === moduleFile);
|
|
774
876
|
if (widened.length > 0)
|
|
775
877
|
filteredCandidates = widened;
|
|
776
878
|
}
|
|
@@ -789,8 +891,8 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
|
|
|
789
891
|
// D1. Resolve the receiver type
|
|
790
892
|
const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
|
|
791
893
|
if (typeResolved && typeResolved.candidates.length > 0) {
|
|
792
|
-
const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
|
|
793
|
-
const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
|
|
894
|
+
const typeNodeIds = new Set(typeResolved.candidates.map((d) => d.nodeId));
|
|
895
|
+
const typeFiles = new Set(typeResolved.candidates.map((d) => d.filePath));
|
|
794
896
|
// D2. Widen candidates: same-file tier may miss the parent's method when
|
|
795
897
|
// it lives in another file. Query the symbol table directly for all
|
|
796
898
|
// global methods with this name, then apply arity/kind filtering.
|
|
@@ -798,32 +900,39 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
|
|
|
798
900
|
? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
|
|
799
901
|
: filteredCandidates;
|
|
800
902
|
// D3. File-based: prefer candidates whose filePath matches the resolved type's file
|
|
801
|
-
const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
|
|
903
|
+
const fileFiltered = methodPool.filter((c) => typeFiles.has(c.filePath));
|
|
802
904
|
if (fileFiltered.length === 1) {
|
|
803
905
|
return toResolveResult(fileFiltered[0], tiered.tier);
|
|
804
906
|
}
|
|
805
907
|
// D4. ownerId fallback: narrow by ownerId matching the type's nodeId
|
|
806
908
|
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
|
|
807
|
-
const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
909
|
+
const ownerFiltered = pool.filter((c) => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
808
910
|
if (ownerFiltered.length === 1) {
|
|
809
911
|
return toResolveResult(ownerFiltered[0], tiered.tier);
|
|
810
912
|
}
|
|
811
913
|
// E. Try overload disambiguation on the narrowed pool
|
|
812
|
-
if (
|
|
914
|
+
if (fileFiltered.length > 1 || ownerFiltered.length > 1) {
|
|
813
915
|
const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
|
|
814
|
-
const disambiguated =
|
|
916
|
+
const disambiguated = overloadHints
|
|
917
|
+
? tryOverloadDisambiguation(overloadPool, overloadHints)
|
|
918
|
+
: preComputedArgTypes
|
|
919
|
+
? matchCandidatesByArgTypes(overloadPool, preComputedArgTypes)
|
|
920
|
+
: null;
|
|
815
921
|
if (disambiguated)
|
|
816
922
|
return toResolveResult(disambiguated, tiered.tier);
|
|
817
|
-
}
|
|
818
|
-
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
819
923
|
return null;
|
|
924
|
+
}
|
|
820
925
|
}
|
|
821
926
|
}
|
|
822
927
|
// E. Overload disambiguation: when multiple candidates survive arity + receiver filtering,
|
|
823
|
-
// try matching argument
|
|
824
|
-
//
|
|
825
|
-
if (filteredCandidates.length > 1
|
|
826
|
-
const disambiguated =
|
|
928
|
+
// try matching argument types against parameter types (Phase P).
|
|
929
|
+
// Sequential path uses AST-based hints; worker path uses pre-computed argTypes.
|
|
930
|
+
if (filteredCandidates.length > 1) {
|
|
931
|
+
const disambiguated = overloadHints
|
|
932
|
+
? tryOverloadDisambiguation(filteredCandidates, overloadHints)
|
|
933
|
+
: preComputedArgTypes
|
|
934
|
+
? matchCandidatesByArgTypes(filteredCandidates, preComputedArgTypes)
|
|
935
|
+
: null;
|
|
827
936
|
if (disambiguated)
|
|
828
937
|
return toResolveResult(disambiguated, tiered.tier);
|
|
829
938
|
}
|
|
@@ -833,8 +942,9 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) =>
|
|
|
833
942
|
// primary definition), they represent the same symbol. Prefer the primary
|
|
834
943
|
// definition (shortest file path: Product.swift over ProductExtension.swift).
|
|
835
944
|
if (filteredCandidates.length > 1) {
|
|
836
|
-
const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type);
|
|
837
|
-
if (allSameType &&
|
|
945
|
+
const allSameType = filteredCandidates.every((c) => c.type === filteredCandidates[0].type);
|
|
946
|
+
if (allSameType &&
|
|
947
|
+
(filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
|
|
838
948
|
const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
|
|
839
949
|
return toResolveResult(sorted[0], tiered.tier);
|
|
840
950
|
}
|
|
@@ -948,8 +1058,12 @@ const resolveFieldOwnership = (receiverName, fieldName, filePath, ctx) => {
|
|
|
948
1058
|
const typeResolved = ctx.resolve(receiverName, filePath);
|
|
949
1059
|
if (!typeResolved)
|
|
950
1060
|
return undefined;
|
|
951
|
-
const classDef = typeResolved.candidates.find(d => d.type === 'Class' ||
|
|
952
|
-
|
|
1061
|
+
const classDef = typeResolved.candidates.find((d) => d.type === 'Class' ||
|
|
1062
|
+
d.type === 'Struct' ||
|
|
1063
|
+
d.type === 'Interface' ||
|
|
1064
|
+
d.type === 'Enum' ||
|
|
1065
|
+
d.type === 'Record' ||
|
|
1066
|
+
d.type === 'Impl');
|
|
953
1067
|
if (!classDef)
|
|
954
1068
|
return undefined;
|
|
955
1069
|
return ctx.symbols.lookupFieldByOwner(classDef.nodeId, fieldName) ?? undefined;
|
|
@@ -1025,7 +1139,7 @@ const walkMixedChain = (chain, startType, filePath, ctx, onFieldResolved) => {
|
|
|
1025
1139
|
* Fast path: resolve pre-extracted call sites from workers.
|
|
1026
1140
|
* No AST parsing — workers already extracted calledName + sourceId.
|
|
1027
1141
|
*/
|
|
1028
|
-
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
|
|
1142
|
+
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings, implementorMap) => {
|
|
1029
1143
|
// Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
|
|
1030
1144
|
// The scope dimension prevents collisions when two functions in the same file
|
|
1031
1145
|
// have same-named locals pointing to different constructor types.
|
|
@@ -1069,9 +1183,15 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1069
1183
|
}
|
|
1070
1184
|
}
|
|
1071
1185
|
// Step 1b: class-as-receiver for static method calls (e.g. UserService.find_user())
|
|
1072
|
-
if (!effectiveCall.receiverTypeName &&
|
|
1186
|
+
if (!effectiveCall.receiverTypeName &&
|
|
1187
|
+
effectiveCall.receiverName &&
|
|
1188
|
+
effectiveCall.callForm === 'member') {
|
|
1073
1189
|
const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
|
|
1074
|
-
if (typeResolved &&
|
|
1190
|
+
if (typeResolved &&
|
|
1191
|
+
typeResolved.candidates.some((d) => d.type === 'Class' ||
|
|
1192
|
+
d.type === 'Interface' ||
|
|
1193
|
+
d.type === 'Struct' ||
|
|
1194
|
+
d.type === 'Enum')) {
|
|
1075
1195
|
effectiveCall = { ...effectiveCall, receiverTypeName: effectiveCall.receiverName };
|
|
1076
1196
|
}
|
|
1077
1197
|
}
|
|
@@ -1087,7 +1207,10 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1087
1207
|
}
|
|
1088
1208
|
if (!currentType && effectiveCall.receiverName) {
|
|
1089
1209
|
const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
|
|
1090
|
-
if (typeResolved?.candidates.some(d => d.type === 'Class' ||
|
|
1210
|
+
if (typeResolved?.candidates.some((d) => d.type === 'Class' ||
|
|
1211
|
+
d.type === 'Interface' ||
|
|
1212
|
+
d.type === 'Struct' ||
|
|
1213
|
+
d.type === 'Enum')) {
|
|
1091
1214
|
currentType = effectiveCall.receiverName;
|
|
1092
1215
|
}
|
|
1093
1216
|
}
|
|
@@ -1098,7 +1221,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1098
1221
|
}
|
|
1099
1222
|
}
|
|
1100
1223
|
}
|
|
1101
|
-
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache);
|
|
1224
|
+
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache, effectiveCall.argTypes);
|
|
1102
1225
|
if (!resolved)
|
|
1103
1226
|
continue;
|
|
1104
1227
|
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
@@ -1110,6 +1233,19 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1110
1233
|
confidence: resolved.confidence,
|
|
1111
1234
|
reason: resolved.reason,
|
|
1112
1235
|
});
|
|
1236
|
+
if (implementorMap && effectiveCall.callForm === 'member' && effectiveCall.receiverTypeName) {
|
|
1237
|
+
const implTargets = findInterfaceDispatchTargets(effectiveCall.calledName, effectiveCall.receiverTypeName, effectiveCall.filePath, ctx, implementorMap, resolved.nodeId);
|
|
1238
|
+
for (const impl of implTargets) {
|
|
1239
|
+
graph.addRelationship({
|
|
1240
|
+
id: generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${impl.nodeId}`),
|
|
1241
|
+
sourceId: effectiveCall.sourceId,
|
|
1242
|
+
targetId: impl.nodeId,
|
|
1243
|
+
type: 'CALLS',
|
|
1244
|
+
confidence: impl.confidence,
|
|
1245
|
+
reason: impl.reason,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1113
1249
|
}
|
|
1114
1250
|
ctx.clearCache();
|
|
1115
1251
|
}
|
|
@@ -1145,8 +1281,12 @@ export const processAssignmentsFromExtracted = (graph, assignments, ctx, constru
|
|
|
1145
1281
|
// Tier 3: static class-as-receiver fallback
|
|
1146
1282
|
if (!receiverTypeName) {
|
|
1147
1283
|
const resolved = ctx.resolve(asn.receiverText, asn.filePath);
|
|
1148
|
-
if (resolved?.candidates.some(d => d.type === 'Class' ||
|
|
1149
|
-
|
|
1284
|
+
if (resolved?.candidates.some((d) => d.type === 'Class' ||
|
|
1285
|
+
d.type === 'Struct' ||
|
|
1286
|
+
d.type === 'Interface' ||
|
|
1287
|
+
d.type === 'Enum' ||
|
|
1288
|
+
d.type === 'Record' ||
|
|
1289
|
+
d.type === 'Impl')) {
|
|
1150
1290
|
receiverTypeName = asn.receiverText;
|
|
1151
1291
|
}
|
|
1152
1292
|
}
|
|
@@ -1232,25 +1372,82 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
|
|
|
1232
1372
|
// that happen to share names with response variables (data, result, response, etc.).
|
|
1233
1373
|
const RESPONSE_ACCESS_BLOCKLIST = new Set([
|
|
1234
1374
|
// Fetch/Response API
|
|
1235
|
-
'json',
|
|
1375
|
+
'json',
|
|
1376
|
+
'text',
|
|
1377
|
+
'blob',
|
|
1378
|
+
'arrayBuffer',
|
|
1379
|
+
'formData',
|
|
1380
|
+
'ok',
|
|
1381
|
+
'status',
|
|
1382
|
+
'headers',
|
|
1383
|
+
'clone',
|
|
1236
1384
|
// Promise
|
|
1237
|
-
'then',
|
|
1385
|
+
'then',
|
|
1386
|
+
'catch',
|
|
1387
|
+
'finally',
|
|
1238
1388
|
// Array
|
|
1239
|
-
'map',
|
|
1240
|
-
'
|
|
1241
|
-
'
|
|
1389
|
+
'map',
|
|
1390
|
+
'filter',
|
|
1391
|
+
'forEach',
|
|
1392
|
+
'reduce',
|
|
1393
|
+
'find',
|
|
1394
|
+
'some',
|
|
1395
|
+
'every',
|
|
1396
|
+
'push',
|
|
1397
|
+
'pop',
|
|
1398
|
+
'shift',
|
|
1399
|
+
'unshift',
|
|
1400
|
+
'splice',
|
|
1401
|
+
'slice',
|
|
1402
|
+
'concat',
|
|
1403
|
+
'join',
|
|
1404
|
+
'sort',
|
|
1405
|
+
'reverse',
|
|
1406
|
+
'includes',
|
|
1407
|
+
'indexOf',
|
|
1242
1408
|
// Object
|
|
1243
|
-
'length',
|
|
1409
|
+
'length',
|
|
1410
|
+
'toString',
|
|
1411
|
+
'valueOf',
|
|
1412
|
+
'keys',
|
|
1413
|
+
'values',
|
|
1414
|
+
'entries',
|
|
1244
1415
|
// DOM methods — file-download patterns often reuse `data`/`response` variable names
|
|
1245
|
-
'appendChild',
|
|
1246
|
-
'
|
|
1247
|
-
'
|
|
1248
|
-
'
|
|
1249
|
-
'
|
|
1250
|
-
'
|
|
1251
|
-
'
|
|
1252
|
-
'
|
|
1253
|
-
'
|
|
1416
|
+
'appendChild',
|
|
1417
|
+
'removeChild',
|
|
1418
|
+
'insertBefore',
|
|
1419
|
+
'replaceChild',
|
|
1420
|
+
'replaceChildren',
|
|
1421
|
+
'createElement',
|
|
1422
|
+
'getElementById',
|
|
1423
|
+
'querySelector',
|
|
1424
|
+
'querySelectorAll',
|
|
1425
|
+
'setAttribute',
|
|
1426
|
+
'getAttribute',
|
|
1427
|
+
'removeAttribute',
|
|
1428
|
+
'hasAttribute',
|
|
1429
|
+
'addEventListener',
|
|
1430
|
+
'removeEventListener',
|
|
1431
|
+
'dispatchEvent',
|
|
1432
|
+
'classList',
|
|
1433
|
+
'className',
|
|
1434
|
+
'parentNode',
|
|
1435
|
+
'parentElement',
|
|
1436
|
+
'childNodes',
|
|
1437
|
+
'children',
|
|
1438
|
+
'nextSibling',
|
|
1439
|
+
'previousSibling',
|
|
1440
|
+
'firstChild',
|
|
1441
|
+
'lastChild',
|
|
1442
|
+
'click',
|
|
1443
|
+
'focus',
|
|
1444
|
+
'blur',
|
|
1445
|
+
'submit',
|
|
1446
|
+
'reset',
|
|
1447
|
+
'innerHTML',
|
|
1448
|
+
'outerHTML',
|
|
1449
|
+
'textContent',
|
|
1450
|
+
'innerText',
|
|
1254
1451
|
]);
|
|
1255
1452
|
export const extractConsumerAccessedKeys = (content) => {
|
|
1256
1453
|
const keys = new Set();
|
|
@@ -1370,7 +1567,9 @@ export const extractFetchCallsFromFiles = async (files, astCache) => {
|
|
|
1370
1567
|
let tree = astCache.get(file.path);
|
|
1371
1568
|
if (!tree) {
|
|
1372
1569
|
try {
|
|
1373
|
-
tree = parser.parse(file.content, undefined, {
|
|
1570
|
+
tree = parser.parse(file.content, undefined, {
|
|
1571
|
+
bufferSize: getTreeSitterBufferSize(file.content.length),
|
|
1572
|
+
});
|
|
1374
1573
|
}
|
|
1375
1574
|
catch {
|
|
1376
1575
|
continue;
|
|
@@ -1388,7 +1587,7 @@ export const extractFetchCallsFromFiles = async (files, astCache) => {
|
|
|
1388
1587
|
}
|
|
1389
1588
|
for (const match of matches) {
|
|
1390
1589
|
const captureMap = {};
|
|
1391
|
-
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
1590
|
+
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
|
1392
1591
|
if (captureMap['route.fetch']) {
|
|
1393
1592
|
const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
|
|
1394
1593
|
if (urlNode) {
|
|
@@ -1404,7 +1603,11 @@ export const extractFetchCallsFromFiles = async (files, astCache) => {
|
|
|
1404
1603
|
const url = captureMap['http_client.url'].text;
|
|
1405
1604
|
const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
|
|
1406
1605
|
if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
|
|
1407
|
-
result.push({
|
|
1606
|
+
result.push({
|
|
1607
|
+
filePath: file.path,
|
|
1608
|
+
fetchURL: url,
|
|
1609
|
+
lineNumber: captureMap['http_client'].startPosition.row,
|
|
1610
|
+
});
|
|
1408
1611
|
}
|
|
1409
1612
|
}
|
|
1410
1613
|
}
|