mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
Merge branch 'feat/Desktop-app' of https://github.com/prajapatisparsh/GitNexus into feat/Desktop-app
This commit is contained in:
commit
bc7729b5e9
43 changed files with 1316 additions and 155 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-overloaded/app.cpp
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-overloaded/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "utils.h"
|
||||
|
||||
namespace caller {
|
||||
void run() {
|
||||
with_callback(utils::worker);
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-overloaded/utils.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-overloaded/utils.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
namespace utils {
|
||||
void worker();
|
||||
void worker(int n);
|
||||
void with_callback(int n);
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref/app.cpp
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "utils.h"
|
||||
|
||||
namespace caller {
|
||||
void run() {
|
||||
with_callback(utils::worker);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref/utils.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref/utils.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace utils {
|
||||
void worker();
|
||||
void with_callback(int n);
|
||||
}
|
||||
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);
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/cpp-adl-local-fp-shadows-free-func/app.cpp
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-adl-local-fp-shadows-free-func/app.cpp
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-local-fp-shadows-free-func/audit.h
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-local-fp-shadows-free-func/audit.h
vendored
Normal 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)());
|
||||
}
|
||||
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);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-adl-param-not-free-func-ref/app.cpp
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-adl-param-not-free-func-ref/app.cpp
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-adl-param-not-free-func-ref/utils.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-adl-param-not-free-func-ref/utils.h
vendored
Normal 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);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-adl-qualified-variable-arg/app.cpp
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-adl-qualified-variable-arg/app.cpp
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-qualified-variable-arg/data.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-qualified-variable-arg/data.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace data {
|
||||
extern int value;
|
||||
void process(int n);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-adl-unqualified-ref-collision/app.cpp
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-adl-unqualified-ref-collision/app.cpp
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/cpp-adl-unqualified-ref-collision/lib.h
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/cpp-adl-unqualified-ref-collision/lib.h
vendored
Normal 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)());
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-diff-sigs/caller.cpp
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-diff-sigs/caller.cpp
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#include "lib.h"
|
||||
|
||||
void run() {
|
||||
outer::foo(42);
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-diff-sigs/lib.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-diff-sigs/lib.h
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
namespace outer {
|
||||
inline namespace v1 {
|
||||
void foo(int x);
|
||||
}
|
||||
inline namespace v2 {
|
||||
void foo(double y);
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous/caller.cpp
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous/caller.cpp
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#include "lib.h"
|
||||
|
||||
void run() {
|
||||
outer::foo();
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous/lib.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous/lib.h
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
namespace outer {
|
||||
inline namespace v1 {
|
||||
void foo();
|
||||
}
|
||||
inline namespace v2 {
|
||||
void foo();
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue