mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
fix(cpp): two-phase template lookup suppresses dependent-base members (U3)
ISO C++ two-phase name lookup: inside a class template body, unqualified calls MUST NOT bind to members of a dependent base class. Only this->name or Base<T>::name forms make the lookup dependent. GCC and Clang both reject the unqualified form with 'declaration of f must be available'. Before this fix, GitNexus's global free-call fallback walked the workspace registry by simple name and bound unqualified calls inside template bodies to dependent-base members, producing CALLS edges the compiler would reject. Implementation: - New languages/cpp/two-phase-lookup.ts module: per-pipeline state recording (className, dependentBaseName) pairs at capture time and resolving them to nodeId sets during populateOwners. - captures.ts detectCppDependentBases walks the AST once finding every template_declaration containing a class/struct definition. For each, it collects template-parameter names (typename T, class T, non-type int N, template-template parameters) and walks each base in the base_class_clause checking whether any inner type_identifier matches a template parameter. Conservative bias: typename T::U, decltype, and template-template-parameter shapes also classified as dependent. - Extended scope-resolution contract's isCallableVisibleFromCaller hook with optional callerScope and scopes fields. C++ implements the hook to consult isCppDependentBaseMember: when the candidate is a member of a dependent base of the caller's enclosing class, the hook returns false and pickUniqueGlobalCallable skips the candidate. - clearFileLocalNames also clears the dependent-base state per pipeline run. Fixtures: - cpp-two-phase-dependent-base: Derived<T> deriving from Base<T>, unqualified f() and i inside Derived's body. Asserts zero CALLS edges and zero ACCESSES edges respectively. - cpp-two-phase-this-qualified, cpp-two-phase-non-dependent-base, cpp-two-phase-namespace-free-call-inside-template: positive fixtures left as documented gaps (this-> and qualified-name resolution inside template bodies are pre-existing resolver weaknesses independent of U3). Tracked separately. Negative test mode-gated to REGISTRY_PRIMARY_CPP=1 via the expected- failures registry; legacy DAG has no two-phase lookup. All 2116 resolver integration tests pass under registry-primary; all 150 cpp tests pass under both modes (5 negative tests skipped in legacy as documented).
This commit is contained in:
parent
f7580f6ab3
commit
10aa731b21
16 changed files with 503 additions and 2 deletions
|
|
@ -11,6 +11,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|||
import { splitCppInclude, splitCppUsingDecl } from './import-decomposer.js';
|
||||
import { computeCppDeclarationArity, computeCppCallArity } from './arity-metadata.js';
|
||||
import { markFileLocal } from './file-local-linkage.js';
|
||||
import { markCppDependentBase } from './two-phase-lookup.js';
|
||||
|
||||
export function emitCppScopeCaptures(
|
||||
sourceText: string,
|
||||
|
|
@ -253,9 +254,215 @@ export function emitCppScopeCaptures(
|
|||
out.push(grouped);
|
||||
}
|
||||
|
||||
// ── Detect dependent-base relationships for two-phase template lookup ──
|
||||
// Walk the tree once, finding every `template_declaration` whose
|
||||
// child is a class/struct definition with a `base_class_clause` whose
|
||||
// base names reference an in-scope template parameter. Record the
|
||||
// (className, dependentBaseName) pair so `populateCppDependentBases`
|
||||
// (called from the `populateOwners` hook) can resolve names to nodeIds
|
||||
// and the resolver can suppress unqualified-call binding to those
|
||||
// bases per ISO C++ two-phase lookup.
|
||||
detectCppDependentBases(tree.rootNode, filePath);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the AST finding every template_declaration containing a class or
|
||||
* struct definition with a dependent base. Records (className, baseName)
|
||||
* pairs into the module-level state via `markCppDependentBase`.
|
||||
*
|
||||
* A base is "dependent" when its name (typically a template_type like
|
||||
* `Base<T>`) uses a template parameter of the enclosing template_declaration.
|
||||
* Conservative bias: `typename T::U`, `decltype(...)` and template-template
|
||||
* parameter shapes are also treated as dependent.
|
||||
*/
|
||||
function detectCppDependentBases(root: SyntaxNode, filePath: string): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (node.type === 'template_declaration') {
|
||||
// Collect template-parameter names declared by this declaration.
|
||||
// Inner template_declarations shadow outer ones — handled by the
|
||||
// recursive descent below (each template_declaration creates its
|
||||
// own parameter scope).
|
||||
const params = collectTemplateParameterNames(node);
|
||||
|
||||
// Find the class/struct definition inside this template_declaration.
|
||||
const classNode = findChildOfType(node, [
|
||||
'class_specifier',
|
||||
'struct_specifier',
|
||||
]);
|
||||
if (classNode !== null) {
|
||||
const className = getTypeIdentifierName(classNode);
|
||||
if (className !== '') {
|
||||
const baseClause = findChildOfType(classNode, ['base_class_clause']);
|
||||
if (baseClause !== null) {
|
||||
for (const base of iterBaseClasses(baseClause)) {
|
||||
if (isBaseDependent(base, params)) {
|
||||
const baseName = extractBaseSimpleName(base);
|
||||
if (baseName !== '') {
|
||||
markCppDependentBase(filePath, className, baseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Collect simple template parameter names from a template_declaration. */
|
||||
function collectTemplateParameterNames(templateDecl: SyntaxNode): Set<string> {
|
||||
const names = new Set<string>();
|
||||
const paramList = findChildOfType(templateDecl, ['template_parameter_list']);
|
||||
if (paramList === null) return names;
|
||||
for (let i = 0; i < paramList.childCount; i++) {
|
||||
const param = paramList.child(i);
|
||||
if (param === null) continue;
|
||||
if (
|
||||
param.type === 'type_parameter_declaration' ||
|
||||
param.type === 'optional_type_parameter_declaration' ||
|
||||
param.type === 'variadic_type_parameter_declaration'
|
||||
) {
|
||||
const idNode = findFirstDescendantOfType(param, 'type_identifier');
|
||||
if (idNode !== null) names.add(idNode.text);
|
||||
} else if (
|
||||
param.type === 'parameter_declaration' ||
|
||||
param.type === 'optional_parameter_declaration' ||
|
||||
param.type === 'variadic_parameter_declaration'
|
||||
) {
|
||||
// Non-type template parameter (e.g. `template<int N>`).
|
||||
const idNode = findFirstDescendantOfType(param, 'identifier');
|
||||
if (idNode !== null) names.add(idNode.text);
|
||||
} else if (param.type === 'template_template_parameter_declaration') {
|
||||
// template-template parameter (e.g. `template<template<class> class TT>`)
|
||||
const idNode = findFirstDescendantOfType(param, 'type_identifier');
|
||||
if (idNode !== null) names.add(idNode.text);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Yield each base-class entry from a `base_class_clause`. */
|
||||
function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator<SyntaxNode> {
|
||||
for (let i = 0; i < baseClause.childCount; i++) {
|
||||
const child = baseClause.child(i);
|
||||
if (child === null) continue;
|
||||
// Skip ':', ',', and access_specifier nodes — the base names are
|
||||
// type_identifier, template_type, or qualified_identifier.
|
||||
if (
|
||||
child.type === 'type_identifier' ||
|
||||
child.type === 'template_type' ||
|
||||
child.type === 'qualified_identifier'
|
||||
) {
|
||||
yield child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A base is dependent when:
|
||||
* - it's a `template_type` and its argument list contains a
|
||||
* `type_identifier` matching one of the enclosing template's params
|
||||
* (e.g., `Base<T>` where `T` is a template parameter), OR
|
||||
* - it contains a `typename`, `decltype`, or `template_template_parameter`
|
||||
* shape (conservatively treated as dependent).
|
||||
*
|
||||
* Non-dependent: `Base<int>`, `ConcreteBase`, `Base<MyConcrete>` where
|
||||
* `MyConcrete` is not a template parameter.
|
||||
*/
|
||||
function isBaseDependent(baseNode: SyntaxNode, templateParams: Set<string>): boolean {
|
||||
if (baseNode.type !== 'template_type') {
|
||||
// Bare `type_identifier` or `qualified_identifier` bases — not
|
||||
// dependent (the base name itself doesn't reference a template
|
||||
// parameter at this level).
|
||||
return false;
|
||||
}
|
||||
// Walk all descendants of the template_argument_list looking for any
|
||||
// type_identifier matching a template parameter, or any conservative-
|
||||
// dependent shape.
|
||||
const stack: SyntaxNode[] = [baseNode];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (node.type === 'type_identifier' && templateParams.has(node.text)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
node.type === 'decltype' ||
|
||||
node.type === 'dependent_type' ||
|
||||
node.type === 'template_template_parameter_declaration'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (node.type === 'qualified_identifier') {
|
||||
// `typename T::U` or `T::nested` — if any inner identifier matches
|
||||
// a template parameter, dependent.
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c !== null) stack.push(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c !== null) stack.push(c);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extract the simple name of a base class node. */
|
||||
function extractBaseSimpleName(baseNode: SyntaxNode): string {
|
||||
if (baseNode.type === 'type_identifier') return baseNode.text;
|
||||
if (baseNode.type === 'template_type') {
|
||||
const nameNode = baseNode.childForFieldName('name');
|
||||
if (nameNode !== null) return nameNode.text;
|
||||
// Fallback: first type_identifier descendant.
|
||||
const id = findFirstDescendantOfType(baseNode, 'type_identifier');
|
||||
if (id !== null) return id.text;
|
||||
}
|
||||
if (baseNode.type === 'qualified_identifier') {
|
||||
const nameNode = baseNode.childForFieldName('name');
|
||||
if (nameNode !== null) return nameNode.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Find the first direct child matching one of the given types. */
|
||||
function findChildOfType(node: SyntaxNode, types: readonly string[]): SyntaxNode | null {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c !== null && types.includes(c.type)) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Recursive search for the first descendant of a given type. */
|
||||
function findFirstDescendantOfType(node: SyntaxNode, type: string): SyntaxNode | null {
|
||||
if (node.type === type) return node;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c === null) continue;
|
||||
const hit = findFirstDescendantOfType(c, type);
|
||||
if (hit !== null) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Get the name of a class/struct/template_type node via its `name` field. */
|
||||
function getTypeIdentifierName(node: SyntaxNode): string {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (nameNode !== null) return nameNode.text;
|
||||
const id = findFirstDescendantOfType(node, 'type_identifier');
|
||||
return id !== null ? id.text : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer argument types from a call_expression or new_expression node.
|
||||
* Used for overload disambiguation by parameter types.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ import {
|
|||
populateCppNonGloballyVisible,
|
||||
isCppDefGloballyVisible,
|
||||
} from './file-local-linkage.js';
|
||||
import {
|
||||
populateCppDependentBases,
|
||||
clearCppDependentBases,
|
||||
isCppDependentBaseMember,
|
||||
} from './two-phase-lookup.js';
|
||||
import { populateCppRangeBindings } from './range-bindings.js';
|
||||
|
||||
/**
|
||||
|
|
@ -39,8 +44,9 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
importEdgeReason: 'cpp-scope: include',
|
||||
|
||||
loadResolutionConfig: (repoPath: string) => {
|
||||
// Clear stale file-local-linkage data from any previous invocation.
|
||||
// Clear stale per-pipeline state from any previous invocation.
|
||||
clearFileLocalNames();
|
||||
clearCppDependentBases();
|
||||
return scanCppHeaderFiles(repoPath);
|
||||
},
|
||||
|
||||
|
|
@ -75,6 +81,10 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
// fallback and wildcard expansion can suppress them as unqualified
|
||||
// cross-file callables.
|
||||
populateCppNonGloballyVisible(parsed);
|
||||
// Resolve recorded template-class → dependent-base simple names to
|
||||
// class nodeIds for two-phase template lookup (U3 of plan
|
||||
// 2026-05-13-001).
|
||||
populateCppDependentBases(parsed);
|
||||
},
|
||||
|
||||
// Simple `isSuperReceiver` returns false for C++. Real super
|
||||
|
|
@ -148,4 +158,18 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
if (!isCppDefGloballyVisible(def.filePath, def.nodeId)) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
// C++ two-phase template lookup: inside a class template body,
|
||||
// unqualified calls MUST NOT bind to members of a dependent base
|
||||
// class. The standard requires `this->name()` or `Base<T>::name()`
|
||||
// forms to make the lookup dependent. Without this gate the global
|
||||
// free-call fallback walks the workspace registry and silently binds
|
||||
// unqualified calls to dependent-base members, producing CALLS edges
|
||||
// the compiler would reject. See plan 2026-05-13-001 U3.
|
||||
isCallableVisibleFromCaller: ({ candidate, callerScope, scopes }) => {
|
||||
if (callerScope === undefined || scopes === undefined) return true;
|
||||
// Reject when the candidate is a member of a dependent base of the
|
||||
// caller's enclosing template class. Otherwise allow.
|
||||
return !isCppDependentBaseMember(callerScope, candidate, scopes);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
133
gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts
Normal file
133
gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* C++ two-phase template lookup support.
|
||||
*
|
||||
* Inside a class template body, names from a dependent base class are NOT
|
||||
* found by ordinary unqualified lookup. The standard requires the
|
||||
* `this->name` or `Base<T>::name` forms to make the lookup dependent.
|
||||
* GitNexus's global free-call fallback otherwise binds such names to the
|
||||
* dependent base's members, producing CALLS edges the compiler would
|
||||
* reject.
|
||||
*
|
||||
* This module records — during `emitCppScopeCaptures` — which template
|
||||
* class declarations have which dependent base class names (per file).
|
||||
* `populateCppDependentBases` then resolves those names to class nodeIds
|
||||
* using the workspace registry, building the per-class set the
|
||||
* `isDependentBaseMember` predicate consumes.
|
||||
*
|
||||
* NOTE: module-level state, single-process-single-repo use only.
|
||||
* `clearFileLocalNames()` clears this state alongside file-local linkage
|
||||
* (see `file-local-linkage.ts`).
|
||||
*/
|
||||
|
||||
import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js';
|
||||
|
||||
/**
|
||||
* Capture-time record: for each template class declaration in a file,
|
||||
* the simple names of its dependent base classes.
|
||||
*
|
||||
* Key: filePath
|
||||
* Value: Map<className, Set<dependentBaseSimpleName>>
|
||||
*/
|
||||
const dependentBasesByFile = new Map<string, Map<string, Set<string>>>();
|
||||
|
||||
/**
|
||||
* Post-`populateOwners` resolution: per-class-nodeId, the set of
|
||||
* dependent-base-class nodeIds. Built by `populateCppDependentBases`
|
||||
* from `dependentBasesByFile` + the workspace registry.
|
||||
*/
|
||||
const dependentBaseNodeIds = new Map<string, Set<string>>();
|
||||
|
||||
/**
|
||||
* Record a dependent-base relationship discovered during scope-capture
|
||||
* emission. `className` is the simple name of the template class;
|
||||
* `baseName` is the simple name of the dependent base class.
|
||||
*
|
||||
* The capture-time recorder uses simple names because the registry
|
||||
* resolution that maps names → nodeIds runs later (in
|
||||
* `populateCppDependentBases`).
|
||||
*/
|
||||
export function markCppDependentBase(filePath: string, className: string, baseName: string): void {
|
||||
let perFile = dependentBasesByFile.get(filePath);
|
||||
if (perFile === undefined) {
|
||||
perFile = new Map();
|
||||
dependentBasesByFile.set(filePath, perFile);
|
||||
}
|
||||
let bases = perFile.get(className);
|
||||
if (bases === undefined) {
|
||||
bases = new Set();
|
||||
perFile.set(className, bases);
|
||||
}
|
||||
bases.add(baseName);
|
||||
}
|
||||
|
||||
/** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */
|
||||
export function clearCppDependentBases(): void {
|
||||
dependentBasesByFile.clear();
|
||||
dependentBaseNodeIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve recorded dependent-base simple names to class nodeIds using
|
||||
* the parsed file's localDefs. Run as part of `populateOwners` so the
|
||||
* resolved set is available before any resolution pass consults it.
|
||||
*
|
||||
* Matches by simple name within the same file (the template class and
|
||||
* its base are typically declared in the same TU; cross-file template
|
||||
* bases are an edge case deferred to V2).
|
||||
*/
|
||||
export function populateCppDependentBases(parsed: ParsedFile): void {
|
||||
const perFile = dependentBasesByFile.get(parsed.filePath);
|
||||
if (perFile === undefined) return;
|
||||
|
||||
// Build simple-name → nodeId index for this file's class-like defs.
|
||||
const classByName = new Map<string, string>();
|
||||
for (const def of parsed.localDefs) {
|
||||
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
|
||||
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
||||
if (simple !== '') classByName.set(simple, def.nodeId);
|
||||
}
|
||||
|
||||
for (const [className, baseNames] of perFile) {
|
||||
const classNodeId = classByName.get(className);
|
||||
if (classNodeId === undefined) continue;
|
||||
let bases = dependentBaseNodeIds.get(classNodeId);
|
||||
if (bases === undefined) {
|
||||
bases = new Set();
|
||||
dependentBaseNodeIds.set(classNodeId, bases);
|
||||
}
|
||||
for (const baseName of baseNames) {
|
||||
const baseNodeId = classByName.get(baseName);
|
||||
if (baseNodeId !== undefined) bases.add(baseNodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two-phase lookup predicate: is the candidate def a member of a
|
||||
* dependent base of the caller's enclosing template class?
|
||||
*
|
||||
* Used as an additional reject-filter in `pickUniqueGlobalCallable` and
|
||||
* the receiver-bound member chain walk. ONLY apply for unqualified
|
||||
* call forms — `this->name` and `Base<T>::name` are dependent lookup
|
||||
* forms that the standard allows.
|
||||
*
|
||||
* Conservative bias: when the caller's enclosing class can't be
|
||||
* identified, return `false` (let normal resolution proceed). Over-
|
||||
* rejection is acceptable for the template case because the standard
|
||||
* itself requires `this->` or qualified forms for dependent base
|
||||
* access; missing edges here match the compiler's diagnostic shape.
|
||||
*/
|
||||
export function isCppDependentBaseMember(
|
||||
callerScopeId: ScopeId,
|
||||
candidateDef: SymbolDefinition,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): boolean {
|
||||
if (candidateDef.ownerId === undefined) return false;
|
||||
const enclosing = findEnclosingClassDef(callerScopeId, scopes);
|
||||
if (enclosing === undefined) return false;
|
||||
const bases = dependentBaseNodeIds.get(enclosing.nodeId);
|
||||
if (bases === undefined) return false;
|
||||
return bases.has(candidateDef.ownerId);
|
||||
}
|
||||
|
|
@ -560,6 +560,13 @@ export interface ScopeResolver {
|
|||
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||
readonly callerParsed: ParsedFile;
|
||||
readonly candidate: SymbolDefinition;
|
||||
/** Caller's enclosing scope id. Languages that gate visibility on
|
||||
* caller scope (e.g. C++ two-phase template lookup) consult it;
|
||||
* others ignore. Optional so existing implementations stay valid. */
|
||||
readonly callerScope?: ScopeId;
|
||||
/** ScopeResolutionIndexes for scope-tree walks. Optional for the
|
||||
* same reason as `callerScope`. */
|
||||
readonly scopes?: ScopeResolutionIndexes;
|
||||
}) => boolean;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export function emitFreeCallFallback(
|
|||
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||
readonly callerParsed: ParsedFile;
|
||||
readonly candidate: SymbolDefinition;
|
||||
readonly callerScope?: ScopeId;
|
||||
readonly scopes?: ScopeResolutionIndexes;
|
||||
}) => boolean;
|
||||
} = {},
|
||||
): number {
|
||||
|
|
@ -89,7 +91,12 @@ export function emitFreeCallFallback(
|
|||
site.arity,
|
||||
options.isCallableVisibleFromCaller !== undefined
|
||||
? (candidate) =>
|
||||
options.isCallableVisibleFromCaller!({ callerParsed: parsed, candidate })
|
||||
options.isCallableVisibleFromCaller!({
|
||||
callerParsed: parsed,
|
||||
candidate,
|
||||
callerScope: site.inScope,
|
||||
scopes,
|
||||
})
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base/base.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base/base.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
template<class T>
|
||||
struct Base {
|
||||
void f();
|
||||
int i;
|
||||
};
|
||||
13
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base/derived.h
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base/derived.h
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "base.h"
|
||||
|
||||
template<class T>
|
||||
struct Derived : Base<T> {
|
||||
void g() {
|
||||
f();
|
||||
}
|
||||
int h() {
|
||||
return i;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
template<class T>
|
||||
struct Base {
|
||||
void unused();
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include "base.h"
|
||||
#include "helpers.h"
|
||||
|
||||
template<class T>
|
||||
struct D : Base<T> {
|
||||
void g() {
|
||||
utils::ns_helper();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
namespace utils {
|
||||
void ns_helper();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-non-dependent-base/concrete-base.h
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-non-dependent-base/concrete-base.h
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
struct ConcreteBase {
|
||||
void f();
|
||||
};
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-non-dependent-base/derived.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-non-dependent-base/derived.h
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include "concrete-base.h"
|
||||
|
||||
template<class T>
|
||||
struct Derived : ConcreteBase {
|
||||
void g() {
|
||||
f();
|
||||
}
|
||||
};
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/base.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/base.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
template<class T>
|
||||
struct Base {
|
||||
void f();
|
||||
int i;
|
||||
};
|
||||
13
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/derived.h
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/derived.h
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "base.h"
|
||||
|
||||
template<class T>
|
||||
struct Derived : Base<T> {
|
||||
void g() {
|
||||
this->f();
|
||||
}
|
||||
int h() {
|
||||
return this->i;
|
||||
}
|
||||
};
|
||||
|
|
@ -1865,3 +1865,42 @@ describe('C++ default-argument overload ambiguity', () => {
|
|||
expect(fCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// U3 (follow-up plan 2026-05-13-001): two-phase template lookup.
|
||||
// Inside a class template body, unqualified calls MUST NOT bind to members
|
||||
// of a dependent base class. Only `this->name()` or `Base<T>::name()` forms
|
||||
// should resolve.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ two-phase template lookup — dependent base suppression', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-two-phase-dependent-base'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('Derived<T>::g() -> f() does NOT bind to Base<T>::f (dependent base)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const leaks = calls.filter((c) => c.source === 'g' && c.target === 'f');
|
||||
expect(leaks.length).toBe(0);
|
||||
});
|
||||
|
||||
it('Derived<T>::h() -> i does NOT bind to Base<T>::i (dependent base)', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const leaks = accesses.filter((c) => c.source === 'h' && c.target === 'i');
|
||||
expect(leaks.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: positive guards (this->f() resolves, non-dependent-base unqualified
|
||||
// f() resolves, namespace-qualified utils::ns_helper() resolves) inside
|
||||
// template bodies are documented gaps in C++ template-context resolution
|
||||
// independent of U3's dependent-base suppression. The U3 core asserts only
|
||||
// the negative behavior (dependent-base members are NOT bound by unqualified
|
||||
// calls); the positive cases would require additional `this` type-binding
|
||||
// and template-body member-lookup work tracked separately. See plan
|
||||
// 2026-05-13-001 follow-ups.
|
||||
|
|
|
|||
|
|
@ -115,6 +115,13 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
// only correctness win (PR #1520 review follow-up plan U4 / Claude
|
||||
// review Finding 7); backporting to legacy is out of scope.
|
||||
'emits zero CALLS edges for ambiguous foo() bound via two using-namespace declarations',
|
||||
// The legacy DAG path lacks two-phase template lookup. Unqualified
|
||||
// calls inside a class template body bind to dependent-base members
|
||||
// there, producing CALLS edges the compiler would reject (ISO C++
|
||||
// two-phase name lookup). Scope-resolver-only correctness win
|
||||
// (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)',
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue