gitnexus 1.4.7 → 1.4.9
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 +29 -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-repo.d.ts +15 -0
- package/dist/cli/index-repo.js +115 -0
- package/dist/cli/index.js +13 -3
- package/dist/cli/setup.js +90 -10
- package/dist/cli/wiki.d.ts +4 -0
- package/dist/cli/wiki.js +174 -53
- package/dist/config/supported-languages.d.ts +33 -1
- package/dist/config/supported-languages.js +32 -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/graph.js +9 -1
- package/dist/core/graph/types.d.ts +11 -2
- package/dist/core/ingestion/call-processor.d.ts +66 -2
- package/dist/core/ingestion/call-processor.js +650 -30
- package/dist/core/ingestion/call-routing.d.ts +9 -18
- package/dist/core/ingestion/call-routing.js +0 -19
- package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
- package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
- package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
- package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
- package/dist/core/ingestion/cobol-processor.d.ts +54 -0
- package/dist/core/ingestion/cobol-processor.js +1186 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
- package/dist/core/ingestion/entry-point-scoring.js +52 -28
- package/dist/core/ingestion/export-detection.d.ts +47 -8
- package/dist/core/ingestion/export-detection.js +29 -50
- package/dist/core/ingestion/field-extractor.d.ts +29 -0
- package/dist/core/ingestion/field-extractor.js +25 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
- package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
- package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
- package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
- package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
- package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
- package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
- package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
- package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
- package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
- package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
- package/dist/core/ingestion/field-extractors/generic.js +111 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
- package/dist/core/ingestion/field-extractors/typescript.js +291 -0
- package/dist/core/ingestion/field-types.d.ts +59 -0
- package/dist/core/ingestion/field-types.js +2 -0
- package/dist/core/ingestion/framework-detection.d.ts +97 -2
- package/dist/core/ingestion/framework-detection.js +114 -14
- package/dist/core/ingestion/heritage-processor.js +62 -66
- package/dist/core/ingestion/import-processor.d.ts +9 -10
- package/dist/core/ingestion/import-processor.js +150 -196
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
- package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/dart.js +44 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +10 -1
- package/dist/core/ingestion/import-resolvers/jvm.js +159 -0
- package/dist/core/ingestion/import-resolvers/php.d.ts +25 -0
- package/dist/core/ingestion/import-resolvers/php.js +80 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
- package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/swift.js +23 -0
- package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
- package/dist/core/ingestion/import-resolvers/types.js +6 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +2 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +7 -0
- package/dist/core/ingestion/language-config.d.ts +6 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/language-provider.d.ts +121 -0
- package/dist/core/ingestion/language-provider.js +24 -0
- package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
- package/dist/core/ingestion/languages/c-cpp.js +71 -0
- package/dist/core/ingestion/languages/cobol.d.ts +1 -0
- package/dist/core/ingestion/languages/cobol.js +26 -0
- package/dist/core/ingestion/languages/csharp.d.ts +8 -0
- package/dist/core/ingestion/languages/csharp.js +49 -0
- package/dist/core/ingestion/languages/dart.d.ts +12 -0
- package/dist/core/ingestion/languages/dart.js +58 -0
- package/dist/core/ingestion/languages/go.d.ts +11 -0
- package/dist/core/ingestion/languages/go.js +28 -0
- package/dist/core/ingestion/languages/index.d.ts +38 -0
- package/dist/core/ingestion/languages/index.js +63 -0
- package/dist/core/ingestion/languages/java.d.ts +9 -0
- package/dist/core/ingestion/languages/java.js +29 -0
- package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
- package/dist/core/ingestion/languages/kotlin.js +53 -0
- package/dist/core/ingestion/languages/php.d.ts +8 -0
- package/dist/core/ingestion/languages/php.js +145 -0
- package/dist/core/ingestion/languages/python.d.ts +12 -0
- package/dist/core/ingestion/languages/python.js +39 -0
- package/dist/core/ingestion/languages/ruby.d.ts +9 -0
- package/dist/core/ingestion/languages/ruby.js +44 -0
- package/dist/core/ingestion/languages/rust.d.ts +12 -0
- package/dist/core/ingestion/languages/rust.js +44 -0
- package/dist/core/ingestion/languages/swift.d.ts +12 -0
- package/dist/core/ingestion/languages/swift.js +133 -0
- package/dist/core/ingestion/languages/typescript.d.ts +10 -0
- package/dist/core/ingestion/languages/typescript.js +60 -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 +22 -18
- package/dist/core/ingestion/named-binding-processor.d.ts +18 -0
- package/dist/core/ingestion/named-binding-processor.js +42 -0
- package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/csharp.js +37 -0
- package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/java.js +29 -0
- package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
- package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/php.js +61 -0
- package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/python.js +49 -0
- package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/rust.js +64 -0
- package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
- package/dist/core/ingestion/named-bindings/types.js +6 -0
- package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/typescript.js +58 -0
- package/dist/core/ingestion/parsing-processor.d.ts +6 -2
- package/dist/core/ingestion/parsing-processor.js +125 -85
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +1235 -317
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +8 -5
- package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
- package/dist/core/ingestion/route-extractors/expo.js +36 -0
- package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
- package/dist/core/ingestion/route-extractors/middleware.js +143 -0
- package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
- package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
- package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
- package/dist/core/ingestion/route-extractors/php.js +21 -0
- package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
- package/dist/core/ingestion/route-extractors/response-shapes.js +290 -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 +10 -9
- package/dist/core/ingestion/tree-sitter-queries.js +274 -11
- package/dist/core/ingestion/type-env.d.ts +42 -18
- package/dist/core/ingestion/type-env.js +481 -106
- 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/dart.d.ts +15 -0
- package/dist/core/ingestion/type-extractors/dart.js +371 -0
- 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 +1 -15
- package/dist/core/ingestion/type-extractors/shared.js +14 -112
- package/dist/core/ingestion/type-extractors/swift.js +338 -7
- package/dist/core/ingestion/type-extractors/types.d.ts +40 -8
- package/dist/core/ingestion/type-extractors/typescript.js +141 -9
- package/dist/core/ingestion/utils/ast-helpers.d.ts +83 -0
- package/dist/core/ingestion/utils/ast-helpers.js +817 -0
- package/dist/core/ingestion/utils/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/utils/call-analysis.js +527 -0
- package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
- package/dist/core/ingestion/utils/event-loop.js +5 -0
- package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
- package/dist/core/ingestion/utils/language-detection.js +70 -0
- package/dist/core/ingestion/utils/verbose.d.ts +1 -0
- package/dist/core/ingestion/utils/verbose.js +7 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +55 -5
- package/dist/core/ingestion/workers/parse-worker.js +415 -225
- package/dist/core/lbug/csv-generator.js +51 -1
- package/dist/core/lbug/lbug-adapter.d.ts +10 -0
- package/dist/core/lbug/lbug-adapter.js +75 -4
- package/dist/core/lbug/schema.d.ts +8 -4
- package/dist/core/lbug/schema.js +65 -4
- package/dist/core/tree-sitter/parser-loader.js +7 -1
- package/dist/core/wiki/cursor-client.d.ts +31 -0
- package/dist/core/wiki/cursor-client.js +127 -0
- package/dist/core/wiki/generator.d.ts +28 -9
- package/dist/core/wiki/generator.js +115 -18
- package/dist/core/wiki/graph-queries.d.ts +4 -0
- package/dist/core/wiki/graph-queries.js +7 -1
- package/dist/core/wiki/llm-client.d.ts +2 -0
- package/dist/core/wiki/llm-client.js +8 -4
- package/dist/core/wiki/prompts.d.ts +3 -3
- package/dist/core/wiki/prompts.js +6 -0
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.d.ts +5 -0
- package/dist/mcp/core/lbug-adapter.js +23 -2
- package/dist/mcp/local/local-backend.d.ts +38 -5
- package/dist/mcp/local/local-backend.js +804 -63
- package/dist/mcp/resources.js +2 -0
- package/dist/mcp/tools.js +73 -4
- 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/dist/storage/repo-manager.d.ts +3 -0
- package/package.json +25 -16
- package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
- package/dist/core/ingestion/named-binding-extraction.js +0 -363
- package/dist/core/ingestion/resolvers/index.d.ts +0 -18
- package/dist/core/ingestion/resolvers/index.js +0 -13
- package/dist/core/ingestion/resolvers/jvm.js +0 -87
- package/dist/core/ingestion/resolvers/php.d.ts +0 -15
- package/dist/core/ingestion/resolvers/php.js +0 -35
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
- package/dist/core/ingestion/type-extractors/index.js +0 -31
- package/dist/core/ingestion/utils.d.ts +0 -138
- package/dist/core/ingestion/utils.js +0 -1290
- package/scripts/patch-tree-sitter-swift.cjs +0 -74
|
@@ -1,13 +1,142 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
2
|
import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
3
3
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getProvider } from './languages/index.js';
|
|
5
5
|
import { generateId } from '../../lib/utils.js';
|
|
6
|
-
import { getLanguageFromFilename
|
|
7
|
-
import {
|
|
6
|
+
import { getLanguageFromFilename } from './utils/language-detection.js';
|
|
7
|
+
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
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';
|
|
11
|
+
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
|
8
12
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
9
|
-
import {
|
|
13
|
+
import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
10
14
|
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
|
15
|
+
const MAX_EXPORTS_PER_FILE = 500;
|
|
16
|
+
const MAX_TYPE_NAME_LENGTH = 256;
|
|
17
|
+
/** Build a map of imported callee names → return types for cross-file call-result binding.
|
|
18
|
+
* Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle). */
|
|
19
|
+
export function buildImportedReturnTypes(filePath, namedImportMap, symbolTable) {
|
|
20
|
+
const result = new Map();
|
|
21
|
+
const fileImports = namedImportMap.get(filePath);
|
|
22
|
+
if (!fileImports)
|
|
23
|
+
return result;
|
|
24
|
+
for (const [localName, binding] of fileImports) {
|
|
25
|
+
const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
|
|
26
|
+
if (!def?.returnType)
|
|
27
|
+
continue;
|
|
28
|
+
const simpleReturn = extractReturnTypeName(def.returnType);
|
|
29
|
+
if (simpleReturn)
|
|
30
|
+
result.set(localName, simpleReturn);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/** Build cross-file RAW return types for imported callables.
|
|
35
|
+
* Unlike buildImportedReturnTypes (which stores extractReturnTypeName output),
|
|
36
|
+
* this stores the raw declared return type string (e.g., 'User[]', 'List<User>').
|
|
37
|
+
* Used by lookupRawReturnType for for-loop element extraction via extractElementTypeFromString. */
|
|
38
|
+
export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTable) {
|
|
39
|
+
const result = new Map();
|
|
40
|
+
const fileImports = namedImportMap.get(filePath);
|
|
41
|
+
if (!fileImports)
|
|
42
|
+
return result;
|
|
43
|
+
for (const [localName, binding] of fileImports) {
|
|
44
|
+
const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
|
|
45
|
+
if (!def?.returnType)
|
|
46
|
+
continue;
|
|
47
|
+
result.set(localName, def.returnType);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/** Collect resolved type bindings for exported file-scope symbols.
|
|
52
|
+
* Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. */
|
|
53
|
+
function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
|
|
54
|
+
const fileScope = typeEnv.fileScope();
|
|
55
|
+
if (!fileScope || fileScope.size === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const exported = new Map();
|
|
58
|
+
for (const [varName, typeName] of fileScope) {
|
|
59
|
+
if (exported.size >= MAX_EXPORTS_PER_FILE)
|
|
60
|
+
break;
|
|
61
|
+
if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
|
|
62
|
+
continue;
|
|
63
|
+
const nodeId = symbolTable.lookupExact(filePath, varName);
|
|
64
|
+
if (!nodeId)
|
|
65
|
+
continue;
|
|
66
|
+
const node = graph.getNode(nodeId);
|
|
67
|
+
if (node?.properties?.isExported) {
|
|
68
|
+
exported.set(varName, typeName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return exported.size > 0 ? exported : null;
|
|
72
|
+
}
|
|
73
|
+
/** Build ExportedTypeMap from graph nodes — used for worker path where TypeEnv
|
|
74
|
+
* is not available in the main thread. Collects returnType/declaredType from
|
|
75
|
+
* exported symbols that have callables with known return types. */
|
|
76
|
+
export function buildExportedTypeMapFromGraph(graph, symbolTable) {
|
|
77
|
+
const result = new Map();
|
|
78
|
+
graph.forEachNode(node => {
|
|
79
|
+
if (!node.properties?.isExported)
|
|
80
|
+
return;
|
|
81
|
+
if (!node.properties?.filePath || !node.properties?.name)
|
|
82
|
+
return;
|
|
83
|
+
const filePath = node.properties.filePath;
|
|
84
|
+
const name = node.properties.name;
|
|
85
|
+
if (!name || name.length > MAX_TYPE_NAME_LENGTH)
|
|
86
|
+
return;
|
|
87
|
+
// For callable symbols, use returnType; for properties/variables, use declaredType
|
|
88
|
+
const def = symbolTable.lookupExactFull(filePath, name);
|
|
89
|
+
if (!def)
|
|
90
|
+
return;
|
|
91
|
+
const typeName = def.returnType ?? def.declaredType;
|
|
92
|
+
if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
|
|
93
|
+
return;
|
|
94
|
+
// Extract simple type name (strip Promise<>, etc.) — reuse shared utility
|
|
95
|
+
const simpleType = extractReturnTypeName(typeName) ?? typeName;
|
|
96
|
+
if (!simpleType)
|
|
97
|
+
return;
|
|
98
|
+
let fileExports = result.get(filePath);
|
|
99
|
+
if (!fileExports) {
|
|
100
|
+
fileExports = new Map();
|
|
101
|
+
result.set(filePath, fileExports);
|
|
102
|
+
}
|
|
103
|
+
if (fileExports.size < MAX_EXPORTS_PER_FILE) {
|
|
104
|
+
fileExports.set(name, simpleType);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/** Seed cross-file receiver types into pre-extracted call records.
|
|
110
|
+
* Fills missing receiverTypeName for single-hop imported variables
|
|
111
|
+
* using ExportedTypeMap + namedImportMap — zero disk I/O, zero AST re-parsing.
|
|
112
|
+
* Mutates calls in-place. Runs BEFORE processCallsFromExtracted. */
|
|
113
|
+
export function seedCrossFileReceiverTypes(calls, namedImportMap, exportedTypeMap) {
|
|
114
|
+
if (namedImportMap.size === 0 || exportedTypeMap.size === 0) {
|
|
115
|
+
return { enrichedCount: 0 };
|
|
116
|
+
}
|
|
117
|
+
let enrichedCount = 0;
|
|
118
|
+
for (const call of calls) {
|
|
119
|
+
if (call.receiverTypeName || !call.receiverName)
|
|
120
|
+
continue;
|
|
121
|
+
if (call.callForm !== 'member')
|
|
122
|
+
continue;
|
|
123
|
+
const fileImports = namedImportMap.get(call.filePath);
|
|
124
|
+
if (!fileImports)
|
|
125
|
+
continue;
|
|
126
|
+
const binding = fileImports.get(call.receiverName);
|
|
127
|
+
if (!binding)
|
|
128
|
+
continue;
|
|
129
|
+
const upstream = exportedTypeMap.get(binding.sourcePath);
|
|
130
|
+
if (!upstream)
|
|
131
|
+
continue;
|
|
132
|
+
const type = upstream.get(binding.exportedName);
|
|
133
|
+
if (type) {
|
|
134
|
+
call.receiverTypeName = type;
|
|
135
|
+
enrichedCount++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { enrichedCount };
|
|
139
|
+
}
|
|
11
140
|
// Stdlib methods that preserve the receiver's type identity. When TypeEnv already
|
|
12
141
|
// strips nullable wrappers (Option<User> → User), these chain steps are no-ops
|
|
13
142
|
// for type resolution — the current type passes through unchanged.
|
|
@@ -21,7 +150,7 @@ const TYPE_PRESERVING_METHODS = new Set([
|
|
|
21
150
|
* Walk up the AST from a node to find the enclosing function/method.
|
|
22
151
|
* Returns null if the call is at module/file level (top-level code).
|
|
23
152
|
*/
|
|
24
|
-
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
153
|
+
const findEnclosingFunction = (node, filePath, ctx, provider) => {
|
|
25
154
|
let current = node.parent;
|
|
26
155
|
while (current) {
|
|
27
156
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
@@ -31,7 +160,33 @@ const findEnclosingFunction = (node, filePath, ctx) => {
|
|
|
31
160
|
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
32
161
|
return resolved.candidates[0].nodeId;
|
|
33
162
|
}
|
|
34
|
-
|
|
163
|
+
// Apply labelOverride so label matches the definition phase (single source of truth).
|
|
164
|
+
let finalLabel = label;
|
|
165
|
+
if (provider.labelOverride) {
|
|
166
|
+
const override = provider.labelOverride(current, label);
|
|
167
|
+
if (override !== null)
|
|
168
|
+
finalLabel = override;
|
|
169
|
+
}
|
|
170
|
+
return generateId(finalLabel, `${filePath}:${funcName}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Language-specific enclosing function resolution (e.g., Dart where
|
|
174
|
+
// function_body is a sibling of function_signature, not a child).
|
|
175
|
+
if (provider.enclosingFunctionFinder) {
|
|
176
|
+
const customResult = provider.enclosingFunctionFinder(current);
|
|
177
|
+
if (customResult) {
|
|
178
|
+
// Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
|
|
179
|
+
const resolved = ctx.resolve(customResult.funcName, filePath);
|
|
180
|
+
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
181
|
+
return resolved.candidates[0].nodeId;
|
|
182
|
+
}
|
|
183
|
+
let finalLabel = customResult.label;
|
|
184
|
+
if (provider.labelOverride) {
|
|
185
|
+
const override = provider.labelOverride(current.previousSibling, finalLabel);
|
|
186
|
+
if (override !== null)
|
|
187
|
+
finalLabel = override;
|
|
188
|
+
}
|
|
189
|
+
return generateId(finalLabel, `${filePath}:${customResult.funcName}`);
|
|
35
190
|
}
|
|
36
191
|
}
|
|
37
192
|
current = current.parent;
|
|
@@ -87,10 +242,23 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
87
242
|
}
|
|
88
243
|
return verified;
|
|
89
244
|
};
|
|
90
|
-
export const processCalls = async (graph, files, astCache, ctx, onProgress
|
|
245
|
+
export const processCalls = async (graph, files, astCache, ctx, onProgress, exportedTypeMap,
|
|
246
|
+
/** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map<localName, typeName>. */
|
|
247
|
+
importedBindingsMap,
|
|
248
|
+
/** Phase 14 E3: cross-file return types for imported callables. Keyed by filePath → Map<calleeName, returnType>.
|
|
249
|
+
* Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
|
|
250
|
+
importedReturnTypesMap,
|
|
251
|
+
/** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
|
|
252
|
+
importedRawReturnTypesMap) => {
|
|
91
253
|
const parser = await loadParser();
|
|
92
254
|
const collectedHeritage = [];
|
|
93
255
|
const pendingWrites = [];
|
|
256
|
+
// Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf.
|
|
257
|
+
// Used as a secondary check when per-file parentMap lacks the relationship — helps
|
|
258
|
+
// when the heritage-declaring file is processed before the call site file.
|
|
259
|
+
// For remaining cases (reverse file order), the SymbolTable class-type fallback applies.
|
|
260
|
+
const globalParentMap = new Map();
|
|
261
|
+
const globalParentSeen = new Map();
|
|
94
262
|
const logSkipped = isVerboseIngestionEnabled();
|
|
95
263
|
const skippedByLang = logSkipped ? new Map() : null;
|
|
96
264
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -107,7 +275,8 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
107
275
|
}
|
|
108
276
|
continue;
|
|
109
277
|
}
|
|
110
|
-
const
|
|
278
|
+
const provider = getProvider(language);
|
|
279
|
+
const queryStr = provider.treeSitterQueries;
|
|
111
280
|
if (!queryStr)
|
|
112
281
|
continue;
|
|
113
282
|
await loadLanguage(language, file.path);
|
|
@@ -132,14 +301,65 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
132
301
|
console.warn(`Query error for ${file.path}:`, queryError);
|
|
133
302
|
continue;
|
|
134
303
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
304
|
+
// Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
|
|
305
|
+
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
|
306
|
+
const fileParentMap = new Map();
|
|
307
|
+
for (const match of matches) {
|
|
308
|
+
const captureMap = {};
|
|
309
|
+
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
310
|
+
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
|
311
|
+
const className = captureMap['heritage.class'].text;
|
|
312
|
+
const parentName = captureMap['heritage.extends'].text;
|
|
313
|
+
const extendsNode = captureMap['heritage.extends'];
|
|
314
|
+
const fieldDecl = extendsNode.parent;
|
|
315
|
+
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
|
|
316
|
+
continue;
|
|
317
|
+
let parents = fileParentMap.get(className);
|
|
318
|
+
if (!parents) {
|
|
319
|
+
parents = [];
|
|
320
|
+
fileParentMap.set(className, parents);
|
|
321
|
+
}
|
|
322
|
+
if (!parents.includes(parentName))
|
|
323
|
+
parents.push(parentName);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const parentMap = fileParentMap;
|
|
327
|
+
// Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
|
|
328
|
+
// Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
|
|
329
|
+
for (const [cls, parents] of fileParentMap) {
|
|
330
|
+
let global = globalParentMap.get(cls);
|
|
331
|
+
let seen = globalParentSeen.get(cls);
|
|
332
|
+
if (!global) {
|
|
333
|
+
global = [];
|
|
334
|
+
globalParentMap.set(cls, global);
|
|
335
|
+
}
|
|
336
|
+
if (!seen) {
|
|
337
|
+
seen = new Set();
|
|
338
|
+
globalParentSeen.set(cls, seen);
|
|
339
|
+
}
|
|
340
|
+
for (const p of parents) {
|
|
341
|
+
if (!seen.has(p)) {
|
|
342
|
+
seen.add(p);
|
|
343
|
+
global.push(p);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const importedBindings = importedBindingsMap?.get(file.path);
|
|
348
|
+
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
|
349
|
+
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
|
350
|
+
const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
|
|
351
|
+
if (typeEnv && exportedTypeMap) {
|
|
352
|
+
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
|
353
|
+
if (fileExports)
|
|
354
|
+
exportedTypeMap.set(file.path, fileExports);
|
|
355
|
+
}
|
|
356
|
+
const callRouter = provider.callRouter;
|
|
357
|
+
const verifiedReceivers = typeEnv.constructorBindings.length > 0
|
|
139
358
|
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
|
140
359
|
: new Map();
|
|
141
360
|
const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
|
|
142
361
|
ctx.enableCache(file.path);
|
|
362
|
+
const widenCache = new Map();
|
|
143
363
|
matches.forEach(match => {
|
|
144
364
|
const captureMap = {};
|
|
145
365
|
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
@@ -155,7 +375,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
155
375
|
}
|
|
156
376
|
// Fall back to verified constructor bindings (mirrors CALLS resolution tier 2)
|
|
157
377
|
if (!receiverTypeName && receiverText && receiverIndex.size > 0) {
|
|
158
|
-
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
|
|
378
|
+
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
|
|
159
379
|
const funcName = enclosing ? extractFuncNameFromSourceId(enclosing) : '';
|
|
160
380
|
receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverText);
|
|
161
381
|
}
|
|
@@ -167,7 +387,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
167
387
|
}
|
|
168
388
|
}
|
|
169
389
|
if (receiverTypeName) {
|
|
170
|
-
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
|
|
390
|
+
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
|
|
171
391
|
const srcId = enclosing || generateId('File', file.path);
|
|
172
392
|
// Defer resolution: Ruby attr_accessor properties are registered during
|
|
173
393
|
// this same loop, so cross-file lookups fail if the declaring file hasn't
|
|
@@ -185,7 +405,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
185
405
|
if (!nameNode)
|
|
186
406
|
return;
|
|
187
407
|
const calledName = nameNode.text;
|
|
188
|
-
const routed = callRouter(calledName, captureMap['call']);
|
|
408
|
+
const routed = callRouter?.(calledName, captureMap['call']);
|
|
189
409
|
if (routed) {
|
|
190
410
|
switch (routed.kind) {
|
|
191
411
|
case 'skip':
|
|
@@ -239,15 +459,53 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
239
459
|
break;
|
|
240
460
|
}
|
|
241
461
|
}
|
|
242
|
-
if (
|
|
462
|
+
if (provider.isBuiltInName(calledName))
|
|
243
463
|
return;
|
|
244
464
|
const callNode = captureMap['call'];
|
|
245
465
|
const callForm = inferCallForm(callNode, nameNode);
|
|
246
466
|
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
247
467
|
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
468
|
+
// Phase P: virtual dispatch override — when the declared type is a base class but
|
|
469
|
+
// the constructor created a known subclass, prefer the more specific type.
|
|
470
|
+
// Checks per-file parentMap first, then falls back to globalParentMap for
|
|
471
|
+
// cross-file heritage (e.g. Dog extends Animal declared in a different file).
|
|
472
|
+
// Reconstructs the exact scope key (funcName@startIndex\0varName) from the
|
|
473
|
+
// enclosing function AST node for a correct, O(1) map lookup.
|
|
474
|
+
if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) {
|
|
475
|
+
// Reconstruct scope key to match constructorTypeMap's scope\0varName format
|
|
476
|
+
let scope = '';
|
|
477
|
+
let p = callNode.parent;
|
|
478
|
+
while (p) {
|
|
479
|
+
if (FUNCTION_NODE_TYPES.has(p.type)) {
|
|
480
|
+
const { funcName } = extractFunctionName(p);
|
|
481
|
+
if (funcName) {
|
|
482
|
+
scope = `${funcName}@${p.startIndex}`;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
p = p.parent;
|
|
487
|
+
}
|
|
488
|
+
const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`);
|
|
489
|
+
if (ctorType && ctorType !== receiverTypeName) {
|
|
490
|
+
// Verify subclass relationship: per-file parentMap first, then cross-file
|
|
491
|
+
// globalParentMap, then fall back to SymbolTable class verification.
|
|
492
|
+
// The SymbolTable fallback handles cross-file cases where heritage is declared
|
|
493
|
+
// in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when
|
|
494
|
+
// processing services/App.kt). Since constructorTypeMap only records entries
|
|
495
|
+
// when a type annotation AND constructor are both present (val x: Base = Sub()),
|
|
496
|
+
// confirming both are class-like types is sufficient — the original code would
|
|
497
|
+
// not compile if Sub didn't extend Base.
|
|
498
|
+
if (isSubclassOf(ctorType, receiverTypeName, parentMap)
|
|
499
|
+
|| isSubclassOf(ctorType, receiverTypeName, globalParentMap)
|
|
500
|
+
|| (ctx.symbols.lookupFuzzy(ctorType).some(d => d.type === 'Class' || d.type === 'Struct')
|
|
501
|
+
&& ctx.symbols.lookupFuzzy(receiverTypeName).some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
|
|
502
|
+
receiverTypeName = ctorType;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
248
506
|
// Fall back to verified constructor bindings for return type inference
|
|
249
507
|
if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
|
|
250
|
-
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
508
|
+
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider);
|
|
251
509
|
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
252
510
|
receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
|
|
253
511
|
}
|
|
@@ -261,7 +519,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
261
519
|
}
|
|
262
520
|
}
|
|
263
521
|
// Hoist sourceId so it's available for ACCESSES edge emission during chain walk.
|
|
264
|
-
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
522
|
+
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx, provider);
|
|
265
523
|
const sourceId = enclosingFuncId || generateId('File', file.path);
|
|
266
524
|
// Fall back to mixed chain resolution when the receiver is a complex expression
|
|
267
525
|
// (field chain, call chain, or interleaved — e.g. user.address.city.save() or
|
|
@@ -290,12 +548,19 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
290
548
|
}
|
|
291
549
|
}
|
|
292
550
|
}
|
|
551
|
+
// Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
|
|
552
|
+
// Only used when multiple candidates survive arity filtering — ~1-3% of calls.
|
|
553
|
+
const langConfig = provider.typeConfig;
|
|
554
|
+
const hints = langConfig?.inferLiteralType
|
|
555
|
+
? { callNode, inferLiteralType: langConfig.inferLiteralType }
|
|
556
|
+
: undefined;
|
|
293
557
|
const resolved = resolveCallTarget({
|
|
294
558
|
calledName,
|
|
295
559
|
argCount: countCallArguments(callNode),
|
|
296
560
|
callForm,
|
|
297
561
|
receiverTypeName,
|
|
298
|
-
|
|
562
|
+
receiverName,
|
|
563
|
+
}, file.path, ctx, hints, widenCache);
|
|
299
564
|
if (!resolved)
|
|
300
565
|
return;
|
|
301
566
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
@@ -362,7 +627,9 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
|
362
627
|
const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
|
|
363
628
|
if (!hasParameterMetadata)
|
|
364
629
|
return kindFiltered;
|
|
365
|
-
return kindFiltered.filter(candidate => candidate.parameterCount === undefined
|
|
630
|
+
return kindFiltered.filter(candidate => candidate.parameterCount === undefined
|
|
631
|
+
|| (argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount)
|
|
632
|
+
&& argCount <= candidate.parameterCount));
|
|
366
633
|
};
|
|
367
634
|
const toResolveResult = (definition, tier) => ({
|
|
368
635
|
nodeId: definition.nodeId,
|
|
@@ -371,19 +638,145 @@ const toResolveResult = (definition, tier) => ({
|
|
|
371
638
|
returnType: definition.returnType,
|
|
372
639
|
});
|
|
373
640
|
/**
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* D. Apply receiver-type filtering for member calls with typed receivers
|
|
641
|
+
* Kotlin (and JVM in general) uses boxed type names in parameter declarations
|
|
642
|
+
* (e.g. `Int`, `Long`, `Boolean`) while inferJvmLiteralType returns unboxed
|
|
643
|
+
* primitives (`int`, `long`, `boolean`). Normalise both sides to lowercase so
|
|
644
|
+
* that the comparison `'Int' === 'int'` does not fail.
|
|
379
645
|
*
|
|
380
|
-
*
|
|
646
|
+
* Only applied to single-word identifiers that look like a JVM primitive alias;
|
|
647
|
+
* multi-word or qualified names are left untouched.
|
|
381
648
|
*/
|
|
382
|
-
const
|
|
649
|
+
const KOTLIN_BOXED_TO_PRIMITIVE = {
|
|
650
|
+
Int: 'int',
|
|
651
|
+
Long: 'long',
|
|
652
|
+
Short: 'short',
|
|
653
|
+
Byte: 'byte',
|
|
654
|
+
Float: 'float',
|
|
655
|
+
Double: 'double',
|
|
656
|
+
Boolean: 'boolean',
|
|
657
|
+
Char: 'char',
|
|
658
|
+
};
|
|
659
|
+
const normalizeJvmTypeName = (name) => KOTLIN_BOXED_TO_PRIMITIVE[name] ?? name;
|
|
660
|
+
/**
|
|
661
|
+
* Try to disambiguate overloaded candidates using argument literal types.
|
|
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))
|
|
667
|
+
return null;
|
|
668
|
+
// Find the argument list node in the call expression.
|
|
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 => {
|
|
702
|
+
// Keep candidates without type info — conservative: partially-annotated codebases
|
|
703
|
+
// (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
|
|
704
|
+
// If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
|
|
705
|
+
if (!c.parameterTypes)
|
|
706
|
+
return true;
|
|
707
|
+
return c.parameterTypes.every((pType, i) => {
|
|
708
|
+
if (i >= argTypes.length || !argTypes[i])
|
|
709
|
+
return true;
|
|
710
|
+
// Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
|
|
711
|
+
// that the stored declaration type matches the inferred literal type.
|
|
712
|
+
return normalizeJvmTypeName(pType) === argTypes[i];
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
if (matched.length === 1)
|
|
716
|
+
return matched[0];
|
|
717
|
+
// Multiple survivors may share the same nodeId (e.g. TypeScript overload signatures +
|
|
718
|
+
// implementation body all collide via generateId). Deduplicate by nodeId — if all
|
|
719
|
+
// matched candidates resolve to the same graph node, disambiguation succeeded.
|
|
720
|
+
if (matched.length > 1) {
|
|
721
|
+
const uniqueIds = new Set(matched.map(c => c.nodeId));
|
|
722
|
+
if (uniqueIds.size === 1)
|
|
723
|
+
return matched[0];
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
};
|
|
727
|
+
const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) => {
|
|
383
728
|
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
384
729
|
if (!tiered)
|
|
385
730
|
return null;
|
|
386
|
-
|
|
731
|
+
let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
|
732
|
+
// Swift/Kotlin: constructor calls look like free function calls (no `new` keyword).
|
|
733
|
+
// If free-form filtering found no callable candidates but the symbol resolves to a
|
|
734
|
+
// Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
|
|
735
|
+
if (filteredCandidates.length === 0 && call.callForm === 'free') {
|
|
736
|
+
const hasTypeTarget = tiered.candidates.some(c => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
|
|
737
|
+
if (hasTypeTarget) {
|
|
738
|
+
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
|
|
742
|
+
// The attribute access gives callForm='member', but the callee may be a Class — a valid
|
|
743
|
+
// constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
|
|
744
|
+
// emits a CALLS edge to the class node.
|
|
745
|
+
if (filteredCandidates.length === 0 && call.callForm === 'member') {
|
|
746
|
+
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
747
|
+
}
|
|
748
|
+
// Module-alias disambiguation: Python `import auth; auth.User()` — receiverName='auth'
|
|
749
|
+
// selects auth.py via moduleAliasMap. Runs for ALL member calls with a known module alias,
|
|
750
|
+
// not just ambiguous ones — same-file tier may shadow the correct cross-module target when
|
|
751
|
+
// the caller defines a function with the same name as the callee (Issue #417).
|
|
752
|
+
if (call.callForm === 'member' && call.receiverName) {
|
|
753
|
+
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
|
754
|
+
if (aliasMap) {
|
|
755
|
+
const moduleFile = aliasMap.get(call.receiverName);
|
|
756
|
+
if (moduleFile) {
|
|
757
|
+
const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
|
|
758
|
+
if (aliasFiltered.length > 0) {
|
|
759
|
+
filteredCandidates = aliasFiltered;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
// Same-file tier returned a local match, but the alias points elsewhere.
|
|
763
|
+
// Widen to global candidates and filter to the aliased module's file.
|
|
764
|
+
// Use per-file widenCache to avoid repeated lookupFuzzy for the same
|
|
765
|
+
// calledName+moduleFile from multiple call sites in the same file.
|
|
766
|
+
const cacheKey = `${call.calledName}\0${moduleFile}`;
|
|
767
|
+
let fuzzyDefs = widenCache?.get(cacheKey);
|
|
768
|
+
if (!fuzzyDefs) {
|
|
769
|
+
fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
|
|
770
|
+
widenCache?.set(cacheKey, fuzzyDefs);
|
|
771
|
+
}
|
|
772
|
+
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm)
|
|
773
|
+
.filter(c => c.filePath === moduleFile);
|
|
774
|
+
if (widened.length > 0)
|
|
775
|
+
filteredCandidates = widened;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
387
780
|
// D. Receiver-type filtering: for member calls with a known receiver type,
|
|
388
781
|
// resolve the type through the same tiered import infrastructure, then
|
|
389
782
|
// filter method candidates to the type's defining file. Fall back to
|
|
@@ -415,12 +808,39 @@ const resolveCallTarget = (call, currentFile, ctx) => {
|
|
|
415
808
|
if (ownerFiltered.length === 1) {
|
|
416
809
|
return toResolveResult(ownerFiltered[0], tiered.tier);
|
|
417
810
|
}
|
|
811
|
+
// E. Try overload disambiguation on the narrowed pool
|
|
812
|
+
if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
|
|
813
|
+
const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
|
|
814
|
+
const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
|
|
815
|
+
if (disambiguated)
|
|
816
|
+
return toResolveResult(disambiguated, tiered.tier);
|
|
817
|
+
}
|
|
418
818
|
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
419
819
|
return null;
|
|
420
820
|
}
|
|
421
821
|
}
|
|
422
|
-
|
|
822
|
+
// E. Overload disambiguation: when multiple candidates survive arity + receiver filtering,
|
|
823
|
+
// try matching argument literal types against parameter types (Phase P).
|
|
824
|
+
// Only available on sequential path (has AST); worker path falls through gracefully.
|
|
825
|
+
if (filteredCandidates.length > 1 && overloadHints) {
|
|
826
|
+
const disambiguated = tryOverloadDisambiguation(filteredCandidates, overloadHints);
|
|
827
|
+
if (disambiguated)
|
|
828
|
+
return toResolveResult(disambiguated, tiered.tier);
|
|
829
|
+
}
|
|
830
|
+
if (filteredCandidates.length !== 1) {
|
|
831
|
+
// Deduplicate: Swift extensions create multiple Class nodes with the same name.
|
|
832
|
+
// When all candidates share the same type and differ only by file (extension vs
|
|
833
|
+
// primary definition), they represent the same symbol. Prefer the primary
|
|
834
|
+
// definition (shortest file path: Product.swift over ProductExtension.swift).
|
|
835
|
+
if (filteredCandidates.length > 1) {
|
|
836
|
+
const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type);
|
|
837
|
+
if (allSameType && (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
|
|
838
|
+
const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
|
|
839
|
+
return toResolveResult(sorted[0], tiered.tier);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
423
842
|
return null;
|
|
843
|
+
}
|
|
424
844
|
return toResolveResult(filteredCandidates[0], tiered.tier);
|
|
425
845
|
};
|
|
426
846
|
// ── Scope key helpers ────────────────────────────────────────────────────
|
|
@@ -636,6 +1056,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
636
1056
|
await yieldToEventLoop();
|
|
637
1057
|
}
|
|
638
1058
|
ctx.enableCache(filePath);
|
|
1059
|
+
const widenCache = new Map();
|
|
639
1060
|
const receiverMap = fileReceiverTypes.get(filePath);
|
|
640
1061
|
for (const call of calls) {
|
|
641
1062
|
let effectiveCall = call;
|
|
@@ -677,7 +1098,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
677
1098
|
}
|
|
678
1099
|
}
|
|
679
1100
|
}
|
|
680
|
-
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
1101
|
+
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache);
|
|
681
1102
|
if (!resolved)
|
|
682
1103
|
continue;
|
|
683
1104
|
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
@@ -791,3 +1212,202 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
|
|
|
791
1212
|
}
|
|
792
1213
|
onProgress?.(extractedRoutes.length, extractedRoutes.length);
|
|
793
1214
|
};
|
|
1215
|
+
/**
|
|
1216
|
+
* Extract property access keys from a consumer file's source code near fetch calls.
|
|
1217
|
+
*
|
|
1218
|
+
* Looks for three patterns after a fetch/response variable assignment:
|
|
1219
|
+
* 1. Destructuring: `const { data, pagination } = await res.json()`
|
|
1220
|
+
* 2. Property access: `response.data`, `result.items`
|
|
1221
|
+
* 3. Optional chaining: `data?.key1?.key2`
|
|
1222
|
+
*
|
|
1223
|
+
* Returns deduplicated top-level property names accessed on the response.
|
|
1224
|
+
*
|
|
1225
|
+
* NOTE: This scans the entire file content, not just code near a specific fetch call.
|
|
1226
|
+
* If a file has multiple fetch calls to different routes, all accessed keys are
|
|
1227
|
+
* attributed to each fetch. This is an acceptable tradeoff for regex-based extraction.
|
|
1228
|
+
*/
|
|
1229
|
+
/** Common method names on response/data objects that are NOT property accesses */
|
|
1230
|
+
// Properties/methods to ignore when extracting consumer accessed keys from `data.X` patterns.
|
|
1231
|
+
// Avoids false positives from Fetch API, Array, Object, Promise, and DOM access on variables
|
|
1232
|
+
// that happen to share names with response variables (data, result, response, etc.).
|
|
1233
|
+
const RESPONSE_ACCESS_BLOCKLIST = new Set([
|
|
1234
|
+
// Fetch/Response API
|
|
1235
|
+
'json', 'text', 'blob', 'arrayBuffer', 'formData', 'ok', 'status', 'headers', 'clone',
|
|
1236
|
+
// Promise
|
|
1237
|
+
'then', 'catch', 'finally',
|
|
1238
|
+
// Array
|
|
1239
|
+
'map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every',
|
|
1240
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat', 'join',
|
|
1241
|
+
'sort', 'reverse', 'includes', 'indexOf',
|
|
1242
|
+
// Object
|
|
1243
|
+
'length', 'toString', 'valueOf', 'keys', 'values', 'entries',
|
|
1244
|
+
// DOM methods — file-download patterns often reuse `data`/`response` variable names
|
|
1245
|
+
'appendChild', 'removeChild', 'insertBefore', 'replaceChild', 'replaceChildren',
|
|
1246
|
+
'createElement', 'getElementById', 'querySelector', 'querySelectorAll',
|
|
1247
|
+
'setAttribute', 'getAttribute', 'removeAttribute', 'hasAttribute',
|
|
1248
|
+
'addEventListener', 'removeEventListener', 'dispatchEvent',
|
|
1249
|
+
'classList', 'className',
|
|
1250
|
+
'parentNode', 'parentElement', 'childNodes', 'children',
|
|
1251
|
+
'nextSibling', 'previousSibling', 'firstChild', 'lastChild',
|
|
1252
|
+
'click', 'focus', 'blur', 'submit', 'reset',
|
|
1253
|
+
'innerHTML', 'outerHTML', 'textContent', 'innerText',
|
|
1254
|
+
]);
|
|
1255
|
+
export const extractConsumerAccessedKeys = (content) => {
|
|
1256
|
+
const keys = new Set();
|
|
1257
|
+
// Pattern 1: Destructuring from .json() — const { key1, key2 } = await res.json()
|
|
1258
|
+
// Also matches: const { key1, key2 } = await (await fetch(...)).json()
|
|
1259
|
+
const destructurePattern = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:await\s+)?(?:\w+\.json\s*\(\)|(?:await\s+)?(?:fetch|axios|got)\s*\([^)]*\)(?:\.then\s*\([^)]*\))?(?:\.json\s*\(\))?)/g;
|
|
1260
|
+
let match;
|
|
1261
|
+
while ((match = destructurePattern.exec(content)) !== null) {
|
|
1262
|
+
const destructuredBody = match[1];
|
|
1263
|
+
// Extract identifiers from destructuring, handling renamed bindings (key: alias)
|
|
1264
|
+
const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
|
|
1265
|
+
let keyMatch;
|
|
1266
|
+
while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
|
|
1267
|
+
keys.add(keyMatch[1]);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// Pattern 2: Destructuring from a data/result/response/json variable
|
|
1271
|
+
// e.g., const { items, total } = data; or const { error } = result;
|
|
1272
|
+
const dataVarDestructure = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:data|result|response|json|body|res)\b/g;
|
|
1273
|
+
while ((match = dataVarDestructure.exec(content)) !== null) {
|
|
1274
|
+
const destructuredBody = match[1];
|
|
1275
|
+
const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
|
|
1276
|
+
let keyMatch;
|
|
1277
|
+
while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
|
|
1278
|
+
keys.add(keyMatch[1]);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
// Pattern 3: Property access on common response variable names
|
|
1282
|
+
// Matches: data.key, response.key, result.key, json.key, body.key
|
|
1283
|
+
// Also matches optional chaining: data?.key
|
|
1284
|
+
const propAccessPattern = /\b(?:data|response|result|json|body|res)\s*(?:\?\.|\.)(\w+)/g;
|
|
1285
|
+
while ((match = propAccessPattern.exec(content)) !== null) {
|
|
1286
|
+
const key = match[1];
|
|
1287
|
+
// Skip common method calls that aren't property accesses
|
|
1288
|
+
if (!RESPONSE_ACCESS_BLOCKLIST.has(key)) {
|
|
1289
|
+
keys.add(key);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return [...keys];
|
|
1293
|
+
};
|
|
1294
|
+
/**
|
|
1295
|
+
* Create FETCHES edges from extracted fetch() calls to matching Route nodes.
|
|
1296
|
+
* When consumerContents is provided, extracts property access patterns from
|
|
1297
|
+
* consumer files and encodes them in the edge reason field.
|
|
1298
|
+
*/
|
|
1299
|
+
export const processNextjsFetchRoutes = (graph, fetchCalls, routeRegistry, // routeURL → handlerFilePath
|
|
1300
|
+
consumerContents) => {
|
|
1301
|
+
// Pre-count how many routes each consumer file matches (for confidence attribution)
|
|
1302
|
+
const routeCountByFile = new Map();
|
|
1303
|
+
for (const call of fetchCalls) {
|
|
1304
|
+
const normalized = normalizeFetchURL(call.fetchURL);
|
|
1305
|
+
if (!normalized)
|
|
1306
|
+
continue;
|
|
1307
|
+
for (const [routeURL] of routeRegistry) {
|
|
1308
|
+
if (routeMatches(normalized, routeURL)) {
|
|
1309
|
+
routeCountByFile.set(call.filePath, (routeCountByFile.get(call.filePath) ?? 0) + 1);
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
for (const call of fetchCalls) {
|
|
1315
|
+
const normalized = normalizeFetchURL(call.fetchURL);
|
|
1316
|
+
if (!normalized)
|
|
1317
|
+
continue;
|
|
1318
|
+
for (const [routeURL] of routeRegistry) {
|
|
1319
|
+
if (routeMatches(normalized, routeURL)) {
|
|
1320
|
+
const sourceId = generateId('File', call.filePath);
|
|
1321
|
+
const routeNodeId = generateId('Route', routeURL);
|
|
1322
|
+
// Extract consumer accessed keys if file content is available
|
|
1323
|
+
let reason = 'fetch-url-match';
|
|
1324
|
+
if (consumerContents) {
|
|
1325
|
+
const content = consumerContents.get(call.filePath);
|
|
1326
|
+
if (content) {
|
|
1327
|
+
const accessedKeys = extractConsumerAccessedKeys(content);
|
|
1328
|
+
if (accessedKeys.length > 0) {
|
|
1329
|
+
reason = `fetch-url-match|keys:${accessedKeys.join(',')}`;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
// Encode multi-fetch count so downstream can set confidence
|
|
1334
|
+
const fetchCount = routeCountByFile.get(call.filePath) ?? 1;
|
|
1335
|
+
if (fetchCount > 1) {
|
|
1336
|
+
reason = `${reason}|fetches:${fetchCount}`;
|
|
1337
|
+
}
|
|
1338
|
+
graph.addRelationship({
|
|
1339
|
+
id: generateId('FETCHES', `${sourceId}->${routeNodeId}`),
|
|
1340
|
+
sourceId,
|
|
1341
|
+
targetId: routeNodeId,
|
|
1342
|
+
type: 'FETCHES',
|
|
1343
|
+
confidence: 0.9,
|
|
1344
|
+
reason,
|
|
1345
|
+
});
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1352
|
+
* Extract fetch() calls from source files (sequential path).
|
|
1353
|
+
* Workers handle this via tree-sitter captures in parse-worker; this function
|
|
1354
|
+
* provides the same extraction for the sequential fallback path.
|
|
1355
|
+
*/
|
|
1356
|
+
export const extractFetchCallsFromFiles = async (files, astCache) => {
|
|
1357
|
+
const parser = await loadParser();
|
|
1358
|
+
const result = [];
|
|
1359
|
+
for (const file of files) {
|
|
1360
|
+
const language = getLanguageFromFilename(file.path);
|
|
1361
|
+
if (!language)
|
|
1362
|
+
continue;
|
|
1363
|
+
if (!isLanguageAvailable(language))
|
|
1364
|
+
continue;
|
|
1365
|
+
const provider = getProvider(language);
|
|
1366
|
+
const queryStr = provider.treeSitterQueries;
|
|
1367
|
+
if (!queryStr)
|
|
1368
|
+
continue;
|
|
1369
|
+
await loadLanguage(language, file.path);
|
|
1370
|
+
let tree = astCache.get(file.path);
|
|
1371
|
+
if (!tree) {
|
|
1372
|
+
try {
|
|
1373
|
+
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
astCache.set(file.path, tree);
|
|
1379
|
+
}
|
|
1380
|
+
let matches;
|
|
1381
|
+
try {
|
|
1382
|
+
const lang = parser.getLanguage();
|
|
1383
|
+
const query = new Parser.Query(lang, queryStr);
|
|
1384
|
+
matches = query.matches(tree.rootNode);
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
for (const match of matches) {
|
|
1390
|
+
const captureMap = {};
|
|
1391
|
+
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
1392
|
+
if (captureMap['route.fetch']) {
|
|
1393
|
+
const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
|
|
1394
|
+
if (urlNode) {
|
|
1395
|
+
result.push({
|
|
1396
|
+
filePath: file.path,
|
|
1397
|
+
fetchURL: urlNode.text,
|
|
1398
|
+
lineNumber: captureMap['route.fetch'].startPosition.row,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
else if (captureMap['http_client'] && captureMap['http_client.url']) {
|
|
1403
|
+
const method = captureMap['http_client.method']?.text;
|
|
1404
|
+
const url = captureMap['http_client.url'].text;
|
|
1405
|
+
const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
|
|
1406
|
+
if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
|
|
1407
|
+
result.push({ filePath: file.path, fetchURL: url, lineNumber: captureMap['http_client'].startPosition.row });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
};
|