gitnexus 1.5.2 → 1.6.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 +10 -0
- package/dist/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -1
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/mro-strategy.d.ts +19 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +28 -4
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +2 -1
- package/dist/cli/group.d.ts +2 -0
- package/dist/cli/group.js +233 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/serve.js +4 -1
- package/dist/cli/setup.js +34 -3
- package/dist/cli/wiki.js +15 -44
- package/dist/config/ignore-service.js +8 -3
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/git-staleness.d.ts +13 -0
- package/dist/core/git-staleness.js +29 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +460 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +3 -0
- package/dist/core/group/config-parser.js +83 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
- package/dist/core/group/extractors/grpc-extractor.js +264 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +428 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
- package/dist/core/group/extractors/topic-extractor.js +234 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +198 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +46 -0
- package/dist/core/group/service.js +160 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +91 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +148 -0
- package/dist/core/group/types.d.ts +130 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
- package/dist/core/ingestion/binding-accumulator.js +332 -0
- package/dist/core/ingestion/call-processor.d.ts +155 -24
- package/dist/core/ingestion/call-processor.js +1129 -247
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
- package/dist/core/ingestion/entry-point-scoring.js +1 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -1
- package/dist/core/ingestion/field-extractors/configs/helpers.js +13 -3
- package/dist/core/ingestion/field-types.d.ts +2 -2
- package/dist/core/ingestion/filesystem-walker.js +8 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +1 -0
- package/dist/core/ingestion/heritage-processor.d.ts +8 -15
- package/dist/core/ingestion/heritage-processor.js +15 -28
- package/dist/core/ingestion/import-processor.d.ts +1 -11
- package/dist/core/ingestion/import-processor.js +0 -12
- package/dist/core/ingestion/import-resolvers/utils.js +1 -0
- package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/vue.js +9 -0
- package/dist/core/ingestion/language-provider.d.ts +6 -3
- package/dist/core/ingestion/languages/c-cpp.js +168 -1
- package/dist/core/ingestion/languages/csharp.js +20 -0
- package/dist/core/ingestion/languages/dart.js +26 -4
- package/dist/core/ingestion/languages/go.js +22 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java.js +17 -0
- package/dist/core/ingestion/languages/kotlin.js +24 -1
- package/dist/core/ingestion/languages/php.js +23 -11
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/languages/ruby.js +28 -0
- package/dist/core/ingestion/languages/rust.js +38 -0
- package/dist/core/ingestion/languages/swift.js +31 -0
- package/dist/core/ingestion/languages/typescript.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript.js +54 -1
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +81 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +338 -0
- package/dist/core/ingestion/method-extractors/generic.js +38 -15
- package/dist/core/ingestion/method-types.d.ts +25 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
- package/dist/core/ingestion/model/heritage-map.js +159 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +41 -0
- package/dist/core/ingestion/model/method-registry.d.ts +62 -0
- package/dist/core/ingestion/model/method-registry.js +130 -0
- package/dist/core/ingestion/model/registration-table.d.ts +139 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +56 -0
- package/dist/core/ingestion/model/resolve.js +242 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
- package/dist/core/ingestion/model/semantic-model.js +120 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +4 -3
- package/dist/core/ingestion/mro-processor.js +310 -106
- package/dist/core/ingestion/parsing-processor.d.ts +5 -4
- package/dist/core/ingestion/parsing-processor.js +210 -85
- package/dist/core/ingestion/pipeline.d.ts +2 -0
- package/dist/core/ingestion/pipeline.js +192 -68
- package/dist/core/ingestion/tree-sitter-queries.d.ts +6 -6
- package/dist/core/ingestion/tree-sitter-queries.js +37 -0
- package/dist/core/ingestion/type-env.d.ts +15 -2
- package/dist/core/ingestion/type-env.js +163 -102
- package/dist/core/ingestion/type-extractors/csharp.js +17 -0
- package/dist/core/ingestion/type-extractors/jvm.js +11 -0
- package/dist/core/ingestion/type-extractors/php.js +0 -55
- package/dist/core/ingestion/type-extractors/ruby.js +0 -32
- package/dist/core/ingestion/type-extractors/swift.js +13 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
- package/dist/core/ingestion/type-extractors/typescript.js +66 -69
- package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
- package/dist/core/ingestion/utils/ast-helpers.js +129 -565
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
- package/dist/core/ingestion/workers/parse-worker.js +463 -198
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +68 -3
- package/dist/core/lbug/pool-adapter.d.ts +76 -0
- package/dist/core/lbug/pool-adapter.js +522 -0
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +1 -1
- package/dist/core/search/bm25-index.js +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -0
- package/dist/core/wiki/graph-queries.js +1 -1
- package/dist/core/wiki/html-viewer.js +6 -4
- package/dist/core/wiki/llm-client.js +4 -6
- package/dist/mcp/core/embedder.js +6 -5
- package/dist/mcp/core/lbug-adapter.d.ts +3 -63
- package/dist/mcp/core/lbug-adapter.js +3 -484
- package/dist/mcp/local/local-backend.d.ts +31 -2
- package/dist/mcp/local/local-backend.js +255 -46
- package/dist/mcp/resources.js +5 -4
- package/dist/mcp/staleness.d.ts +3 -13
- package/dist/mcp/staleness.js +2 -31
- package/dist/mcp/tools.js +80 -4
- package/dist/server/analyze-job.d.ts +2 -0
- package/dist/server/analyze-job.js +4 -0
- package/dist/server/api.d.ts +20 -1
- package/dist/server/api.js +306 -71
- package/dist/server/git-clone.d.ts +2 -1
- package/dist/server/git-clone.js +98 -5
- package/dist/storage/git.d.ts +13 -0
- package/dist/storage/git.js +25 -0
- package/dist/storage/repo-manager.js +1 -1
- package/package.json +8 -2
- package/scripts/patch-tree-sitter-swift.cjs +78 -0
- package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
- package/dist/core/ingestion/named-binding-processor.js +0 -42
- package/dist/core/ingestion/resolution-context.d.ts +0 -58
- package/dist/core/ingestion/resolution-context.js +0 -135
- package/dist/core/ingestion/symbol-table.d.ts +0 -79
- package/dist/core/ingestion/symbol-table.js +0 -115
|
@@ -1,22 +1,76 @@
|
|
|
1
|
+
import { CLASS_TYPES, CALL_TARGET_TYPES } from './model/symbol-table.js';
|
|
1
2
|
import Parser from 'tree-sitter';
|
|
2
|
-
import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
3
|
+
import { TIER_CONFIDENCE } from './model/resolution-context.js';
|
|
3
4
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
4
5
|
import { getProvider } from './languages/index.js';
|
|
5
6
|
import { generateId } from '../../lib/utils.js';
|
|
6
|
-
import { getLanguageFromFilename } from '../../_shared/index.js';
|
|
7
|
+
import { getLanguageFromFilename, SupportedLanguages } from '../../_shared/index.js';
|
|
7
8
|
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
8
9
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
9
|
-
import { FUNCTION_NODE_TYPES,
|
|
10
|
+
import { FUNCTION_NODE_TYPES, findEnclosingClassId, findEnclosingClassInfo, genericFuncName, inferFunctionLabel, } from './utils/ast-helpers.js';
|
|
11
|
+
import { typeTagForId, constTagForId, buildCollisionGroups } from './utils/method-props.js';
|
|
10
12
|
import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, extractCallArgTypes, } from './utils/call-analysis.js';
|
|
11
13
|
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
|
12
|
-
import { resolveExtendsType } from './heritage-processor.js';
|
|
13
14
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
14
15
|
import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
16
|
+
import { extractTemplateComponents } from './vue-sfc-extractor.js';
|
|
15
17
|
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
|
18
|
+
import { extractParsedCallSite } from './call-sites/extract-language-call-site.js';
|
|
19
|
+
import { lookupMethodByOwnerWithMRO } from './model/resolve.js';
|
|
20
|
+
/**
|
|
21
|
+
* Type labels treated as class-like **method-dispatch receivers** by the call
|
|
22
|
+
* resolver — the set walked by the MRO / heritage path for member and static
|
|
23
|
+
* method calls.
|
|
24
|
+
*
|
|
25
|
+
* Derived from `CLASS_TYPES` (the heritage-index set in symbol-table) plus
|
|
26
|
+
* `Impl` — Rust `impl` blocks are the definition site of methods for a struct
|
|
27
|
+
* and must be walkable as receiver-type candidates even though they are not
|
|
28
|
+
* indexed by `lookupClassByName` (which keys off struct/trait names). Keeping
|
|
29
|
+
* this set a strict superset of `CLASS_TYPES` guarantees that anything
|
|
30
|
+
* reachable via `lookupClassByName` also passes this filter, so the two call
|
|
31
|
+
* paths cannot diverge silently.
|
|
32
|
+
*
|
|
33
|
+
* `Interface` is included even though interfaces cannot be directly
|
|
34
|
+
* instantiated in Java/C#/TypeScript: the resolver still needs to reach
|
|
35
|
+
* interface nodes for static-method dispatch (`Interface.staticMethod()`) and
|
|
36
|
+
* default-method resolution via the MRO walker.
|
|
37
|
+
*
|
|
38
|
+
* **Do not reuse this set for constructor-fallback filtering.** Constructors
|
|
39
|
+
* can only instantiate a narrower subset — see `INSTANTIABLE_CLASS_TYPES`
|
|
40
|
+
* below. `resolveStaticCall`'s step-5 class-node fallback uses the narrower
|
|
41
|
+
* set to prevent false `CALLS` edges from constructor-shaped calls to
|
|
42
|
+
* `Interface`, `Trait`, or `Impl` nodes.
|
|
43
|
+
*/
|
|
44
|
+
const CLASS_LIKE_TYPES = new Set([...CLASS_TYPES, 'Impl']);
|
|
45
|
+
/**
|
|
46
|
+
* Type labels that can be the target of a constructor-shaped call when no
|
|
47
|
+
* explicit `Constructor` symbol is indexed — the "return the type itself as
|
|
48
|
+
* the call target" fallback set.
|
|
49
|
+
*
|
|
50
|
+
* Strict subset of both `CLASS_LIKE_TYPES` and `CONSTRUCTOR_TARGET_TYPES`.
|
|
51
|
+
* Excludes:
|
|
52
|
+
* - `Interface` / `Trait` — not instantiable by definition in any
|
|
53
|
+
* supported language.
|
|
54
|
+
* - `Impl` — Rust `impl` blocks are method-definition containers, not
|
|
55
|
+
* the type itself; the owning `Struct` is the correct target.
|
|
56
|
+
* - `Enum` — excluded pending language-specific support with motivating
|
|
57
|
+
* test fixtures (matches `CONSTRUCTOR_TARGET_TYPES`).
|
|
58
|
+
*
|
|
59
|
+
* Used exclusively by `resolveStaticCall`'s step-5 class-node fallback.
|
|
60
|
+
* Keep in sync with `CONSTRUCTOR_TARGET_TYPES` (which additionally contains
|
|
61
|
+
* `'Constructor'` for explicit-constructor-node filtering) when extending.
|
|
62
|
+
*/
|
|
63
|
+
const INSTANTIABLE_CLASS_TYPES = new Set(['Class', 'Struct', 'Record']);
|
|
16
64
|
const MAX_EXPORTS_PER_FILE = 500;
|
|
17
65
|
const MAX_TYPE_NAME_LENGTH = 256;
|
|
18
66
|
/** Build a map of imported callee names → return types for cross-file call-result binding.
|
|
19
|
-
* Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle).
|
|
67
|
+
* Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle).
|
|
68
|
+
*
|
|
69
|
+
* Overlapping mechanism (1 of 3): this is the SymbolTable-backed path.
|
|
70
|
+
* See also:
|
|
71
|
+
* 2. collectExportedBindings (~line 168) / enrichExportedTypeMap — TypeEnv + graph isExported
|
|
72
|
+
* 3. Phase 9 fallback in verifyConstructorBindings (~line 563) — namedImportMap + BindingAccumulator
|
|
73
|
+
* A future cleanup should merge these into a single resolution pass. */
|
|
20
74
|
export function buildImportedReturnTypes(filePath, namedImportMap, symbolTable) {
|
|
21
75
|
const result = new Map();
|
|
22
76
|
const fileImports = namedImportMap.get(filePath);
|
|
@@ -50,7 +104,20 @@ export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTabl
|
|
|
50
104
|
return result;
|
|
51
105
|
}
|
|
52
106
|
/** Collect resolved type bindings for exported file-scope symbols.
|
|
53
|
-
* Uses graph node isExported flag — does NOT require isExported on SymbolDefinition.
|
|
107
|
+
* Uses graph node isExported flag — does NOT require isExported on SymbolDefinition.
|
|
108
|
+
*
|
|
109
|
+
* **Counterpart**: the worker path populates `exportedTypeMap` via the
|
|
110
|
+
* accumulator enrichment loop in `pipeline.ts` (search for "Worker path
|
|
111
|
+
* quality enrichment"). Both sites populate the same map with subtly
|
|
112
|
+
* different export-check semantics — this site uses SymbolTable +
|
|
113
|
+
* graph lookup, the worker loop uses three-candidate-ID graph lookup.
|
|
114
|
+
* They must stay in sync until unified. If you edit one, check the other.
|
|
115
|
+
*
|
|
116
|
+
* Overlapping mechanism (2 of 3): this is the TypeEnv + graph isExported path.
|
|
117
|
+
* See also:
|
|
118
|
+
* 1. buildImportedReturnTypes (~line 109) — namedImportMap + SymbolTable
|
|
119
|
+
* 3. Phase 9 fallback in verifyConstructorBindings (~line 563) — namedImportMap + BindingAccumulator
|
|
120
|
+
* A future cleanup should merge these into a single resolution pass. */
|
|
54
121
|
function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
|
|
55
122
|
const fileScope = typeEnv.fileScope();
|
|
56
123
|
if (!fileScope || fileScope.size === 0)
|
|
@@ -85,8 +152,10 @@ export function buildExportedTypeMapFromGraph(graph, symbolTable) {
|
|
|
85
152
|
const name = node.properties.name;
|
|
86
153
|
if (!name || name.length > MAX_TYPE_NAME_LENGTH)
|
|
87
154
|
return;
|
|
88
|
-
// For callable symbols, use returnType; for properties/variables, use declaredType
|
|
89
|
-
|
|
155
|
+
// For callable symbols, use returnType; for properties/variables, use declaredType.
|
|
156
|
+
// Use lookupExactAll + nodeId match to handle same-name methods in different classes.
|
|
157
|
+
const defs = symbolTable.lookupExactAll(filePath, name);
|
|
158
|
+
const def = defs.find((d) => d.nodeId === node.id) ?? defs[0];
|
|
90
159
|
if (!def)
|
|
91
160
|
return;
|
|
92
161
|
const typeName = def.returnType ?? def.declaredType;
|
|
@@ -156,6 +225,10 @@ const TYPE_PRESERVING_METHODS = new Set([
|
|
|
156
225
|
'get', // Kotlin/Java Optional.get()
|
|
157
226
|
'orElseThrow', // Java Optional
|
|
158
227
|
]);
|
|
228
|
+
/** Cache for method extraction results in findEnclosingFunction fallback path.
|
|
229
|
+
* Keyed by classNode.id to avoid re-extracting the same class body per call site.
|
|
230
|
+
* Cleared between files at line ~611 in the processCalls file loop. */
|
|
231
|
+
const enclosingFnExtractCache = new Map();
|
|
159
232
|
/**
|
|
160
233
|
* Walk up the AST from a node to find the enclosing function/method.
|
|
161
234
|
* Returns null if the call is at module/file level (top-level code).
|
|
@@ -164,20 +237,100 @@ const findEnclosingFunction = (node, filePath, ctx, provider) => {
|
|
|
164
237
|
let current = node.parent;
|
|
165
238
|
while (current) {
|
|
166
239
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
167
|
-
const
|
|
240
|
+
const efnResult = provider.methodExtractor?.extractFunctionName?.(current);
|
|
241
|
+
const funcName = efnResult?.funcName ?? genericFuncName(current);
|
|
242
|
+
const label = efnResult?.label ?? inferFunctionLabel(current.type);
|
|
168
243
|
if (funcName) {
|
|
169
244
|
const resolved = ctx.resolve(funcName, filePath);
|
|
170
245
|
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
171
|
-
|
|
246
|
+
// Disambiguate by enclosing class when multiple candidates
|
|
247
|
+
if (resolved.candidates.length === 1) {
|
|
248
|
+
return resolved.candidates[0].nodeId;
|
|
249
|
+
}
|
|
250
|
+
const classInfo = findEnclosingClassInfo(current, filePath);
|
|
251
|
+
if (classInfo) {
|
|
252
|
+
const classMatches = resolved.candidates.filter((c) => c.ownerId === classInfo.classId);
|
|
253
|
+
// Unique class match — return it (no same-arity ambiguity)
|
|
254
|
+
if (classMatches.length === 1)
|
|
255
|
+
return classMatches[0].nodeId;
|
|
256
|
+
// Multiple same-class candidates (same-arity overloads) — fall through
|
|
257
|
+
// to the fallback path which computes the exact ID with type-hash.
|
|
258
|
+
if (classMatches.length > 1) {
|
|
259
|
+
/* fall through to manual ID construction below */
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// No class match — return first candidate as before
|
|
263
|
+
return resolved.candidates[0].nodeId;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
return resolved.candidates[0].nodeId;
|
|
268
|
+
}
|
|
172
269
|
}
|
|
173
|
-
//
|
|
270
|
+
// Fallback: qualify the generated ID to match definition-phase node IDs
|
|
174
271
|
let finalLabel = label;
|
|
175
272
|
if (provider.labelOverride) {
|
|
176
273
|
const override = provider.labelOverride(current, label);
|
|
177
274
|
if (override !== null)
|
|
178
275
|
finalLabel = override;
|
|
179
276
|
}
|
|
180
|
-
|
|
277
|
+
const classInfo2 = findEnclosingClassInfo(current, filePath);
|
|
278
|
+
const qualifiedName = classInfo2 ? `${classInfo2.className}.${funcName}` : funcName;
|
|
279
|
+
// Include #<arity> and ~typeTag suffix to match definition-phase Method/Constructor IDs.
|
|
280
|
+
const language = getLanguageFromFilename(filePath);
|
|
281
|
+
let arity;
|
|
282
|
+
let encTypeTag = '';
|
|
283
|
+
if ((finalLabel === 'Method' || finalLabel === 'Constructor') &&
|
|
284
|
+
provider.methodExtractor &&
|
|
285
|
+
language) {
|
|
286
|
+
// Get class method map (cached per classNode.id) and look up current method
|
|
287
|
+
// by funcName:line. This avoids per-call-site extractFromNode AST walks.
|
|
288
|
+
let classNode = current.parent;
|
|
289
|
+
while (classNode && !provider.methodExtractor.isTypeDeclaration(classNode)) {
|
|
290
|
+
classNode = classNode.parent;
|
|
291
|
+
}
|
|
292
|
+
let info;
|
|
293
|
+
if (classNode) {
|
|
294
|
+
let extracted = enclosingFnExtractCache.get(classNode.id);
|
|
295
|
+
if (extracted === undefined) {
|
|
296
|
+
extracted =
|
|
297
|
+
provider.methodExtractor.extract(classNode, { filePath, language }) ?? null;
|
|
298
|
+
enclosingFnExtractCache.set(classNode.id, extracted);
|
|
299
|
+
}
|
|
300
|
+
if (extracted?.methods?.length) {
|
|
301
|
+
const defLine = current.startPosition.row + 1;
|
|
302
|
+
info = extracted.methods.find((m) => m.name === funcName && m.line === defLine);
|
|
303
|
+
if (info) {
|
|
304
|
+
arity = info.parameters.some((p) => p.isVariadic)
|
|
305
|
+
? undefined
|
|
306
|
+
: info.parameters.length;
|
|
307
|
+
}
|
|
308
|
+
if (arity !== undefined && info) {
|
|
309
|
+
const methodMap = new Map();
|
|
310
|
+
for (const m of extracted.methods)
|
|
311
|
+
methodMap.set(`${m.name}:${m.line}`, m);
|
|
312
|
+
const groups = buildCollisionGroups(methodMap);
|
|
313
|
+
encTypeTag =
|
|
314
|
+
typeTagForId(methodMap, funcName, arity, info, language, groups) +
|
|
315
|
+
constTagForId(methodMap, funcName, arity, info, groups);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Fallback: extractFromNode for top-level methods without a class
|
|
320
|
+
if (!info && provider.methodExtractor.extractFromNode) {
|
|
321
|
+
const nodeInfo = provider.methodExtractor.extractFromNode(current, {
|
|
322
|
+
filePath,
|
|
323
|
+
language,
|
|
324
|
+
});
|
|
325
|
+
if (nodeInfo) {
|
|
326
|
+
arity = nodeInfo.parameters.some((p) => p.isVariadic)
|
|
327
|
+
? undefined
|
|
328
|
+
: nodeInfo.parameters.length;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const arityTag = arity !== undefined ? `#${arity}${encTypeTag}` : '';
|
|
333
|
+
return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`);
|
|
181
334
|
}
|
|
182
335
|
}
|
|
183
336
|
// Language-specific enclosing function resolution (e.g., Dart where
|
|
@@ -185,10 +338,26 @@ const findEnclosingFunction = (node, filePath, ctx, provider) => {
|
|
|
185
338
|
if (provider.enclosingFunctionFinder) {
|
|
186
339
|
const customResult = provider.enclosingFunctionFinder(current);
|
|
187
340
|
if (customResult) {
|
|
188
|
-
// Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
|
|
189
341
|
const resolved = ctx.resolve(customResult.funcName, filePath);
|
|
190
342
|
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
191
|
-
|
|
343
|
+
if (resolved.candidates.length === 1) {
|
|
344
|
+
return resolved.candidates[0].nodeId;
|
|
345
|
+
}
|
|
346
|
+
const classInfo = findEnclosingClassInfo(current.previousSibling ?? current, filePath);
|
|
347
|
+
if (classInfo) {
|
|
348
|
+
const classMatches = resolved.candidates.filter((c) => c.ownerId === classInfo.classId);
|
|
349
|
+
if (classMatches.length === 1)
|
|
350
|
+
return classMatches[0].nodeId;
|
|
351
|
+
if (classMatches.length > 1) {
|
|
352
|
+
/* fall through to manual ID construction below */
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
return resolved.candidates[0].nodeId;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
return resolved.candidates[0].nodeId;
|
|
360
|
+
}
|
|
192
361
|
}
|
|
193
362
|
let finalLabel = customResult.label;
|
|
194
363
|
if (provider.labelOverride) {
|
|
@@ -196,7 +365,63 @@ const findEnclosingFunction = (node, filePath, ctx, provider) => {
|
|
|
196
365
|
if (override !== null)
|
|
197
366
|
finalLabel = override;
|
|
198
367
|
}
|
|
199
|
-
|
|
368
|
+
const classInfo2 = findEnclosingClassInfo(current.previousSibling ?? current, filePath);
|
|
369
|
+
const qualifiedName = classInfo2
|
|
370
|
+
? `${classInfo2.className}.${customResult.funcName}`
|
|
371
|
+
: customResult.funcName;
|
|
372
|
+
// Include #<arity> and ~typeTag suffix to match definition-phase Method/Constructor IDs.
|
|
373
|
+
const sigNode = current.previousSibling ?? current;
|
|
374
|
+
const language2 = getLanguageFromFilename(filePath);
|
|
375
|
+
let arity2;
|
|
376
|
+
let encTypeTag2 = '';
|
|
377
|
+
if ((finalLabel === 'Method' || finalLabel === 'Constructor') &&
|
|
378
|
+
provider.methodExtractor &&
|
|
379
|
+
language2) {
|
|
380
|
+
let classNode2 = (current.previousSibling ?? current).parent;
|
|
381
|
+
while (classNode2 && !provider.methodExtractor.isTypeDeclaration(classNode2)) {
|
|
382
|
+
classNode2 = classNode2.parent;
|
|
383
|
+
}
|
|
384
|
+
let info2;
|
|
385
|
+
if (classNode2) {
|
|
386
|
+
let extracted2 = enclosingFnExtractCache.get(classNode2.id);
|
|
387
|
+
if (extracted2 === undefined) {
|
|
388
|
+
extracted2 =
|
|
389
|
+
provider.methodExtractor.extract(classNode2, { filePath, language: language2 }) ??
|
|
390
|
+
null;
|
|
391
|
+
enclosingFnExtractCache.set(classNode2.id, extracted2);
|
|
392
|
+
}
|
|
393
|
+
if (extracted2?.methods?.length) {
|
|
394
|
+
const defLine2 = sigNode.startPosition.row + 1;
|
|
395
|
+
info2 = extracted2.methods.find((m) => m.name === customResult.funcName && m.line === defLine2);
|
|
396
|
+
if (info2) {
|
|
397
|
+
arity2 = info2.parameters.some((p) => p.isVariadic)
|
|
398
|
+
? undefined
|
|
399
|
+
: info2.parameters.length;
|
|
400
|
+
}
|
|
401
|
+
if (arity2 !== undefined && info2) {
|
|
402
|
+
const methodMap = new Map();
|
|
403
|
+
for (const m of extracted2.methods)
|
|
404
|
+
methodMap.set(`${m.name}:${m.line}`, m);
|
|
405
|
+
const groups2 = buildCollisionGroups(methodMap);
|
|
406
|
+
encTypeTag2 =
|
|
407
|
+
typeTagForId(methodMap, customResult.funcName, arity2, info2, language2, groups2) + constTagForId(methodMap, customResult.funcName, arity2, info2, groups2);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!info2 && provider.methodExtractor.extractFromNode) {
|
|
412
|
+
const nodeInfo = provider.methodExtractor.extractFromNode(sigNode, {
|
|
413
|
+
filePath,
|
|
414
|
+
language: language2,
|
|
415
|
+
});
|
|
416
|
+
if (nodeInfo) {
|
|
417
|
+
arity2 = nodeInfo.parameters.some((p) => p.isVariadic)
|
|
418
|
+
? undefined
|
|
419
|
+
: nodeInfo.parameters.length;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const arityTag2 = arity2 !== undefined ? `#${arity2}${encTypeTag2}` : '';
|
|
424
|
+
return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`);
|
|
200
425
|
}
|
|
201
426
|
}
|
|
202
427
|
current = current.parent;
|
|
@@ -207,7 +432,7 @@ const findEnclosingFunction = (node, filePath, ctx, provider) => {
|
|
|
207
432
|
* Verify constructor bindings against SymbolTable and infer receiver types.
|
|
208
433
|
* Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths.
|
|
209
434
|
*/
|
|
210
|
-
const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
435
|
+
const verifyConstructorBindings = (bindings, filePath, ctx, graph, bindingAccumulator) => {
|
|
211
436
|
const verified = new Map();
|
|
212
437
|
for (const { scope, varName, calleeName, receiverClassName } of bindings) {
|
|
213
438
|
const tiered = ctx.resolve(calleeName, filePath);
|
|
@@ -242,74 +467,66 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
242
467
|
}
|
|
243
468
|
}
|
|
244
469
|
}
|
|
470
|
+
let typeName;
|
|
245
471
|
if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) {
|
|
246
|
-
|
|
247
|
-
if (typeName) {
|
|
248
|
-
verified.set(receiverKey(scope, varName), typeName);
|
|
249
|
-
}
|
|
472
|
+
typeName = extractReturnTypeName(callableDefs[0].returnType);
|
|
250
473
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
474
|
+
// Phase 9: BindingAccumulator fallback for cross-file return types.
|
|
475
|
+
// Used when the SymbolTable has no return type for a cross-file callee
|
|
476
|
+
// (e.g., a return type that TypeEnv resolved via fixpoint in the source
|
|
477
|
+
// file but was not stored as a SymbolTable returnType annotation).
|
|
478
|
+
// namedImportMap tells us which source file exported the callee so we
|
|
479
|
+
// can look up its file-scope binding via the O(1) fileScopeGet method.
|
|
480
|
+
//
|
|
481
|
+
// Tier gating: only fall back to the accumulator when resolution is
|
|
482
|
+
// unambiguously import-scoped or global. When tiered.tier is 'same-file',
|
|
483
|
+
// the local definition is authoritative even without a return type
|
|
484
|
+
// annotation — using the accumulator here would let an imported callee
|
|
485
|
+
// with the same name shadow the local one, producing false CALLS edges.
|
|
486
|
+
// When multiple callable candidates exist, the accumulator would pick
|
|
487
|
+
// arbitrarily — skip to avoid fabricated edges.
|
|
488
|
+
//
|
|
489
|
+
// Quality note: worker-path accumulator entries are Tier 0/1 only
|
|
490
|
+
// (annotation-declared + same-file constructor inference) — see the
|
|
491
|
+
// BindingAccumulator class JSDoc. For large repos where the worker
|
|
492
|
+
// path dominates, Phase 9 binding accuracy is structurally lower
|
|
493
|
+
// than for sequential-path repos where Tier 2 cross-file propagation
|
|
494
|
+
// is available.
|
|
495
|
+
//
|
|
496
|
+
// Overlapping mechanism note: this is one of three cross-file
|
|
497
|
+
// return-type resolution paths in the codebase:
|
|
498
|
+
// 1. buildImportedReturnTypes (~line 109) — namedImportMap +
|
|
499
|
+
// SymbolTable.lookupExactFull (structure-processor captured)
|
|
500
|
+
// 2. collectExportedBindings (~line 168) / enrichExportedTypeMap
|
|
501
|
+
// — TypeEnv + graph isExported flag
|
|
502
|
+
// 3. This fallback — namedImportMap + BindingAccumulator
|
|
503
|
+
// A future cleanup should merge these into a single resolution pass.
|
|
504
|
+
const shouldFallback = tiered?.tier !== 'same-file' && (!callableDefs || callableDefs.length <= 1);
|
|
505
|
+
if (!typeName && bindingAccumulator && shouldFallback) {
|
|
506
|
+
const namedImports = ctx.namedImportMap.get(filePath);
|
|
507
|
+
const importBinding = namedImports?.get(calleeName);
|
|
508
|
+
if (importBinding) {
|
|
509
|
+
const rawType = bindingAccumulator.fileScopeGet(importBinding.sourcePath, importBinding.exportedName);
|
|
510
|
+
if (rawType) {
|
|
511
|
+
typeName = extractReturnTypeName(rawType);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
278
514
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
let files = map.get(h.parentName);
|
|
282
|
-
if (!files) {
|
|
283
|
-
files = new Set();
|
|
284
|
-
map.set(h.parentName, files);
|
|
515
|
+
if (typeName) {
|
|
516
|
+
verified.set(receiverKey(scope, varName), typeName);
|
|
285
517
|
}
|
|
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
518
|
}
|
|
301
|
-
for (const f of files)
|
|
302
|
-
existing.add(f);
|
|
303
519
|
}
|
|
520
|
+
return verified;
|
|
304
521
|
};
|
|
305
522
|
/**
|
|
306
523
|
* After resolving a call to an interface method, find additional targets
|
|
307
524
|
* in classes implementing that interface. Returns implementation method
|
|
308
525
|
* results with lower confidence ('interface-dispatch').
|
|
309
526
|
*/
|
|
310
|
-
function findInterfaceDispatchTargets(calledName, receiverTypeName, currentFile, ctx,
|
|
311
|
-
const implFiles =
|
|
312
|
-
if (
|
|
527
|
+
function findInterfaceDispatchTargets(calledName, receiverTypeName, currentFile, ctx, heritageMap, primaryNodeId) {
|
|
528
|
+
const implFiles = heritageMap.getImplementorFiles(receiverTypeName);
|
|
529
|
+
if (implFiles.size === 0)
|
|
313
530
|
return [];
|
|
314
531
|
const typeResolved = ctx.resolve(receiverTypeName, currentFile);
|
|
315
532
|
if (!typeResolved)
|
|
@@ -318,7 +535,7 @@ function findInterfaceDispatchTargets(calledName, receiverTypeName, currentFile,
|
|
|
318
535
|
return [];
|
|
319
536
|
const results = [];
|
|
320
537
|
for (const implFile of implFiles) {
|
|
321
|
-
const methods = ctx.symbols.lookupExactAll(implFile, calledName);
|
|
538
|
+
const methods = ctx.model.symbols.lookupExactAll(implFile, calledName);
|
|
322
539
|
for (const method of methods) {
|
|
323
540
|
if (method.nodeId !== primaryNodeId) {
|
|
324
541
|
results.push({
|
|
@@ -338,7 +555,7 @@ importedBindingsMap,
|
|
|
338
555
|
* Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
|
|
339
556
|
importedReturnTypesMap,
|
|
340
557
|
/** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
|
|
341
|
-
importedRawReturnTypesMap,
|
|
558
|
+
importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
342
559
|
const parser = await loadParser();
|
|
343
560
|
const collectedHeritage = [];
|
|
344
561
|
const pendingWrites = [];
|
|
@@ -350,9 +567,9 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
350
567
|
const globalParentSeen = new Map();
|
|
351
568
|
const logSkipped = isVerboseIngestionEnabled();
|
|
352
569
|
const skippedByLang = logSkipped ? new Map() : null;
|
|
570
|
+
const prepared = [];
|
|
353
571
|
for (let i = 0; i < files.length; i++) {
|
|
354
572
|
const file = files[i];
|
|
355
|
-
onProgress?.(i + 1, files.length);
|
|
356
573
|
if (i % 20 === 0)
|
|
357
574
|
await yieldToEventLoop();
|
|
358
575
|
const language = getLanguageFromFilename(file.path);
|
|
@@ -381,18 +598,17 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
381
598
|
}
|
|
382
599
|
astCache.set(file.path, tree);
|
|
383
600
|
}
|
|
384
|
-
let query;
|
|
385
601
|
let matches;
|
|
386
602
|
try {
|
|
387
|
-
const
|
|
388
|
-
query = new Parser.Query(
|
|
603
|
+
const lang = parser.getLanguage();
|
|
604
|
+
const query = new Parser.Query(lang, queryStr);
|
|
389
605
|
matches = query.matches(tree.rootNode);
|
|
390
606
|
}
|
|
391
607
|
catch (queryError) {
|
|
392
608
|
console.warn(`Query error for ${file.path}:`, queryError);
|
|
393
609
|
continue;
|
|
394
610
|
}
|
|
395
|
-
//
|
|
611
|
+
// Extract heritage from query matches to build parentMap for buildTypeEnv.
|
|
396
612
|
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
|
397
613
|
const fileParentMap = new Map();
|
|
398
614
|
for (const match of matches) {
|
|
@@ -416,7 +632,6 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
416
632
|
}
|
|
417
633
|
const parentMap = fileParentMap;
|
|
418
634
|
// Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
|
|
419
|
-
// Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
|
|
420
635
|
for (const [cls, parents] of fileParentMap) {
|
|
421
636
|
let global = globalParentMap.get(cls);
|
|
422
637
|
let seen = globalParentSeen.get(cls);
|
|
@@ -439,21 +654,38 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
439
654
|
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
|
440
655
|
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
|
441
656
|
const typeEnv = buildTypeEnv(tree, language, {
|
|
442
|
-
|
|
657
|
+
model: ctx.model,
|
|
443
658
|
parentMap,
|
|
444
659
|
importedBindings,
|
|
445
660
|
importedReturnTypes,
|
|
446
661
|
importedRawReturnTypes,
|
|
447
662
|
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
|
663
|
+
extractFunctionName: provider?.methodExtractor?.extractFunctionName,
|
|
448
664
|
});
|
|
449
665
|
if (typeEnv && exportedTypeMap) {
|
|
450
|
-
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
|
666
|
+
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.model.symbols, graph);
|
|
451
667
|
if (fileExports)
|
|
452
668
|
exportedTypeMap.set(file.path, fileExports);
|
|
453
669
|
}
|
|
670
|
+
if (bindingAccumulator) {
|
|
671
|
+
typeEnv.flush(file.path, bindingAccumulator);
|
|
672
|
+
}
|
|
673
|
+
prepared.push({ file, language, provider, tree, matches, parentMap, typeEnv });
|
|
674
|
+
}
|
|
675
|
+
// ── Resolution loop: verify constructor bindings and resolve calls ──
|
|
676
|
+
// The accumulator (if present) is now fully populated from the preparation
|
|
677
|
+
// loop above, so verifyConstructorBindings sees all provider bindings
|
|
678
|
+
// regardless of file processing order.
|
|
679
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
680
|
+
const { file, language, provider, tree, matches, parentMap, typeEnv } = prepared[i];
|
|
681
|
+
enclosingFnExtractCache.clear();
|
|
682
|
+
onProgress?.(i + 1, files.length);
|
|
683
|
+
if (i % 20 === 0)
|
|
684
|
+
await yieldToEventLoop();
|
|
454
685
|
const callRouter = provider.callRouter;
|
|
455
686
|
const verifiedReceivers = typeEnv.constructorBindings.length > 0
|
|
456
|
-
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx
|
|
687
|
+
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx, undefined, // graph not available on the sequential path here
|
|
688
|
+
bindingAccumulator)
|
|
457
689
|
: new Map();
|
|
458
690
|
const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
|
|
459
691
|
ctx.enableCache(file.path);
|
|
@@ -481,12 +713,7 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
481
713
|
}
|
|
482
714
|
if (!receiverTypeName && receiverText) {
|
|
483
715
|
const resolved = ctx.resolve(receiverText, file.path);
|
|
484
|
-
if (resolved?.candidates.some((d) => d.type
|
|
485
|
-
d.type === 'Struct' ||
|
|
486
|
-
d.type === 'Interface' ||
|
|
487
|
-
d.type === 'Enum' ||
|
|
488
|
-
d.type === 'Record' ||
|
|
489
|
-
d.type === 'Impl')) {
|
|
716
|
+
if (resolved?.candidates.some((d) => CLASS_LIKE_TYPES.has(d.type))) {
|
|
490
717
|
receiverTypeName = receiverText;
|
|
491
718
|
}
|
|
492
719
|
}
|
|
@@ -505,6 +732,54 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
505
732
|
}
|
|
506
733
|
if (!captureMap['call'])
|
|
507
734
|
return;
|
|
735
|
+
const callNode = captureMap['call'];
|
|
736
|
+
const languageSeed = extractParsedCallSite(language, callNode);
|
|
737
|
+
if (languageSeed) {
|
|
738
|
+
if (provider.isBuiltInName(languageSeed.calledName))
|
|
739
|
+
return;
|
|
740
|
+
const sourceId = findEnclosingFunction(callNode, file.path, ctx, provider) ||
|
|
741
|
+
generateId('File', file.path);
|
|
742
|
+
const receiverName = languageSeed.callForm === 'member' ? languageSeed.receiverName : undefined;
|
|
743
|
+
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
744
|
+
if (receiverName !== undefined &&
|
|
745
|
+
receiverTypeName === undefined &&
|
|
746
|
+
languageSeed.callForm === 'member' &&
|
|
747
|
+
(language === 'java' || language === 'csharp' || language === 'kotlin')) {
|
|
748
|
+
const c0 = receiverName.charCodeAt(0);
|
|
749
|
+
if (c0 >= 65 && c0 <= 90)
|
|
750
|
+
receiverTypeName = receiverName;
|
|
751
|
+
}
|
|
752
|
+
const resolved = resolveCallTarget({
|
|
753
|
+
calledName: languageSeed.calledName,
|
|
754
|
+
callForm: languageSeed.callForm,
|
|
755
|
+
...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
|
|
756
|
+
...(receiverName !== undefined ? { receiverName } : {}),
|
|
757
|
+
}, file.path, ctx, undefined, widenCache, undefined, heritageMap);
|
|
758
|
+
if (!resolved)
|
|
759
|
+
return;
|
|
760
|
+
graph.addRelationship({
|
|
761
|
+
id: generateId('CALLS', `${sourceId}:${languageSeed.calledName}->${resolved.nodeId}`),
|
|
762
|
+
sourceId,
|
|
763
|
+
targetId: resolved.nodeId,
|
|
764
|
+
type: 'CALLS',
|
|
765
|
+
confidence: resolved.confidence,
|
|
766
|
+
reason: resolved.reason,
|
|
767
|
+
});
|
|
768
|
+
if (heritageMap && languageSeed.callForm === 'member' && receiverTypeName) {
|
|
769
|
+
const implTargets = findInterfaceDispatchTargets(languageSeed.calledName, receiverTypeName, file.path, ctx, heritageMap, resolved.nodeId);
|
|
770
|
+
for (const impl of implTargets) {
|
|
771
|
+
graph.addRelationship({
|
|
772
|
+
id: generateId('CALLS', `${sourceId}:${languageSeed.calledName}->${impl.nodeId}`),
|
|
773
|
+
sourceId,
|
|
774
|
+
targetId: impl.nodeId,
|
|
775
|
+
type: 'CALLS',
|
|
776
|
+
confidence: impl.confidence,
|
|
777
|
+
reason: impl.reason,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
508
783
|
const nameNode = captureMap['call.name'];
|
|
509
784
|
if (!nameNode)
|
|
510
785
|
return;
|
|
@@ -543,7 +818,7 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
543
818
|
description: item.accessorType,
|
|
544
819
|
},
|
|
545
820
|
});
|
|
546
|
-
ctx.symbols.add(file.path, item.propName, nodeId, 'Property', {
|
|
821
|
+
ctx.model.symbols.add(file.path, item.propName, nodeId, 'Property', {
|
|
547
822
|
...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
|
|
548
823
|
...(item.declaredType ? { declaredType: item.declaredType } : {}),
|
|
549
824
|
});
|
|
@@ -575,7 +850,6 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
575
850
|
}
|
|
576
851
|
if (provider.isBuiltInName(calledName))
|
|
577
852
|
return;
|
|
578
|
-
const callNode = captureMap['call'];
|
|
579
853
|
const callForm = inferCallForm(callNode, nameNode);
|
|
580
854
|
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
581
855
|
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
@@ -591,7 +865,7 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
591
865
|
let p = callNode.parent;
|
|
592
866
|
while (p) {
|
|
593
867
|
if (FUNCTION_NODE_TYPES.has(p.type)) {
|
|
594
|
-
const
|
|
868
|
+
const funcName = provider.methodExtractor?.extractFunctionName?.(p)?.funcName ?? genericFuncName(p);
|
|
595
869
|
if (funcName) {
|
|
596
870
|
scope = `${funcName}@${p.startIndex}`;
|
|
597
871
|
break;
|
|
@@ -611,12 +885,8 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
611
885
|
// not compile if Sub didn't extend Base.
|
|
612
886
|
if (isSubclassOf(ctorType, receiverTypeName, parentMap) ||
|
|
613
887
|
isSubclassOf(ctorType, receiverTypeName, globalParentMap) ||
|
|
614
|
-
(ctx.
|
|
615
|
-
.
|
|
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'))) {
|
|
888
|
+
(ctx.model.types.lookupClassByName(ctorType).length > 0 &&
|
|
889
|
+
ctx.model.types.lookupClassByName(receiverTypeName).length > 0)) {
|
|
620
890
|
receiverTypeName = ctorType;
|
|
621
891
|
}
|
|
622
892
|
}
|
|
@@ -668,7 +938,7 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
668
938
|
}
|
|
669
939
|
}
|
|
670
940
|
if (currentType) {
|
|
671
|
-
receiverTypeName = walkMixedChain(extracted.chain, currentType, file.path, ctx, makeAccessEmitter(graph, sourceId));
|
|
941
|
+
receiverTypeName = walkMixedChain(extracted.chain, currentType, file.path, ctx, makeAccessEmitter(graph, sourceId), heritageMap);
|
|
672
942
|
}
|
|
673
943
|
}
|
|
674
944
|
}
|
|
@@ -685,7 +955,7 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
685
955
|
callForm,
|
|
686
956
|
receiverTypeName,
|
|
687
957
|
receiverName,
|
|
688
|
-
}, file.path, ctx, hints, widenCache);
|
|
958
|
+
}, file.path, ctx, hints, widenCache, undefined, heritageMap);
|
|
689
959
|
if (!resolved)
|
|
690
960
|
return;
|
|
691
961
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
@@ -697,8 +967,8 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
697
967
|
confidence: resolved.confidence,
|
|
698
968
|
reason: resolved.reason,
|
|
699
969
|
});
|
|
700
|
-
if (
|
|
701
|
-
const implTargets = findInterfaceDispatchTargets(calledName, receiverTypeName, file.path, ctx,
|
|
970
|
+
if (heritageMap && callForm === 'member' && receiverTypeName) {
|
|
971
|
+
const implTargets = findInterfaceDispatchTargets(calledName, receiverTypeName, file.path, ctx, heritageMap, resolved.nodeId);
|
|
702
972
|
for (const impl of implTargets) {
|
|
703
973
|
graph.addRelationship({
|
|
704
974
|
id: generateId('CALLS', `${sourceId}:${calledName}->${impl.nodeId}`),
|
|
@@ -711,6 +981,39 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
711
981
|
}
|
|
712
982
|
}
|
|
713
983
|
});
|
|
984
|
+
// Vue: emit CALLS edges for PascalCase components used in <template>.
|
|
985
|
+
// Template components are default-imported (not named), so we match the
|
|
986
|
+
// component name against imported .vue file basenames via the import map.
|
|
987
|
+
if (language === SupportedLanguages.Vue) {
|
|
988
|
+
const templateComponents = extractTemplateComponents(file.content);
|
|
989
|
+
if (templateComponents.length > 0) {
|
|
990
|
+
const fileId = generateId('File', file.path);
|
|
991
|
+
const importedFiles = ctx.importMap.get(file.path);
|
|
992
|
+
if (importedFiles) {
|
|
993
|
+
for (const componentName of templateComponents) {
|
|
994
|
+
for (const importedPath of importedFiles) {
|
|
995
|
+
if (!importedPath.endsWith('.vue'))
|
|
996
|
+
continue;
|
|
997
|
+
const basename = importedPath.slice(importedPath.lastIndexOf('/') + 1, importedPath.lastIndexOf('.'));
|
|
998
|
+
if (basename !== componentName)
|
|
999
|
+
continue;
|
|
1000
|
+
const targetFileId = generateId('File', importedPath);
|
|
1001
|
+
if (graph.getNode(targetFileId)) {
|
|
1002
|
+
graph.addRelationship({
|
|
1003
|
+
id: generateId('CALLS', `${fileId}:${componentName}->${targetFileId}`),
|
|
1004
|
+
sourceId: fileId,
|
|
1005
|
+
targetId: targetFileId,
|
|
1006
|
+
type: 'CALLS',
|
|
1007
|
+
confidence: 0.9,
|
|
1008
|
+
reason: 'vue-template-component',
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
714
1017
|
ctx.clearCache();
|
|
715
1018
|
}
|
|
716
1019
|
// ── Resolve deferred write-access edges ──
|
|
@@ -735,7 +1038,7 @@ importedRawReturnTypesMap, implementorMap) => {
|
|
|
735
1038
|
}
|
|
736
1039
|
return collectedHeritage;
|
|
737
1040
|
};
|
|
738
|
-
|
|
1041
|
+
// FREE_CALLABLE_TYPES imported from symbol-table.ts — single source of truth.
|
|
739
1042
|
const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
|
|
740
1043
|
const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
741
1044
|
let kindFiltered;
|
|
@@ -747,11 +1050,15 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
|
747
1050
|
else {
|
|
748
1051
|
const types = candidates.filter((c) => CONSTRUCTOR_TARGET_TYPES.has(c.type));
|
|
749
1052
|
kindFiltered =
|
|
750
|
-
types.length > 0 ? types : candidates.filter((c) =>
|
|
1053
|
+
types.length > 0 ? types : candidates.filter((c) => CALL_TARGET_TYPES.has(c.type));
|
|
751
1054
|
}
|
|
752
1055
|
}
|
|
753
1056
|
else {
|
|
754
|
-
|
|
1057
|
+
// CALL_TARGET_TYPES (not FREE_CALLABLE_TYPES) — the post-A4 filter must
|
|
1058
|
+
// also admit Method and Constructor candidates, which are now unioned
|
|
1059
|
+
// into the pool from `model.methods.lookupMethodByName` rather than
|
|
1060
|
+
// `symbols.lookupCallableByName`.
|
|
1061
|
+
kindFiltered = candidates.filter((c) => CALL_TARGET_TYPES.has(c.type));
|
|
755
1062
|
}
|
|
756
1063
|
if (kindFiltered.length === 0)
|
|
757
1064
|
return [];
|
|
@@ -764,6 +1071,33 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
|
764
1071
|
(argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount) &&
|
|
765
1072
|
argCount <= candidate.parameterCount));
|
|
766
1073
|
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Count callable candidates matching the kind + arity filter without
|
|
1076
|
+
* allocating an intermediate array. Short-circuits once count exceeds
|
|
1077
|
+
* `threshold` (default 1) — used by the dispatcher's `skipMember` check
|
|
1078
|
+
* where we only need to know "more than one survivor".
|
|
1079
|
+
*/
|
|
1080
|
+
const countCallableCandidates = (candidates, argCount, callForm, threshold = 1) => {
|
|
1081
|
+
let count = 0;
|
|
1082
|
+
for (const c of candidates) {
|
|
1083
|
+
// Kind filter (mirrors filterCallableCandidates)
|
|
1084
|
+
const typeOk = callForm === 'constructor'
|
|
1085
|
+
? CONSTRUCTOR_TARGET_TYPES.has(c.type)
|
|
1086
|
+
: CALL_TARGET_TYPES.has(c.type);
|
|
1087
|
+
if (!typeOk)
|
|
1088
|
+
continue;
|
|
1089
|
+
// Arity filter
|
|
1090
|
+
if (argCount !== undefined &&
|
|
1091
|
+
c.parameterCount !== undefined &&
|
|
1092
|
+
(argCount < (c.requiredParameterCount ?? c.parameterCount) || argCount > c.parameterCount)) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
count++;
|
|
1096
|
+
if (count > threshold)
|
|
1097
|
+
return count; // early exit
|
|
1098
|
+
}
|
|
1099
|
+
return count;
|
|
1100
|
+
};
|
|
767
1101
|
const toResolveResult = (definition, tier) => ({
|
|
768
1102
|
nodeId: definition.nodeId,
|
|
769
1103
|
confidence: TIER_CONFIDENCE[tier],
|
|
@@ -827,131 +1161,265 @@ const tryOverloadDisambiguation = (candidates, hints) => {
|
|
|
827
1161
|
return null;
|
|
828
1162
|
return matchCandidatesByArgTypes(candidates, argTypes);
|
|
829
1163
|
};
|
|
830
|
-
|
|
831
|
-
|
|
1164
|
+
/**
|
|
1165
|
+
* Apply overload-hint or arg-type disambiguation to a pre-filtered candidate
|
|
1166
|
+
* pool. Returns the unique survivor, or null when neither signal is present,
|
|
1167
|
+
* neither can disambiguate, or the pool remains ambiguous.
|
|
1168
|
+
*
|
|
1169
|
+
* Precedence rule: `overloadHints` wins over `preComputedArgTypes` when both
|
|
1170
|
+
* are supplied. The AST-based disambiguator has access to live type inference
|
|
1171
|
+
* hooks, whereas `preComputedArgTypes` is a worker-path pre-computation that
|
|
1172
|
+
* may be coarser-grained.
|
|
1173
|
+
*
|
|
1174
|
+
* Single source of truth for the narrowing-signal precedence used by member
|
|
1175
|
+
* and constructor resolution paths. Add a new narrowing signal here once, not
|
|
1176
|
+
* at each call site.
|
|
1177
|
+
*/
|
|
1178
|
+
const disambiguateByOverloadOrArgTypes = (pool, overloadHints, preComputedArgTypes) => {
|
|
1179
|
+
if (!overloadHints && !preComputedArgTypes)
|
|
1180
|
+
return null;
|
|
1181
|
+
if (overloadHints)
|
|
1182
|
+
return tryOverloadDisambiguation(pool, overloadHints);
|
|
1183
|
+
if (preComputedArgTypes)
|
|
1184
|
+
return matchCandidatesByArgTypes(pool, preComputedArgTypes);
|
|
1185
|
+
return null;
|
|
1186
|
+
};
|
|
1187
|
+
/**
|
|
1188
|
+
* Collapse Swift-extension duplicate Class/Struct candidates to the primary
|
|
1189
|
+
* definition, preferring the shortest file path.
|
|
1190
|
+
*
|
|
1191
|
+
* Swift extensions (`extension User { ... }` in a separate file) create
|
|
1192
|
+
* multiple `Class` nodes sharing the same symbol name — one for the primary
|
|
1193
|
+
* declaration and one per extension file. When overload disambiguation and
|
|
1194
|
+
* receiver narrowing both fail to converge on a single candidate, this
|
|
1195
|
+
* heuristic picks the primary definition based on the assumption that it
|
|
1196
|
+
* lives at the shortest file path (e.g. `User.swift` over `UserExtensions.swift`).
|
|
1197
|
+
*
|
|
1198
|
+
* Intentionally narrower than {@link INSTANTIABLE_CLASS_TYPES}: only `Class`
|
|
1199
|
+
* and `Struct` are considered, not `Record`. Swift extensions only produce
|
|
1200
|
+
* `Class` duplicates in practice, and C#/Kotlin records do not exhibit the
|
|
1201
|
+
* same multi-file-definition pattern, so widening this set risks accidental
|
|
1202
|
+
* dedup of legitimately distinct record types.
|
|
1203
|
+
*
|
|
1204
|
+
* Returns a `ResolveResult` when the heuristic fires, `null` when the
|
|
1205
|
+
* candidate pool does not match the shape (mixed types, non-Class/Struct
|
|
1206
|
+
* kinds, or `length <= 1`). Callers should fall through to their own null
|
|
1207
|
+
* return when this helper returns `null`.
|
|
1208
|
+
*
|
|
1209
|
+
* Used by `resolveFreeCall`. Having a single source of truth prevents
|
|
1210
|
+
* duplication if the heuristic is ever tuned.
|
|
1211
|
+
*/
|
|
1212
|
+
const dedupSwiftExtensionCandidates = (candidates, tier) => {
|
|
1213
|
+
if (candidates.length <= 1)
|
|
1214
|
+
return null;
|
|
1215
|
+
const allSameType = candidates.every((c) => c.type === candidates[0].type);
|
|
1216
|
+
if (!allSameType)
|
|
1217
|
+
return null;
|
|
1218
|
+
if (candidates[0].type !== 'Class' && candidates[0].type !== 'Struct')
|
|
1219
|
+
return null;
|
|
1220
|
+
const sorted = [...candidates].sort((a, b) => a.filePath.length - b.filePath.length);
|
|
1221
|
+
return toResolveResult(sorted[0], tier);
|
|
1222
|
+
};
|
|
1223
|
+
/**
|
|
1224
|
+
* Thin dispatcher that routes a call to the appropriate specialized resolver.
|
|
1225
|
+
*
|
|
1226
|
+
* - `free` → {@link resolveFreeCall}
|
|
1227
|
+
* - `constructor` → {@link resolveStaticCall} (with pre-resolved tiered pool)
|
|
1228
|
+
* - `member` with a known receiver type → {@link resolveMemberCall}, with
|
|
1229
|
+
* file-based fallback for traits/interfaces
|
|
1230
|
+
* - `member` without receiver type → module-alias check, then tiered lookup
|
|
1231
|
+
*
|
|
1232
|
+
* Replaces the former 200+ line function (SM-19: fuzzy-free call resolution).
|
|
1233
|
+
*/
|
|
1234
|
+
/**
|
|
1235
|
+
* Module-alias resolution for member calls without a receiver type.
|
|
1236
|
+
*
|
|
1237
|
+
* Handles Python/Ruby `import mod; mod.Symbol()` patterns where the receiver
|
|
1238
|
+
* is a module name, not a typed variable. Uses `moduleAliasMap` to scope
|
|
1239
|
+
* candidates to the correct module file.
|
|
1240
|
+
*/
|
|
1241
|
+
const resolveModuleAliasedCall = (call, currentFile, ctx, widenCache, tieredOverride) => {
|
|
1242
|
+
if (!call.receiverName)
|
|
1243
|
+
return null;
|
|
1244
|
+
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
|
1245
|
+
if (!aliasMap)
|
|
1246
|
+
return null;
|
|
1247
|
+
const moduleFile = aliasMap.get(call.receiverName);
|
|
1248
|
+
if (!moduleFile)
|
|
1249
|
+
return null;
|
|
1250
|
+
// Reuse the caller's pre-computed tiered result when available —
|
|
1251
|
+
// the dispatcher already called ctx.resolve(call.calledName, currentFile).
|
|
1252
|
+
const tiered = tieredOverride ?? ctx.resolve(call.calledName, currentFile);
|
|
832
1253
|
if (!tiered)
|
|
833
1254
|
return null;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (filteredCandidates.length === 0 && call.callForm === 'free') {
|
|
839
|
-
const hasTypeTarget = tiered.candidates.some((c) => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
|
|
840
|
-
if (hasTypeTarget) {
|
|
841
|
-
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
// Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
|
|
845
|
-
// The attribute access gives callForm='member', but the callee may be a Class — a valid
|
|
846
|
-
// constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
|
|
847
|
-
// emits a CALLS edge to the class node.
|
|
848
|
-
if (filteredCandidates.length === 0 && call.callForm === 'member') {
|
|
849
|
-
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
850
|
-
}
|
|
851
|
-
// Module-alias disambiguation: Python `import auth; auth.User()` — receiverName='auth'
|
|
852
|
-
// selects auth.py via moduleAliasMap. Runs for ALL member calls with a known module alias,
|
|
853
|
-
// not just ambiguous ones — same-file tier may shadow the correct cross-module target when
|
|
854
|
-
// the caller defines a function with the same name as the callee (Issue #417).
|
|
855
|
-
if (call.callForm === 'member' && call.receiverName) {
|
|
856
|
-
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
|
857
|
-
if (aliasMap) {
|
|
858
|
-
const moduleFile = aliasMap.get(call.receiverName);
|
|
859
|
-
if (moduleFile) {
|
|
860
|
-
const aliasFiltered = filteredCandidates.filter((c) => c.filePath === moduleFile);
|
|
861
|
-
if (aliasFiltered.length > 0) {
|
|
862
|
-
filteredCandidates = aliasFiltered;
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
// Same-file tier returned a local match, but the alias points elsewhere.
|
|
866
|
-
// Widen to global candidates and filter to the aliased module's file.
|
|
867
|
-
// Use per-file widenCache to avoid repeated lookupFuzzy for the same
|
|
868
|
-
// calledName+moduleFile from multiple call sites in the same file.
|
|
869
|
-
const cacheKey = `${call.calledName}\0${moduleFile}`;
|
|
870
|
-
let fuzzyDefs = widenCache?.get(cacheKey);
|
|
871
|
-
if (!fuzzyDefs) {
|
|
872
|
-
fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
|
|
873
|
-
widenCache?.set(cacheKey, fuzzyDefs);
|
|
874
|
-
}
|
|
875
|
-
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm).filter((c) => c.filePath === moduleFile);
|
|
876
|
-
if (widened.length > 0)
|
|
877
|
-
filteredCandidates = widened;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
1255
|
+
// Try member-form, then constructor-form (for `module.ClassName()` patterns)
|
|
1256
|
+
let filtered = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm).filter((c) => c.filePath === moduleFile);
|
|
1257
|
+
if (filtered.length === 0) {
|
|
1258
|
+
filtered = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor').filter((c) => c.filePath === moduleFile);
|
|
881
1259
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
// global methods with this name, then apply arity/kind filtering.
|
|
899
|
-
const methodPool = filteredCandidates.length <= 1
|
|
900
|
-
? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
|
|
901
|
-
: filteredCandidates;
|
|
902
|
-
// D3. File-based: prefer candidates whose filePath matches the resolved type's file
|
|
903
|
-
const fileFiltered = methodPool.filter((c) => typeFiles.has(c.filePath));
|
|
904
|
-
if (fileFiltered.length === 1) {
|
|
905
|
-
return toResolveResult(fileFiltered[0], tiered.tier);
|
|
906
|
-
}
|
|
907
|
-
// D4. ownerId fallback: narrow by ownerId matching the type's nodeId
|
|
908
|
-
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
|
|
909
|
-
const ownerFiltered = pool.filter((c) => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
910
|
-
if (ownerFiltered.length === 1) {
|
|
911
|
-
return toResolveResult(ownerFiltered[0], tiered.tier);
|
|
1260
|
+
if (filtered.length === 0) {
|
|
1261
|
+
// Widen to global callable+method indexes scoped to the aliased module
|
|
1262
|
+
// file. Function+ownerId (Python/Rust/Kotlin) is still routed to both
|
|
1263
|
+
// indexes until Unit 5 unblocks, so dedup by nodeId.
|
|
1264
|
+
const cacheKey = `${call.calledName}\0${moduleFile}`;
|
|
1265
|
+
let defs = widenCache?.get(cacheKey);
|
|
1266
|
+
if (!defs) {
|
|
1267
|
+
const rawCallable = ctx.model.symbols.lookupCallableByName(call.calledName);
|
|
1268
|
+
const rawMethods = ctx.model.methods.lookupMethodByName(call.calledName);
|
|
1269
|
+
const widenCombined = [];
|
|
1270
|
+
const widenSeen = new Set();
|
|
1271
|
+
for (const d of rawCallable) {
|
|
1272
|
+
if (widenSeen.has(d.nodeId))
|
|
1273
|
+
continue;
|
|
1274
|
+
widenSeen.add(d.nodeId);
|
|
1275
|
+
widenCombined.push(d);
|
|
912
1276
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
: preComputedArgTypes
|
|
919
|
-
? matchCandidatesByArgTypes(overloadPool, preComputedArgTypes)
|
|
920
|
-
: null;
|
|
921
|
-
if (disambiguated)
|
|
922
|
-
return toResolveResult(disambiguated, tiered.tier);
|
|
923
|
-
return null;
|
|
1277
|
+
for (const d of rawMethods) {
|
|
1278
|
+
if (widenSeen.has(d.nodeId))
|
|
1279
|
+
continue;
|
|
1280
|
+
widenSeen.add(d.nodeId);
|
|
1281
|
+
widenCombined.push(d);
|
|
924
1282
|
}
|
|
1283
|
+
defs = widenCombined;
|
|
1284
|
+
widenCache?.set(cacheKey, defs);
|
|
1285
|
+
}
|
|
1286
|
+
filtered = filterCallableCandidates(defs, call.argCount, call.callForm).filter((c) => c.filePath === moduleFile);
|
|
1287
|
+
if (filtered.length === 0) {
|
|
1288
|
+
filtered = filterCallableCandidates(defs, call.argCount, 'constructor').filter((c) => c.filePath === moduleFile);
|
|
925
1289
|
}
|
|
926
1290
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1291
|
+
return filtered.length === 1 ? toResolveResult(filtered[0], tiered.tier) : null;
|
|
1292
|
+
};
|
|
1293
|
+
/**
|
|
1294
|
+
* File-based fallback for member calls where owner-scoped resolution fails.
|
|
1295
|
+
*
|
|
1296
|
+
* Resolves the receiver type via `ctx.resolve()` and narrows all callable
|
|
1297
|
+
* symbols with the method name to the receiver type's defining file(s),
|
|
1298
|
+
* then applies ownerId filtering and overload disambiguation.
|
|
1299
|
+
*
|
|
1300
|
+
* Handles Rust trait dispatch (`repo.find()` where `find` is on a trait impl),
|
|
1301
|
+
* cross-file overloaded methods, and similar patterns where ownerId
|
|
1302
|
+
* relationships may not be established on all candidates.
|
|
1303
|
+
*/
|
|
1304
|
+
const resolveMemberCallByFile = (calledName, receiverTypeName, currentFile, ctx, argCount, callForm, overloadHints, preComputedArgTypes) => {
|
|
1305
|
+
const typeResolved = ctx.resolve(receiverTypeName, currentFile);
|
|
1306
|
+
if (!typeResolved || typeResolved.candidates.length === 0)
|
|
1307
|
+
return null;
|
|
1308
|
+
const typeNodeIds = new Set(typeResolved.candidates.map((d) => d.nodeId));
|
|
1309
|
+
const typeFiles = new Set(typeResolved.candidates.map((d) => d.filePath));
|
|
1310
|
+
// A4 (plan 006, Unit 4): consult both indexes. Strictly-labeled
|
|
1311
|
+
// Method/Constructor are disjoint, but Function+ownerId (Python/Rust/
|
|
1312
|
+
// Kotlin) is routed into BOTH indexes by `wrappedAdd` until Unit 5
|
|
1313
|
+
// unblocks — dedup by nodeId so overload disambiguation doesn't see
|
|
1314
|
+
// phantom duplicates.
|
|
1315
|
+
const rawCallablePool = ctx.model.symbols.lookupCallableByName(calledName);
|
|
1316
|
+
const rawMethodPool = ctx.model.methods.lookupMethodByName(calledName);
|
|
1317
|
+
const combinedPool = [];
|
|
1318
|
+
const combinedSeen = new Set();
|
|
1319
|
+
for (const def of rawCallablePool) {
|
|
1320
|
+
if (combinedSeen.has(def.nodeId))
|
|
1321
|
+
continue;
|
|
1322
|
+
combinedSeen.add(def.nodeId);
|
|
1323
|
+
combinedPool.push(def);
|
|
1324
|
+
}
|
|
1325
|
+
for (const def of rawMethodPool) {
|
|
1326
|
+
if (combinedSeen.has(def.nodeId))
|
|
1327
|
+
continue;
|
|
1328
|
+
combinedSeen.add(def.nodeId);
|
|
1329
|
+
combinedPool.push(def);
|
|
1330
|
+
}
|
|
1331
|
+
const methodPool = filterCallableCandidates(combinedPool, argCount, callForm);
|
|
1332
|
+
const fileFiltered = methodPool.filter((c) => typeFiles.has(c.filePath));
|
|
1333
|
+
if (fileFiltered.length === 1) {
|
|
1334
|
+
return toResolveResult(fileFiltered[0], typeResolved.tier);
|
|
1335
|
+
}
|
|
1336
|
+
// ownerId fallback: narrow by ownerId matching the type's nodeId
|
|
1337
|
+
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
|
|
1338
|
+
const ownerFiltered = pool.filter((c) => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
1339
|
+
if (ownerFiltered.length === 1)
|
|
1340
|
+
return toResolveResult(ownerFiltered[0], typeResolved.tier);
|
|
1341
|
+
// Overload disambiguation on the narrowed pool
|
|
1342
|
+
if (fileFiltered.length > 1 || ownerFiltered.length > 1) {
|
|
1343
|
+
const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
|
|
1344
|
+
const disambiguated = disambiguateByOverloadOrArgTypes(overloadPool, overloadHints, preComputedArgTypes);
|
|
936
1345
|
if (disambiguated)
|
|
937
|
-
return toResolveResult(disambiguated,
|
|
1346
|
+
return toResolveResult(disambiguated, typeResolved.tier);
|
|
938
1347
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1348
|
+
// Zero-match null-route: receiver type resolved but no candidate matched
|
|
1349
|
+
// after file-based and owner-based narrowing. Refuse to emit a CALLS edge
|
|
1350
|
+
// rather than guess — matches the SM-10 R3 null-route contract.
|
|
1351
|
+
return null;
|
|
1352
|
+
};
|
|
1353
|
+
/** Return the sole survivor from a tiered pool after callable + arity filtering, or null. */
|
|
1354
|
+
const singleCandidate = (tiered, argCount, callForm) => {
|
|
1355
|
+
const filtered = filterCallableCandidates(tiered.candidates, argCount, callForm);
|
|
1356
|
+
return filtered.length === 1 ? toResolveResult(filtered[0], tiered.tier) : null;
|
|
1357
|
+
};
|
|
1358
|
+
/** @internal Exported for unit tests. Do not use outside tests. */
|
|
1359
|
+
export const _resolveCallTargetForTesting = (call, currentFile, ctx, opts) => resolveCallTarget(call, currentFile, ctx, opts?.overloadHints, opts?.widenCache, opts?.preComputedArgTypes, opts?.heritageMap);
|
|
1360
|
+
const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes, heritageMap) => {
|
|
1361
|
+
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
1362
|
+
if (!tiered)
|
|
952
1363
|
return null;
|
|
1364
|
+
if (call.callForm === 'free') {
|
|
1365
|
+
return resolveFreeCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes);
|
|
953
1366
|
}
|
|
954
|
-
|
|
1367
|
+
if (call.callForm === 'constructor') {
|
|
1368
|
+
return (resolveStaticCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes) ?? singleCandidate(tiered, call.argCount, 'constructor'));
|
|
1369
|
+
}
|
|
1370
|
+
if (call.receiverTypeName) {
|
|
1371
|
+
// Skip the owner-scoped MRO path when the tiered pool has genuine
|
|
1372
|
+
// overload ambiguity that needs D1-D4+E handling, not D0.
|
|
1373
|
+
const skipMember = (!!overloadHints || !!preComputedArgTypes) &&
|
|
1374
|
+
countCallableCandidates(tiered.candidates, call.argCount, call.callForm) > 1;
|
|
1375
|
+
// Try owner-scoped (resolveMemberCall) then file-scoped (resolveMemberCallByFile).
|
|
1376
|
+
const memberResult = (!skipMember
|
|
1377
|
+
? resolveMemberCall(call.receiverTypeName, call.calledName, currentFile, ctx, heritageMap, call.argCount)
|
|
1378
|
+
: null) ??
|
|
1379
|
+
resolveMemberCallByFile(call.calledName, call.receiverTypeName, currentFile, ctx, call.argCount, call.callForm, overloadHints, preComputedArgTypes);
|
|
1380
|
+
if (memberResult)
|
|
1381
|
+
return memberResult;
|
|
1382
|
+
// Module-alias narrowing runs as a FALLBACK, after owner/file-scoped
|
|
1383
|
+
// resolvers have returned null. This ordering is load-bearing: placing
|
|
1384
|
+
// alias narrowing first would short-circuit unique owner-scoped answers
|
|
1385
|
+
// when a local variable coincidentally matches an alias name, leaking
|
|
1386
|
+
// unrelated homonyms from the aliased file onto the wrong receiver type.
|
|
1387
|
+
//
|
|
1388
|
+
// The type-file verification guard is load-bearing for SM-10 R3: an
|
|
1389
|
+
// alias is only a VALID narrowing signal when the alias target file is
|
|
1390
|
+
// among the receiver type's defining files. If the alias points at a
|
|
1391
|
+
// file that does not hold `receiverTypeName`, any candidate we would
|
|
1392
|
+
// pick from there would belong to an unrelated class — a cross-type
|
|
1393
|
+
// false positive. ctx.resolve is cached per (name, file), so resolving
|
|
1394
|
+
// the receiver type a second time here is free.
|
|
1395
|
+
const typeResolves = ctx.resolve(call.receiverTypeName, currentFile);
|
|
1396
|
+
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
|
1397
|
+
const aliasTargetFile = call.receiverName && aliasMap ? aliasMap.get(call.receiverName) : undefined;
|
|
1398
|
+
if (aliasTargetFile &&
|
|
1399
|
+
typeResolves &&
|
|
1400
|
+
typeResolves.candidates.some((c) => c.filePath === aliasTargetFile)) {
|
|
1401
|
+
const aliasResult = resolveModuleAliasedCall(call, currentFile, ctx, widenCache, tiered);
|
|
1402
|
+
if (aliasResult)
|
|
1403
|
+
return aliasResult;
|
|
1404
|
+
}
|
|
1405
|
+
// SM-10 R3 null-route: when the receiver type resolves to indexed types
|
|
1406
|
+
// but no scoped resolver (nor the guarded alias fallback) produced a
|
|
1407
|
+
// match, that's a genuine miss — refuse to emit a CALLS edge rather
|
|
1408
|
+
// than guess via an unscoped singleCandidate that ignores the class
|
|
1409
|
+
// hierarchy. When the type is NOT in the index (PHP `mixed`, dynamic
|
|
1410
|
+
// types, unresolvable aliases), the scoped resolvers had nothing to
|
|
1411
|
+
// work with and singleCandidate is the correct last resort.
|
|
1412
|
+
if (typeResolves && typeResolves.candidates.length > 0) {
|
|
1413
|
+
return null; // null-route: type resolved, no candidate matched
|
|
1414
|
+
}
|
|
1415
|
+
return singleCandidate(tiered, call.argCount, call.callForm);
|
|
1416
|
+
}
|
|
1417
|
+
// Member call with no inferred receiver type — e.g. Python `mod.fn()`
|
|
1418
|
+
// where `mod` is a module alias. Module-alias narrowing is the primary
|
|
1419
|
+
// disambiguation signal here. Also consulted from the typed-member
|
|
1420
|
+
// branch above as a guarded fallback after owner/file-scoped resolvers.
|
|
1421
|
+
return (resolveModuleAliasedCall(call, currentFile, ctx, widenCache, tiered) ??
|
|
1422
|
+
singleCandidate(tiered, call.argCount, call.callForm));
|
|
955
1423
|
};
|
|
956
1424
|
// ── Scope key helpers ────────────────────────────────────────────────────
|
|
957
1425
|
// Scope keys use the format "funcName@startIndex" (produced by type-env.ts).
|
|
@@ -963,12 +1431,18 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, pr
|
|
|
963
1431
|
// collisions between overloaded methods with the same name in different
|
|
964
1432
|
// classes (e.g. User.save@100 and Repo.save@200 are distinct keys).
|
|
965
1433
|
// Lookup uses a secondary funcName-only index built in lookupReceiverType.
|
|
966
|
-
/** Extract the function name from a
|
|
967
|
-
|
|
968
|
-
|
|
1434
|
+
/** Extract the bare function name from a sourceId.
|
|
1435
|
+
* Handles both unqualified ("Function:filepath:funcName" → "funcName")
|
|
1436
|
+
* and qualified ("Function:filepath:ClassName.funcName" → "funcName").
|
|
1437
|
+
* Strips any trailing #<arity> suffix from Method/Constructor IDs. */
|
|
969
1438
|
const extractFuncNameFromSourceId = (sourceId) => {
|
|
970
1439
|
const lastColon = sourceId.lastIndexOf(':');
|
|
971
|
-
|
|
1440
|
+
const segment = lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
|
|
1441
|
+
const dotIdx = segment.lastIndexOf('.');
|
|
1442
|
+
const raw = dotIdx >= 0 ? segment.slice(dotIdx + 1) : segment;
|
|
1443
|
+
// Strip #<arity> suffix (e.g. "save#2" → "save")
|
|
1444
|
+
const hashIdx = raw.indexOf('#');
|
|
1445
|
+
return hashIdx >= 0 ? raw.slice(0, hashIdx) : raw;
|
|
972
1446
|
};
|
|
973
1447
|
/**
|
|
974
1448
|
* Build a composite key for receiver type storage.
|
|
@@ -981,6 +1455,12 @@ const receiverKey = (scope, varName) => `${scope}\0${varName}`;
|
|
|
981
1455
|
* The verified map is keyed by `scope\0varName` where scope is either
|
|
982
1456
|
* "funcName@startIndex" (inside a function) or "" (file level).
|
|
983
1457
|
* Index structure: Map<funcName, Map<varName, ReceiverTypeEntry>>
|
|
1458
|
+
*
|
|
1459
|
+
* Known limitation: the index collapses scope keys to bare funcName,
|
|
1460
|
+
* so two same-arity overloads with the same local variable name but
|
|
1461
|
+
* different types will mark that variable as ambiguous. A future
|
|
1462
|
+
* enhancement should key by full scope (funcName@startIndex) and carry
|
|
1463
|
+
* scope keys through findEnclosingFunction's return type.
|
|
984
1464
|
*/
|
|
985
1465
|
const buildReceiverTypeIndex = (map) => {
|
|
986
1466
|
const index = new Map();
|
|
@@ -1058,15 +1538,373 @@ const resolveFieldOwnership = (receiverName, fieldName, filePath, ctx) => {
|
|
|
1058
1538
|
const typeResolved = ctx.resolve(receiverName, filePath);
|
|
1059
1539
|
if (!typeResolved)
|
|
1060
1540
|
return undefined;
|
|
1061
|
-
const classDef = typeResolved.candidates.find((d) => d.type
|
|
1062
|
-
d.type === 'Struct' ||
|
|
1063
|
-
d.type === 'Interface' ||
|
|
1064
|
-
d.type === 'Enum' ||
|
|
1065
|
-
d.type === 'Record' ||
|
|
1066
|
-
d.type === 'Impl');
|
|
1541
|
+
const classDef = typeResolved.candidates.find((d) => CLASS_LIKE_TYPES.has(d.type));
|
|
1067
1542
|
if (!classDef)
|
|
1068
1543
|
return undefined;
|
|
1069
|
-
return ctx.
|
|
1544
|
+
return ctx.model.fields.lookupFieldByOwner(classDef.nodeId, fieldName) ?? undefined;
|
|
1545
|
+
};
|
|
1546
|
+
/**
|
|
1547
|
+
* Resolve a method by owner type name using the eagerly-populated methodByOwner index.
|
|
1548
|
+
* Returns `{ def, tier }` when an unambiguous method is found, `undefined` otherwise.
|
|
1549
|
+
*
|
|
1550
|
+
* **Multi-candidate iteration (homonym disambiguation):** when `ctx.resolve(ownerType)`
|
|
1551
|
+
* returns multiple class-like candidates (e.g. two classes named `User` in different
|
|
1552
|
+
* files reachable from the call site), each is probed with `lookupMethodByOwnerWithMRO`.
|
|
1553
|
+
* Results are deduplicated by `nodeId` so that:
|
|
1554
|
+
*
|
|
1555
|
+
* - homonym classes that both walk up to the SAME ancestor's method collapse to 1 hit
|
|
1556
|
+
* - aliased re-exports that produce two candidates pointing at the same def collapse too
|
|
1557
|
+
*
|
|
1558
|
+
* After deduplication:
|
|
1559
|
+
*
|
|
1560
|
+
* - 0 unique matches → `undefined` (owner-scoped path has no answer)
|
|
1561
|
+
* - 1 unique match → return it
|
|
1562
|
+
* - ≥2 unique matches → `undefined` (genuine homonym ambiguity; don't silently pick one)
|
|
1563
|
+
*
|
|
1564
|
+
* The returned `tier` reflects how the owner TYPE was resolved (not the method name).
|
|
1565
|
+
* Threaded out here so callers don't need a second `ctx.resolve(ownerType, ...)` call —
|
|
1566
|
+
* this decouples callers from `ctx.resolve`'s per-file caching contract.
|
|
1567
|
+
*/
|
|
1568
|
+
const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, heritageMap, argCount) => {
|
|
1569
|
+
const typeResolved = ctx.resolve(receiverTypeName, filePath);
|
|
1570
|
+
if (!typeResolved)
|
|
1571
|
+
return undefined;
|
|
1572
|
+
// MRO walking needs a language hint so we can derive the per-language
|
|
1573
|
+
// strategy; compute it once and reuse for every candidate. Unknown
|
|
1574
|
+
// extension → fall back to plain direct lookup (D1-D4 still runs on miss).
|
|
1575
|
+
const language = heritageMap ? getLanguageFromFilename(filePath) : null;
|
|
1576
|
+
const mroStrategy = language != null ? getProvider(language).mroStrategy : null;
|
|
1577
|
+
const canWalkMRO = heritageMap != null && mroStrategy != null;
|
|
1578
|
+
// Iterate all class-like candidates tracking the first unambiguous hit.
|
|
1579
|
+
// Zero-allocation fast path: the common case is exactly one class candidate,
|
|
1580
|
+
// so we avoid building a Map. A second hit with a different `nodeId` flips
|
|
1581
|
+
// `ambiguous` and short-circuits the loop. Diamond MRO convergence on the
|
|
1582
|
+
// same inherited method collapses to one hit because `nodeId` matches.
|
|
1583
|
+
//
|
|
1584
|
+
// firstDef === undefined → owner-scoped resolution found nothing
|
|
1585
|
+
// firstDef && !ambiguous → unambiguous answer
|
|
1586
|
+
// ambiguous → genuine homonym ambiguity — refuse to pick
|
|
1587
|
+
//
|
|
1588
|
+
// argCount is threaded through so arity-differing overloads
|
|
1589
|
+
// (e.g. C++ `greet()` vs `greet(string)`) are disambiguated inside the
|
|
1590
|
+
// owner-scoped lookup rather than collapsing to an arbitrary first pick.
|
|
1591
|
+
let firstDef;
|
|
1592
|
+
let ambiguous = false;
|
|
1593
|
+
for (const candidate of typeResolved.candidates) {
|
|
1594
|
+
if (!CLASS_LIKE_TYPES.has(candidate.type))
|
|
1595
|
+
continue;
|
|
1596
|
+
const def = canWalkMRO
|
|
1597
|
+
? lookupMethodByOwnerWithMRO(candidate.nodeId, methodName, heritageMap, ctx.model, mroStrategy, argCount)
|
|
1598
|
+
: ctx.model.methods.lookupMethodByOwner(candidate.nodeId, methodName, argCount);
|
|
1599
|
+
if (!def)
|
|
1600
|
+
continue;
|
|
1601
|
+
if (!firstDef) {
|
|
1602
|
+
firstDef = def;
|
|
1603
|
+
}
|
|
1604
|
+
else if (def.nodeId !== firstDef.nodeId) {
|
|
1605
|
+
ambiguous = true;
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (!firstDef || ambiguous)
|
|
1610
|
+
return undefined;
|
|
1611
|
+
return { def: firstDef, tier: typeResolved.tier };
|
|
1612
|
+
};
|
|
1613
|
+
// ---------------------------------------------------------------------------
|
|
1614
|
+
// SM-11: Owner-scoped + MRO member-call resolution (no fuzzy lookup)
|
|
1615
|
+
// ---------------------------------------------------------------------------
|
|
1616
|
+
/**
|
|
1617
|
+
* Resolve a member call using owner-scoped + MRO resolution only (no fuzzy lookup).
|
|
1618
|
+
* Used for `obj.method()` calls where the receiver type is known.
|
|
1619
|
+
*
|
|
1620
|
+
* Delegates to {@link resolveMethodByOwner} which performs an O(1) owner-scoped
|
|
1621
|
+
* method lookup and, when a {@link HeritageMap} is provided, walks the MRO chain
|
|
1622
|
+
* via {@link lookupMethodByOwnerWithMRO}.
|
|
1623
|
+
*
|
|
1624
|
+
* {@link resolveCallTarget} delegates here for member calls.
|
|
1625
|
+
*
|
|
1626
|
+
* **SEMANTIC CHANGE (2026-04-09):** The confidence tier reflects how the
|
|
1627
|
+
* owner TYPE was resolved, not how the method NAME was resolved globally.
|
|
1628
|
+
* more accurate for owner-scoped resolution (the discriminant IS the class,
|
|
1629
|
+
* not the method name). Downstream consumers that filter CALLS edges by
|
|
1630
|
+
* confidence threshold may see shifted values on otherwise-unchanged code.
|
|
1631
|
+
* See the "returns result with correct confidence tier" tests below for the
|
|
1632
|
+
* locked-in behavior.
|
|
1633
|
+
*
|
|
1634
|
+
* **Performance:** Callers that only need the return type (e.g. `walkMixedChain`)
|
|
1635
|
+
* should call {@link resolveMethodByOwner} directly and use the `.def.returnType`
|
|
1636
|
+
* field instead, to avoid building a throwaway `ResolveResult`.
|
|
1637
|
+
*
|
|
1638
|
+
* @param ownerType - The receiver's type name (e.g. 'User')
|
|
1639
|
+
* @param methodName - The method being called (e.g. 'save')
|
|
1640
|
+
* @param currentFile - File path of the call site
|
|
1641
|
+
* @param ctx - Resolution context
|
|
1642
|
+
* @param heritageMap - Optional heritage map for MRO-aware ancestor walking
|
|
1643
|
+
*/
|
|
1644
|
+
export const resolveMemberCall = (ownerType, methodName, currentFile, ctx, heritageMap, argCount) => {
|
|
1645
|
+
const resolved = resolveMethodByOwner(ownerType, methodName, currentFile, ctx, heritageMap, argCount);
|
|
1646
|
+
if (!resolved)
|
|
1647
|
+
return null;
|
|
1648
|
+
return toResolveResult(resolved.def, resolved.tier);
|
|
1649
|
+
};
|
|
1650
|
+
// ---------------------------------------------------------------------------
|
|
1651
|
+
// SM-13: Free-function call resolution
|
|
1652
|
+
// ---------------------------------------------------------------------------
|
|
1653
|
+
/**
|
|
1654
|
+
* Resolve a free-function call using `lookupExact` (same-file) + import-scoped
|
|
1655
|
+
* resolution via `ctx.resolve()`.
|
|
1656
|
+
*
|
|
1657
|
+
* Used for `foo()`, `doStuff()` — unqualified calls with no receiver.
|
|
1658
|
+
* Also handles Swift/Kotlin implicit constructors (`User()` without `new`)
|
|
1659
|
+
* by delegating to {@link resolveStaticCall} when the tiered pool contains
|
|
1660
|
+
* class-like targets.
|
|
1661
|
+
*
|
|
1662
|
+
* {@link resolveCallTarget} delegates here for `callForm === 'free'`.
|
|
1663
|
+
*
|
|
1664
|
+
* `resolveFreeCall` does not take a `widenCache` parameter. Free calls
|
|
1665
|
+
* have no receiver type and rely exclusively on the tiered pool
|
|
1666
|
+
* from `ctx.resolve()`.
|
|
1667
|
+
*
|
|
1668
|
+
* @param calledName - The called function name (e.g. 'doStuff')
|
|
1669
|
+
* @param filePath - File path of the call site
|
|
1670
|
+
* @param ctx - Resolution context
|
|
1671
|
+
* @param argCount - Optional argument count for arity filtering
|
|
1672
|
+
* @param tieredOverride - Pre-computed tiered candidates from an upstream
|
|
1673
|
+
* `ctx.resolve` call. When provided, skips the redundant
|
|
1674
|
+
* lookup inside this function.
|
|
1675
|
+
* @param overloadHints - Optional AST-based overload disambiguation hints
|
|
1676
|
+
* @param preComputedArgTypes - Optional pre-computed argument types (worker path)
|
|
1677
|
+
*/
|
|
1678
|
+
export const resolveFreeCall = (calledName, filePath, ctx, argCount, tieredOverride, overloadHints, preComputedArgTypes) => {
|
|
1679
|
+
const tiered = tieredOverride ?? ctx.resolve(calledName, filePath);
|
|
1680
|
+
if (!tiered)
|
|
1681
|
+
return null;
|
|
1682
|
+
let filteredCandidates = filterCallableCandidates(tiered.candidates, argCount, 'free');
|
|
1683
|
+
// Class-target fast path: Swift/Kotlin `User()` — free-form call targeting a
|
|
1684
|
+
// class. Delegates to resolveStaticCall for O(1) class + constructor lookup.
|
|
1685
|
+
// The `.some()` trigger must stay aligned with `INSTANTIABLE_CLASS_TYPES` —
|
|
1686
|
+
// any type admitted here that is not in that set will cause resolveStaticCall
|
|
1687
|
+
// to return null, wasting two lookup passes per call. `Enum` is deliberately
|
|
1688
|
+
// excluded; `Record` is included so C# records and Kotlin data classes reach
|
|
1689
|
+
// the fast path.
|
|
1690
|
+
// Align with INSTANTIABLE_CLASS_TYPES by reusing the set directly rather
|
|
1691
|
+
// than enumerating literal strings. This converts an invariant that was
|
|
1692
|
+
// previously enforced by a comment ("keep this list aligned with
|
|
1693
|
+
// INSTANTIABLE_CLASS_TYPES") into one enforced structurally — any future
|
|
1694
|
+
// extension of the set (e.g. Kotlin `object`) propagates here automatically.
|
|
1695
|
+
// The `dedupSwiftExtensionCandidates` helper used in the tail of this
|
|
1696
|
+
// function deliberately uses a narrower literal `'Class' | 'Struct'` check
|
|
1697
|
+
// — Swift extensions only produce Class duplicates in practice, so Record
|
|
1698
|
+
// is excluded there by design. Do not collapse that helper into
|
|
1699
|
+
// INSTANTIABLE_CLASS_TYPES.
|
|
1700
|
+
const hasClassTarget = filteredCandidates.length === 0 &&
|
|
1701
|
+
tiered.candidates.some((c) => INSTANTIABLE_CLASS_TYPES.has(c.type));
|
|
1702
|
+
if (hasClassTarget) {
|
|
1703
|
+
const staticResult = resolveStaticCall(calledName, filePath, ctx, argCount, tiered);
|
|
1704
|
+
if (staticResult)
|
|
1705
|
+
return staticResult;
|
|
1706
|
+
// Retry with constructor form: Swift/Kotlin constructor calls look like
|
|
1707
|
+
// free function calls (no `new` keyword). If resolveStaticCall didn't
|
|
1708
|
+
// match, re-filter with constructor form so CONSTRUCTOR_TARGET_TYPES
|
|
1709
|
+
// applies.
|
|
1710
|
+
//
|
|
1711
|
+
// The retry fires for every null return from `resolveStaticCall`, which
|
|
1712
|
+
// can happen for three distinct reasons — all three are handled below:
|
|
1713
|
+
//
|
|
1714
|
+
// (a) No explicit `Constructor` node found and zero instantiable
|
|
1715
|
+
// class candidates (e.g. Interface/Trait/Impl only — the SM-12
|
|
1716
|
+
// null-route contract). `filterCallableCandidates` with
|
|
1717
|
+
// `'constructor'` form will also return nothing → we fall
|
|
1718
|
+
// through to the final null return. Correct.
|
|
1719
|
+
//
|
|
1720
|
+
// (b) Homonym ambiguity — two or more instantiable class candidates
|
|
1721
|
+
// share the name (e.g. `User` in two files, same tier). The
|
|
1722
|
+
// retry repopulates `filteredCandidates` with both Classes and
|
|
1723
|
+
// they flow into `dedupSwiftExtensionCandidates` below, which
|
|
1724
|
+
// either picks the shortest-path primary or null-routes.
|
|
1725
|
+
// Covered by the R7 Swift-extension dedup test.
|
|
1726
|
+
//
|
|
1727
|
+
// (c) `resolveStaticCall` step 4 bailed because the tiered pool
|
|
1728
|
+
// contains ownerless `Constructor` nodes (some extractors emit
|
|
1729
|
+
// constructors without `ownerId`). Those `Constructor` nodes
|
|
1730
|
+
// survive the constructor-form filter below and reach overload
|
|
1731
|
+
// disambiguation, giving the existing filter path a chance to
|
|
1732
|
+
// pick the right one. Correct but currently uncovered by a
|
|
1733
|
+
// dedicated test — the R5 `preComputedArgTypes` path exercises
|
|
1734
|
+
// overload disambiguation for Functions, which is structurally
|
|
1735
|
+
// the same code.
|
|
1736
|
+
filteredCandidates = filterCallableCandidates(tiered.candidates, argCount, 'constructor');
|
|
1737
|
+
}
|
|
1738
|
+
// E. Overload disambiguation
|
|
1739
|
+
if (filteredCandidates.length > 1) {
|
|
1740
|
+
const disambiguated = overloadHints
|
|
1741
|
+
? tryOverloadDisambiguation(filteredCandidates, overloadHints)
|
|
1742
|
+
: preComputedArgTypes
|
|
1743
|
+
? matchCandidatesByArgTypes(filteredCandidates, preComputedArgTypes)
|
|
1744
|
+
: null;
|
|
1745
|
+
if (disambiguated)
|
|
1746
|
+
return toResolveResult(disambiguated, tiered.tier);
|
|
1747
|
+
}
|
|
1748
|
+
if (filteredCandidates.length !== 1) {
|
|
1749
|
+
// See `dedupSwiftExtensionCandidates` — shared helper, single source of
|
|
1750
|
+
// truth for the Swift-extension same-name collision heuristic.
|
|
1751
|
+
const deduped = dedupSwiftExtensionCandidates(filteredCandidates, tiered.tier);
|
|
1752
|
+
if (deduped)
|
|
1753
|
+
return deduped;
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
return toResolveResult(filteredCandidates[0], tiered.tier);
|
|
1757
|
+
};
|
|
1758
|
+
// ---------------------------------------------------------------------------
|
|
1759
|
+
// SM-12: Constructor/static call resolution (no fuzzy lookup)
|
|
1760
|
+
// ---------------------------------------------------------------------------
|
|
1761
|
+
/**
|
|
1762
|
+
* Resolve a constructor or static call using class-scoped lookup (no fuzzy lookup).
|
|
1763
|
+
* Used for `new User()` / `User()` calls where the calledName targets a class.
|
|
1764
|
+
*
|
|
1765
|
+
* Uses {@link TypeRegistry.lookupClassByName} for O(1) class lookup and
|
|
1766
|
+
* {@link MethodRegistry.lookupMethodByOwner} for constructor resolution.
|
|
1767
|
+
* {@link resolveCallTarget} delegates here for constructor and free-form calls
|
|
1768
|
+
* that target a class.
|
|
1769
|
+
*
|
|
1770
|
+
* Resolution strategy:
|
|
1771
|
+
* 1. `lookupClassByName(className)` — O(1) pre-check; bail early if no class exists.
|
|
1772
|
+
* 2. `ctx.resolve(className, currentFile)` — import-scoped tier for confidence.
|
|
1773
|
+
* 3. Filter to class-like candidates via `CLASS_LIKE_TYPES` and walk each
|
|
1774
|
+
* with `lookupMethodByOwner(classNodeId, className, argCount)` — O(1)
|
|
1775
|
+
* constructor lookup. Only accept results with `type === 'Constructor'`.
|
|
1776
|
+
* 4. If step 3 found nothing and the tiered pool contains ownerless
|
|
1777
|
+
* `Constructor` nodes (common in some extractors), bail out so
|
|
1778
|
+
* `filterCallableCandidates` downstream handles Constructor-vs-Class
|
|
1779
|
+
* preference correctly.
|
|
1780
|
+
* 5. Class-node fallback: filter `classCandidates` through
|
|
1781
|
+
* `INSTANTIABLE_CLASS_TYPES` and return the sole survivor when there is
|
|
1782
|
+
* exactly one. Null-route on zero survivors (Interface / Trait / Impl
|
|
1783
|
+
* stripped) or multiple (homonym ambiguity).
|
|
1784
|
+
*
|
|
1785
|
+
* @param className - The class name (e.g. 'User'). Also used as the method
|
|
1786
|
+
* name for the `lookupMethodByOwner` scan, because the
|
|
1787
|
+
* only constructor-shaped call we handle today is
|
|
1788
|
+
* `ClassName(...)` / `new ClassName(...)`. Named
|
|
1789
|
+
* constructors like Dart `User.fromJson()` arrive as
|
|
1790
|
+
* member calls and route through `resolveMemberCall`,
|
|
1791
|
+
* so this function does not yet need a separate
|
|
1792
|
+
* `methodName` parameter. Revisit if a language surfaces
|
|
1793
|
+
* a static-method-shaped call with a distinct member
|
|
1794
|
+
* name.
|
|
1795
|
+
* @param currentFile - File path of the call site
|
|
1796
|
+
* @param ctx - Resolution context
|
|
1797
|
+
* @param argCount - Optional argument count for arity filtering
|
|
1798
|
+
* @param tieredOverride - Pre-computed tiered candidates for `className` from
|
|
1799
|
+
* an upstream `ctx.resolve` call. When provided, skips
|
|
1800
|
+
* the redundant lookup inside this function. Leave
|
|
1801
|
+
* unset for direct callers without a prior resolution.
|
|
1802
|
+
*/
|
|
1803
|
+
export const resolveStaticCall = (className, currentFile, ctx, argCount, tieredOverride, overloadHints, preComputedArgTypes) => {
|
|
1804
|
+
// 1. Pre-check: does a class with this name exist at all? (O(1))
|
|
1805
|
+
// This guards against the expensive `ctx.resolve` walk when the name
|
|
1806
|
+
// is clearly not class-like (e.g. plain functions). When `tieredOverride`
|
|
1807
|
+
// is supplied, the caller has already paid for the tiered lookup, so this
|
|
1808
|
+
// pre-check still prevents the class-candidate filter + lookupMethodByOwner
|
|
1809
|
+
// loop from running on obviously non-class targets.
|
|
1810
|
+
const allClasses = ctx.model.types.lookupClassByName(className);
|
|
1811
|
+
if (allClasses.length === 0)
|
|
1812
|
+
return null;
|
|
1813
|
+
// 2. Scope via ctx.resolve for import-tier information. Reuse the caller's
|
|
1814
|
+
// tiered result when provided — it is computed from the same name and
|
|
1815
|
+
// file context, so re-running the walk would be a pure waste.
|
|
1816
|
+
const typeResolved = tieredOverride ?? ctx.resolve(className, currentFile);
|
|
1817
|
+
if (!typeResolved)
|
|
1818
|
+
return null;
|
|
1819
|
+
const classCandidates = typeResolved.candidates.filter((c) => CLASS_LIKE_TYPES.has(c.type));
|
|
1820
|
+
if (classCandidates.length === 0)
|
|
1821
|
+
return null;
|
|
1822
|
+
// 3. Try lookupMethodByOwner for explicit Constructor nodes.
|
|
1823
|
+
// Only accept results with type === 'Constructor' — a Method or Function
|
|
1824
|
+
// that happens to share the class name (e.g. C++ methods named after
|
|
1825
|
+
// their class) is not a constructor for resolution purposes.
|
|
1826
|
+
// Same dedup logic as resolveMethodByOwner: diamond inheritance converging
|
|
1827
|
+
// on the same constructor collapses to one hit.
|
|
1828
|
+
//
|
|
1829
|
+
// Same-name assumption: the lookup key is `${candidate.nodeId}\0${className}`,
|
|
1830
|
+
// so this finds Constructor nodes whose symbol name equals the class name
|
|
1831
|
+
// (`class User` with a `Constructor` named `User`). Constructors indexed
|
|
1832
|
+
// under a different name (e.g. Python `__init__`) will not be found here —
|
|
1833
|
+
// but they also won't appear in the tiered pool for `ctx.resolve(className)`
|
|
1834
|
+
// for the same reason, so step 4's Constructor-presence check will not
|
|
1835
|
+
// see them either. The two miss cases are symmetric. If a future extractor
|
|
1836
|
+
// indexes Constructor nodes under an alternative name while still setting
|
|
1837
|
+
// `ownerId`, this assumption will need revisiting.
|
|
1838
|
+
let firstDef;
|
|
1839
|
+
let ambiguous = false;
|
|
1840
|
+
for (const candidate of classCandidates) {
|
|
1841
|
+
const def = ctx.model.methods.lookupMethodByOwner(candidate.nodeId, className, argCount);
|
|
1842
|
+
if (!def || def.type !== 'Constructor')
|
|
1843
|
+
continue;
|
|
1844
|
+
if (!firstDef) {
|
|
1845
|
+
firstDef = def;
|
|
1846
|
+
}
|
|
1847
|
+
else if (def.nodeId !== firstDef.nodeId) {
|
|
1848
|
+
ambiguous = true;
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (firstDef && !ambiguous) {
|
|
1853
|
+
return toResolveResult(firstDef, typeResolved.tier);
|
|
1854
|
+
}
|
|
1855
|
+
// 4. lookupMethodByOwner found nothing — check whether the tiered pool
|
|
1856
|
+
// contains Constructor nodes that lack ownerId (common in some extractors).
|
|
1857
|
+
// If so, bail out so the existing filterCallableCandidates path handles
|
|
1858
|
+
// Constructor-vs-Class preference correctly.
|
|
1859
|
+
//
|
|
1860
|
+
// This branch also catches the step-3 ambiguous case (`ambiguous = true`
|
|
1861
|
+
// with two distinct Constructor nodes across multiple class candidates):
|
|
1862
|
+
// the same Constructor nodes are indexed under the class name in the
|
|
1863
|
+
// tiered pool, so `.some(Constructor)` is true here and we defer to
|
|
1864
|
+
// step 4.5 (overload/arg-type disambiguation) or the caller's fallback.
|
|
1865
|
+
// Do not remove this check without also handling the ambiguous step-3
|
|
1866
|
+
// path explicitly.
|
|
1867
|
+
if (typeResolved.candidates.some((c) => c.type === 'Constructor')) {
|
|
1868
|
+
// 4.5. Overload / arg-type disambiguation for ambiguous or ownerless
|
|
1869
|
+
// Constructor pools. When the caller supplied a narrowing signal
|
|
1870
|
+
// (AST-based overload hints from the sequential path, or pre-
|
|
1871
|
+
// computed arg types from the worker path), give disambiguation a
|
|
1872
|
+
// chance before null-routing. Symmetric with resolveMemberCallByFile's
|
|
1873
|
+
// disambiguation pass — both resolvers now share the same signal
|
|
1874
|
+
// precedence via disambiguateByOverloadOrArgTypes. Only fires when
|
|
1875
|
+
// at least one narrowing signal is present; preserves SM-10 R3 for
|
|
1876
|
+
// genuinely ambiguous cases with no disambiguating input.
|
|
1877
|
+
if (overloadHints || preComputedArgTypes) {
|
|
1878
|
+
const ctorPool = filterCallableCandidates(typeResolved.candidates, argCount, 'constructor');
|
|
1879
|
+
if (ctorPool.length > 1) {
|
|
1880
|
+
const disambiguated = disambiguateByOverloadOrArgTypes(ctorPool, overloadHints, preComputedArgTypes);
|
|
1881
|
+
if (disambiguated)
|
|
1882
|
+
return toResolveResult(disambiguated, typeResolved.tier);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
// 5. No constructor nodes at all — fall back to the class node itself, but
|
|
1888
|
+
// ONLY when it is actually instantiable. Interface / Trait / Impl / Enum
|
|
1889
|
+
// are deliberately excluded via `INSTANTIABLE_CLASS_TYPES` to prevent
|
|
1890
|
+
// false `CALLS` edges from constructor-shaped calls to non-instantiable
|
|
1891
|
+
// nodes. This also disambiguates the Rust same-file shadowing case
|
|
1892
|
+
// (`struct User` + `impl User` both present at same-file tier): the
|
|
1893
|
+
// Impl is stripped, leaving the Struct as the sole instantiable target.
|
|
1894
|
+
// Addresses Codex review finding on PR #754.
|
|
1895
|
+
const instantiableCandidates = classCandidates.filter((c) => INSTANTIABLE_CLASS_TYPES.has(c.type));
|
|
1896
|
+
// Three outcomes below, in order of likelihood after the fix:
|
|
1897
|
+
// length === 0 → all candidates were stripped as non-instantiable (e.g.
|
|
1898
|
+
// Interface / Trait / Impl). Null-route via the fall-through `return
|
|
1899
|
+
// null` — this is the dominant Codex-fix case.
|
|
1900
|
+
// length === 1 → a single instantiable candidate remains, return it.
|
|
1901
|
+
// length > 1 → two or more instantiable classes share the name (e.g.
|
|
1902
|
+
// homonym classes across files with no import narrowing). Fall through
|
|
1903
|
+
// to `return null` so the caller null-routes rather than guess.
|
|
1904
|
+
if (instantiableCandidates.length === 1) {
|
|
1905
|
+
return toResolveResult(instantiableCandidates[0], typeResolved.tier);
|
|
1906
|
+
}
|
|
1907
|
+
return null;
|
|
1070
1908
|
};
|
|
1071
1909
|
/**
|
|
1072
1910
|
* Create a deduplicated ACCESSES edge emitter for a single source node.
|
|
@@ -1089,7 +1927,7 @@ const makeAccessEmitter = (graph, sourceId) => {
|
|
|
1089
1927
|
});
|
|
1090
1928
|
};
|
|
1091
1929
|
};
|
|
1092
|
-
const walkMixedChain = (chain, startType, filePath, ctx, onFieldResolved) => {
|
|
1930
|
+
const walkMixedChain = (chain, startType, filePath, ctx, onFieldResolved, heritageMap) => {
|
|
1093
1931
|
let currentType = startType;
|
|
1094
1932
|
for (const step of chain) {
|
|
1095
1933
|
if (!currentType)
|
|
@@ -1113,7 +1951,24 @@ const walkMixedChain = (chain, startType, filePath, ctx, onFieldResolved) => {
|
|
|
1113
1951
|
currentType = fieldResolved.typeName;
|
|
1114
1952
|
continue;
|
|
1115
1953
|
}
|
|
1116
|
-
|
|
1954
|
+
// Fast path: O(1) owner-scoped method lookup via methodByOwner index.
|
|
1955
|
+
// Note: CALLS edges for intermediate chain steps are NOT emitted here — walkMixedChain
|
|
1956
|
+
// only threads types. CALLS edges come from the outer per-call-expression loop in processCalls.
|
|
1957
|
+
//
|
|
1958
|
+
// We call `resolveMethodByOwner` directly (NOT `resolveMemberCall`) because this is
|
|
1959
|
+
// a hot path — called per chain step per call expression — and we only need the
|
|
1960
|
+
// return type string. Going through `resolveMemberCall` would allocate a throwaway
|
|
1961
|
+
// `ResolveResult` with confidence/reason that we immediately discard.
|
|
1962
|
+
const owned = resolveMethodByOwner(currentType, step.name, filePath, ctx, heritageMap);
|
|
1963
|
+
if (owned?.def.returnType) {
|
|
1964
|
+
const fastRetType = extractReturnTypeName(owned.def.returnType);
|
|
1965
|
+
if (fastRetType) {
|
|
1966
|
+
currentType = fastRetType;
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
// Fallback: resolve via resolveCallTarget dispatcher (delegates to resolveMemberCall)
|
|
1971
|
+
const resolved = resolveCallTarget({ calledName: step.name, callForm: 'member', receiverTypeName: currentType }, filePath, ctx, undefined, undefined, undefined, heritageMap);
|
|
1117
1972
|
if (!resolved) {
|
|
1118
1973
|
// Stdlib passthrough: unwrap(), clone(), etc. preserve the receiver type
|
|
1119
1974
|
if (TYPE_PRESERVING_METHODS.has(step.name))
|
|
@@ -1138,15 +1993,21 @@ const walkMixedChain = (chain, startType, filePath, ctx, onFieldResolved) => {
|
|
|
1138
1993
|
/**
|
|
1139
1994
|
* Fast path: resolve pre-extracted call sites from workers.
|
|
1140
1995
|
* No AST parsing — workers already extracted calledName + sourceId.
|
|
1996
|
+
*
|
|
1997
|
+
* @param bindingAccumulator Phase 9: optional accumulator carrying file-scope
|
|
1998
|
+
* TypeEnv bindings from all worker-processed files. When the SymbolTable has
|
|
1999
|
+
* no return type for a cross-file callee, `verifyConstructorBindings` falls
|
|
2000
|
+
* back to the accumulator via `namedImportMap` to bind the variable to the
|
|
2001
|
+
* callee's resolved type (e.g. `var x = getUser()` → `x: User`).
|
|
1141
2002
|
*/
|
|
1142
|
-
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings,
|
|
2003
|
+
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings, heritageMap, bindingAccumulator) => {
|
|
1143
2004
|
// Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
|
|
1144
2005
|
// The scope dimension prevents collisions when two functions in the same file
|
|
1145
2006
|
// have same-named locals pointing to different constructor types.
|
|
1146
2007
|
const fileReceiverTypes = new Map();
|
|
1147
2008
|
if (constructorBindings) {
|
|
1148
2009
|
for (const { filePath, bindings } of constructorBindings) {
|
|
1149
|
-
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
|
|
2010
|
+
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph, bindingAccumulator);
|
|
1150
2011
|
if (verified.size > 0) {
|
|
1151
2012
|
fileReceiverTypes.set(filePath, buildReceiverTypeIndex(verified));
|
|
1152
2013
|
}
|
|
@@ -1215,15 +2076,41 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1215
2076
|
}
|
|
1216
2077
|
}
|
|
1217
2078
|
if (currentType) {
|
|
1218
|
-
const walkedType = walkMixedChain(effectiveCall.receiverMixedChain, currentType, effectiveCall.filePath, ctx, makeAccessEmitter(graph, effectiveCall.sourceId));
|
|
2079
|
+
const walkedType = walkMixedChain(effectiveCall.receiverMixedChain, currentType, effectiveCall.filePath, ctx, makeAccessEmitter(graph, effectiveCall.sourceId), heritageMap);
|
|
1219
2080
|
if (walkedType) {
|
|
1220
2081
|
effectiveCall = { ...effectiveCall, receiverTypeName: walkedType };
|
|
1221
2082
|
}
|
|
1222
2083
|
}
|
|
1223
2084
|
}
|
|
1224
|
-
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache, effectiveCall.argTypes);
|
|
1225
|
-
if (!resolved)
|
|
2085
|
+
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache, effectiveCall.argTypes, heritageMap);
|
|
2086
|
+
if (!resolved) {
|
|
2087
|
+
// Vue template component fallback: match calledName against imported .vue basenames
|
|
2088
|
+
if (effectiveCall.filePath.endsWith('.vue') && effectiveCall.sourceId.startsWith('File:')) {
|
|
2089
|
+
const importedFiles = ctx.importMap.get(effectiveCall.filePath);
|
|
2090
|
+
if (importedFiles) {
|
|
2091
|
+
for (const importedPath of importedFiles) {
|
|
2092
|
+
if (!importedPath.endsWith('.vue'))
|
|
2093
|
+
continue;
|
|
2094
|
+
const basename = importedPath.slice(importedPath.lastIndexOf('/') + 1, importedPath.lastIndexOf('.'));
|
|
2095
|
+
if (basename !== effectiveCall.calledName)
|
|
2096
|
+
continue;
|
|
2097
|
+
const targetFileId = generateId('File', importedPath);
|
|
2098
|
+
if (graph.getNode(targetFileId)) {
|
|
2099
|
+
graph.addRelationship({
|
|
2100
|
+
id: generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${targetFileId}`),
|
|
2101
|
+
sourceId: effectiveCall.sourceId,
|
|
2102
|
+
targetId: targetFileId,
|
|
2103
|
+
type: 'CALLS',
|
|
2104
|
+
confidence: 0.9,
|
|
2105
|
+
reason: 'vue-template-component',
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
1226
2112
|
continue;
|
|
2113
|
+
}
|
|
1227
2114
|
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
1228
2115
|
graph.addRelationship({
|
|
1229
2116
|
id: relId,
|
|
@@ -1233,8 +2120,8 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1233
2120
|
confidence: resolved.confidence,
|
|
1234
2121
|
reason: resolved.reason,
|
|
1235
2122
|
});
|
|
1236
|
-
if (
|
|
1237
|
-
const implTargets = findInterfaceDispatchTargets(effectiveCall.calledName, effectiveCall.receiverTypeName, effectiveCall.filePath, ctx,
|
|
2123
|
+
if (heritageMap && effectiveCall.callForm === 'member' && effectiveCall.receiverTypeName) {
|
|
2124
|
+
const implTargets = findInterfaceDispatchTargets(effectiveCall.calledName, effectiveCall.receiverTypeName, effectiveCall.filePath, ctx, heritageMap, resolved.nodeId);
|
|
1238
2125
|
for (const impl of implTargets) {
|
|
1239
2126
|
graph.addRelationship({
|
|
1240
2127
|
id: generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${impl.nodeId}`),
|
|
@@ -1256,12 +2143,12 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1256
2143
|
* Accepts optional constructorBindings for return-type-aware receiver inference,
|
|
1257
2144
|
* mirroring processCallsFromExtracted's verified binding lookup.
|
|
1258
2145
|
*/
|
|
1259
|
-
export const processAssignmentsFromExtracted = (graph, assignments, ctx, constructorBindings) => {
|
|
2146
|
+
export const processAssignmentsFromExtracted = (graph, assignments, ctx, constructorBindings, bindingAccumulator) => {
|
|
1260
2147
|
// Build per-file receiver type indexes from verified constructor bindings
|
|
1261
2148
|
const fileReceiverTypes = new Map();
|
|
1262
2149
|
if (constructorBindings) {
|
|
1263
2150
|
for (const { filePath, bindings } of constructorBindings) {
|
|
1264
|
-
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
|
|
2151
|
+
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph, bindingAccumulator);
|
|
1265
2152
|
if (verified.size > 0) {
|
|
1266
2153
|
fileReceiverTypes.set(filePath, buildReceiverTypeIndex(verified));
|
|
1267
2154
|
}
|
|
@@ -1281,12 +2168,7 @@ export const processAssignmentsFromExtracted = (graph, assignments, ctx, constru
|
|
|
1281
2168
|
// Tier 3: static class-as-receiver fallback
|
|
1282
2169
|
if (!receiverTypeName) {
|
|
1283
2170
|
const resolved = ctx.resolve(asn.receiverText, asn.filePath);
|
|
1284
|
-
if (resolved?.candidates.some((d) => d.type
|
|
1285
|
-
d.type === 'Struct' ||
|
|
1286
|
-
d.type === 'Interface' ||
|
|
1287
|
-
d.type === 'Enum' ||
|
|
1288
|
-
d.type === 'Record' ||
|
|
1289
|
-
d.type === 'Impl')) {
|
|
2171
|
+
if (resolved?.candidates.some((d) => CLASS_LIKE_TYPES.has(d.type))) {
|
|
1290
2172
|
receiverTypeName = asn.receiverText;
|
|
1291
2173
|
}
|
|
1292
2174
|
}
|