mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
Merge branch 'main' into feat/Desktop-app
This commit is contained in:
commit
9e23c80cab
23 changed files with 451 additions and 46 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* C++ argument-dependent lookup (ADL / Koenig lookup) — V1.
|
||||
* C++ argument-dependent lookup (ADL / Koenig lookup).
|
||||
*
|
||||
* When ordinary unqualified lookup fails for a free-call site, ADL also
|
||||
* considers candidates declared in the **associated namespaces** of the
|
||||
|
|
@ -13,17 +13,18 @@
|
|||
* `using` anything. With V1 ADL: `audit::record` is discovered via
|
||||
* `audit::Event`'s associated namespace.
|
||||
*
|
||||
* ## V1 boundary
|
||||
* ## Current boundary
|
||||
*
|
||||
* V1 covers ONE associated-entity rule: an argument that's a directly-named
|
||||
* The current implementation covers ONE associated-entity rule: an argument that's a directly-named
|
||||
* class type (`audit::Event e`) contributes its **direct enclosing
|
||||
* namespace** to the candidate set. Anything else — pointer/reference
|
||||
* namespace** to the candidate set. V2 extends that one step to
|
||||
* pointer-typed class args (`audit::Event* p`, `audit::Event** pp`):
|
||||
* they contribute the pointee class's enclosing namespace too. Reference
|
||||
* arguments, function-pointer arguments, template specializations,
|
||||
* base-class associated namespaces — is V2 closure work and is
|
||||
* deliberately excluded. The `cpp-adl-pointer-arg-boundary` fixture
|
||||
* locks the exclusion in CI.
|
||||
* base-class associated namespaces, and the rest of the full closure are
|
||||
* still deliberately excluded.
|
||||
*
|
||||
* V1 also short-circuits to ADL only when ordinary lookup is empty
|
||||
* 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.
|
||||
|
|
@ -60,16 +61,16 @@ import {
|
|||
} from '../../scope-resolution/passes/overload-narrowing.js';
|
||||
|
||||
/**
|
||||
* Per-argument shape information collected at capture time. ADL only
|
||||
* fires for arguments where `simpleClassName !== ''` AND `!isPointer`
|
||||
* AND `!isReference` — i.e., directly-named class-type values.
|
||||
* Per-argument shape information collected at capture time. ADL fires for
|
||||
* arguments where `simpleClassName !== ''` AND `!isReference`, including
|
||||
* class pointers whose declarator chain resolves to a named class type.
|
||||
*/
|
||||
export interface CppAdlArgInfo {
|
||||
/** Simple class-like type name (last segment of qualified name); empty
|
||||
* for primitives, literals, function pointers, template specs, etc. */
|
||||
readonly simpleClassName: string;
|
||||
/** True when the variable's declarator was a `pointer_declarator`. V1
|
||||
* excludes pointer-typed args (closure rules deferred to V2). */
|
||||
/** True when the variable's declarator contained one or more
|
||||
* `pointer_declarator` wrappers. */
|
||||
readonly isPointer: boolean;
|
||||
/** True when the variable's declarator was a `reference_declarator`. */
|
||||
readonly isReference: boolean;
|
||||
|
|
@ -151,8 +152,8 @@ export function populateCppAssociatedNamespaces(parsed: ParsedFile): void {
|
|||
*
|
||||
* Fires only when:
|
||||
* - the call site is not in `noAdlSites` (parenthesized form), AND
|
||||
* - at least one argument is a directly-named class type (not pointer,
|
||||
* not reference, not literal/primitive).
|
||||
* - at least one argument resolves to a named class type (value or
|
||||
* pointer, but not reference, function pointer, literal, or primitive).
|
||||
*/
|
||||
export function pickCppAdlCandidates(
|
||||
site: {
|
||||
|
|
@ -170,11 +171,11 @@ export function pickCppAdlCandidates(
|
|||
const args = argInfoBySite.get(key);
|
||||
if (args === undefined || args.length === 0) return undefined;
|
||||
|
||||
// Collect associated namespace QNames from every value-class-typed arg.
|
||||
// Collect associated namespace QNames from every participating class-typed arg.
|
||||
const associatedNamespaces = new Set<string>();
|
||||
for (const arg of args) {
|
||||
if (arg.simpleClassName === '') continue;
|
||||
if (arg.isPointer || arg.isReference) continue;
|
||||
if (arg.isReference) continue;
|
||||
const classDef = findCppClassDefBySimpleName(arg.simpleClassName, scopes);
|
||||
if (classDef === undefined) continue;
|
||||
const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId);
|
||||
|
|
|
|||
|
|
@ -720,14 +720,15 @@ function isParenthesizedFunctionCall(callNode: SyntaxNode): boolean {
|
|||
|
||||
/**
|
||||
* Per-argument ADL classification: walk each argument of a free call and
|
||||
* decide whether it's a directly-named class type (V1 ADL fires) or
|
||||
* something V1 excludes (pointer, reference, primitive, literal, function
|
||||
* pointer, template specialization).
|
||||
* decide whether it resolves to a directly-named class or class-pointer
|
||||
* type (ADL fires) or to an excluded shape such as a reference, function
|
||||
* pointer, primitive, literal, or template specialization.
|
||||
*
|
||||
* V1 only fires for value class-typed args: `void f(N::S); N::S s; f(s);`.
|
||||
* Pointer args (`N::S* p; f(p);`) intentionally return `simpleClassName=''`
|
||||
* to lock the V1 boundary — the `cpp-adl-pointer-arg-boundary` fixture
|
||||
* regression-tests this.
|
||||
* Class-typed values and class pointers (`N::S`, `N::S*`, `N::S**`) all
|
||||
* preserve the pointee class name for associated-namespace lookup.
|
||||
* Function pointers remain excluded even when their return type names a
|
||||
* class, because the associated entity is the pointed-to function type,
|
||||
* not the return type.
|
||||
*/
|
||||
function inferCppCallAdlArgs(callNode: SyntaxNode): CppAdlArgInfo[] {
|
||||
const argList = callNode.childForFieldName('arguments');
|
||||
|
|
@ -792,15 +793,23 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
|
|||
|
||||
// Unwrap declarator chain to find pointer/reference markers and the
|
||||
// variable name. `init_declarator > pointer_declarator > identifier`
|
||||
// means pointer-typed; `init_declarator > reference_declarator > ...`
|
||||
// means reference-typed; bare `init_declarator > identifier` is value.
|
||||
// means pointer-typed; repeated pointer wrappers still count as pointer
|
||||
// typed; `init_declarator > reference_declarator > ...` means
|
||||
// reference-typed; bare `init_declarator > identifier` is value.
|
||||
// Function-pointer wrappers (`pointer_declarator > function_declarator`)
|
||||
// must not contribute ADL associated namespaces.
|
||||
let isPointer = false;
|
||||
let isReference = false;
|
||||
let isFunctionPointer = false;
|
||||
let inner: SyntaxNode = declarator;
|
||||
let nameText: string | null = null;
|
||||
let safety = 16; // bound walk depth defensively
|
||||
while (safety-- > 0) {
|
||||
if (inner.type === 'pointer_declarator') {
|
||||
if (findFirstDescendantOfType(inner, 'function_declarator') !== null) {
|
||||
isFunctionPointer = true;
|
||||
break;
|
||||
}
|
||||
isPointer = true;
|
||||
const next = inner.childForFieldName('declarator');
|
||||
if (next === null) break;
|
||||
|
|
@ -828,11 +837,15 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
|
|||
inner = next;
|
||||
continue;
|
||||
}
|
||||
if (inner.type === 'function_declarator') {
|
||||
isFunctionPointer = true;
|
||||
break;
|
||||
}
|
||||
// Reached the leaf — usually `identifier`. Take its text.
|
||||
nameText = inner.text;
|
||||
break;
|
||||
}
|
||||
if (nameText !== varName) continue;
|
||||
if (isFunctionPointer || nameText !== varName) continue;
|
||||
|
||||
const simpleClassName = extractAdlSimpleTypeName(typeNode);
|
||||
return { simpleClassName, isPointer, isReference };
|
||||
|
|
@ -841,9 +854,9 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
|
|||
}
|
||||
|
||||
/** Extract the simple class-like type name from a `type:` field node.
|
||||
* Returns '' for primitives, template specializations, function pointers,
|
||||
* and any other shape V1 ADL doesn't support — those args are excluded
|
||||
* from associated-namespace closure. */
|
||||
* Returns '' for primitives, template specializations, and any other
|
||||
* unsupported type-only shape. Function pointers are filtered at the
|
||||
* declarator level in `lookupAdlIdentifierType`. */
|
||||
function extractAdlSimpleTypeName(typeNode: SyntaxNode): string {
|
||||
if (typeNode.type === 'primitive_type') return '';
|
||||
if (typeNode.type === 'sized_type_specifier') return '';
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ import {
|
|||
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
|
||||
|
|
@ -178,6 +182,8 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
// for cross-file propagation and compound-receiver chain resolution.
|
||||
// cppBindingScopeFor hoists @type-binding.return to Module scope.
|
||||
hoistTypeBindingsToModule: true,
|
||||
// Enable receiver-bound explicit-`this` fallback only for C++.
|
||||
resolveThisViaEnclosingClass: true,
|
||||
// The `isFileLocalDef` hook on the global free-call fallback names
|
||||
// file-local linkage historically, but semantically gates "logically
|
||||
// invisible cross-file" defs. C++ extends this to also reject class-
|
||||
|
|
@ -219,6 +225,33 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
// V1 limitation: only direct enclosing-namespace closure for value
|
||||
// class-typed args; pointer/reference/template-spec args 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
|
||||
// named-using visibility; recover by resolving the imported namespace
|
||||
// member directly when the local call name matches a named using import.
|
||||
const usingNamedHits: SymbolDefinition[] = [];
|
||||
const seenUsing = new Set<string>();
|
||||
for (const imp of callerParsed.parsedImports) {
|
||||
if (imp.kind !== 'named') continue;
|
||||
if (imp.localName !== site.name) continue;
|
||||
const member = resolveCppQualifiedNamespaceMember(
|
||||
imp.targetRaw,
|
||||
imp.importedName,
|
||||
parsedFiles,
|
||||
scopes,
|
||||
);
|
||||
if (member === undefined) 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 result = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
|
||||
if (result === ADL_AMBIGUOUS) return 'ambiguous';
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -627,6 +627,18 @@ export interface ScopeResolver {
|
|||
parsedFiles: readonly ParsedFile[],
|
||||
) => SymbolDefinition | undefined;
|
||||
|
||||
/**
|
||||
* Enable the receiver-bound Case 0.5 fallback for explicit `this`
|
||||
* receivers (`this->m()` / `this.m()`) that resolves against the
|
||||
* enclosing class + MRO even when no explicit `this` typeBinding is
|
||||
* present in scope.
|
||||
*
|
||||
* Keep disabled for languages where the existing type-binding path
|
||||
* (Case 4) already handles `this` correctly and overload ambiguity
|
||||
* suppression must remain unchanged.
|
||||
*/
|
||||
readonly resolveThisViaEnclosingClass?: boolean;
|
||||
|
||||
/**
|
||||
* Optional post-finalize hook to inject cross-file bindings that
|
||||
* aren't modeled via explicit imports. Runs after
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'unwrapCollectionAccessor'
|
||||
| 'hoistTypeBindingsToModule'
|
||||
| 'resolveQualifiedReceiverMember'
|
||||
| 'resolveThisViaEnclosingClass'
|
||||
>;
|
||||
|
||||
function normalizeTemplateArgToken(value: string): string {
|
||||
|
|
@ -321,6 +322,88 @@ export function emitReceiverBoundCalls(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Case 0.5: implicit `this` receiver ───────────────────────
|
||||
// C++ `this->member()` (and same-shape receivers in other OO
|
||||
// languages) should resolve against the enclosing class + MRO
|
||||
// even when there is no explicit `this` typeBinding in scope.
|
||||
if (provider.resolveThisViaEnclosingClass === true && receiverName === 'this') {
|
||||
const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
|
||||
if (enclosingClass !== undefined) {
|
||||
const chain = [
|
||||
enclosingClass.nodeId,
|
||||
...scopes.methodDispatch.mroFor(enclosingClass.nodeId),
|
||||
];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
let ambiguous = false;
|
||||
let hiddenByName = false;
|
||||
for (const ownerId of chain) {
|
||||
const methodOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
|
||||
if (methodOverloads.length > 0) {
|
||||
const narrowed = narrowOverloadCandidates(
|
||||
methodOverloads,
|
||||
site.arity,
|
||||
site.argumentTypes,
|
||||
);
|
||||
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
|
||||
ambiguous = true;
|
||||
break;
|
||||
}
|
||||
if (narrowed.length === 0) {
|
||||
// C++ name hiding: if the derived class declares `f`, base-class
|
||||
// overloads named `f` are hidden for member lookup
|
||||
// ([basic.lookup.classref]). A non-viable derived overload set
|
||||
// therefore terminates lookup instead of falling through to base.
|
||||
hiddenByName = true;
|
||||
break;
|
||||
}
|
||||
memberDef = narrowed[0] ?? methodOverloads[0];
|
||||
break;
|
||||
}
|
||||
|
||||
// Field/property lookup intentionally runs only after the method
|
||||
// lookup above: in C++ member-name lookup, functions with this
|
||||
// name hide same-named base members; we therefore prefer method
|
||||
// candidates first and only target a field when no methods with
|
||||
// this name exist on the current owner.
|
||||
memberDef = model.fields.lookupFieldByOwner(ownerId, memberName);
|
||||
if (memberDef !== undefined) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ambiguous) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (hiddenByName) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
: memberDef.filePath !== parsed.filePath
|
||||
? 'import-resolved'
|
||||
: 'global';
|
||||
const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
reason,
|
||||
seen,
|
||||
confidence,
|
||||
collapse,
|
||||
);
|
||||
if (ok) emitted++;
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Case 1: namespace receiver ───────────────────────────────
|
||||
const targetFiles = namespaceTargets.get(receiverName);
|
||||
if (targetFiles !== undefined) {
|
||||
|
|
|
|||
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
void (*g)();
|
||||
record(g);
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/audit.h
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/audit.h
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
namespace audit {
|
||||
void record(void (*g)());
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/app.cpp
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
void (*fp)();
|
||||
audit::Event e;
|
||||
record(e);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/audit.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/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-function-pointer-class-return-arg/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
audit::Event (*factory)();
|
||||
record(factory);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/audit.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/audit.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace audit {
|
||||
struct Event {};
|
||||
void record(Event (*factory)());
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "audit.h"
|
||||
|
||||
namespace app {
|
||||
void run() {
|
||||
audit::Event** pp;
|
||||
record(pp);
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/audit.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/audit.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
namespace audit {
|
||||
struct Event {};
|
||||
void record(Event** e);
|
||||
}
|
||||
|
|
@ -3,9 +3,12 @@
|
|||
#include "base.h"
|
||||
#include "helpers.h"
|
||||
|
||||
using utils::ns_helper_2;
|
||||
|
||||
template<class T>
|
||||
struct D : Base<T> {
|
||||
void g() {
|
||||
utils::ns_helper();
|
||||
ns_helper_2();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
namespace utils {
|
||||
void ns_helper();
|
||||
void ns_helper_2();
|
||||
}
|
||||
|
|
|
|||
6
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/base.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/base.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
template<class T>
|
||||
struct Base {
|
||||
void f();
|
||||
};
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/derived.h
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/derived.h
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "base.h"
|
||||
|
||||
template<class T>
|
||||
struct Derived : Base<T> {
|
||||
void g_unqualified() {
|
||||
f();
|
||||
}
|
||||
|
||||
void g_this() {
|
||||
this->f();
|
||||
}
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/base.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/base.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
template<class T>
|
||||
struct Base {
|
||||
void f();
|
||||
};
|
||||
16
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/derived.h
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/derived.h
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "base.h"
|
||||
|
||||
template<class T>
|
||||
struct Derived : Base<T> {
|
||||
void f(int);
|
||||
|
||||
void g() {
|
||||
this->f();
|
||||
}
|
||||
|
||||
void g_ok() {
|
||||
this->f(42);
|
||||
}
|
||||
};
|
||||
|
|
@ -3,5 +3,6 @@
|
|||
template<class T>
|
||||
struct Base {
|
||||
void f();
|
||||
void base_method();
|
||||
int i;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ struct Derived : Base<T> {
|
|||
void g() {
|
||||
this->f();
|
||||
}
|
||||
void k() {
|
||||
this->base_method();
|
||||
}
|
||||
int h() {
|
||||
return this->i;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1972,14 +1972,100 @@ describe('C++ two-phase template lookup — dependent base suppression', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// 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.
|
||||
describe('C++ two-phase template lookup — positive this-qualified calls', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-two-phase-this-qualified'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('Derived<T>::g() -> this->f() resolves to f (1 edge)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const thisCalls = calls.filter((c) => c.source === 'g' && c.target === 'f');
|
||||
expect(thisCalls.length).toBe(1);
|
||||
expect(thisCalls[0].targetFilePath).toContain('base.h');
|
||||
});
|
||||
|
||||
it('Derived<T>::k() -> this->base_method() resolves via EXTENDS chain (1 edge)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const inheritedCalls = calls.filter((c) => c.source === 'k' && c.target === 'base_method');
|
||||
expect(inheritedCalls.length).toBe(1);
|
||||
expect(inheritedCalls[0].targetFilePath).toContain('base.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ two-phase template lookup — paired unqualified + this-qualified in one fixture', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-two-phase-paired'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('Derived<T>::g_unqualified() -> f() does NOT bind to Base<T>::f', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const leaks = calls.filter((c) => c.source === 'g_unqualified' && c.target === 'f');
|
||||
expect(leaks.length).toBe(0);
|
||||
});
|
||||
|
||||
it('Derived<T>::g_this() -> this->f() resolves to Base<T>::f (1 edge)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const resolved = calls.filter((c) => c.source === 'g_this' && c.target === 'f');
|
||||
expect(resolved.length).toBe(1);
|
||||
expect(resolved[0].targetFilePath).toContain('base.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ two-phase template lookup — namespace calls inside template body', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-two-phase-namespace-free-call-inside-template'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('D<T>::g() -> utils::ns_helper() resolves (1 edge)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const qualifiedCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper');
|
||||
expect(qualifiedCalls.length).toBe(1);
|
||||
expect(qualifiedCalls[0].targetFilePath).toContain('helpers.h');
|
||||
});
|
||||
|
||||
it('D<T>::g() -> ns_helper_2() resolves after using-declaration (1 edge)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const usingCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper_2');
|
||||
expect(usingCalls.length).toBe(1);
|
||||
expect(usingCalls[0].targetFilePath).toContain('helpers.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ two-phase template lookup — this-> name-hiding arity mismatch', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-two-phase-this-name-hiding-arity'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('Derived<T>::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const fCalls = calls.filter((c) => c.source === 'g' && c.target === 'f');
|
||||
expect(fCalls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('Derived<T>::g_ok() -> this->f(42) resolves to derived overload (1 edge)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const fCalls = calls.filter((c) => c.source === 'g_ok' && c.target === 'f');
|
||||
expect(fCalls.length).toBe(1);
|
||||
expect(fCalls[0].targetFilePath).toContain('derived.h');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// U3 cross-file namespace variant: Base lives in a different file AND
|
||||
|
|
@ -2057,7 +2143,7 @@ describe('C++ ADL — parenthesized name suppresses ADL', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — pointer-arg V1 boundary', () => {
|
||||
describe('C++ ADL — pointer arg unwrapping', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
|
|
@ -2067,19 +2153,81 @@ describe('C++ ADL — pointer-arg V1 boundary', () => {
|
|||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(p) where p is audit::Event* emits zero CALLS — V1 ADL excludes pointer args', () => {
|
||||
it('record(p) where p is audit::Event* resolves to audit::record via ADL', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
expect(recordCalls.length).toBe(1);
|
||||
expect(recordCalls[0].targetFilePath).toContain('audit.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — function pointer args do not participate', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-function-pointer-arg'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(g) where g is void (*)() emits zero CALLS edges', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
// Exact .toBe(0): V1 ADL covers only directly-named class-type values
|
||||
// (per plan 2026-05-13-001 R4). Pointer-typed args fall under
|
||||
// associated-entity closure rules deferred to V2. This fixture locks
|
||||
// the boundary in CI so the implementer cannot accidentally extend
|
||||
// V1 to include pointer types. Real ISO C++ would resolve via V2
|
||||
// closure; matching that requires the V2 follow-up plan.
|
||||
expect(recordCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — preceding function-pointer declarations do not block class args', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-function-pointer-before-class-arg'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(e) still resolves via ADL when an earlier declaration is void (*)()', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
expect(recordCalls.length).toBe(1);
|
||||
expect(recordCalls[0].targetFilePath).toContain('audit.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — class-returning function pointer args do not participate', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-function-pointer-class-return-arg'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('record(factory) where factory is audit::Event (*)() emits zero CALLS edges', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
expect(recordCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — pointer-to-pointer args participate', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-pointer-to-pointer'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('record(pp) where pp is audit::Event** resolves to audit::record via ADL', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
|
||||
expect(recordCalls.length).toBe(1);
|
||||
expect(recordCalls[0].targetFilePath).toContain('audit.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUOUS', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,15 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
'emits distinct Class nodes for List<User> and List<Order>',
|
||||
'callSave() in each specialization resolves to its own save()',
|
||||
'save specialization bodies route to their own sibling method',
|
||||
// PR #1590 follow-up: explicit `this->` resolution in template class
|
||||
// bodies and paired two-phase assertions are scope-resolver-only.
|
||||
// Legacy DAG lacks this receiver-bound template semantics and
|
||||
// dependent-base suppression parity for these shapes.
|
||||
'Derived<T>::g() -> this->f() resolves to f (1 edge)',
|
||||
'Derived<T>::k() -> this->base_method() resolves via EXTENDS chain (1 edge)',
|
||||
'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',
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue