gitnexus 1.6.5-rc.42 → 1.6.5-rc.44
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/dist/core/ingestion/languages/c-cpp.js +10 -2
- package/dist/core/ingestion/languages/cpp/adl.d.ts +36 -23
- package/dist/core/ingestion/languages/cpp/adl.js +203 -46
- package/dist/core/ingestion/languages/cpp/captures.js +139 -2
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +22 -18
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +10 -11
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +1 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +64 -26
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +30 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +90 -8
- package/package.json +1 -1
|
@@ -270,12 +270,20 @@ const cCppExtractFunctionName = (node) => {
|
|
|
270
270
|
}
|
|
271
271
|
return { funcName, label };
|
|
272
272
|
};
|
|
273
|
-
/** Check if a C/C++ function_definition is inside a class or struct body
|
|
273
|
+
/** Check if a C/C++ function_definition is inside a class or struct body
|
|
274
|
+
* (and NOT a friend declaration).
|
|
274
275
|
* Used by cppLabelOverride to skip duplicate function captures
|
|
275
|
-
* that are already covered by definition.method queries.
|
|
276
|
+
* that are already covered by definition.method queries.
|
|
277
|
+
* Friend functions are free functions defined inside class bodies —
|
|
278
|
+
* they must NOT be skipped (ISO C++ hidden-friend idiom). */
|
|
276
279
|
function isCppInsideClassOrStruct(functionNode) {
|
|
277
280
|
let ancestor = functionNode?.parent ?? null;
|
|
278
281
|
while (ancestor) {
|
|
282
|
+
// Friend declarations: the function_definition is wrapped in
|
|
283
|
+
// `friend_declaration` → `field_declaration_list` → class_specifier.
|
|
284
|
+
// These are free functions, not methods — don't skip them.
|
|
285
|
+
if (ancestor.type === 'friend_declaration')
|
|
286
|
+
return false;
|
|
279
287
|
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier')
|
|
280
288
|
return true;
|
|
281
289
|
ancestor = ancestor.parent;
|
|
@@ -21,14 +21,28 @@
|
|
|
21
21
|
* - `audit::Event& r`, `audit::Event&& rr`
|
|
22
22
|
* - `std::vector<audit::Event>` (template namespace + template-arg namespaces)
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* so base-class enclosing namespaces also contribute associated namespaces.
|
|
24
|
+
* V2 additionally walks class ancestors (via MRO), so base-class enclosing
|
|
25
|
+
* namespaces also contribute associated namespaces.
|
|
27
26
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
|
|
28
|
+
* function reference like `utils::worker` contributes `utils` to the associated
|
|
29
|
+
* set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
|
|
30
|
+
* to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
|
|
31
|
+
* entities for function-type arguments come from the **parameter types and return
|
|
32
|
+
* type** of each function in the overload set — NOT the function's enclosing
|
|
33
|
+
* namespace. For `void worker()`, the standard-compliant associated set is empty.
|
|
34
|
+
* GitNexus instead contributes the enclosing namespace of any Function/Method
|
|
35
|
+
* def whose simple name matches, because it enables the dominant real-world ADL
|
|
36
|
+
* pattern at reasonable precision cost.
|
|
37
|
+
*
|
|
38
|
+
* For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
|
|
39
|
+
* workspace lookup (only contributed when a Function/Method named `worker` exists
|
|
40
|
+
* in `utils`). For unqualified refs the workspace is searched for any Function
|
|
41
|
+
* def with that simple name. Locally-declared function-pointer variables
|
|
42
|
+
* (e.g. `void (*g)()`) and function parameters are excluded from this path.
|
|
43
|
+
*
|
|
44
|
+
* ADL candidates are merged with ordinary unqualified-lookup candidates
|
|
45
|
+
* in the free-call fallback before overload narrowing.
|
|
32
46
|
*
|
|
33
47
|
* ## Parenthesized-name suppression
|
|
34
48
|
*
|
|
@@ -59,6 +73,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
|
|
|
59
73
|
* Per-argument shape information collected at capture time. ADL fires for
|
|
60
74
|
* arguments where `simpleClassName !== ''`, including class pointers and
|
|
61
75
|
* references whose declarator chain resolves to a named class type.
|
|
76
|
+
* Free-function reference arguments use `functionRefText`.
|
|
62
77
|
*/
|
|
63
78
|
export interface CppAdlArgInfo {
|
|
64
79
|
/** Simple class-like type name (last segment of qualified name); empty
|
|
@@ -76,13 +91,16 @@ export interface CppAdlArgInfo {
|
|
|
76
91
|
/** Enclosing namespaces extracted from explicit type template arguments,
|
|
77
92
|
* recursively bounded. */
|
|
78
93
|
readonly templateArgNamespaces: readonly string[];
|
|
94
|
+
/** When set, the arg is a potential free-function reference (not a locally-
|
|
95
|
+
* declared function-pointer variable or function parameter). Contains the
|
|
96
|
+
* identifier text as written in source (e.g. `"utils::worker"` or
|
|
97
|
+
* `"worker"`). GitNexus approximation: the function's enclosing namespace
|
|
98
|
+
* is contributed to the ADL associated set. For qualified refs a workspace
|
|
99
|
+
* lookup confirms a Function/Method with that simple name exists in the
|
|
100
|
+
* namespace before contributing; for unqualified refs every namespace
|
|
101
|
+
* containing a matching Function/Method def is contributed. */
|
|
102
|
+
readonly functionRefText?: string;
|
|
79
103
|
}
|
|
80
|
-
/** Sentinel returned by `pickCppAdlCandidates` when ADL surfaces multiple
|
|
81
|
-
* candidates that share normalized parameter types — the caller MUST
|
|
82
|
-
* suppress (zero edges) rather than pick arbitrarily. Mirrors the
|
|
83
|
-
* OVERLOAD_AMBIGUOUS contract from the receiver-bound path. */
|
|
84
|
-
export declare const ADL_AMBIGUOUS: unique symbol;
|
|
85
|
-
export type AdlResult = SymbolDefinition | typeof ADL_AMBIGUOUS | undefined;
|
|
86
104
|
/** Record per-call-site argument info. Called once per call site from
|
|
87
105
|
* `emitCppScopeCaptures`. */
|
|
88
106
|
export declare function markCppAdlSiteArgs(filePath: string, line: number, col: number, args: readonly CppAdlArgInfo[]): void;
|
|
@@ -104,13 +122,10 @@ export declare function clearCppAdlState(): void;
|
|
|
104
122
|
*/
|
|
105
123
|
export declare function populateCppAssociatedNamespaces(parsed: ParsedFile): void;
|
|
106
124
|
/**
|
|
107
|
-
*
|
|
108
|
-
* - `SymbolDefinition` —
|
|
109
|
-
*
|
|
110
|
-
* - `
|
|
111
|
-
* caller MUST suppress (zero edges).
|
|
112
|
-
* - `undefined` — no ADL candidates; caller falls through to ordinary
|
|
113
|
-
* `pickUniqueGlobalCallable` fallback.
|
|
125
|
+
* ADL candidate collector. Returns:
|
|
126
|
+
* - `readonly SymbolDefinition[]` — ADL candidates to merge with
|
|
127
|
+
* ordinary unqualified lookup candidates.
|
|
128
|
+
* - `undefined` — no ADL candidates.
|
|
114
129
|
*
|
|
115
130
|
* Fires only when:
|
|
116
131
|
* - the call site is not in `noAdlSites` (parenthesized form), AND
|
|
@@ -119,10 +134,8 @@ export declare function populateCppAssociatedNamespaces(parsed: ParsedFile): voi
|
|
|
119
134
|
*/
|
|
120
135
|
export declare function pickCppAdlCandidates(site: {
|
|
121
136
|
readonly name: string;
|
|
122
|
-
readonly arity?: number;
|
|
123
|
-
readonly argumentTypes?: readonly string[];
|
|
124
137
|
readonly atRange: {
|
|
125
138
|
startLine: number;
|
|
126
139
|
startCol: number;
|
|
127
140
|
};
|
|
128
|
-
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]):
|
|
141
|
+
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]): readonly SymbolDefinition[] | undefined;
|
|
@@ -21,14 +21,28 @@
|
|
|
21
21
|
* - `audit::Event& r`, `audit::Event&& rr`
|
|
22
22
|
* - `std::vector<audit::Event>` (template namespace + template-arg namespaces)
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* so base-class enclosing namespaces also contribute associated namespaces.
|
|
24
|
+
* V2 additionally walks class ancestors (via MRO), so base-class enclosing
|
|
25
|
+
* namespaces also contribute associated namespaces.
|
|
27
26
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
|
|
28
|
+
* function reference like `utils::worker` contributes `utils` to the associated
|
|
29
|
+
* set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
|
|
30
|
+
* to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
|
|
31
|
+
* entities for function-type arguments come from the **parameter types and return
|
|
32
|
+
* type** of each function in the overload set — NOT the function's enclosing
|
|
33
|
+
* namespace. For `void worker()`, the standard-compliant associated set is empty.
|
|
34
|
+
* GitNexus instead contributes the enclosing namespace of any Function/Method
|
|
35
|
+
* def whose simple name matches, because it enables the dominant real-world ADL
|
|
36
|
+
* pattern at reasonable precision cost.
|
|
37
|
+
*
|
|
38
|
+
* For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
|
|
39
|
+
* workspace lookup (only contributed when a Function/Method named `worker` exists
|
|
40
|
+
* in `utils`). For unqualified refs the workspace is searched for any Function
|
|
41
|
+
* def with that simple name. Locally-declared function-pointer variables
|
|
42
|
+
* (e.g. `void (*g)()`) and function parameters are excluded from this path.
|
|
43
|
+
*
|
|
44
|
+
* ADL candidates are merged with ordinary unqualified-lookup candidates
|
|
45
|
+
* in the free-call fallback before overload narrowing.
|
|
32
46
|
*
|
|
33
47
|
* ## Parenthesized-name suppression
|
|
34
48
|
*
|
|
@@ -53,15 +67,10 @@
|
|
|
53
67
|
* but logically share the same namespace. ADL must consider candidates
|
|
54
68
|
* declared in either file.
|
|
55
69
|
*/
|
|
56
|
-
import {
|
|
70
|
+
import { isCppInlineNamespaceScope } from './inline-namespaces.js';
|
|
57
71
|
const argInfoBySite = new Map();
|
|
58
72
|
const noAdlSites = new Set();
|
|
59
73
|
const classToNamespaceQualifiedName = new Map();
|
|
60
|
-
/** Sentinel returned by `pickCppAdlCandidates` when ADL surfaces multiple
|
|
61
|
-
* candidates that share normalized parameter types — the caller MUST
|
|
62
|
-
* suppress (zero edges) rather than pick arbitrarily. Mirrors the
|
|
63
|
-
* OVERLOAD_AMBIGUOUS contract from the receiver-bound path. */
|
|
64
|
-
export const ADL_AMBIGUOUS = Symbol('ADL_AMBIGUOUS');
|
|
65
74
|
function siteKey(filePath, line, col) {
|
|
66
75
|
return `${filePath}:${line}:${col}`;
|
|
67
76
|
}
|
|
@@ -108,15 +117,27 @@ export function populateCppAssociatedNamespaces(parsed) {
|
|
|
108
117
|
classToNamespaceQualifiedName.set(def.nodeId, nsQName);
|
|
109
118
|
}
|
|
110
119
|
}
|
|
120
|
+
// Enum defs live in Namespace scopes directly (not inside Class scopes).
|
|
121
|
+
// Map each Enum def to its enclosing namespace so ADL on enum-typed
|
|
122
|
+
// arguments contributes the correct associated namespace.
|
|
123
|
+
for (const scope of parsed.scopes) {
|
|
124
|
+
if (scope.kind !== 'Namespace')
|
|
125
|
+
continue;
|
|
126
|
+
const nsQName = computeNamespaceQName(scope, scopesById);
|
|
127
|
+
if (nsQName === '')
|
|
128
|
+
continue;
|
|
129
|
+
for (const def of scope.ownedDefs) {
|
|
130
|
+
if (def.type !== 'Enum')
|
|
131
|
+
continue;
|
|
132
|
+
classToNamespaceQualifiedName.set(def.nodeId, nsQName);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
111
135
|
}
|
|
112
136
|
/**
|
|
113
|
-
*
|
|
114
|
-
* - `SymbolDefinition` —
|
|
115
|
-
*
|
|
116
|
-
* - `
|
|
117
|
-
* caller MUST suppress (zero edges).
|
|
118
|
-
* - `undefined` — no ADL candidates; caller falls through to ordinary
|
|
119
|
-
* `pickUniqueGlobalCallable` fallback.
|
|
137
|
+
* ADL candidate collector. Returns:
|
|
138
|
+
* - `readonly SymbolDefinition[]` — ADL candidates to merge with
|
|
139
|
+
* ordinary unqualified lookup candidates.
|
|
140
|
+
* - `undefined` — no ADL candidates.
|
|
120
141
|
*
|
|
121
142
|
* Fires only when:
|
|
122
143
|
* - the call site is not in `noAdlSites` (parenthesized form), AND
|
|
@@ -130,16 +151,22 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
|
|
|
130
151
|
const args = argInfoBySite.get(key);
|
|
131
152
|
if (args === undefined || args.length === 0)
|
|
132
153
|
return undefined;
|
|
133
|
-
// Collect associated namespace QNames from every participating class-typed arg
|
|
154
|
+
// Collect associated namespace QNames from every participating class-typed arg
|
|
155
|
+
// and from function-reference args.
|
|
134
156
|
const associatedNamespaces = new Set();
|
|
135
157
|
for (const arg of args) {
|
|
136
158
|
collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
|
|
159
|
+
if (arg.functionRefText !== undefined) {
|
|
160
|
+
collectFunctionRefNamespaces(arg.functionRefText, parsedFiles, associatedNamespaces);
|
|
161
|
+
}
|
|
137
162
|
}
|
|
138
163
|
if (associatedNamespaces.size === 0)
|
|
139
164
|
return undefined;
|
|
140
165
|
// Walk every namespace scope in every parsed file; collect callable
|
|
141
166
|
// ownedDefs whose enclosing namespace matches one of the associated
|
|
142
167
|
// QNames AND whose simple name matches the call's name.
|
|
168
|
+
// ISO C++: inline namespaces are transparent — candidates in inline
|
|
169
|
+
// children of an associated namespace are also ADL-reachable.
|
|
143
170
|
const candidates = [];
|
|
144
171
|
const seenKey = new Set();
|
|
145
172
|
for (const parsed of parsedFiles) {
|
|
@@ -150,8 +177,20 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
|
|
|
150
177
|
if (scope.kind !== 'Namespace')
|
|
151
178
|
continue;
|
|
152
179
|
const qName = computeNamespaceQName(scope, scopesById);
|
|
153
|
-
if (!associatedNamespaces.has(qName))
|
|
154
|
-
|
|
180
|
+
if (!associatedNamespaces.has(qName)) {
|
|
181
|
+
// Check if this is an inline-namespace child of an associated NS.
|
|
182
|
+
// ISO C++ inline namespaces are transparent for ADL: if the outer
|
|
183
|
+
// namespace is in the associated set, candidates in the inline child
|
|
184
|
+
// are also reachable.
|
|
185
|
+
if (!isCppInlineNamespaceScope(scope.id))
|
|
186
|
+
continue;
|
|
187
|
+
const parentScope = scope.parent !== null ? scopesById.get(scope.parent) : undefined;
|
|
188
|
+
if (parentScope === undefined || parentScope.kind !== 'Namespace')
|
|
189
|
+
continue;
|
|
190
|
+
const parentQName = computeNamespaceQName(parentScope, scopesById);
|
|
191
|
+
if (!associatedNamespaces.has(parentQName))
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
155
194
|
for (const def of scope.ownedDefs) {
|
|
156
195
|
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
|
|
157
196
|
continue;
|
|
@@ -169,26 +208,66 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
|
|
|
169
208
|
candidates.push(def);
|
|
170
209
|
}
|
|
171
210
|
}
|
|
211
|
+
// ISO C++ `[basic.lookup.argdep]` §2: hidden friend functions declared
|
|
212
|
+
// inside a class body are visible via ADL when the class is an associated
|
|
213
|
+
// class. Scan Class scopes whose enclosing namespace is in the associated
|
|
214
|
+
// set for callable ownedDefs matching the call name. This enables the
|
|
215
|
+
// canonical "hidden friend" idiom:
|
|
216
|
+
// struct Foo { friend void swap(Foo&, Foo&) {} };
|
|
217
|
+
for (const scope of parsed.scopes) {
|
|
218
|
+
if (scope.kind !== 'Class')
|
|
219
|
+
continue;
|
|
220
|
+
// Check if ANY class def in this scope has an associated namespace.
|
|
221
|
+
let isAssociatedClass = false;
|
|
222
|
+
for (const def of scope.ownedDefs) {
|
|
223
|
+
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
|
|
224
|
+
continue;
|
|
225
|
+
const nsQName = classToNamespaceQualifiedName.get(def.nodeId);
|
|
226
|
+
if (nsQName !== undefined && associatedNamespaces.has(nsQName)) {
|
|
227
|
+
isAssociatedClass = true;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!isAssociatedClass)
|
|
232
|
+
continue;
|
|
233
|
+
// Also scan Function scopes that are direct children of this class
|
|
234
|
+
// scope — friend function definitions create their own Function scope
|
|
235
|
+
// underneath the Class scope.
|
|
236
|
+
for (const childScope of parsed.scopes) {
|
|
237
|
+
if (childScope.parent !== scope.id)
|
|
238
|
+
continue;
|
|
239
|
+
if (childScope.kind !== 'Function')
|
|
240
|
+
continue;
|
|
241
|
+
for (const def of childScope.ownedDefs) {
|
|
242
|
+
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
|
246
|
+
if (simple !== site.name)
|
|
247
|
+
continue;
|
|
248
|
+
if (seenKey.has(def.nodeId))
|
|
249
|
+
continue;
|
|
250
|
+
seenKey.add(def.nodeId);
|
|
251
|
+
candidates.push(def);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const def of scope.ownedDefs) {
|
|
255
|
+
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
|
259
|
+
if (simple !== site.name)
|
|
260
|
+
continue;
|
|
261
|
+
if (seenKey.has(def.nodeId))
|
|
262
|
+
continue;
|
|
263
|
+
seenKey.add(def.nodeId);
|
|
264
|
+
candidates.push(def);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
172
267
|
}
|
|
173
268
|
if (candidates.length === 0)
|
|
174
269
|
return undefined;
|
|
175
|
-
|
|
176
|
-
return candidates[0];
|
|
177
|
-
// Multi-candidate: narrow then check ambiguity. Reuses the OVERLOAD_AMBIGUOUS
|
|
178
|
-
// sentinel contract from `overload-narrowing.ts` so int/long-collision-style
|
|
179
|
-
// ambiguity also suppresses on the ADL path.
|
|
180
|
-
const narrowed = narrowOverloadCandidates(candidates, site.arity, site.argumentTypes);
|
|
181
|
-
if (narrowed.length === 1)
|
|
182
|
-
return narrowed[0];
|
|
183
|
-
if (narrowed.length === 0)
|
|
184
|
-
return undefined;
|
|
185
|
-
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity))
|
|
186
|
-
return ADL_AMBIGUOUS;
|
|
187
|
-
// Multiple surviving candidates that aren't normalization-ambiguous —
|
|
188
|
-
// ISO C++ would run overload resolution; V1 lacks conversion ranking so
|
|
189
|
-
// suppress rather than pick arbitrarily. Mirrors `pickImplicitThisOverload`'s
|
|
190
|
-
// unique-survivor requirement (see `pick-implicit-this-overload.test.ts`).
|
|
191
|
-
return ADL_AMBIGUOUS;
|
|
270
|
+
return candidates;
|
|
192
271
|
}
|
|
193
272
|
function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces) {
|
|
194
273
|
// For template args this may be the template name itself (e.g. `vector`);
|
|
@@ -196,8 +275,8 @@ function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces)
|
|
|
196
275
|
// V1/V2 simplification).
|
|
197
276
|
addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, associatedNamespaces);
|
|
198
277
|
// Includes template-owner namespaces (e.g. `std` in std::vector<T>). If
|
|
199
|
-
// that surfaces extra candidates,
|
|
200
|
-
// arbitrary edge emission.
|
|
278
|
+
// that surfaces extra candidates, merged-candidate overload narrowing in
|
|
279
|
+
// free-call-fallback suppresses arbitrary edge emission.
|
|
201
280
|
if (arg.templateNamespace.length > 0)
|
|
202
281
|
associatedNamespaces.add(arg.templateNamespace);
|
|
203
282
|
for (const ns of arg.templateArgNamespaces) {
|
|
@@ -296,15 +375,21 @@ function findNamespaceDefInScope(scope) {
|
|
|
296
375
|
}
|
|
297
376
|
return undefined;
|
|
298
377
|
}
|
|
299
|
-
/** Find a class-like def by simple name across the workspace.
|
|
300
|
-
* still arbitrary-picks the first
|
|
378
|
+
/** Find a class-like or enum def by simple name across the workspace.
|
|
379
|
+
* V1 still arbitrary-picks the first match on collisions (multiple defs
|
|
301
380
|
* share the simple name), but reports the collision so callers can avoid
|
|
302
381
|
* amplifying that uncertainty (for example by skipping MRO expansion).
|
|
303
|
-
* C++ ADL strictness would require full type-driven lookup.
|
|
382
|
+
* C++ ADL strictness would require full type-driven lookup.
|
|
383
|
+
*
|
|
384
|
+
* ISO C++ `[basic.lookup.argdep]` §2: enumerations contribute their
|
|
385
|
+
* enclosing namespace to the associated set, just like class types. */
|
|
304
386
|
function findCppClassDefBySimpleName(simpleName, scopes) {
|
|
305
387
|
let firstMatch;
|
|
306
388
|
for (const def of scopes.defs.byId.values()) {
|
|
307
|
-
if (def.type !== 'Class' &&
|
|
389
|
+
if (def.type !== 'Class' &&
|
|
390
|
+
def.type !== 'Struct' &&
|
|
391
|
+
def.type !== 'Interface' &&
|
|
392
|
+
def.type !== 'Enum')
|
|
308
393
|
continue;
|
|
309
394
|
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
|
310
395
|
if (simple !== simpleName)
|
|
@@ -319,3 +404,75 @@ function findCppClassDefBySimpleName(simpleName, scopes) {
|
|
|
319
404
|
return undefined;
|
|
320
405
|
return { classDef: firstMatch, ambiguous: false };
|
|
321
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Contribute associated namespaces for a function-reference argument.
|
|
409
|
+
*
|
|
410
|
+
* - **Qualified refs** (`utils::worker`, `outer::inner::fn`): the namespace
|
|
411
|
+
* is extracted from the qualifier text (converting `::` to `.` for dot-joined
|
|
412
|
+
* QName matching). A workspace lookup then **verifies** that a Function or
|
|
413
|
+
* Method def named `worker` (the simple name after the last `::`) actually
|
|
414
|
+
* exists in the extracted namespace. This prevents false positives from
|
|
415
|
+
* namespace-qualified variables, enum values, and static data members, which
|
|
416
|
+
* also produce `qualified_identifier` AST nodes in tree-sitter-cpp (the
|
|
417
|
+
* AST node type alone does not distinguish functions from non-function names).
|
|
418
|
+
* - **Unqualified refs** (`worker`): the workspace is searched for any
|
|
419
|
+
* Function/Method def whose simple name matches. Every distinct enclosing
|
|
420
|
+
* namespace found is added — overloads across the same namespace produce
|
|
421
|
+
* a single entry; GitNexus does not select a specific overload at this stage.
|
|
422
|
+
*/
|
|
423
|
+
function collectFunctionRefNamespaces(refText, parsedFiles, out) {
|
|
424
|
+
const colonIdx = refText.lastIndexOf('::');
|
|
425
|
+
if (colonIdx !== -1) {
|
|
426
|
+
// Qualified ref: extract namespace prefix and normalise :: → dot notation.
|
|
427
|
+
const nsText = refText.slice(0, colonIdx).replace(/::/g, '.');
|
|
428
|
+
if (nsText === '')
|
|
429
|
+
return;
|
|
430
|
+
const simpleName = refText.slice(colonIdx + 2);
|
|
431
|
+
// Verify that a Function/Method named `simpleName` exists in `nsText`.
|
|
432
|
+
// Without this guard every `a::b` qualified_identifier arg (variable,
|
|
433
|
+
// enum value, static member, type alias) would blindly contribute `a`
|
|
434
|
+
// to the associated set and risk a false-positive CALLS edge.
|
|
435
|
+
for (const parsed of parsedFiles) {
|
|
436
|
+
const scopesById = new Map();
|
|
437
|
+
for (const sc of parsed.scopes)
|
|
438
|
+
scopesById.set(sc.id, sc);
|
|
439
|
+
for (const scope of parsed.scopes) {
|
|
440
|
+
if (scope.kind !== 'Namespace')
|
|
441
|
+
continue;
|
|
442
|
+
if (computeNamespaceQName(scope, scopesById) !== nsText)
|
|
443
|
+
continue;
|
|
444
|
+
for (const def of scope.ownedDefs) {
|
|
445
|
+
if (def.type !== 'Function' && def.type !== 'Method')
|
|
446
|
+
continue;
|
|
447
|
+
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
|
448
|
+
if (simple === simpleName) {
|
|
449
|
+
out.add(nsText);
|
|
450
|
+
return; // Namespace confirmed; no need to scan further files.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Unqualified: search all namespace scopes for a Function def with this
|
|
458
|
+
// simple name and contribute its enclosing namespace.
|
|
459
|
+
for (const parsed of parsedFiles) {
|
|
460
|
+
const scopesById = new Map();
|
|
461
|
+
for (const sc of parsed.scopes)
|
|
462
|
+
scopesById.set(sc.id, sc);
|
|
463
|
+
for (const scope of parsed.scopes) {
|
|
464
|
+
if (scope.kind !== 'Namespace')
|
|
465
|
+
continue;
|
|
466
|
+
for (const def of scope.ownedDefs) {
|
|
467
|
+
if (def.type !== 'Function' && def.type !== 'Method')
|
|
468
|
+
continue;
|
|
469
|
+
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
|
470
|
+
if (simple !== refText)
|
|
471
|
+
continue;
|
|
472
|
+
const nsQName = computeNamespaceQName(scope, scopesById);
|
|
473
|
+
if (nsQName !== '')
|
|
474
|
+
out.add(nsQName);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -717,15 +717,93 @@ function classifyAdlArg(argNode) {
|
|
|
717
717
|
argNode.type === 'nullptr') {
|
|
718
718
|
return EMPTY_ADL_ARG;
|
|
719
719
|
}
|
|
720
|
+
// Qualified expression (a::b) — may be a function, variable, enum value,
|
|
721
|
+
// or static member. Record as a potential function reference; resolution
|
|
722
|
+
// time verifies via workspace lookup that a Function/Method with this simple
|
|
723
|
+
// name exists in the extracted namespace before contributing to the set.
|
|
724
|
+
if (argNode.type === 'qualified_identifier') {
|
|
725
|
+
return {
|
|
726
|
+
simpleClassName: '',
|
|
727
|
+
templateSimpleClassName: '',
|
|
728
|
+
templateNamespace: '',
|
|
729
|
+
templateArgClassNames: [],
|
|
730
|
+
templateArgNamespaces: [],
|
|
731
|
+
functionRefText: argNode.text,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
720
734
|
// Variable reference — look up its declared type (preserving pointer /
|
|
721
735
|
// reference / qualified-name shape; the existing arity-narrowing helper
|
|
722
736
|
// strips this info).
|
|
723
737
|
if (argNode.type === 'identifier') {
|
|
724
|
-
|
|
738
|
+
const result = lookupAdlIdentifierType(argNode);
|
|
739
|
+
if (result === null) {
|
|
740
|
+
// Not found in the local compound_statement scope — could be a
|
|
741
|
+
// free-function reference (unqualified name, namespace scope).
|
|
742
|
+
return {
|
|
743
|
+
simpleClassName: '',
|
|
744
|
+
templateSimpleClassName: '',
|
|
745
|
+
templateNamespace: '',
|
|
746
|
+
templateArgClassNames: [],
|
|
747
|
+
templateArgNamespaces: [],
|
|
748
|
+
functionRefText: argNode.text,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return result;
|
|
725
752
|
}
|
|
726
753
|
// Other shapes (calls, member access, operators) — V1 unsupported.
|
|
727
754
|
return EMPTY_ADL_ARG;
|
|
728
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* Returns `true` when `varName` appears as a parameter name in the nearest
|
|
758
|
+
* enclosing `function_definition` or `function_declarator` that contains
|
|
759
|
+
* `identNode`. Parameters live in `parameter_list` (a sibling of the
|
|
760
|
+
* `compound_statement`), so the `compound_statement`-local declaration scan
|
|
761
|
+
* in `lookupAdlIdentifierType` would not find them — causing them to be
|
|
762
|
+
* mistakenly classified as potential free-function references.
|
|
763
|
+
*
|
|
764
|
+
* In tree-sitter-cpp a `function_definition` does NOT expose `parameters`
|
|
765
|
+
* as a direct named field; parameters live inside the nested
|
|
766
|
+
* `function_declarator`. For `function_declarator` nodes the `parameters`
|
|
767
|
+
* field IS direct. Both cases are handled below.
|
|
768
|
+
*/
|
|
769
|
+
function isIdentifierAFunctionParameter(identNode, varName) {
|
|
770
|
+
let node = identNode.parent;
|
|
771
|
+
let safety = 64;
|
|
772
|
+
while (node !== null && safety-- > 0) {
|
|
773
|
+
let params = null;
|
|
774
|
+
if (node.type === 'function_declarator') {
|
|
775
|
+
// parameters is a direct field on function_declarator.
|
|
776
|
+
params = node.childForFieldName('parameters');
|
|
777
|
+
}
|
|
778
|
+
else if (node.type === 'function_definition') {
|
|
779
|
+
// function_definition carries parameters inside its `declarator` field
|
|
780
|
+
// (which is a function_declarator). Walk through it.
|
|
781
|
+
const decl = node.childForFieldName('declarator');
|
|
782
|
+
if (decl !== null && decl.type === 'function_declarator') {
|
|
783
|
+
params = decl.childForFieldName('parameters');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (params !== null) {
|
|
787
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
788
|
+
const param = params.namedChild(i);
|
|
789
|
+
if (param === null)
|
|
790
|
+
continue;
|
|
791
|
+
const declNode = param.childForFieldName('declarator');
|
|
792
|
+
if (declNode === null)
|
|
793
|
+
continue;
|
|
794
|
+
const leafName = extractDeclaratorLeafName(declNode);
|
|
795
|
+
if (leafName === varName)
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
// Only check the immediately enclosing function — do not climb further.
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
if (node.type === 'translation_unit')
|
|
802
|
+
break;
|
|
803
|
+
node = node.parent;
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
729
807
|
function lookupAdlIdentifierType(identNode) {
|
|
730
808
|
const varName = identNode.text;
|
|
731
809
|
let scope = identNode.parent;
|
|
@@ -735,7 +813,15 @@ function lookupAdlIdentifierType(identNode) {
|
|
|
735
813
|
scope = scope.parent;
|
|
736
814
|
}
|
|
737
815
|
if (scope === null)
|
|
816
|
+
return null;
|
|
817
|
+
// Function parameters live in the enclosing function's `parameter_list`,
|
|
818
|
+
// NOT inside the `compound_statement`, so the declaration scan below would
|
|
819
|
+
// never find them and would return `null` — incorrectly triggering the
|
|
820
|
+
// free-function-reference path. Check the parameter_list first.
|
|
821
|
+
if (isIdentifierAFunctionParameter(identNode, varName)) {
|
|
738
822
|
return EMPTY_ADL_ARG;
|
|
823
|
+
}
|
|
824
|
+
let foundAsLocalFunctionPointer = false;
|
|
739
825
|
for (let i = 0; i < scope.childCount; i++) {
|
|
740
826
|
const stmt = scope.child(i);
|
|
741
827
|
if (stmt === null || stmt.type !== 'declaration')
|
|
@@ -764,6 +850,9 @@ function lookupAdlIdentifierType(identNode) {
|
|
|
764
850
|
if (inner.type === 'pointer_declarator') {
|
|
765
851
|
if (findFirstDescendantOfType(inner, 'function_declarator') !== null) {
|
|
766
852
|
isFunctionPointer = true;
|
|
853
|
+
// Extract the name from within the function-pointer declarator chain
|
|
854
|
+
// so `foundAsLocalFunctionPointer` can detect a matching declaration.
|
|
855
|
+
nameText = extractDeclaratorLeafName(inner);
|
|
767
856
|
break;
|
|
768
857
|
}
|
|
769
858
|
const next = inner.childForFieldName('declarator');
|
|
@@ -796,12 +885,22 @@ function lookupAdlIdentifierType(identNode) {
|
|
|
796
885
|
}
|
|
797
886
|
if (inner.type === 'function_declarator') {
|
|
798
887
|
isFunctionPointer = true;
|
|
888
|
+
// Extract the name from the inner declarator (e.g. `(*g)` in `void (*g)()`).
|
|
889
|
+
const innerDecl = inner.childForFieldName('declarator');
|
|
890
|
+
if (innerDecl !== null)
|
|
891
|
+
nameText = extractDeclaratorLeafName(innerDecl);
|
|
799
892
|
break;
|
|
800
893
|
}
|
|
801
894
|
// Reached the leaf — usually `identifier`. Take its text.
|
|
802
895
|
nameText = inner.text;
|
|
803
896
|
break;
|
|
804
897
|
}
|
|
898
|
+
if (nameText === varName && isFunctionPointer) {
|
|
899
|
+
// Explicitly declared as a function-pointer variable — must not be
|
|
900
|
+
// treated as a free-function reference by the caller.
|
|
901
|
+
foundAsLocalFunctionPointer = true;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
805
904
|
if (isFunctionPointer || nameText !== varName)
|
|
806
905
|
continue;
|
|
807
906
|
const simpleClassName = extractAdlSimpleTypeName(typeNode);
|
|
@@ -814,7 +913,22 @@ function lookupAdlIdentifierType(identNode) {
|
|
|
814
913
|
templateArgNamespaces,
|
|
815
914
|
};
|
|
816
915
|
}
|
|
817
|
-
|
|
916
|
+
// If the identifier was found in local scope as a function-pointer variable,
|
|
917
|
+
// return EMPTY_ADL_ARG so the caller does NOT treat it as a free-function
|
|
918
|
+
// reference. Otherwise return null to indicate "not in local scope".
|
|
919
|
+
//
|
|
920
|
+
// Known limitation (Finding 4): variables whose type is a typedef/using alias
|
|
921
|
+
// for a function-pointer type are NOT detected here. For example:
|
|
922
|
+
// using Callback = void (*)();
|
|
923
|
+
// Callback g;
|
|
924
|
+
// foo(g); // `g`'s declarator is `identifier` with type `Callback`
|
|
925
|
+
// The declarator has no `pointer_declarator` wrapper, so `isFunctionPointer`
|
|
926
|
+
// stays false and `extractAdlSimpleTypeName` returns `"Callback"`. ADL then
|
|
927
|
+
// looks for a class named `Callback`; if none exists, this degrades to
|
|
928
|
+
// EMPTY_ADL_ARG (class not found → no namespace contributed). If a class
|
|
929
|
+
// named `Callback` does exist, a spurious namespace contribution could occur.
|
|
930
|
+
// Risk is low in practice; a future fix should resolve the typedef/alias chain.
|
|
931
|
+
return foundAsLocalFunctionPointer ? EMPTY_ADL_ARG : null;
|
|
818
932
|
}
|
|
819
933
|
/** Extract the simple class-like type name from a `type:` field node.
|
|
820
934
|
* Returns '' for primitives and any other
|
|
@@ -963,6 +1077,29 @@ function extractNamespaceFromQualifiedText(text) {
|
|
|
963
1077
|
return '';
|
|
964
1078
|
return normalizeCppNamespaceQName(cleaned.slice(0, idx));
|
|
965
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Walk a declarator node chain, unwrapping pointer/reference/function/
|
|
1082
|
+
* parenthesized wrappers, and return the text of the innermost identifier.
|
|
1083
|
+
* Returns `null` when no identifier is found within `safety` steps.
|
|
1084
|
+
* Used by `lookupAdlIdentifierType` to extract the variable name from
|
|
1085
|
+
* function-pointer declarator trees such as `(*g)()` in `void (*g)()`.
|
|
1086
|
+
*/
|
|
1087
|
+
function extractDeclaratorLeafName(node) {
|
|
1088
|
+
let cur = node;
|
|
1089
|
+
let safety = 16;
|
|
1090
|
+
while (safety-- > 0) {
|
|
1091
|
+
if (cur.type === 'identifier' || cur.type === 'type_identifier')
|
|
1092
|
+
return cur.text;
|
|
1093
|
+
// Common wrapper nodes — follow the 'declarator' field when present.
|
|
1094
|
+
const next = cur.childForFieldName('declarator') ??
|
|
1095
|
+
// parenthesized_declarator: single named child
|
|
1096
|
+
(cur.type === 'parenthesized_declarator' ? cur.namedChild(0) : null);
|
|
1097
|
+
if (next === null)
|
|
1098
|
+
return null;
|
|
1099
|
+
cur = next;
|
|
1100
|
+
}
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
966
1103
|
/**
|
|
967
1104
|
* Check if a C++ function_definition or declaration has `static` storage class.
|
|
968
1105
|
*/
|
|
@@ -9,10 +9,9 @@ import { resolveCppImportTarget } from './import-target.js';
|
|
|
9
9
|
import { scanCppHeaderFiles } from './header-scan.js';
|
|
10
10
|
import { expandCppWildcardNames, isFileLocal, clearFileLocalNames, populateCppAnonymousNamespaceScopes, populateCppNonGloballyVisible, isCppDefGloballyVisible, } from './file-local-linkage.js';
|
|
11
11
|
import { populateCppDependentBases, clearCppDependentBases, isCppDependentBaseMember, } from './two-phase-lookup.js';
|
|
12
|
-
import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates
|
|
12
|
+
import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates } from './adl.js';
|
|
13
13
|
import { clearCppInlineNamespaces, populateCppInlineNamespaceScopes, resolveCppQualifiedNamespaceMember, } from './inline-namespaces.js';
|
|
14
14
|
import { populateCppRangeBindings } from './range-bindings.js';
|
|
15
|
-
import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
|
|
16
15
|
/**
|
|
17
16
|
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
|
18
17
|
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
|
@@ -187,12 +186,12 @@ export const cppScopeResolver = {
|
|
|
187
186
|
return !isCppDependentBaseMember(callerScope, candidate, scopes);
|
|
188
187
|
},
|
|
189
188
|
// C++ argument-dependent / Koenig lookup (U2 of plan 2026-05-13-001).
|
|
190
|
-
//
|
|
191
|
-
//
|
|
189
|
+
// Contributes candidates from associated namespaces of class-typed
|
|
190
|
+
// arguments; caller merges with ordinary unqualified lookup candidates.
|
|
192
191
|
// Current boundary: class-typed value/pointer/reference args and template
|
|
193
192
|
// specializations with explicit type arguments contribute associated
|
|
194
|
-
// namespaces. Function-pointer args
|
|
195
|
-
//
|
|
193
|
+
// namespaces. Function-pointer args and full conversion-ranking remain
|
|
194
|
+
// excluded.
|
|
196
195
|
resolveAdlCandidates: (site, callerParsed, scopes, parsedFiles) => {
|
|
197
196
|
// `using ns::name;` introduces `name` into ordinary unqualified lookup.
|
|
198
197
|
// For template-class method bodies, lexical scope walks can miss this
|
|
@@ -213,19 +212,24 @@ export const cppScopeResolver = {
|
|
|
213
212
|
seenUsing.add(member.nodeId);
|
|
214
213
|
usingNamedHits.push(member);
|
|
215
214
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
215
|
+
const adlHits = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
|
|
216
|
+
if (usingNamedHits.length === 0)
|
|
217
|
+
return adlHits;
|
|
218
|
+
if (adlHits === undefined || adlHits.length === 0)
|
|
219
|
+
return usingNamedHits;
|
|
220
|
+
const merged = [];
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
for (const hit of usingNamedHits) {
|
|
223
|
+
seen.add(hit.nodeId);
|
|
224
|
+
merged.push(hit);
|
|
224
225
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
for (const hit of adlHits) {
|
|
227
|
+
if (seen.has(hit.nodeId))
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(hit.nodeId);
|
|
230
|
+
merged.push(hit);
|
|
231
|
+
}
|
|
232
|
+
return merged;
|
|
229
233
|
},
|
|
230
234
|
// C++ qualified namespace-member resolution (U5 of plan 2026-05-13-001).
|
|
231
235
|
// Handles `outer::foo()` where `outer` is a namespace (not a class).
|
|
@@ -506,16 +506,15 @@ export interface ScopeResolver {
|
|
|
506
506
|
* Optional argument-dependent-lookup (ADL / Koenig lookup) hook for
|
|
507
507
|
* languages with C++-style associated-namespace candidate addition.
|
|
508
508
|
*
|
|
509
|
-
* Runs in the free-call fallback
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
* - `undefined`
|
|
518
|
-
* global free-call fallback (`pickUniqueGlobalCallable`).
|
|
509
|
+
* Runs in the free-call fallback alongside ordinary unqualified lookup.
|
|
510
|
+
* The fallback merges ordinary candidates with ADL candidates and applies
|
|
511
|
+
* overload narrowing over the union.
|
|
512
|
+
*
|
|
513
|
+
* The hook inspects the call site's argument types, computes the
|
|
514
|
+
* associated namespace set, and returns either:
|
|
515
|
+
* - an array of candidate `SymbolDefinition`s to add to the
|
|
516
|
+
* ordinary-lookup candidate pool.
|
|
517
|
+
* - `undefined` when ADL contributes no candidates.
|
|
519
518
|
*
|
|
520
519
|
* Languages without C++-style ADL leave this undefined. The
|
|
521
520
|
* cross-language contract is "additive tier" — defining the hook never
|
|
@@ -529,7 +528,7 @@ export interface ScopeResolver {
|
|
|
529
528
|
readonly startLine: number;
|
|
530
529
|
readonly startCol: number;
|
|
531
530
|
};
|
|
532
|
-
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition |
|
|
531
|
+
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
|
|
533
532
|
/**
|
|
534
533
|
* Optional resolver for qualified-receiver member calls where the
|
|
535
534
|
* receiver is a namespace (not a class) and ordinary scope-chain /
|
|
@@ -41,7 +41,7 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
|
|
|
41
41
|
readonly startLine: number;
|
|
42
42
|
readonly startCol: number;
|
|
43
43
|
};
|
|
44
|
-
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition |
|
|
44
|
+
}, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
|
|
45
45
|
}): number;
|
|
46
46
|
/** Walk up from the call-site scope to the enclosing class scope,
|
|
47
47
|
* pick a method member by name with overload narrowing on arity +
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* generalization plan.
|
|
18
18
|
*/
|
|
19
19
|
import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
|
|
20
|
-
import { findCallableBindingInScope, findClassBindingInScope } from '../scope/walkers.js';
|
|
21
|
-
import { narrowOverloadCandidates } from './overload-narrowing.js';
|
|
20
|
+
import { findCallableBindingInScope, findCallableBindingsAndAdlBlocker, findClassBindingInScope, } from '../scope/walkers.js';
|
|
21
|
+
import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from './overload-narrowing.js';
|
|
22
22
|
export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _referenceIndex, handledSites, model, workspaceIndex, options = {}) {
|
|
23
23
|
let emitted = 0;
|
|
24
24
|
const seen = new Set();
|
|
@@ -48,36 +48,74 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
48
48
|
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model);
|
|
49
49
|
}
|
|
50
50
|
if (fnDef === undefined) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// V1 ADL tier (C++ Koenig lookup, opt-in via provider.resolveAdlCandidates).
|
|
54
|
-
// Fires only when ordinary lookup is empty — V1 limitation per
|
|
55
|
-
// plan 2026-05-13-001 U2; ISO C++ would merge ADL with ordinary lookup
|
|
56
|
-
// and run overload resolution over the union.
|
|
57
|
-
//
|
|
58
|
-
// Sentinel 'ambiguous': ADL surfaced multiple candidates with
|
|
59
|
-
// identical normalized parameter types (mirrors OVERLOAD_AMBIGUOUS).
|
|
60
|
-
// We mark the site handled so `emit-references` does not retry, and
|
|
61
|
-
// continue to the next site without emitting an edge.
|
|
62
|
-
if (fnDef === undefined && options.resolveAdlCandidates !== undefined) {
|
|
63
|
-
const adlResult = options.resolveAdlCandidates({
|
|
64
|
-
name: site.name,
|
|
65
|
-
arity: site.arity,
|
|
66
|
-
argumentTypes: site.argumentTypes,
|
|
67
|
-
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
|
68
|
-
}, parsed, scopes, parsedFiles);
|
|
69
|
-
if (adlResult === 'ambiguous') {
|
|
70
|
-
handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`);
|
|
71
|
-
continue;
|
|
51
|
+
if (options.resolveAdlCandidates === undefined) {
|
|
52
|
+
fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
|
|
72
53
|
}
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
else {
|
|
55
|
+
// ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when
|
|
56
|
+
// ordinary lookup finds a non-function name (variable, class, enum)
|
|
57
|
+
// or a block-scope function declaration (not via using-declaration)
|
|
58
|
+
// at the nearest scope where the name exists.
|
|
59
|
+
const { callables: ordinary, nonCallableFound, blockScopeDeclFound, } = findCallableBindingsAndAdlBlocker(site.inScope, site.name, scopes);
|
|
60
|
+
const adlSuppressed = nonCallableFound || blockScopeDeclFound;
|
|
61
|
+
const adl = adlSuppressed
|
|
62
|
+
? undefined
|
|
63
|
+
: options.resolveAdlCandidates({
|
|
64
|
+
name: site.name,
|
|
65
|
+
arity: site.arity,
|
|
66
|
+
argumentTypes: site.argumentTypes,
|
|
67
|
+
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
|
68
|
+
}, parsed, scopes, parsedFiles);
|
|
69
|
+
// Preserve existing ordinary-lookup behavior when ADL contributed
|
|
70
|
+
// no candidates.
|
|
71
|
+
if (adl === undefined || adl.length === 0) {
|
|
72
|
+
fnDef = ordinary[0];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
76
|
+
const merged = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const push = (defs) => {
|
|
79
|
+
for (const d of defs) {
|
|
80
|
+
if (seen.has(d.nodeId))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(d.nodeId);
|
|
83
|
+
merged.push(d);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
push(ordinary);
|
|
87
|
+
push(adl);
|
|
88
|
+
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes);
|
|
89
|
+
if (narrowed.length === 1) {
|
|
90
|
+
fnDef = narrowed[0];
|
|
91
|
+
}
|
|
92
|
+
else if (narrowed.length === 0) {
|
|
93
|
+
// ADL contributed candidates, but none survived arity/type
|
|
94
|
+
// narrowing. Treat as handled to avoid global-name fallback
|
|
95
|
+
// binding to the same mismatched symbol by simple-name
|
|
96
|
+
// uniqueness.
|
|
97
|
+
handledSites.add(siteKey);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
else if (narrowed.length > 1) {
|
|
101
|
+
// Suppress ambiguous overload calls (emit zero edges) when
|
|
102
|
+
// merged ordinary+ADL candidate sets cannot be disambiguated.
|
|
103
|
+
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
|
|
104
|
+
handledSites.add(siteKey);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Multiple survivors remain but no conversion-ranking step
|
|
108
|
+
// exists yet; suppress instead of picking arbitrarily.
|
|
109
|
+
handledSites.add(siteKey);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
75
113
|
}
|
|
76
114
|
}
|
|
77
115
|
// V1: pickUniqueGlobalCallable ignores import context — resolves to any
|
|
78
116
|
// globally-unique callable. False cross-package edges are possible when
|
|
79
117
|
// the caller does not import the target package. Same-package calls are
|
|
80
|
-
// caught by
|
|
118
|
+
// usually caught by nearest-scope lookup before reaching here.
|
|
81
119
|
if (fnDef === undefined && options.allowGlobalFallback === true) {
|
|
82
120
|
fnDef = pickUniqueGlobalCallable(site.name, model, scopes, parsed.filePath, options.isFileLocalDef, site.arity, options.isCallableVisibleFromCaller !== undefined
|
|
83
121
|
? (candidate) => options.isCallableVisibleFromCaller({
|
|
@@ -102,6 +102,36 @@ export declare function findClassBindingInScope(startScope: ScopeId, receiverNam
|
|
|
102
102
|
* def-type predicate differs.
|
|
103
103
|
*/
|
|
104
104
|
export declare function findCallableBindingInScope(startScope: ScopeId, callableName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Look up all callable bindings (Function/Method/Constructor) by name
|
|
107
|
+
* from the nearest scope in the chain that binds `callableName`.
|
|
108
|
+
*
|
|
109
|
+
* Preserves the original scope-walk boundary used by
|
|
110
|
+
* `findCallableBindingInScope`: once any callable binding is found in a
|
|
111
|
+
* scope, outer scopes are not consulted.
|
|
112
|
+
*/
|
|
113
|
+
export declare function findAllCallableBindingsInScope(startScope: ScopeId, callableName: string, scopes: ScopeResolutionIndexes): readonly SymbolDefinition[];
|
|
114
|
+
/**
|
|
115
|
+
* ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when ordinary
|
|
116
|
+
* unqualified lookup finds:
|
|
117
|
+
* - a name that is NOT a function or function template, OR
|
|
118
|
+
* - a block-scope function declaration that is NOT a using-declaration.
|
|
119
|
+
*
|
|
120
|
+
* Combined walker that stops at the **nearest scope** where `name` has any
|
|
121
|
+
* binding (callable or non-callable) and returns:
|
|
122
|
+
* - `callables`: Function/Method/Constructor defs found at that scope
|
|
123
|
+
* - `nonCallableFound`: a non-function binding was present (variable, class, etc.)
|
|
124
|
+
* - `blockScopeDeclFound`: a callable was found at a Function or Block scope
|
|
125
|
+
* (block-scope function declaration that blocks ADL)
|
|
126
|
+
*
|
|
127
|
+
* One pass, one stop — no divergence between callable collection and blocker
|
|
128
|
+
* detection.
|
|
129
|
+
*/
|
|
130
|
+
export declare function findCallableBindingsAndAdlBlocker(startScope: ScopeId, name: string, scopes: ScopeResolutionIndexes): {
|
|
131
|
+
callables: readonly SymbolDefinition[];
|
|
132
|
+
nonCallableFound: boolean;
|
|
133
|
+
blockScopeDeclFound: boolean;
|
|
134
|
+
};
|
|
105
135
|
/**
|
|
106
136
|
* Populate `ownerId` on every def structurally owned by a Class
|
|
107
137
|
* scope — methods (defs in Function scopes whose parent is Class)
|
|
@@ -209,32 +209,114 @@ export function findClassBindingInScope(startScope, receiverName, scopes) {
|
|
|
209
209
|
* def-type predicate differs.
|
|
210
210
|
*/
|
|
211
211
|
export function findCallableBindingInScope(startScope, callableName, scopes) {
|
|
212
|
+
return findAllCallableBindingsInScope(startScope, callableName, scopes)[0];
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Look up all callable bindings (Function/Method/Constructor) by name
|
|
216
|
+
* from the nearest scope in the chain that binds `callableName`.
|
|
217
|
+
*
|
|
218
|
+
* Preserves the original scope-walk boundary used by
|
|
219
|
+
* `findCallableBindingInScope`: once any callable binding is found in a
|
|
220
|
+
* scope, outer scopes are not consulted.
|
|
221
|
+
*/
|
|
222
|
+
export function findAllCallableBindingsInScope(startScope, callableName, scopes) {
|
|
212
223
|
let currentId = startScope;
|
|
213
224
|
const visited = new Set();
|
|
214
225
|
while (currentId !== null) {
|
|
215
226
|
if (visited.has(currentId))
|
|
216
|
-
return
|
|
227
|
+
return [];
|
|
217
228
|
visited.add(currentId);
|
|
218
229
|
const scope = scopes.scopeTree.getScope(currentId);
|
|
219
230
|
if (scope === undefined)
|
|
220
|
-
return
|
|
231
|
+
return [];
|
|
232
|
+
const out = [];
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
const pushCallable = (def) => {
|
|
235
|
+
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
|
|
236
|
+
return;
|
|
237
|
+
if (seen.has(def.nodeId))
|
|
238
|
+
return;
|
|
239
|
+
seen.add(def.nodeId);
|
|
240
|
+
out.push(def);
|
|
241
|
+
};
|
|
221
242
|
const localBindings = scope.bindings.get(callableName);
|
|
222
243
|
if (localBindings !== undefined) {
|
|
223
244
|
for (const b of localBindings) {
|
|
224
|
-
|
|
225
|
-
return b.def;
|
|
226
|
-
}
|
|
245
|
+
pushCallable(b.def);
|
|
227
246
|
}
|
|
228
247
|
}
|
|
229
248
|
const importedBindings = lookupBindingsAt(currentId, callableName, scopes);
|
|
230
249
|
for (const b of importedBindings) {
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
pushCallable(b.def);
|
|
251
|
+
}
|
|
252
|
+
if (out.length > 0)
|
|
253
|
+
return out;
|
|
254
|
+
currentId = scope.parent;
|
|
255
|
+
}
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when ordinary
|
|
260
|
+
* unqualified lookup finds:
|
|
261
|
+
* - a name that is NOT a function or function template, OR
|
|
262
|
+
* - a block-scope function declaration that is NOT a using-declaration.
|
|
263
|
+
*
|
|
264
|
+
* Combined walker that stops at the **nearest scope** where `name` has any
|
|
265
|
+
* binding (callable or non-callable) and returns:
|
|
266
|
+
* - `callables`: Function/Method/Constructor defs found at that scope
|
|
267
|
+
* - `nonCallableFound`: a non-function binding was present (variable, class, etc.)
|
|
268
|
+
* - `blockScopeDeclFound`: a callable was found at a Function or Block scope
|
|
269
|
+
* (block-scope function declaration that blocks ADL)
|
|
270
|
+
*
|
|
271
|
+
* One pass, one stop — no divergence between callable collection and blocker
|
|
272
|
+
* detection.
|
|
273
|
+
*/
|
|
274
|
+
export function findCallableBindingsAndAdlBlocker(startScope, name, scopes) {
|
|
275
|
+
let currentId = startScope;
|
|
276
|
+
const visited = new Set();
|
|
277
|
+
while (currentId !== null) {
|
|
278
|
+
if (visited.has(currentId))
|
|
279
|
+
return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
|
|
280
|
+
visited.add(currentId);
|
|
281
|
+
const scope = scopes.scopeTree.getScope(currentId);
|
|
282
|
+
if (scope === undefined)
|
|
283
|
+
return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
|
|
284
|
+
const callables = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
let nonCallableFound = false;
|
|
287
|
+
let anyBinding = false;
|
|
288
|
+
const process = (def) => {
|
|
289
|
+
anyBinding = true;
|
|
290
|
+
if (def.type === 'Function' || def.type === 'Method' || def.type === 'Constructor') {
|
|
291
|
+
if (!seen.has(def.nodeId)) {
|
|
292
|
+
seen.add(def.nodeId);
|
|
293
|
+
callables.push(def);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
nonCallableFound = true;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const localBindings = scope.bindings.get(name);
|
|
301
|
+
if (localBindings !== undefined) {
|
|
302
|
+
for (const b of localBindings) {
|
|
303
|
+
process(b.def);
|
|
233
304
|
}
|
|
234
305
|
}
|
|
306
|
+
const importedBindings = lookupBindingsAt(currentId, name, scopes);
|
|
307
|
+
for (const b of importedBindings) {
|
|
308
|
+
process(b.def);
|
|
309
|
+
}
|
|
310
|
+
if (anyBinding) {
|
|
311
|
+
// ISO C++: a block-scope function declaration (Function or Block scope)
|
|
312
|
+
// that is NOT a using-declaration blocks ADL. If we found callables at
|
|
313
|
+
// a function/block scope, ADL must be suppressed.
|
|
314
|
+
const blockScopeDeclFound = callables.length > 0 && (scope.kind === 'Function' || scope.kind === 'Block');
|
|
315
|
+
return { callables, nonCallableFound, blockScopeDeclFound };
|
|
316
|
+
}
|
|
235
317
|
currentId = scope.parent;
|
|
236
318
|
}
|
|
237
|
-
return
|
|
319
|
+
return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
|
|
238
320
|
}
|
|
239
321
|
/**
|
|
240
322
|
* Populate `ownerId` on every def structurally owned by a Class
|
package/package.json
CHANGED