mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
Merge branch 'main' into feat/wiki
This commit is contained in:
commit
94c240acc9
25 changed files with 657 additions and 131 deletions
|
|
@ -312,12 +312,19 @@ const cCppExtractFunctionName = (
|
|||
return { funcName, label };
|
||||
};
|
||||
|
||||
/** Check if a C/C++ function_definition is inside a class or struct body.
|
||||
/** Check if a C/C++ function_definition is inside a class or struct body
|
||||
* (and NOT a friend declaration).
|
||||
* Used by cppLabelOverride to skip duplicate function captures
|
||||
* that are already covered by definition.method queries. */
|
||||
* that are already covered by definition.method queries.
|
||||
* Friend functions are free functions defined inside class bodies —
|
||||
* they must NOT be skipped (ISO C++ hidden-friend idiom). */
|
||||
function isCppInsideClassOrStruct(functionNode: SyntaxNode): boolean {
|
||||
let ancestor: SyntaxNode | null = functionNode?.parent ?? null;
|
||||
while (ancestor) {
|
||||
// Friend declarations: the function_definition is wrapped in
|
||||
// `friend_declaration` → `field_declaration_list` → class_specifier.
|
||||
// These are free functions, not methods — don't skip them.
|
||||
if (ancestor.type === 'friend_declaration') return false;
|
||||
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,8 @@
|
|||
* def with that simple name. Locally-declared function-pointer variables
|
||||
* (e.g. `void (*g)()`) and function parameters are excluded from this path.
|
||||
*
|
||||
* The current implementation also short-circuits to ADL only when ordinary lookup is empty
|
||||
* (`findCallableBindingInScope` returned undefined). ISO C++ would
|
||||
* normally merge ADL candidates with ordinary-lookup candidates and
|
||||
* run overload resolution over the union; V1 defers that merge to V2.
|
||||
* ADL candidates are merged with ordinary unqualified-lookup candidates
|
||||
* in the free-call fallback before overload narrowing.
|
||||
*
|
||||
* ## Parenthesized-name suppression
|
||||
*
|
||||
|
|
@ -72,10 +70,7 @@
|
|||
|
||||
import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import {
|
||||
isOverloadAmbiguousAfterNormalization,
|
||||
narrowOverloadCandidates,
|
||||
} from '../../scope-resolution/passes/overload-narrowing.js';
|
||||
import { isCppInlineNamespaceScope } from './inline-namespaces.js';
|
||||
|
||||
/**
|
||||
* Per-argument shape information collected at capture time. ADL fires for
|
||||
|
|
@ -114,13 +109,6 @@ const argInfoBySite = new Map<string, readonly CppAdlArgInfo[]>();
|
|||
const noAdlSites = new Set<string>();
|
||||
const classToNamespaceQualifiedName = new Map<string, string>();
|
||||
|
||||
/** Sentinel returned by `pickCppAdlCandidates` when ADL surfaces multiple
|
||||
* candidates that share normalized parameter types — the caller MUST
|
||||
* suppress (zero edges) rather than pick arbitrarily. Mirrors the
|
||||
* OVERLOAD_AMBIGUOUS contract from the receiver-bound path. */
|
||||
export const ADL_AMBIGUOUS = Symbol('ADL_AMBIGUOUS');
|
||||
export type AdlResult = SymbolDefinition | typeof ADL_AMBIGUOUS | undefined;
|
||||
|
||||
function siteKey(filePath: string, line: number, col: number): string {
|
||||
return `${filePath}:${line}:${col}`;
|
||||
}
|
||||
|
|
@ -173,16 +161,26 @@ export function populateCppAssociatedNamespaces(parsed: ParsedFile): void {
|
|||
classToNamespaceQualifiedName.set(def.nodeId, nsQName);
|
||||
}
|
||||
}
|
||||
|
||||
// Enum defs live in Namespace scopes directly (not inside Class scopes).
|
||||
// Map each Enum def to its enclosing namespace so ADL on enum-typed
|
||||
// arguments contributes the correct associated namespace.
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Namespace') continue;
|
||||
const nsQName = computeNamespaceQName(scope, scopesById);
|
||||
if (nsQName === '') continue;
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def.type !== 'Enum') continue;
|
||||
classToNamespaceQualifiedName.set(def.nodeId, nsQName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 ADL candidate picker. Returns:
|
||||
* - `SymbolDefinition` — exactly one ADL candidate (or unique survivor
|
||||
* after narrowing); caller emits the CALLS edge.
|
||||
* - `ADL_AMBIGUOUS` — multiple candidates with no disambiguator;
|
||||
* caller MUST suppress (zero edges).
|
||||
* - `undefined` — no ADL candidates; caller falls through to ordinary
|
||||
* `pickUniqueGlobalCallable` fallback.
|
||||
* ADL candidate collector. Returns:
|
||||
* - `readonly SymbolDefinition[]` — ADL candidates to merge with
|
||||
* ordinary unqualified lookup candidates.
|
||||
* - `undefined` — no ADL candidates.
|
||||
*
|
||||
* Fires only when:
|
||||
* - the call site is not in `noAdlSites` (parenthesized form), AND
|
||||
|
|
@ -192,14 +190,12 @@ export function populateCppAssociatedNamespaces(parsed: ParsedFile): void {
|
|||
export function pickCppAdlCandidates(
|
||||
site: {
|
||||
readonly name: string;
|
||||
readonly arity?: number;
|
||||
readonly argumentTypes?: readonly string[];
|
||||
readonly atRange: { startLine: number; startCol: number };
|
||||
},
|
||||
callerParsed: ParsedFile,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): AdlResult {
|
||||
): readonly SymbolDefinition[] | undefined {
|
||||
const key = siteKey(callerParsed.filePath, site.atRange.startLine, site.atRange.startCol);
|
||||
if (noAdlSites.has(key)) return undefined;
|
||||
const args = argInfoBySite.get(key);
|
||||
|
|
@ -219,6 +215,8 @@ export function pickCppAdlCandidates(
|
|||
// Walk every namespace scope in every parsed file; collect callable
|
||||
// ownedDefs whose enclosing namespace matches one of the associated
|
||||
// QNames AND whose simple name matches the call's name.
|
||||
// ISO C++: inline namespaces are transparent — candidates in inline
|
||||
// children of an associated namespace are also ADL-reachable.
|
||||
const candidates: SymbolDefinition[] = [];
|
||||
const seenKey = new Set<string>();
|
||||
for (const parsed of parsedFiles) {
|
||||
|
|
@ -227,7 +225,17 @@ export function pickCppAdlCandidates(
|
|||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Namespace') continue;
|
||||
const qName = computeNamespaceQName(scope, scopesById);
|
||||
if (!associatedNamespaces.has(qName)) continue;
|
||||
if (!associatedNamespaces.has(qName)) {
|
||||
// Check if this is an inline-namespace child of an associated NS.
|
||||
// ISO C++ inline namespaces are transparent for ADL: if the outer
|
||||
// namespace is in the associated set, candidates in the inline child
|
||||
// are also reachable.
|
||||
if (!isCppInlineNamespaceScope(scope.id)) continue;
|
||||
const parentScope = scope.parent !== null ? scopesById.get(scope.parent) : undefined;
|
||||
if (parentScope === undefined || parentScope.kind !== 'Namespace') continue;
|
||||
const parentQName = computeNamespaceQName(parentScope, scopesById);
|
||||
if (!associatedNamespaces.has(parentQName)) continue;
|
||||
}
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
|
||||
continue;
|
||||
|
|
@ -243,22 +251,56 @@ export function pickCppAdlCandidates(
|
|||
candidates.push(def);
|
||||
}
|
||||
}
|
||||
// ISO C++ `[basic.lookup.argdep]` §2: hidden friend functions declared
|
||||
// inside a class body are visible via ADL when the class is an associated
|
||||
// class. Scan Class scopes whose enclosing namespace is in the associated
|
||||
// set for callable ownedDefs matching the call name. This enables the
|
||||
// canonical "hidden friend" idiom:
|
||||
// struct Foo { friend void swap(Foo&, Foo&) {} };
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Class') continue;
|
||||
// Check if ANY class def in this scope has an associated namespace.
|
||||
let isAssociatedClass = false;
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
|
||||
const nsQName = classToNamespaceQualifiedName.get(def.nodeId);
|
||||
if (nsQName !== undefined && associatedNamespaces.has(nsQName)) {
|
||||
isAssociatedClass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isAssociatedClass) continue;
|
||||
// Also scan Function scopes that are direct children of this class
|
||||
// scope — friend function definitions create their own Function scope
|
||||
// underneath the Class scope.
|
||||
for (const childScope of parsed.scopes) {
|
||||
if (childScope.parent !== scope.id) continue;
|
||||
if (childScope.kind !== 'Function') continue;
|
||||
for (const def of childScope.ownedDefs) {
|
||||
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
|
||||
continue;
|
||||
}
|
||||
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
||||
if (simple !== site.name) continue;
|
||||
if (seenKey.has(def.nodeId)) continue;
|
||||
seenKey.add(def.nodeId);
|
||||
candidates.push(def);
|
||||
}
|
||||
}
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
|
||||
continue;
|
||||
}
|
||||
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
||||
if (simple !== site.name) continue;
|
||||
if (seenKey.has(def.nodeId)) continue;
|
||||
seenKey.add(def.nodeId);
|
||||
candidates.push(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidates.length === 0) return undefined;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
// Multi-candidate: narrow then check ambiguity. Reuses the OVERLOAD_AMBIGUOUS
|
||||
// sentinel contract from `overload-narrowing.ts` so int/long-collision-style
|
||||
// ambiguity also suppresses on the ADL path.
|
||||
const narrowed = narrowOverloadCandidates(candidates, site.arity, site.argumentTypes);
|
||||
if (narrowed.length === 1) return narrowed[0];
|
||||
if (narrowed.length === 0) return undefined;
|
||||
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) return ADL_AMBIGUOUS;
|
||||
// Multiple surviving candidates that aren't normalization-ambiguous —
|
||||
// ISO C++ would run overload resolution; V1 lacks conversion ranking so
|
||||
// suppress rather than pick arbitrarily. Mirrors `pickImplicitThisOverload`'s
|
||||
// unique-survivor requirement (see `pick-implicit-this-overload.test.ts`).
|
||||
return ADL_AMBIGUOUS;
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function collectAssociatedNamespacesForAdlArg(
|
||||
|
|
@ -272,8 +314,8 @@ function collectAssociatedNamespacesForAdlArg(
|
|||
addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, associatedNamespaces);
|
||||
|
||||
// Includes template-owner namespaces (e.g. `std` in std::vector<T>). If
|
||||
// that surfaces extra candidates, ADL_AMBIGUOUS suppression below prevents
|
||||
// arbitrary edge emission.
|
||||
// that surfaces extra candidates, merged-candidate overload narrowing in
|
||||
// free-call-fallback suppresses arbitrary edge emission.
|
||||
if (arg.templateNamespace.length > 0) associatedNamespaces.add(arg.templateNamespace);
|
||||
|
||||
for (const ns of arg.templateArgNamespaces) {
|
||||
|
|
@ -396,18 +438,27 @@ function findNamespaceDefInScope(scope: {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/** Find a class-like def by simple name across the workspace. V1
|
||||
* still arbitrary-picks the first class on collisions (multiple classes
|
||||
/** Find a class-like or enum def by simple name across the workspace.
|
||||
* V1 still arbitrary-picks the first match on collisions (multiple defs
|
||||
* share the simple name), but reports the collision so callers can avoid
|
||||
* amplifying that uncertainty (for example by skipping MRO expansion).
|
||||
* C++ ADL strictness would require full type-driven lookup. */
|
||||
* C++ ADL strictness would require full type-driven lookup.
|
||||
*
|
||||
* ISO C++ `[basic.lookup.argdep]` §2: enumerations contribute their
|
||||
* enclosing namespace to the associated set, just like class types. */
|
||||
function findCppClassDefBySimpleName(
|
||||
simpleName: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): { classDef: SymbolDefinition; ambiguous: boolean } | undefined {
|
||||
let firstMatch: SymbolDefinition | undefined;
|
||||
for (const def of scopes.defs.byId.values()) {
|
||||
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
|
||||
if (
|
||||
def.type !== 'Class' &&
|
||||
def.type !== 'Struct' &&
|
||||
def.type !== 'Interface' &&
|
||||
def.type !== 'Enum'
|
||||
)
|
||||
continue;
|
||||
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
||||
if (simple !== simpleName) continue;
|
||||
if (firstMatch === undefined) {
|
||||
|
|
|
|||
|
|
@ -25,22 +25,13 @@ import {
|
|||
clearCppDependentBases,
|
||||
isCppDependentBaseMember,
|
||||
} from './two-phase-lookup.js';
|
||||
import {
|
||||
populateCppAssociatedNamespaces,
|
||||
clearCppAdlState,
|
||||
pickCppAdlCandidates,
|
||||
ADL_AMBIGUOUS,
|
||||
} from './adl.js';
|
||||
import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates } from './adl.js';
|
||||
import {
|
||||
clearCppInlineNamespaces,
|
||||
populateCppInlineNamespaceScopes,
|
||||
resolveCppQualifiedNamespaceMember,
|
||||
} from './inline-namespaces.js';
|
||||
import { populateCppRangeBindings } from './range-bindings.js';
|
||||
import {
|
||||
isOverloadAmbiguousAfterNormalization,
|
||||
narrowOverloadCandidates,
|
||||
} from '../../scope-resolution/passes/overload-narrowing.js';
|
||||
|
||||
/**
|
||||
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
||||
|
|
@ -222,12 +213,12 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
},
|
||||
|
||||
// C++ argument-dependent / Koenig lookup (U2 of plan 2026-05-13-001).
|
||||
// Fires after `findCallableBindingInScope` returns undefined; surfaces
|
||||
// candidates from the associated namespaces of class-typed arguments.
|
||||
// Contributes candidates from associated namespaces of class-typed
|
||||
// arguments; caller merges with ordinary unqualified lookup candidates.
|
||||
// Current boundary: class-typed value/pointer/reference args and template
|
||||
// specializations with explicit type arguments contribute associated
|
||||
// namespaces. Function-pointer args, base-class associated namespaces,
|
||||
// and full ordinary+ADL merge remain excluded.
|
||||
// namespaces. Function-pointer args and full conversion-ranking remain
|
||||
// excluded.
|
||||
resolveAdlCandidates: (site, callerParsed, scopes, parsedFiles) => {
|
||||
// `using ns::name;` introduces `name` into ordinary unqualified lookup.
|
||||
// For template-class method bodies, lexical scope walks can miss this
|
||||
|
|
@ -249,16 +240,21 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
seenUsing.add(member.nodeId);
|
||||
usingNamedHits.push(member);
|
||||
}
|
||||
if (usingNamedHits.length > 0) {
|
||||
const narrowed = narrowOverloadCandidates(usingNamedHits, site.arity, site.argumentTypes);
|
||||
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) return 'ambiguous';
|
||||
if (narrowed.length === 1) return narrowed[0];
|
||||
if (narrowed.length > 1) return 'ambiguous';
|
||||
const adlHits = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
|
||||
if (usingNamedHits.length === 0) return adlHits;
|
||||
if (adlHits === undefined || adlHits.length === 0) return usingNamedHits;
|
||||
const merged: SymbolDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const hit of usingNamedHits) {
|
||||
seen.add(hit.nodeId);
|
||||
merged.push(hit);
|
||||
}
|
||||
|
||||
const result = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
|
||||
if (result === ADL_AMBIGUOUS) return 'ambiguous';
|
||||
return result;
|
||||
for (const hit of adlHits) {
|
||||
if (seen.has(hit.nodeId)) continue;
|
||||
seen.add(hit.nodeId);
|
||||
merged.push(hit);
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
|
||||
// C++ qualified namespace-member resolution (U5 of plan 2026-05-13-001).
|
||||
|
|
|
|||
|
|
@ -576,16 +576,15 @@ export interface ScopeResolver {
|
|||
* Optional argument-dependent-lookup (ADL / Koenig lookup) hook for
|
||||
* languages with C++-style associated-namespace candidate addition.
|
||||
*
|
||||
* Runs in the free-call fallback AFTER `findCallableBindingInScope`
|
||||
* returns `undefined` and BEFORE `pickUniqueGlobalCallable`. The hook
|
||||
* inspects the call site's argument types, computes the associated
|
||||
* namespace set, and returns either:
|
||||
* - a unique `SymbolDefinition` — emit the CALLS edge to it.
|
||||
* - `'ambiguous'` — multiple candidates share normalized parameter
|
||||
* types; the caller MUST suppress (zero edges). Mirrors the
|
||||
* OVERLOAD_AMBIGUOUS sentinel from `overload-narrowing.ts`.
|
||||
* - `undefined` — no ADL candidates; caller falls through to the
|
||||
* global free-call fallback (`pickUniqueGlobalCallable`).
|
||||
* Runs in the free-call fallback alongside ordinary unqualified lookup.
|
||||
* The fallback merges ordinary candidates with ADL candidates and applies
|
||||
* overload narrowing over the union.
|
||||
*
|
||||
* The hook inspects the call site's argument types, computes the
|
||||
* associated namespace set, and returns either:
|
||||
* - an array of candidate `SymbolDefinition`s to add to the
|
||||
* ordinary-lookup candidate pool.
|
||||
* - `undefined` when ADL contributes no candidates.
|
||||
*
|
||||
* Languages without C++-style ADL leave this undefined. The
|
||||
* cross-language contract is "additive tier" — defining the hook never
|
||||
|
|
@ -601,7 +600,7 @@ export interface ScopeResolver {
|
|||
callerParsed: ParsedFile,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
) => SymbolDefinition | 'ambiguous' | undefined;
|
||||
) => readonly SymbolDefinition[] | undefined;
|
||||
|
||||
/**
|
||||
* Optional resolver for qualified-receiver member calls where the
|
||||
|
|
|
|||
|
|
@ -24,8 +24,15 @@ import type { SemanticModel } from '../../model/semantic-model.js';
|
|||
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
||||
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
||||
import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
|
||||
import { findCallableBindingInScope, findClassBindingInScope } from '../scope/walkers.js';
|
||||
import { narrowOverloadCandidates } from './overload-narrowing.js';
|
||||
import {
|
||||
findCallableBindingInScope,
|
||||
findCallableBindingsAndAdlBlocker,
|
||||
findClassBindingInScope,
|
||||
} from '../scope/walkers.js';
|
||||
import {
|
||||
isOverloadAmbiguousAfterNormalization,
|
||||
narrowOverloadCandidates,
|
||||
} from './overload-narrowing.js';
|
||||
|
||||
export function emitFreeCallFallback(
|
||||
graph: KnowledgeGraph,
|
||||
|
|
@ -55,7 +62,7 @@ export function emitFreeCallFallback(
|
|||
callerParsed: ParsedFile,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
) => SymbolDefinition | 'ambiguous' | undefined;
|
||||
) => readonly SymbolDefinition[] | undefined;
|
||||
} = {},
|
||||
): number {
|
||||
let emitted = 0;
|
||||
|
|
@ -86,41 +93,80 @@ export function emitFreeCallFallback(
|
|||
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model);
|
||||
}
|
||||
if (fnDef === undefined) {
|
||||
fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
|
||||
}
|
||||
// V1 ADL tier (C++ Koenig lookup, opt-in via provider.resolveAdlCandidates).
|
||||
// Fires only when ordinary lookup is empty — V1 limitation per
|
||||
// plan 2026-05-13-001 U2; ISO C++ would merge ADL with ordinary lookup
|
||||
// and run overload resolution over the union.
|
||||
//
|
||||
// Sentinel 'ambiguous': ADL surfaced multiple candidates with
|
||||
// identical normalized parameter types (mirrors OVERLOAD_AMBIGUOUS).
|
||||
// We mark the site handled so `emit-references` does not retry, and
|
||||
// continue to the next site without emitting an edge.
|
||||
if (fnDef === undefined && options.resolveAdlCandidates !== undefined) {
|
||||
const adlResult = options.resolveAdlCandidates(
|
||||
{
|
||||
name: site.name,
|
||||
arity: site.arity,
|
||||
argumentTypes: site.argumentTypes,
|
||||
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
||||
},
|
||||
parsed,
|
||||
scopes,
|
||||
parsedFiles,
|
||||
);
|
||||
if (adlResult === 'ambiguous') {
|
||||
handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`);
|
||||
continue;
|
||||
}
|
||||
if (adlResult !== undefined) {
|
||||
fnDef = adlResult;
|
||||
if (options.resolveAdlCandidates === undefined) {
|
||||
fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
|
||||
} else {
|
||||
// ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when
|
||||
// ordinary lookup finds a non-function name (variable, class, enum)
|
||||
// or a block-scope function declaration (not via using-declaration)
|
||||
// at the nearest scope where the name exists.
|
||||
const {
|
||||
callables: ordinary,
|
||||
nonCallableFound,
|
||||
blockScopeDeclFound,
|
||||
} = findCallableBindingsAndAdlBlocker(site.inScope, site.name, scopes);
|
||||
const adlSuppressed = nonCallableFound || blockScopeDeclFound;
|
||||
const adl = adlSuppressed
|
||||
? undefined
|
||||
: options.resolveAdlCandidates(
|
||||
{
|
||||
name: site.name,
|
||||
arity: site.arity,
|
||||
argumentTypes: site.argumentTypes,
|
||||
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
||||
},
|
||||
parsed,
|
||||
scopes,
|
||||
parsedFiles,
|
||||
);
|
||||
|
||||
// Preserve existing ordinary-lookup behavior when ADL contributed
|
||||
// no candidates.
|
||||
if (adl === undefined || adl.length === 0) {
|
||||
fnDef = ordinary[0];
|
||||
} else {
|
||||
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
||||
const merged: SymbolDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (defs: readonly SymbolDefinition[]): void => {
|
||||
for (const d of defs) {
|
||||
if (seen.has(d.nodeId)) continue;
|
||||
seen.add(d.nodeId);
|
||||
merged.push(d);
|
||||
}
|
||||
};
|
||||
push(ordinary);
|
||||
push(adl);
|
||||
|
||||
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes);
|
||||
if (narrowed.length === 1) {
|
||||
fnDef = narrowed[0];
|
||||
} else if (narrowed.length === 0) {
|
||||
// ADL contributed candidates, but none survived arity/type
|
||||
// narrowing. Treat as handled to avoid global-name fallback
|
||||
// binding to the same mismatched symbol by simple-name
|
||||
// uniqueness.
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
} else if (narrowed.length > 1) {
|
||||
// Suppress ambiguous overload calls (emit zero edges) when
|
||||
// merged ordinary+ADL candidate sets cannot be disambiguated.
|
||||
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
// Multiple survivors remain but no conversion-ranking step
|
||||
// exists yet; suppress instead of picking arbitrarily.
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// V1: pickUniqueGlobalCallable ignores import context — resolves to any
|
||||
// globally-unique callable. False cross-package edges are possible when
|
||||
// the caller does not import the target package. Same-package calls are
|
||||
// caught by findCallableBindingInScope above before reaching here.
|
||||
// usually caught by nearest-scope lookup before reaching here.
|
||||
if (fnDef === undefined && options.allowGlobalFallback === true) {
|
||||
fnDef = pickUniqueGlobalCallable(
|
||||
site.name,
|
||||
|
|
|
|||
|
|
@ -226,33 +226,132 @@ export function findCallableBindingInScope(
|
|||
callableName: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): SymbolDefinition | undefined {
|
||||
return findAllCallableBindingsInScope(startScope, callableName, scopes)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up all callable bindings (Function/Method/Constructor) by name
|
||||
* from the nearest scope in the chain that binds `callableName`.
|
||||
*
|
||||
* Preserves the original scope-walk boundary used by
|
||||
* `findCallableBindingInScope`: once any callable binding is found in a
|
||||
* scope, outer scopes are not consulted.
|
||||
*/
|
||||
export function findAllCallableBindingsInScope(
|
||||
startScope: ScopeId,
|
||||
callableName: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): readonly SymbolDefinition[] {
|
||||
let currentId: ScopeId | null = startScope;
|
||||
const visited = new Set<ScopeId>();
|
||||
while (currentId !== null) {
|
||||
if (visited.has(currentId)) return undefined;
|
||||
if (visited.has(currentId)) return [];
|
||||
visited.add(currentId);
|
||||
const scope = scopes.scopeTree.getScope(currentId);
|
||||
if (scope === undefined) return undefined;
|
||||
if (scope === undefined) return [];
|
||||
|
||||
const out: SymbolDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
const pushCallable = (def: SymbolDefinition): void => {
|
||||
if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') return;
|
||||
if (seen.has(def.nodeId)) return;
|
||||
seen.add(def.nodeId);
|
||||
out.push(def);
|
||||
};
|
||||
|
||||
const localBindings = scope.bindings.get(callableName);
|
||||
if (localBindings !== undefined) {
|
||||
for (const b of localBindings) {
|
||||
if (b.def.type === 'Function' || b.def.type === 'Method' || b.def.type === 'Constructor') {
|
||||
return b.def;
|
||||
}
|
||||
pushCallable(b.def);
|
||||
}
|
||||
}
|
||||
|
||||
const importedBindings = lookupBindingsAt(currentId, callableName, scopes);
|
||||
for (const b of importedBindings) {
|
||||
if (b.def.type === 'Function' || b.def.type === 'Method' || b.def.type === 'Constructor') {
|
||||
return b.def;
|
||||
pushCallable(b.def);
|
||||
}
|
||||
|
||||
if (out.length > 0) return out;
|
||||
currentId = scope.parent;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when ordinary
|
||||
* unqualified lookup finds:
|
||||
* - a name that is NOT a function or function template, OR
|
||||
* - a block-scope function declaration that is NOT a using-declaration.
|
||||
*
|
||||
* Combined walker that stops at the **nearest scope** where `name` has any
|
||||
* binding (callable or non-callable) and returns:
|
||||
* - `callables`: Function/Method/Constructor defs found at that scope
|
||||
* - `nonCallableFound`: a non-function binding was present (variable, class, etc.)
|
||||
* - `blockScopeDeclFound`: a callable was found at a Function or Block scope
|
||||
* (block-scope function declaration that blocks ADL)
|
||||
*
|
||||
* One pass, one stop — no divergence between callable collection and blocker
|
||||
* detection.
|
||||
*/
|
||||
export function findCallableBindingsAndAdlBlocker(
|
||||
startScope: ScopeId,
|
||||
name: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): {
|
||||
callables: readonly SymbolDefinition[];
|
||||
nonCallableFound: boolean;
|
||||
blockScopeDeclFound: boolean;
|
||||
} {
|
||||
let currentId: ScopeId | null = startScope;
|
||||
const visited = new Set<ScopeId>();
|
||||
while (currentId !== null) {
|
||||
if (visited.has(currentId))
|
||||
return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
|
||||
visited.add(currentId);
|
||||
const scope = scopes.scopeTree.getScope(currentId);
|
||||
if (scope === undefined)
|
||||
return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
|
||||
|
||||
const callables: SymbolDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
let nonCallableFound = false;
|
||||
let anyBinding = false;
|
||||
|
||||
const process = (def: SymbolDefinition): void => {
|
||||
anyBinding = true;
|
||||
if (def.type === 'Function' || def.type === 'Method' || def.type === 'Constructor') {
|
||||
if (!seen.has(def.nodeId)) {
|
||||
seen.add(def.nodeId);
|
||||
callables.push(def);
|
||||
}
|
||||
} else {
|
||||
nonCallableFound = true;
|
||||
}
|
||||
};
|
||||
|
||||
const localBindings = scope.bindings.get(name);
|
||||
if (localBindings !== undefined) {
|
||||
for (const b of localBindings) {
|
||||
process(b.def);
|
||||
}
|
||||
}
|
||||
|
||||
const importedBindings = lookupBindingsAt(currentId, name, scopes);
|
||||
for (const b of importedBindings) {
|
||||
process(b.def);
|
||||
}
|
||||
|
||||
if (anyBinding) {
|
||||
// ISO C++: a block-scope function declaration (Function or Block scope)
|
||||
// that is NOT a using-declaration blocks ADL. If we found callables at
|
||||
// a function/block scope, ADL must be suppressed.
|
||||
const blockScopeDeclFound =
|
||||
callables.length > 0 && (scope.kind === 'Function' || scope.kind === 'Block');
|
||||
return { callables, nonCallableFound, blockScopeDeclFound };
|
||||
}
|
||||
currentId = scope.parent;
|
||||
}
|
||||
return undefined;
|
||||
return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
13
gitnexus/test/fixtures/lang-resolution/cpp-adl-block-scope-decl-blocks/app.cpp
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-adl-block-scope-decl-blocks/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
// Block-scope function declaration (not via using-declaration).
|
||||
// Per ISO C++ [basic.lookup.argdep], this suppresses ADL — even
|
||||
// though `e` is audit::Event, audit::record should NOT be discovered.
|
||||
void record(int);
|
||||
|
||||
audit::Event e;
|
||||
record(e);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-block-scope-decl-blocks/audit.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-block-scope-decl-blocks/audit.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace audit {
|
||||
struct Event {};
|
||||
void record(Event e);
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-enum-arg/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-enum-arg/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "color.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
color::Channel ch = color::Channel::R;
|
||||
serialize(ch);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-enum-arg/color.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-enum-arg/color.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace color {
|
||||
enum class Channel { R, G, B };
|
||||
void serialize(Channel c);
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-hidden-friend/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-hidden-friend/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "lib.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
lib::Foo f;
|
||||
process(f);
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-hidden-friend/lib.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-hidden-friend/lib.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
namespace lib {
|
||||
struct Foo {
|
||||
friend void process(Foo& f) {}
|
||||
};
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-inline-ns-expansion/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-inline-ns-expansion/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
audit::Event e;
|
||||
record(e);
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/cpp-adl-inline-ns-expansion/audit.h
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-adl-inline-ns-expansion/audit.h
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
namespace audit {
|
||||
struct Event {};
|
||||
|
||||
inline namespace v1 {
|
||||
void record(Event e);
|
||||
}
|
||||
}
|
||||
|
||||
namespace other {
|
||||
void record(int x);
|
||||
}
|
||||
21
gitnexus/test/fixtures/lang-resolution/cpp-adl-inner-callable-outer-noncallable/app.cpp
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/cpp-adl-inner-callable-outer-noncallable/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include "data.h"
|
||||
|
||||
// Outer namespace has a non-callable `swap` (variable).
|
||||
namespace app {
|
||||
int swap = 0;
|
||||
|
||||
namespace inner {
|
||||
// Inner namespace re-declares `swap` as a function.
|
||||
void swap(int, int);
|
||||
|
||||
void run() {
|
||||
data::Pair a, b;
|
||||
// Ordinary lookup finds `inner::swap(int,int)` first (callable at
|
||||
// nearest scope). The outer `app::swap` variable should NOT suppress
|
||||
// ADL because ordinary lookup stopped at `inner` scope where a
|
||||
// callable was found. ADL contributes `data::swap(Pair&,Pair&)` which
|
||||
// wins via argTypes narrowing.
|
||||
swap(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-inner-callable-outer-noncallable/data.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-inner-callable-outer-noncallable/data.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace data {
|
||||
struct Pair {};
|
||||
void swap(Pair& a, Pair& b);
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-merge-nonempty-ordinary/app.cpp
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-merge-nonempty-ordinary/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#include "data.h"
|
||||
|
||||
namespace app {
|
||||
void swap(int a, int b);
|
||||
|
||||
void run() {
|
||||
data::Pair a;
|
||||
data::Pair b;
|
||||
swap(a, b);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-merge-nonempty-ordinary/data.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-merge-nonempty-ordinary/data.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace data {
|
||||
struct Pair {};
|
||||
void swap(Pair& a, Pair& b);
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-merged-narrow-zero/alpha.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-merged-narrow-zero/alpha.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace alpha {
|
||||
struct Token {};
|
||||
void probe(Token t);
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-merged-narrow-zero/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-merged-narrow-zero/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "alpha.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
alpha::Token t;
|
||||
probe(t, 42);
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-adl-non-function-blocks/app.cpp
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-adl-non-function-blocks/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
int record = 0;
|
||||
|
||||
void run() {
|
||||
audit::Event e;
|
||||
record(e);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-non-function-blocks/audit.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-non-function-blocks/audit.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace audit {
|
||||
struct Event {};
|
||||
void record(Event e);
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ namespace caller {
|
|||
// worker() in BOTH alpha and beta namespaces, so BOTH are added to the
|
||||
// associated set. run_with() exists in both namespaces as well, so the
|
||||
// lookup yields two candidates (alpha::run_with, beta::run_with).
|
||||
// GitNexus ADL_AMBIGUOUS sentinel suppresses the edge — the caller emits
|
||||
// Merged-narrowing ambiguity suppression in free-call-fallback emits
|
||||
// zero CALLS edges rather than picking one arbitrarily.
|
||||
run_with(worker);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2127,6 +2127,24 @@ describe('C++ ADL — basic associated-namespace closure', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — merges with non-empty ordinary lookup', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-merge-nonempty-ordinary'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('swap(a, b) prefers data::swap(Pair&, Pair&) over app::swap(int, int)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const swapCalls = calls.filter((c) => c.source === 'run' && c.target === 'swap');
|
||||
expect(swapCalls.length).toBe(1);
|
||||
expect(swapCalls[0].targetFilePath).toContain('data.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — base-class associated namespaces', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
|
|
@ -2417,13 +2435,146 @@ describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUO
|
|||
// 'int', so both candidates have parameterTypes ['Token', 'int'].
|
||||
// narrowOverloadCandidates can't disambiguate (arg-types are
|
||||
// ['', 'int']), and isOverloadAmbiguousAfterNormalization detects
|
||||
// the collision → ADL_AMBIGUOUS sentinel → caller suppresses.
|
||||
// the collision in merged ordinary+ADL narrowing, so fallback suppresses.
|
||||
// count=1 is the bug (arbitrary first-pick); count=2 would require
|
||||
// an ambiguous-target edge model GitNexus does not have.
|
||||
expect(processCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — merged narrowing to zero suppresses global fallback', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-merged-narrow-zero'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('probe(t, 42) emits zero CALLS when ADL contributes only arity-mismatched candidates', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const probeCalls = calls.filter((c) => c.source === 'run' && c.target === 'probe');
|
||||
// ADL surfaces alpha::probe(Token), but call arity is 2 (`probe(t, 42)`),
|
||||
// so merged overload narrowing yields zero survivors. The site is treated
|
||||
// as handled and must NOT fall through to global simple-name fallback.
|
||||
expect(probeCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADL V2 — ISO C++ `[basic.lookup.argdep]` §2: enum types contribute their
|
||||
// enclosing namespace to the associated set, just like class types.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ ADL — enum-typed argument contributes enclosing namespace', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-enum-arg'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('serialize(ch) where ch is color::Channel resolves to color::serialize via ADL', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const serializeCalls = calls.filter((c) => c.source === 'run' && c.target === 'serialize');
|
||||
// Exactly 1: ordinary lookup in app::run finds nothing for `serialize`.
|
||||
// ADL surfaces color::serialize because color::Channel's enclosing
|
||||
// namespace is `color`. Before the enum gap fix, this was 0.
|
||||
expect(serializeCalls.length).toBe(1);
|
||||
expect(serializeCalls[0].targetFilePath).toContain('color.h');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADL V2 — ISO C++ `[basic.lookup.argdep]` §2: "hidden friend" functions
|
||||
// declared inside a class body are visible via ADL. They are not namespace-
|
||||
// scope declarations (owned by the class scope in tree-sitter-cpp), so they
|
||||
// require scanning associated class scopes in addition to namespace scopes.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ ADL — hidden friend function visible via ADL', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-hidden-friend'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('process(f) where f is lib::Foo resolves to hidden friend process(Foo&) via ADL', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const processCalls = calls.filter((c) => c.source === 'run' && c.target === 'process');
|
||||
// Exactly 1: process(Foo&) is a hidden friend declared inside Foo's
|
||||
// class body. Ordinary namespace-scope lookup won't find it — only ADL
|
||||
// scanning the associated class's ownedDefs can surface it.
|
||||
expect(processCalls.length).toBe(1);
|
||||
expect(processCalls[0].targetFilePath).toContain('lib.h');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADL V2 — ISO C++ `[basic.lookup.unqual]` §7: non-function ordinary lookup
|
||||
// result blocks ADL. If the name resolves to a variable/class/enum in scope,
|
||||
// ADL does not fire even if class-typed arguments are present.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ ADL — non-function ordinary lookup suppresses ADL', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-non-function-blocks'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(e) emits zero CALLS when a variable named record exists in scope', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
// ISO C++: `int record = 0;` in namespace app means ordinary lookup
|
||||
// finds a non-function entity. ADL should be suppressed — even though
|
||||
// `e` is audit::Event, audit::record should NOT be discovered.
|
||||
expect(recordCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — inner callable + outer non-callable: ADL not suppressed', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-inner-callable-outer-noncallable'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('swap(a,b) resolves to data::swap when inner scope has callable swap and outer has variable', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const swapCalls = calls.filter((c) => c.source === 'run' && c.target === 'swap');
|
||||
// Ordinary lookup finds `inner::swap(int,int)` at the nearest scope.
|
||||
// The outer `app::swap` (variable) does NOT suppress ADL because
|
||||
// ordinary lookup stopped at the inner scope. ADL contributes
|
||||
// data::swap(Pair&,Pair&) which wins via argTypes narrowing.
|
||||
expect(swapCalls.length).toBe(1);
|
||||
expect(swapCalls[0].targetFilePath).toContain('data.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — block-scope function declaration suppresses ADL', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-block-scope-decl-blocks'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(e) emits zero CALLS when a block-scope function declaration exists', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
// ISO C++ [basic.lookup.argdep]: a block-scope function declaration
|
||||
// (not via using-declaration) suppresses ADL — even though `e` is
|
||||
// audit::Event, audit::record should NOT be discovered.
|
||||
expect(recordCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADL V2 — free-function reference args contribute their namespace.
|
||||
//
|
||||
|
|
@ -2577,8 +2728,8 @@ describe('C++ ADL — unqualified free-function ref with namespace collision', (
|
|||
const runWithCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_with');
|
||||
// Unqualified `worker` → workspace scan finds alpha::worker and beta::worker.
|
||||
// Both alpha and beta are added to the associated set. run_with() exists in
|
||||
// both namespaces → two candidates → ADL_AMBIGUOUS sentinel → zero CALLS
|
||||
// edges (suppressed rather than arbitrary pick).
|
||||
// both namespaces → two candidates → merged narrowing suppression →
|
||||
// zero CALLS edges (suppressed rather than arbitrary pick).
|
||||
expect(runWithCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -2673,6 +2824,29 @@ describe('C++ inline namespace — ADL participation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — inline namespace expansion in associated set', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-inline-ns-expansion'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(e) resolves to audit::v1::record when Event is in outer audit and record is in inline v1 (arity-disambiguated from other::record(int))', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
// ISO C++: inline namespaces are transparent — candidates in
|
||||
// `audit::v1` are visible as if declared at `audit` level. With a
|
||||
// competing `other::record(int)` (different arity), the merged
|
||||
// ordinary+ADL overload narrowing must select `audit::v1::record(Event)`
|
||||
// since it's the only arity-matching candidate for `record(e)`.
|
||||
expect(recordCalls.length).toBe(1);
|
||||
expect(recordCalls[0].targetFilePath).toContain('audit.h');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 5 (follow-up plan 2026-05-13-001): cross-unit composition tests.
|
||||
// Lock in correct interaction between U1 (super-receiver context), U2 (ADL),
|
||||
|
|
|
|||
|
|
@ -122,14 +122,18 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
// (PR #1520 review follow-up plan 2026-05-13-001 U3); backporting
|
||||
// is out of scope.
|
||||
'Derived<T>::g() -> f() does NOT bind to Base<T>::f (dependent base)',
|
||||
// The legacy DAG path has no ADL_AMBIGUOUS suppression sentinel.
|
||||
// The legacy DAG path does not apply merged ordinary+ADL narrowing
|
||||
// with ambiguity suppression.
|
||||
// When ADL surfaces multiple overloads that collide after C++
|
||||
// int/long normalization, legacy picks the first match arbitrarily.
|
||||
// The scope-resolver path suppresses via the ADL_AMBIGUOUS sentinel
|
||||
// (mirroring OVERLOAD_AMBIGUOUS for receiver-bound paths). Scope-
|
||||
// resolver-only correctness win (PR #1520 review follow-up plan
|
||||
// The scope-resolver path suppresses in free-call-fallback after
|
||||
// merged-candidate overload narrowing. Scope-resolver-only
|
||||
// correctness win (PR #1520 review follow-up plan
|
||||
// 2026-05-13-001 U2); backporting is out of scope.
|
||||
'process(t, 42) emits zero CALLS edges when ADL surfaces process(Token,int)/process(Token,long) (collide after C++ int normalization)',
|
||||
// Legacy DAG path does not merge ordinary and ADL candidate sets for
|
||||
// non-empty ordinary lookup, so it misses ADL's better-match overload.
|
||||
'swap(a, b) prefers data::swap(Pair&, Pair&) over app::swap(int, int)',
|
||||
// The legacy DAG path has no qualified namespace-member resolver
|
||||
// and no inline-namespace awareness. For the versioned fixture
|
||||
// (`outer::v1::foo` inline, `outer::v0::foo` not), the registry-
|
||||
|
|
@ -180,6 +184,13 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
// Scope-resolver-only correctness wins; backporting is out of scope.
|
||||
'process(data::value) emits zero CALLS edges \u2014 data::value is a variable, not a function',
|
||||
'run_with(callback) emits zero CALLS edges when callback is a parameter, not a function reference',
|
||||
// PR #1599 adversarial review findings: nearest-scope ADL blocker
|
||||
// semantics and block-scope function declaration ADL suppression are
|
||||
// scope-resolver-only. The legacy DAG has no scope-aware ADL blocker
|
||||
// detection; it falls back to `pickUniqueGlobalCallable`. Scope-
|
||||
// resolver-only correctness wins; backporting is out of scope.
|
||||
'swap(a,b) resolves to data::swap when inner scope has callable swap and outer has variable',
|
||||
'record(e) emits zero CALLS when a block-scope function declaration exists',
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue