Merge branch 'feat/Desktop-app' of https://github.com/prajapatisparsh/GitNexus into feat/Desktop-app

This commit is contained in:
Sparsh 2026-05-16 10:55:43 +05:30
commit bc7729b5e9
43 changed files with 1316 additions and 155 deletions

View file

@ -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;
}

View file

@ -21,14 +21,28 @@
* - `audit::Event& r`, `audit::Event&& rr`
* - `std::vector<audit::Event>` (template namespace + template-arg namespaces)
*
* Function-pointer arguments and the rest of the full closure are still
* deliberately excluded. V2 additionally walks class ancestors (via MRO),
* so base-class enclosing namespaces also contribute associated namespaces.
* V2 additionally walks class ancestors (via MRO), so base-class enclosing
* namespaces also contribute associated namespaces.
*
* 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.
* **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
* function reference like `utils::worker` contributes `utils` to the associated
* set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
* to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
* entities for function-type arguments come from the **parameter types and return
* type** of each function in the overload set NOT the function's enclosing
* namespace. For `void worker()`, the standard-compliant associated set is empty.
* GitNexus instead contributes the enclosing namespace of any Function/Method
* def whose simple name matches, because it enables the dominant real-world ADL
* pattern at reasonable precision cost.
*
* For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
* workspace lookup (only contributed when a Function/Method named `worker` exists
* in `utils`). For unqualified refs the workspace is searched for any Function
* def with that simple name. Locally-declared function-pointer variables
* (e.g. `void (*g)()`) and function parameters are excluded from this path.
*
* ADL candidates are merged with ordinary unqualified-lookup candidates
* in the free-call fallback before overload narrowing.
*
* ## Parenthesized-name suppression
*
@ -56,15 +70,13 @@
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
* arguments where `simpleClassName !== ''`, including class pointers and
* references whose declarator chain resolves to a named class type.
* Free-function reference arguments use `functionRefText`.
*/
export interface CppAdlArgInfo {
/** Simple class-like type name (last segment of qualified name); empty
@ -82,19 +94,21 @@ export interface CppAdlArgInfo {
/** Enclosing namespaces extracted from explicit type template arguments,
* recursively bounded. */
readonly templateArgNamespaces: readonly string[];
/** When set, the arg is a potential free-function reference (not a locally-
* declared function-pointer variable or function parameter). Contains the
* identifier text as written in source (e.g. `"utils::worker"` or
* `"worker"`). GitNexus approximation: the function's enclosing namespace
* is contributed to the ADL associated set. For qualified refs a workspace
* lookup confirms a Function/Method with that simple name exists in the
* namespace before contributing; for unqualified refs every namespace
* containing a matching Function/Method def is contributed. */
readonly functionRefText?: string;
}
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}`;
}
@ -147,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
@ -166,29 +190,33 @@ 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);
if (args === undefined || args.length === 0) return undefined;
// Collect associated namespace QNames from every participating class-typed arg.
// Collect associated namespace QNames from every participating class-typed arg
// and from function-reference args.
const associatedNamespaces = new Set<string>();
for (const arg of args) {
collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
if (arg.functionRefText !== undefined) {
collectFunctionRefNamespaces(arg.functionRefText, parsedFiles, associatedNamespaces);
}
}
if (associatedNamespaces.size === 0) return undefined;
// 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) {
@ -197,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;
@ -213,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(
@ -242,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) {
@ -366,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) {
@ -389,3 +470,71 @@ function findCppClassDefBySimpleName(
if (firstMatch === undefined) return undefined;
return { classDef: firstMatch, ambiguous: false };
}
/**
* Contribute associated namespaces for a function-reference argument.
*
* - **Qualified refs** (`utils::worker`, `outer::inner::fn`): the namespace
* is extracted from the qualifier text (converting `::` to `.` for dot-joined
* QName matching). A workspace lookup then **verifies** that a Function or
* Method def named `worker` (the simple name after the last `::`) actually
* exists in the extracted namespace. This prevents false positives from
* namespace-qualified variables, enum values, and static data members, which
* also produce `qualified_identifier` AST nodes in tree-sitter-cpp (the
* AST node type alone does not distinguish functions from non-function names).
* - **Unqualified refs** (`worker`): the workspace is searched for any
* Function/Method def whose simple name matches. Every distinct enclosing
* namespace found is added overloads across the same namespace produce
* a single entry; GitNexus does not select a specific overload at this stage.
*/
function collectFunctionRefNamespaces(
refText: string,
parsedFiles: readonly ParsedFile[],
out: Set<string>,
): void {
const colonIdx = refText.lastIndexOf('::');
if (colonIdx !== -1) {
// Qualified ref: extract namespace prefix and normalise :: → dot notation.
const nsText = refText.slice(0, colonIdx).replace(/::/g, '.');
if (nsText === '') return;
const simpleName = refText.slice(colonIdx + 2);
// Verify that a Function/Method named `simpleName` exists in `nsText`.
// Without this guard every `a::b` qualified_identifier arg (variable,
// enum value, static member, type alias) would blindly contribute `a`
// to the associated set and risk a false-positive CALLS edge.
for (const parsed of parsedFiles) {
const scopesById = new Map<ScopeId, (typeof parsed.scopes)[number]>();
for (const sc of parsed.scopes) scopesById.set(sc.id, sc);
for (const scope of parsed.scopes) {
if (scope.kind !== 'Namespace') continue;
if (computeNamespaceQName(scope, scopesById) !== nsText) continue;
for (const def of scope.ownedDefs) {
if (def.type !== 'Function' && def.type !== 'Method') continue;
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
if (simple === simpleName) {
out.add(nsText);
return; // Namespace confirmed; no need to scan further files.
}
}
}
}
return;
}
// Unqualified: search all namespace scopes for a Function def with this
// simple name and contribute its enclosing namespace.
for (const parsed of parsedFiles) {
const scopesById = new Map<ScopeId, (typeof parsed.scopes)[number]>();
for (const sc of parsed.scopes) scopesById.set(sc.id, sc);
for (const scope of parsed.scopes) {
if (scope.kind !== 'Namespace') continue;
for (const def of scope.ownedDefs) {
if (def.type !== 'Function' && def.type !== 'Method') continue;
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
if (simple !== refText) continue;
const nsQName = computeNamespaceQName(scope, scopesById);
if (nsQName !== '') out.add(nsQName);
}
}
}
}

View file

@ -785,17 +785,91 @@ function classifyAdlArg(argNode: SyntaxNode): CppAdlArgInfo {
) {
return EMPTY_ADL_ARG;
}
// Qualified expression (a::b) — may be a function, variable, enum value,
// or static member. Record as a potential function reference; resolution
// time verifies via workspace lookup that a Function/Method with this simple
// name exists in the extracted namespace before contributing to the set.
if (argNode.type === 'qualified_identifier') {
return {
simpleClassName: '',
templateSimpleClassName: '',
templateNamespace: '',
templateArgClassNames: [],
templateArgNamespaces: [],
functionRefText: argNode.text,
};
}
// Variable reference — look up its declared type (preserving pointer /
// reference / qualified-name shape; the existing arity-narrowing helper
// strips this info).
if (argNode.type === 'identifier') {
return lookupAdlIdentifierType(argNode);
const result = lookupAdlIdentifierType(argNode);
if (result === null) {
// Not found in the local compound_statement scope — could be a
// free-function reference (unqualified name, namespace scope).
return {
simpleClassName: '',
templateSimpleClassName: '',
templateNamespace: '',
templateArgClassNames: [],
templateArgNamespaces: [],
functionRefText: argNode.text,
};
}
return result;
}
// Other shapes (calls, member access, operators) — V1 unsupported.
return EMPTY_ADL_ARG;
}
function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
/**
* Returns `true` when `varName` appears as a parameter name in the nearest
* enclosing `function_definition` or `function_declarator` that contains
* `identNode`. Parameters live in `parameter_list` (a sibling of the
* `compound_statement`), so the `compound_statement`-local declaration scan
* in `lookupAdlIdentifierType` would not find them causing them to be
* mistakenly classified as potential free-function references.
*
* In tree-sitter-cpp a `function_definition` does NOT expose `parameters`
* as a direct named field; parameters live inside the nested
* `function_declarator`. For `function_declarator` nodes the `parameters`
* field IS direct. Both cases are handled below.
*/
function isIdentifierAFunctionParameter(identNode: SyntaxNode, varName: string): boolean {
let node: SyntaxNode | null = identNode.parent;
let safety = 64;
while (node !== null && safety-- > 0) {
let params: SyntaxNode | null = null;
if (node.type === 'function_declarator') {
// parameters is a direct field on function_declarator.
params = node.childForFieldName('parameters');
} else if (node.type === 'function_definition') {
// function_definition carries parameters inside its `declarator` field
// (which is a function_declarator). Walk through it.
const decl = node.childForFieldName('declarator');
if (decl !== null && decl.type === 'function_declarator') {
params = decl.childForFieldName('parameters');
}
}
if (params !== null) {
for (let i = 0; i < params.namedChildCount; i++) {
const param = params.namedChild(i);
if (param === null) continue;
const declNode = param.childForFieldName('declarator');
if (declNode === null) continue;
const leafName = extractDeclaratorLeafName(declNode);
if (leafName === varName) return true;
}
// Only check the immediately enclosing function — do not climb further.
break;
}
if (node.type === 'translation_unit') break;
node = node.parent;
}
return false;
}
function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo | null {
const varName = identNode.text;
let scope: SyntaxNode | null = identNode.parent;
while (
@ -805,8 +879,17 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
) {
scope = scope.parent;
}
if (scope === null) return EMPTY_ADL_ARG;
if (scope === null) return null;
// Function parameters live in the enclosing function's `parameter_list`,
// NOT inside the `compound_statement`, so the declaration scan below would
// never find them and would return `null` — incorrectly triggering the
// free-function-reference path. Check the parameter_list first.
if (isIdentifierAFunctionParameter(identNode, varName)) {
return EMPTY_ADL_ARG;
}
let foundAsLocalFunctionPointer = false;
for (let i = 0; i < scope.childCount; i++) {
const stmt = scope.child(i);
if (stmt === null || stmt.type !== 'declaration') continue;
@ -833,6 +916,9 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
if (inner.type === 'pointer_declarator') {
if (findFirstDescendantOfType(inner, 'function_declarator') !== null) {
isFunctionPointer = true;
// Extract the name from within the function-pointer declarator chain
// so `foundAsLocalFunctionPointer` can detect a matching declaration.
nameText = extractDeclaratorLeafName(inner);
break;
}
const next = inner.childForFieldName('declarator');
@ -862,12 +948,21 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
}
if (inner.type === 'function_declarator') {
isFunctionPointer = true;
// Extract the name from the inner declarator (e.g. `(*g)` in `void (*g)()`).
const innerDecl = inner.childForFieldName('declarator');
if (innerDecl !== null) nameText = extractDeclaratorLeafName(innerDecl);
break;
}
// Reached the leaf — usually `identifier`. Take its text.
nameText = inner.text;
break;
}
if (nameText === varName && isFunctionPointer) {
// Explicitly declared as a function-pointer variable — must not be
// treated as a free-function reference by the caller.
foundAsLocalFunctionPointer = true;
continue;
}
if (isFunctionPointer || nameText !== varName) continue;
const simpleClassName = extractAdlSimpleTypeName(typeNode);
@ -885,7 +980,22 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
templateArgNamespaces,
};
}
return EMPTY_ADL_ARG;
// If the identifier was found in local scope as a function-pointer variable,
// return EMPTY_ADL_ARG so the caller does NOT treat it as a free-function
// reference. Otherwise return null to indicate "not in local scope".
//
// Known limitation (Finding 4): variables whose type is a typedef/using alias
// for a function-pointer type are NOT detected here. For example:
// using Callback = void (*)();
// Callback g;
// foo(g); // `g`'s declarator is `identifier` with type `Callback`
// The declarator has no `pointer_declarator` wrapper, so `isFunctionPointer`
// stays false and `extractAdlSimpleTypeName` returns `"Callback"`. ADL then
// looks for a class named `Callback`; if none exists, this degrades to
// EMPTY_ADL_ARG (class not found → no namespace contributed). If a class
// named `Callback` does exist, a spurious namespace contribution could occur.
// Risk is low in practice; a future fix should resolve the typedef/alias chain.
return foundAsLocalFunctionPointer ? EMPTY_ADL_ARG : null;
}
/** Extract the simple class-like type name from a `type:` field node.
@ -1040,6 +1150,29 @@ function extractNamespaceFromQualifiedText(text: string): string {
return normalizeCppNamespaceQName(cleaned.slice(0, idx));
}
/**
* Walk a declarator node chain, unwrapping pointer/reference/function/
* parenthesized wrappers, and return the text of the innermost identifier.
* Returns `null` when no identifier is found within `safety` steps.
* Used by `lookupAdlIdentifierType` to extract the variable name from
* function-pointer declarator trees such as `(*g)()` in `void (*g)()`.
*/
function extractDeclaratorLeafName(node: SyntaxNode): string | null {
let cur: SyntaxNode = node;
let safety = 16;
while (safety-- > 0) {
if (cur.type === 'identifier' || cur.type === 'type_identifier') return cur.text;
// Common wrapper nodes — follow the 'declarator' field when present.
const next =
cur.childForFieldName('declarator') ??
// parenthesized_declarator: single named child
(cur.type === 'parenthesized_declarator' ? cur.namedChild(0) : null);
if (next === null) return null;
cur = next;
}
return null;
}
/**
* Check if a C++ function_definition or declaration has `static` storage class.
*/

View file

@ -29,6 +29,10 @@
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';
interface RangeKey {
readonly startLine: number;
@ -95,15 +99,17 @@ export function isCppInlineNamespaceScope(scopeId: ScopeId): boolean {
* Returns the most specific (innermost) match for `outer::foo()`
* where `inline namespace v1` declares `foo`, returns `v1::foo`. When
* multiple inline-namespace children declare the same name, ISO C++
* leaves the call ambiguous; V1 returns the first match in source
* order (stable across runs).
* leaves the call ambiguous; returns `'ambiguous'` so the caller
* suppresses edge emission rather than picking arbitrarily (#1564).
*/
export function resolveCppQualifiedNamespaceMember(
receiverName: string,
memberName: string,
parsedFiles: readonly ParsedFile[],
_scopes: ScopeResolutionIndexes,
): SymbolDefinition | undefined {
): SymbolDefinition | 'ambiguous' | undefined {
const allHits: SymbolDefinition[] = [];
const seenNodeId = new Set<string>();
for (const parsed of parsedFiles) {
const scopesById = new Map<ScopeId, (typeof parsed.scopes)[number]>();
for (const sc of parsed.scopes) scopesById.set(sc.id, sc);
@ -113,19 +119,45 @@ export function resolveCppQualifiedNamespaceMember(
if (nsDef === undefined) continue;
const nsName = nsDef.qualifiedName?.split('.').pop() ?? nsDef.qualifiedName ?? '';
if (nsName !== receiverName) continue;
// Found a matching namespace scope in this file. Collect the
// member transitively through any inline-namespace children.
const hit = findMemberInNamespaceTransitive(scope, scopesById, memberName);
if (hit !== undefined) return hit;
// Found a matching namespace scope in this file. Collect ALL
// members transitively through any inline-namespace children.
const hits = findMemberInNamespaceTransitive(scope, scopesById, memberName);
for (const hit of hits) {
if (seenNodeId.has(hit.nodeId)) continue;
seenNodeId.add(hit.nodeId);
allHits.push(hit);
}
}
}
return undefined;
if (allHits.length === 0) return undefined;
if (allHits.length === 1) return allHits[0];
// Multi-candidate: the `resolveQualifiedReceiverMember` hook has no
// access to call-site arity or argument types, so
// `narrowOverloadCandidates` cannot actually narrow here — the call
// with `(allHits, undefined, undefined)` is effectively a pass-through.
// We retain it so that `isOverloadAmbiguousAfterNormalization` can
// still detect int/long-style normalization collisions on this path,
// but for any multi-hit case where candidates have genuinely distinct
// signatures (e.g. `foo(int)` vs `foo(double)` in different inline
// children), we conservatively suppress rather than pick arbitrarily.
// A future enhancement could thread call-site argument info through
// the `resolveQualifiedReceiverMember` contract to enable real
// narrowing here.
const narrowed = narrowOverloadCandidates(allHits, undefined, undefined);
if (narrowed.length === 1) return narrowed[0];
if (narrowed.length === 0) return undefined;
if (isOverloadAmbiguousAfterNormalization(narrowed, undefined)) return 'ambiguous';
// Multiple surviving candidates (distinct signatures) — conservative
// suppress because we lack call-site info to disambiguate.
return 'ambiguous';
}
/** Recursively search a namespace scope and any inline-namespace
* descendants for a callable def with the given simple name. Non-inline
* descendants for callable defs with the given simple name. Non-inline
* nested namespaces are NOT traversed they require explicit
* qualification (`outer::nested::foo`). */
* qualification (`outer::nested::foo`). Returns ALL matches so the
* caller can detect same-name ambiguity across inline children (#1564). */
function findMemberInNamespaceTransitive(
scope: {
readonly id: ScopeId;
@ -142,22 +174,23 @@ function findMemberInNamespaceTransitive(
}
>,
memberName: string,
): SymbolDefinition | undefined {
): SymbolDefinition[] {
const results: SymbolDefinition[] = [];
// Check this scope's own ownedDefs first.
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 === memberName) return def;
if (simple === memberName) results.push(def);
}
// Descend into inline-namespace children.
for (const childScope of scopesById.values()) {
if (childScope.parent !== scope.id) continue;
if (childScope.kind !== 'Namespace') continue;
if (!inlineNamespaceScopeIds.has(childScope.id)) continue;
const hit = findMemberInNamespaceTransitive(childScope, scopesById, memberName);
if (hit !== undefined) return hit;
const childHits = findMemberInNamespaceTransitive(childScope, scopesById, memberName);
for (const hit of childHits) results.push(hit);
}
return undefined;
return results;
}
function findNamespaceDefInScope(scope: {

View file

@ -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
@ -244,21 +235,26 @@ export const cppScopeResolver: ScopeResolver = {
parsedFiles,
scopes,
);
if (member === undefined) continue;
if (member === undefined || member === 'ambiguous') continue;
if (seenUsing.has(member.nodeId)) continue;
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).

View file

@ -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
@ -616,8 +615,9 @@ export interface ScopeResolver {
*
* Receiver-bound-calls invokes this hook AFTER Case 1 (namespace
* imports) and AFTER Case 2 (class-name receiver) fail to resolve.
* Returns the target def, or `undefined` to fall through to the
* remaining cases.
* Returns the target def, `'ambiguous'` when multiple inline-namespace
* children declare the same name (suppresses edge emission), or
* `undefined` to fall through to the remaining cases.
*/
readonly resolveQualifiedReceiverMember?: (
receiverName: string,
@ -625,7 +625,7 @@ export interface ScopeResolver {
callerScope: ScopeId,
scopes: ScopeResolutionIndexes,
parsedFiles: readonly ParsedFile[],
) => SymbolDefinition | undefined;
) => SymbolDefinition | 'ambiguous' | undefined;
/**
* Enable the receiver-bound Case 0.5 fallback for explicit `this`

View file

@ -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,

View file

@ -445,6 +445,12 @@ export function emitReceiverBoundCalls(
scopes,
parsedFiles,
);
if (memberDef === 'ambiguous') {
// Same-name ambiguity across inline-namespace children (#1564):
// suppress edge emission, mark site handled.
handledSites.add(siteKey);
continue;
}
if (memberDef !== undefined) {
const ok = tryEmitEdge(
graph,

View file

@ -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 };
}
/**

View 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);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace audit {
struct Event {};
void record(Event e);
}

View file

@ -0,0 +1,8 @@
#include "color.h"
namespace app {
void run() {
color::Channel ch = color::Channel::R;
serialize(ch);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace color {
enum class Channel { R, G, B };
void serialize(Channel c);
}

View file

@ -0,0 +1,7 @@
#include "utils.h"
namespace caller {
void run() {
with_callback(utils::worker);
}
}

View file

@ -0,0 +1,7 @@
#pragma once
namespace utils {
void worker();
void worker(int n);
void with_callback(int n);
}

View file

@ -0,0 +1,7 @@
#include "utils.h"
namespace caller {
void run() {
with_callback(utils::worker);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace utils {
void worker();
void with_callback(int n);
}

View file

@ -0,0 +1,8 @@
#include "lib.h"
namespace app {
void run() {
lib::Foo f;
process(f);
}
}

View file

@ -0,0 +1,7 @@
#pragma once
namespace lib {
struct Foo {
friend void process(Foo& f) {}
};
}

View file

@ -0,0 +1,8 @@
#include "audit.h"
namespace app {
void run() {
audit::Event e;
record(e);
}
}

View file

@ -0,0 +1,13 @@
#pragma once
namespace audit {
struct Event {};
inline namespace v1 {
void record(Event e);
}
}
namespace other {
void record(int x);
}

View 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);
}
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace data {
struct Pair {};
void swap(Pair& a, Pair& b);
}

View file

@ -0,0 +1,13 @@
#include "audit.h"
namespace app {
void run() {
// `g` is a locally-declared function-pointer variable. audit::g() also
// exists in the workspace. The local-fp guard (foundAsLocalFunctionPointer)
// must detect `g` as a function-pointer variable declaration and return
// EMPTY_ADL_ARG, preventing the workspace scan that would otherwise find
// audit::g and contribute `audit` to the ADL associated set.
void (*g)();
record(g);
}
}

View file

@ -0,0 +1,11 @@
#pragma once
namespace audit {
// A free function named `g` exists in the workspace. Without the local-fp
// guard, a locally-declared `void (*g)()` variable would fall through to
// EMPTY_ADL_ARG and not be treated as a free-function ref — but this test
// specifically verifies that the local fp variable shadows the workspace
// function of the same name and no namespace is contributed.
void g();
void record(void (*fn)());
}

View 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);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace data {
struct Pair {};
void swap(Pair& a, Pair& b);
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace alpha {
struct Token {};
void probe(Token t);
}

View file

@ -0,0 +1,8 @@
#include "alpha.h"
namespace app {
void run() {
alpha::Token t;
probe(t, 42);
}
}

View file

@ -0,0 +1,10 @@
#include "audit.h"
namespace app {
int record = 0;
void run() {
audit::Event e;
record(e);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace audit {
struct Event {};
void record(Event e);
}

View file

@ -0,0 +1,14 @@
#include "utils.h"
namespace caller {
// `callback` is a plain int parameter, not a function reference.
// Without the fix: `callback` is not found in the compound_statement
// (parameters live in parameter_list) → lookupAdlIdentifierType returns null
// → treated as free-function ref → workspace scan finds utils::callback
// → `utils` added to ADL set → run_with resolves to utils::run_with (false positive).
// With the fix: isIdentifierAFunctionParameter detects `callback` in the
// parameter_list → returns EMPTY_ADL_ARG → no namespace contributed → 0 CALLS edges.
void run(int callback) {
run_with(callback);
}
}

View file

@ -0,0 +1,10 @@
#pragma once
namespace utils {
// A function named `callback` exists in the `utils` namespace. Without the
// parameter-list guard, passing a function *parameter* also named `callback`
// would trigger a workspace scan, find utils::callback, contribute `utils`
// to the ADL set, and emit a false-positive CALLS edge to utils::run_with.
void callback();
void run_with(int n);
}

View file

@ -0,0 +1,14 @@
#include "data.h"
namespace caller {
// data::value is a namespace-qualified VARIABLE, not a function.
// ADL must NOT contribute `data` to the associated namespace set — the
// argument type is `int`, which has no associated namespaces in ISO C++.
// GitNexus guards: collectFunctionRefNamespaces verifies a Function/Method
// named `value` exists in `data` before contributing. Since `data::value`
// is a variable (not a function), `data` is NOT added, and process() is
// not resolved via ADL.
void run() {
process(data::value);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace data {
extern int value;
void process(int n);
}

View file

@ -0,0 +1,14 @@
#include "lib.h"
namespace caller {
void run() {
// Unqualified `worker` — not in local compound_statement scope → treated
// as a potential free-function reference. The workspace scan finds
// 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).
// Merged-narrowing ambiguity suppression in free-call-fallback emits
// zero CALLS edges rather than picking one arbitrarily.
run_with(worker);
}
}

View file

@ -0,0 +1,12 @@
#pragma once
namespace alpha {
// `worker` exists in both alpha and beta namespaces.
void worker();
void run_with(void (*fn)());
}
namespace beta {
void worker();
void run_with(void (*fn)());
}

View file

@ -0,0 +1,5 @@
#include "lib.h"
void run() {
outer::foo(42);
}

View file

@ -0,0 +1,10 @@
#pragma once
namespace outer {
inline namespace v1 {
void foo(int x);
}
inline namespace v2 {
void foo(double y);
}
}

View file

@ -0,0 +1,5 @@
#include "lib.h"
void run() {
outer::foo();
}

View file

@ -0,0 +1,10 @@
#pragma once
namespace outer {
inline namespace v1 {
void foo();
}
inline namespace v2 {
void foo();
}
}

View file

@ -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,252 @@ 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.
//
// GitNexus approximation (not strict ISO C++ ADL): when a qualified_identifier
// like `utils::worker` is passed as an argument, GitNexus contributes the
// enclosing namespace (`utils`) to the associated set, provided a Function or
// Method named `worker` is found in the `utils` namespace at resolution time.
// Under ISO C++ [basic.lookup.argdep] the associated entities for a function-type
// argument come from the parameter types and return type of the overload set —
// NOT the function's enclosing namespace. For `void worker()`, the standard-
// compliant associated set is empty. The approximation captures the dominant
// real-world pattern (pass a utility function → find its sibling) at the cost
// of potential false positives when an unrelated function with the same simple
// name exists in the same namespace (bounded by the workspace-function lookup).
// ---------------------------------------------------------------------------
describe('C++ ADL — qualified free-function reference contributes its namespace', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-free-func-ref'), () => {});
}, 60000);
it('with_callback(utils::worker) resolves to utils::with_callback via ADL', () => {
const calls = getRelationships(result, 'CALLS');
const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'with_callback');
// Ordinary lookup inside caller::run finds nothing (no `using`, no local
// declaration). utils::worker is a qualified_identifier argument, so ADL
// contributes `utils` to the associated-namespace set. utils::with_callback
// is then discovered as the sole candidate.
expect(cbCalls.length).toBe(1);
expect(cbCalls[0].targetFilePath).toContain('utils.h');
});
});
describe('C++ ADL — overloaded free-function reference does not crash', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-free-func-ref-overloaded'),
() => {},
);
}, 60000);
it('with_callback(utils::worker) with overloaded utils::worker still resolves utils::with_callback via ADL', () => {
const calls = getRelationships(result, 'CALLS');
const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'with_callback');
// utils::worker has two overloads (worker() and worker(int)). V1
// simplification: contribute the namespace if ANY overload exists in the
// workspace, regardless of which one would be selected. The namespace
// `utils` is still added, and utils::with_callback is discovered.
expect(cbCalls.length).toBe(1);
expect(cbCalls[0].targetFilePath).toContain('utils.h');
});
});
describe('C++ ADL — namespace-qualified variable arg does NOT contribute namespace', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-qualified-variable-arg'),
() => {},
);
}, 60000);
it('process(data::value) emits zero CALLS edges — data::value is a variable, not a function', () => {
const calls = getRelationships(result, 'CALLS');
const processCalls = calls.filter((c) => c.source === 'run' && c.target === 'process');
// data::value is a namespace-qualified integer variable. tree-sitter-cpp
// produces a qualified_identifier AST node regardless of whether `value`
// denotes a function, variable, enum, or static member. The GitNexus guard
// in collectFunctionRefNamespaces verifies that a Function/Method named
// `value` exists in the `data` namespace before contributing it. Since
// `data::value` is an int variable, `data` is never added to the associated
// set, so data::process is never found as an ADL candidate.
expect(processCalls.length).toBe(0);
});
});
describe('C++ ADL — function parameter does NOT trigger free-function-ref ADL', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-param-not-free-func-ref'),
() => {},
);
}, 60000);
it('run_with(callback) emits zero CALLS edges when callback is a parameter, not a function reference', () => {
const calls = getRelationships(result, 'CALLS');
const runWithCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_with');
// `callback` is an int parameter of `caller::run`. Function parameters
// live in the parameter_list, not in the compound_statement, so the
// local-scope declaration scan would not find it and would return null —
// previously misclassifying it as an unqualified free-function reference.
// The workspace contains utils::callback(), so the scan would find it and
// contribute `utils` to the ADL set, emitting a false-positive CALLS edge
// to utils::run_with. isIdentifierAFunctionParameter now catches this and
// returns EMPTY_ADL_ARG, preventing the workspace scan entirely.
expect(runWithCalls.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// U5 (follow-up plan 2026-05-13-001): inline namespace transitive walking.
// `inline namespace v1 { ... }` makes its members reachable through the
@ -2432,6 +2689,51 @@ describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUO
// `resolveQualifiedReceiverMember` hook on the ScopeResolver contract.
// ---------------------------------------------------------------------------
describe('C++ ADL — local function-pointer var shadows same-named free function', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-local-fp-shadows-free-func'),
() => {},
);
}, 60000);
it('record(g) emits zero CALLS edges even though audit::g() exists in the workspace', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
// `g` is a locally-declared `void (*g)()` variable. `audit::g()` also
// exists in the workspace. Without the foundAsLocalFunctionPointer guard,
// `g` would not be detected in the compound_statement (it IS there, but
// as a function-pointer declarator), and the workspace scan would find
// audit::g, contribute `audit` to the ADL set, and emit a false-positive
// CALLS edge to audit::record. The guard correctly returns EMPTY_ADL_ARG,
// so no namespace is contributed and no edge is emitted.
expect(recordCalls.length).toBe(0);
});
});
describe('C++ ADL — unqualified free-function ref with namespace collision', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-unqualified-ref-collision'),
() => {},
);
}, 60000);
it('run_with(worker) emits zero CALLS edges when worker exists in two namespaces', () => {
const calls = getRelationships(result, 'CALLS');
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 → merged narrowing suppression →
// zero CALLS edges (suppressed rather than arbitrary pick).
expect(runWithCalls.length).toBe(0);
});
});
describe('C++ inline namespace — outer::foo resolves to inline child', () => {
let result: PipelineResult;
@ -2474,6 +2776,47 @@ describe('C++ inline namespace — versioned (v1 inline, v0 not)', () => {
});
});
describe('C++ inline namespace — ambiguous same-name across inline children (#1564)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-inline-namespace-ambiguous'),
() => {},
);
}, 60000);
it('outer::foo() emits zero CALLS edges when v1 and v2 both declare foo', () => {
const calls = getRelationships(result, 'CALLS');
const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo');
// ISO C++ leaves this ambiguous — both inline namespace children declare
// the same name. The resolver must suppress rather than pick arbitrarily.
expect(fooCalls.length).toBe(0);
});
});
describe('C++ inline namespace — ambiguous distinct signatures (conservative suppress)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-inline-namespace-ambiguous-diff-sigs'),
() => {},
);
}, 60000);
it('outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(double)', () => {
const calls = getRelationships(result, 'CALLS');
const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo');
// Even though the two overloads have distinct signatures and a compiler
// could disambiguate via argument types, the `resolveQualifiedReceiverMember`
// hook lacks call-site arity/argument-type information, so multi-hit cases
// are conservatively suppressed. Documents the limitation noted in
// inline-namespaces.ts (Finding 1 of Claude review on #1600).
expect(fooCalls.length).toBe(0);
});
});
describe('C++ inline namespace — nested (STL __1-style)', () => {
let result: PipelineResult;
@ -2522,6 +2865,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),

View file

@ -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-
@ -171,6 +175,34 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'Derived<T>::g_unqualified() -> f() does NOT bind to Base<T>::f',
'Derived<T>::g_this() -> this->f() resolves to Base<T>::f (1 edge)',
'Derived<T>::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible',
// The legacy DAG path has no inline-namespace same-name ambiguity
// detection. When two inline children declare the same name, the
// legacy path picks an arbitrary match. The scope-resolver returns
// 'ambiguous' and suppresses edge emission. Scope-resolver-only
// correctness win (#1564); backporting to legacy is out of scope.
'outer::foo() emits zero CALLS edges when v1 and v2 both declare foo',
// Distinct-signature inline-namespace ambiguity: `foo(int)` in v1 and
// `foo(double)` in v2. The scope-resolver conservatively suppresses
// because `resolveQualifiedReceiverMember` lacks call-site argument
// types. Legacy DAG has no inline-namespace resolver. Scope-resolver-
// only correctness win (#1600 / Claude review Finding 1).
'outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(double)',
// PR #1598: ADL free-function reference arg negative fixtures rely on
// scope-resolver-only correctness. The legacy DAG falls back to
// `pickUniqueGlobalCallable` which resolves the callee by simple-name
// workspace lookup, ignoring argument analysis. These fixtures expect
// zero CALLS edges (the registry-primary path correctly avoids a false-
// positive), but the legacy path emits one edge via the global fallback.
// 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',
]),
};