mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
feat: C++ ADL V2: free-function reference args contribute enclosing namespace (#1598)
* Initial plan * cpp ADL V2: free-function reference args contribute enclosing namespace Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/24805583-c0c4-4ef8-978f-b874bd917947 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * merge: resolve conflicts with origin/main and fix overloaded fixture app.cpp Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/136aeffe-45da-47e2-95dd-e3883e85fad7 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(finding-1): replace ISO C++ [basic.lookup.argdep] misstatement with GitNexus-approximation label Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fa37c0dd-65f9-4dd4-9811-617227a37073 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(finding-2): verify Function/Method exists in namespace before contributing via qualified_identifier arg Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fa37c0dd-65f9-4dd4-9811-617227a37073 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(finding-3): function parameters in parameter_list no longer misclassified as free-function refs Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fa37c0dd-65f9-4dd4-9811-617227a37073 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * doc(finding-4): document typedef/using-aliased function-pointer limitation in lookupAdlIdentifierType Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fa37c0dd-65f9-4dd4-9811-617227a37073 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * test(finding-5): add negative fixtures for local-fp shadowing free-func and unqualified namespace collision Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/fa37c0dd-65f9-4dd4-9811-617227a37073 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(legacy-parity): skip two new negative-fixture tests from legacy DAG parity run Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/64ffbaf1-f442-4a2b-8542-4afa500d9182 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
87f5c03997
commit
74e555c3bd
16 changed files with 520 additions and 8 deletions
|
|
@ -21,9 +21,25 @@
|
|||
* - `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.
|
||||
*
|
||||
* **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.
|
||||
*
|
||||
* The current implementation also short-circuits to ADL only when ordinary lookup is empty
|
||||
* (`findCallableBindingInScope` returned undefined). ISO C++ would
|
||||
|
|
@ -65,6 +81,7 @@ import {
|
|||
* 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,6 +99,15 @@ 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[]>();
|
||||
|
|
@ -179,10 +205,14 @@ export function pickCppAdlCandidates(
|
|||
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;
|
||||
|
||||
|
|
@ -389,3 +419,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.
|
||||
*/
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
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)());
|
||||
}
|
||||
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).
|
||||
// GitNexus ADL_AMBIGUOUS sentinel suppresses the edge — the caller 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)());
|
||||
}
|
||||
|
|
@ -2424,6 +2424,112 @@ describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUO
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 +2538,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 → ADL_AMBIGUOUS sentinel → 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,15 @@ 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',
|
||||
// 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',
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue