mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(cpp): suppress deleted overload winners (#2094)
* fix(cpp): suppress deleted overload winners * test(cpp): update scope capture fingerprint --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
31a2b19416
commit
e26002c37a
23 changed files with 744 additions and 49 deletions
|
|
@ -57,6 +57,10 @@ export interface SymbolDefinition {
|
|||
* Currently used by C++ overload ranking to exclude explicit constructors
|
||||
* from implicit user-defined conversion candidates. */
|
||||
isExplicit?: boolean;
|
||||
/** True when the callable is declared unavailable (for example C++ `= delete`).
|
||||
* Unavailable callables still participate in overload selection, but a
|
||||
* selected unavailable target must suppress edge emission. */
|
||||
isDeleted?: boolean;
|
||||
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
|
||||
ownerId?: string;
|
||||
/** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`)
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@
|
|||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
|
||||
},
|
||||
"cpp": {
|
||||
"fingerprint": "f56625342f73e182170e2c964d538e316c079fa6e9466a7f076bff2ebcf8aac4",
|
||||
"fingerprint": "9b5b4393d158d76dcf1ef9807e0326462c45a5266310f0ae7894d017f3858219",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).",
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression). #2094: deleted C++ declarations retain @declaration.is-deleted metadata; deleted operator and pointer-return shapes plus the expanded deleted-overload fixture are included. Intended capture drift; scaling remains linear (1.139 < 1.5).",
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5)."
|
||||
},
|
||||
"csharp": {
|
||||
|
|
|
|||
|
|
@ -222,8 +222,14 @@ function findFuncDeclarator(node: SyntaxNode): SyntaxNode | null {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
// Unwrap pointer_declarator / reference_declarator
|
||||
while (decl.type === 'pointer_declarator' || decl.type === 'reference_declarator') {
|
||||
// Unwrap declarator wrappers. Deleted free functions are represented as
|
||||
// `init_declarator(function_declarator, delete_expression)` by
|
||||
// tree-sitter-cpp 0.23.
|
||||
while (
|
||||
decl.type === 'pointer_declarator' ||
|
||||
decl.type === 'reference_declarator' ||
|
||||
decl.type === 'init_declarator'
|
||||
) {
|
||||
const next = decl.childForFieldName('declarator');
|
||||
if (next === null) {
|
||||
// reference_declarator may not use field name
|
||||
|
|
|
|||
|
|
@ -163,6 +163,13 @@ export function emitCppScopeCaptures(
|
|||
'true',
|
||||
);
|
||||
}
|
||||
if (hasDeletedMethodClause(fnNode, grouped['@declaration.name']?.text)) {
|
||||
grouped['@declaration.is-deleted'] = syntheticCapture(
|
||||
'@declaration.is-deleted',
|
||||
fnNode,
|
||||
'true',
|
||||
);
|
||||
}
|
||||
|
||||
// Detect static storage class (file-local linkage)
|
||||
if (hasStaticStorageClass(fnNode)) {
|
||||
|
|
@ -1685,7 +1692,13 @@ 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;
|
||||
if (
|
||||
cur.type === 'identifier' ||
|
||||
cur.type === 'type_identifier' ||
|
||||
cur.type === 'operator_name'
|
||||
) {
|
||||
return cur.text;
|
||||
}
|
||||
// Common wrapper nodes — follow the 'declarator' field when present.
|
||||
const next =
|
||||
cur.childForFieldName('declarator') ??
|
||||
|
|
@ -1713,6 +1726,25 @@ function hasExplicitSpecifier(node: SyntaxNode): boolean {
|
|||
return /\bexplicit\b/.test(node.text.slice(0, 128));
|
||||
}
|
||||
|
||||
function hasDeletedMethodClause(node: SyntaxNode, callableName: string | undefined): boolean {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'delete_method_clause') return true;
|
||||
// tree-sitter-cpp 0.23 parses a deleted free-function declaration as
|
||||
// `declaration > init_declarator > delete_expression`, while class
|
||||
// members use the dedicated `delete_method_clause`.
|
||||
if (
|
||||
child?.type === 'init_declarator' &&
|
||||
child.childForFieldName('value')?.type === 'delete_expression' &&
|
||||
callableName !== undefined &&
|
||||
extractDeclaratorLeafName(child.childForFieldName('declarator') ?? child) === callableName
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a C++ function_definition or declaration has `static` storage class.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -194,6 +194,29 @@ const CPP_SCOPE_QUERY = `
|
|||
declarator: (function_declarator
|
||||
declarator: (identifier) @declaration.name)) @declaration.function
|
||||
|
||||
;; tree-sitter-cpp 0.23 represents a deleted free function as an
|
||||
;; init_declarator whose value is a delete_expression.
|
||||
(declaration
|
||||
declarator: (init_declarator
|
||||
declarator: (function_declarator
|
||||
declarator: (identifier) @declaration.name)
|
||||
value: (delete_expression))) @declaration.function
|
||||
|
||||
;; Deleted free operator declaration.
|
||||
(declaration
|
||||
declarator: (init_declarator
|
||||
declarator: (function_declarator
|
||||
declarator: (operator_name) @declaration.name)
|
||||
value: (delete_expression))) @declaration.function
|
||||
|
||||
;; Deleted free function with a pointer return type.
|
||||
(declaration
|
||||
declarator: (init_declarator
|
||||
declarator: (pointer_declarator
|
||||
declarator: (function_declarator
|
||||
declarator: (identifier) @declaration.name))
|
||||
value: (delete_expression))) @declaration.function
|
||||
|
||||
;; Free operator prototype: std::ostream& operator<<(std::ostream&, T)
|
||||
(declaration
|
||||
declarator: (function_declarator
|
||||
|
|
|
|||
|
|
@ -42,18 +42,11 @@ function findFunctionDeclarator(node: SyntaxNode): SyntaxNode | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect `= delete` and `= default` special member function declarations.
|
||||
* These are not callable methods and should be suppressed from extraction.
|
||||
* tree-sitter-cpp ^0.23.4 emits `delete_method_clause` / `default_method_clause`
|
||||
* as named children of the function_definition node.
|
||||
*/
|
||||
function isDeletedOrDefaulted(node: SyntaxNode): boolean {
|
||||
/** Detect a C++ special member clause by its tree-sitter node type. */
|
||||
function hasSpecialMethodClause(node: SyntaxNode, clauseType: string): boolean {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'delete_method_clause' || child?.type === 'default_method_clause') {
|
||||
return true;
|
||||
}
|
||||
if (child?.type === clauseType) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -67,10 +60,6 @@ function extractCppMethodName(node: SyntaxNode): string | undefined {
|
|||
const funcDecl = findFunctionDeclarator(node);
|
||||
if (!funcDecl) return undefined;
|
||||
|
||||
// Suppress `= delete` and `= default` special members — these are not callable
|
||||
// methods and should not appear in HAS_METHOD edges.
|
||||
if (isDeletedOrDefaulted(node)) return undefined;
|
||||
|
||||
const nameNode = funcDecl.childForFieldName('declarator');
|
||||
if (!nameNode) return undefined;
|
||||
// destructor_name: ~ClassName
|
||||
|
|
@ -387,6 +376,10 @@ export const cppMethodConfig: MethodExtractionConfig = {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isDeleted(node) {
|
||||
return hasSpecialMethodClause(node, 'delete_method_clause');
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ function buildMethod(
|
|||
...(config.isAsync?.(node) ? { isAsync: true } : {}),
|
||||
...(config.isPartial?.(node) ? { isPartial: true } : {}),
|
||||
...(config.isConst?.(node) ? { isConst: true } : {}),
|
||||
...(config.isDeleted?.(node) ? { isDeleted: true } : {}),
|
||||
annotations: config.extractAnnotations?.(node) ?? [],
|
||||
sourceFile: context.filePath,
|
||||
line: node.startPosition.row + 1,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface MethodInfo {
|
|||
isAsync?: boolean;
|
||||
isPartial?: boolean;
|
||||
isConst?: boolean;
|
||||
isDeleted?: boolean;
|
||||
annotations: string[];
|
||||
sourceFile: string;
|
||||
line: number;
|
||||
|
|
@ -84,6 +85,7 @@ export interface MethodExtractionConfig {
|
|||
isAsync?: (node: SyntaxNode) => boolean;
|
||||
isPartial?: (node: SyntaxNode) => boolean;
|
||||
isConst?: (node: SyntaxNode) => boolean;
|
||||
isDeleted?: (node: SyntaxNode) => boolean;
|
||||
/** Owner node types where member functions are effectively static (e.g.
|
||||
* Ruby singleton_class, Kotlin companion_object / object_declaration).
|
||||
* When the ownerNode matches one of these types, isStatic is forced true. */
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ export interface AddMetadata {
|
|||
templateArguments?: string[];
|
||||
ownerId?: string;
|
||||
qualifiedName?: string;
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -285,6 +286,7 @@ export const createSymbolTable = (): InternalSymbolTable => {
|
|||
? { templateArguments: metadata.templateArguments }
|
||||
: {}),
|
||||
...(metadata?.ownerId !== undefined ? { ownerId: metadata.ownerId } : {}),
|
||||
...(metadata?.isDeleted === true ? { isDeleted: true } : {}),
|
||||
};
|
||||
|
||||
// A. File Index — unconditional.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ export const mergeChunkResults = (
|
|||
templateArguments: sym.templateArguments,
|
||||
ownerId: sym.ownerId,
|
||||
qualifiedName: sym.qualifiedName,
|
||||
isDeleted: sym.isDeleted,
|
||||
});
|
||||
}
|
||||
if (exportedTypeMap) {
|
||||
|
|
|
|||
|
|
@ -575,6 +575,7 @@ function buildDefFromDeclarationMatch(
|
|||
const returnType = match['@declaration.return-type']?.text;
|
||||
const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
|
||||
const isExplicit = parseBooleanCapture(match['@declaration.is-explicit']);
|
||||
const isDeleted = parseBooleanCapture(match['@declaration.is-deleted']);
|
||||
|
||||
return {
|
||||
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
|
||||
|
|
@ -590,6 +591,7 @@ function buildDefFromDeclarationMatch(
|
|||
...(templateArguments !== undefined ? { templateArguments } : {}),
|
||||
...(templateConstraints !== undefined ? { templateConstraints } : {}),
|
||||
...(isExplicit === true ? { isExplicit: true } : {}),
|
||||
...(isDeleted === true ? { isDeleted: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1158,6 +1160,7 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
|
|||
'@declaration.return-type',
|
||||
'@declaration.template-constraints',
|
||||
'@declaration.is-explicit',
|
||||
'@declaration.is-deleted',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -383,6 +383,18 @@ export function emitFreeCallFallback(
|
|||
);
|
||||
}
|
||||
if (fnDef === undefined) continue;
|
||||
if (fnDef.isDeleted === true) {
|
||||
recordSuppressedOutcome(options.recordResolutionOutcome, {
|
||||
phase: 'free-call-fallback',
|
||||
filePath: parsed.filePath,
|
||||
name: site.name,
|
||||
range: site.atRange,
|
||||
reason: 'selected-callable-deleted',
|
||||
candidates: [fnDef],
|
||||
});
|
||||
handledSites.add(siteKey(parsed.filePath, site));
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(fnDefFromImplicitThis || fnDef.type === 'Method' || fnDef.type === 'Constructor') &&
|
||||
options.isCallableVisibleFromCaller !== undefined &&
|
||||
|
|
|
|||
|
|
@ -205,8 +205,14 @@ export function emitReceiverBoundCalls(
|
|||
if (impls === undefined) return 0;
|
||||
let n = 0;
|
||||
for (const implDef of impls) {
|
||||
const implMember = findOwnedMember(implDef.nodeId, memberName, model);
|
||||
if (implMember === undefined) continue;
|
||||
const implMember = pickOverload(implDef.nodeId, memberName, site, model, provider);
|
||||
if (
|
||||
implMember === undefined ||
|
||||
implMember === OVERLOAD_AMBIGUOUS ||
|
||||
implMember.isDeleted === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (implMember.nodeId === primaryMemberDef.nodeId) continue;
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
|
|
@ -257,11 +263,46 @@ export function emitReceiverBoundCalls(
|
|||
? extendsOnly(enclosingClass.nodeId)
|
||||
: scopes.methodDispatch.mroFor(enclosingClass.nodeId);
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
let ambiguousOwnerId: string | undefined;
|
||||
for (const ownerId of ancestors) {
|
||||
memberDef = findOwnedMember(ownerId, memberName, model);
|
||||
if (memberDef !== undefined) break;
|
||||
const picked =
|
||||
site.kind === 'call'
|
||||
? pickOverload(ownerId, memberName, site, model, provider)
|
||||
: findOwnedMember(ownerId, memberName, model);
|
||||
if (picked === OVERLOAD_AMBIGUOUS) {
|
||||
ambiguousOwnerId = ownerId;
|
||||
break;
|
||||
}
|
||||
if (picked !== undefined) {
|
||||
memberDef = picked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ambiguousOwnerId !== undefined) {
|
||||
recordReceiverOverloadSuppression(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
ambiguousOwnerId,
|
||||
memberName,
|
||||
model,
|
||||
provider,
|
||||
);
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
// Super/base calls resolve through the MRO chain, not
|
||||
// through imports — the ancestor method is found by
|
||||
// walking `methodDispatch.mroFor(enclosingClass)`, which
|
||||
|
|
@ -312,9 +353,9 @@ export function emitReceiverBoundCalls(
|
|||
if (currentClass !== undefined) {
|
||||
const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
let ambiguousOwnerId: string | undefined;
|
||||
// Static-only filter (#1756 / U3): same shape as Case 4's
|
||||
// chain walk (skip-and-walk-on) but without overload
|
||||
// narrowing — Case 0 uses `findOwnedMember` directly. When
|
||||
// overload-aware chain walk (skip-and-walk-on). When
|
||||
// an owner's resolved candidate is static-only (Kotlin
|
||||
// companion-promoted), continue to the next ancestor in
|
||||
// the MRO chain so a legitimate instance member can bind.
|
||||
|
|
@ -326,16 +367,45 @@ export function emitReceiverBoundCalls(
|
|||
// shapes like `Logger.create("a")`), so there's no wrong
|
||||
// target to suppress.
|
||||
for (const ownerId of chain) {
|
||||
const candidate = findOwnedMember(ownerId, memberName, model);
|
||||
if (candidate === undefined) continue;
|
||||
if (provider.isStaticOnly?.(candidate) === true) {
|
||||
// Skip static-only candidate; walk to next ancestor.
|
||||
const picked =
|
||||
site.kind === 'call'
|
||||
? pickFirstNonStaticOnly(ownerId, memberName, site, model, provider)
|
||||
: findOwnedMember(ownerId, memberName, model);
|
||||
if (picked === OVERLOAD_AMBIGUOUS) {
|
||||
ambiguousOwnerId = ownerId;
|
||||
break;
|
||||
}
|
||||
if (picked === STATIC_ONLY_FILTERED || picked === undefined) {
|
||||
continue;
|
||||
}
|
||||
memberDef = candidate;
|
||||
memberDef = picked;
|
||||
break;
|
||||
}
|
||||
if (ambiguousOwnerId !== undefined) {
|
||||
recordReceiverOverloadSuppression(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
ambiguousOwnerId,
|
||||
memberName,
|
||||
model,
|
||||
provider,
|
||||
);
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
|
|
@ -398,6 +468,17 @@ export function emitReceiverBoundCalls(
|
|||
}
|
||||
if (languageResolution?.kind === 'resolved') {
|
||||
const memberDef = languageResolution.definition;
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
|
|
@ -482,6 +563,17 @@ export function emitReceiverBoundCalls(
|
|||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
|
|
@ -509,11 +601,23 @@ export function emitReceiverBoundCalls(
|
|||
|
||||
// ── Case 1: namespace receiver ───────────────────────────────
|
||||
const targetFiles = namespaceTargets.get(receiverName);
|
||||
if (targetFiles !== undefined) {
|
||||
if (targetFiles !== undefined && provider.resolveQualifiedReceiverMember === undefined) {
|
||||
let found = false;
|
||||
for (const targetFile of targetFiles) {
|
||||
const memberDef = findExportedDef(targetFile, memberName, index);
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
|
|
@ -565,6 +669,17 @@ export function emitReceiverBoundCalls(
|
|||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
|
|
@ -587,9 +702,18 @@ export function emitReceiverBoundCalls(
|
|||
if (classDef !== undefined) {
|
||||
const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
let ambiguousOwnerId: string | undefined;
|
||||
for (const ownerId of chain) {
|
||||
memberDef = findOwnedMember(ownerId, memberName, model);
|
||||
if (memberDef !== undefined) {
|
||||
const picked =
|
||||
site.kind === 'call'
|
||||
? pickOverload(ownerId, memberName, site, model, provider)
|
||||
: findOwnedMember(ownerId, memberName, model);
|
||||
if (picked === OVERLOAD_AMBIGUOUS) {
|
||||
ambiguousOwnerId = ownerId;
|
||||
break;
|
||||
}
|
||||
if (picked !== undefined) {
|
||||
memberDef = picked;
|
||||
// The MRO chain is most-derived-first ([classDef, ...ancestors]).
|
||||
// If the most-derived definition is arity-incompatible with the
|
||||
// call site, PHP throws ArgumentCountError at runtime — it does
|
||||
|
|
@ -605,7 +729,31 @@ export function emitReceiverBoundCalls(
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (ambiguousOwnerId !== undefined) {
|
||||
recordReceiverOverloadSuppression(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
ambiguousOwnerId,
|
||||
memberName,
|
||||
model,
|
||||
provider,
|
||||
);
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
|
|
@ -641,8 +789,38 @@ export function emitReceiverBoundCalls(
|
|||
for (const targetFile3 of targetFiles3) {
|
||||
const classDef3 = findExportedDef(targetFile3, className, index);
|
||||
if (classDef3 !== undefined) {
|
||||
const memberDef = findOwnedMember(classDef3.nodeId, memberName, model);
|
||||
if (memberDef !== undefined) {
|
||||
const picked =
|
||||
site.kind === 'call'
|
||||
? pickOverload(classDef3.nodeId, memberName, site, model, provider)
|
||||
: findOwnedMember(classDef3.nodeId, memberName, model);
|
||||
if (picked === OVERLOAD_AMBIGUOUS) {
|
||||
recordReceiverOverloadSuppression(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
classDef3.nodeId,
|
||||
memberName,
|
||||
model,
|
||||
provider,
|
||||
);
|
||||
handledSites.add(siteKey);
|
||||
found3 = true;
|
||||
break;
|
||||
}
|
||||
if (picked !== undefined) {
|
||||
const memberDef = picked;
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
found3 = true;
|
||||
break;
|
||||
}
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
|
|
@ -700,8 +878,9 @@ export function emitReceiverBoundCalls(
|
|||
if (ownerDef !== undefined) {
|
||||
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
// Static-only filter (#1756 / U3): mirrors Case 0's chain
|
||||
// walk — `findOwnedMember` without overload narrowing. When
|
||||
let ambiguousOwnerId: string | undefined;
|
||||
// Static-only filter (#1756 / U3): mirrors Case 0's
|
||||
// overload-aware chain walk. When
|
||||
// a static-only candidate is found at an ancestor, walk on
|
||||
// so a legitimate instance member can bind. If the entire
|
||||
// chain is static-only, no edge is emitted (Case 3b is fed
|
||||
|
|
@ -709,15 +888,45 @@ export function emitReceiverBoundCalls(
|
|||
// `emitReferencesViaLookup` for compound shapes, so no
|
||||
// handled-site marker is needed for chain-only-static).
|
||||
for (const ownerId of chain) {
|
||||
const candidate = findOwnedMember(ownerId, memberName, model);
|
||||
if (candidate === undefined) continue;
|
||||
if (provider.isStaticOnly?.(candidate) === true) {
|
||||
const picked =
|
||||
site.kind === 'call'
|
||||
? pickFirstNonStaticOnly(ownerId, memberName, site, model, provider)
|
||||
: findOwnedMember(ownerId, memberName, model);
|
||||
if (picked === OVERLOAD_AMBIGUOUS) {
|
||||
ambiguousOwnerId = ownerId;
|
||||
break;
|
||||
}
|
||||
if (picked === STATIC_ONLY_FILTERED || picked === undefined) {
|
||||
continue;
|
||||
}
|
||||
memberDef = candidate;
|
||||
memberDef = picked;
|
||||
break;
|
||||
}
|
||||
if (ambiguousOwnerId !== undefined) {
|
||||
recordReceiverOverloadSuppression(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
ambiguousOwnerId,
|
||||
memberName,
|
||||
model,
|
||||
provider,
|
||||
);
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
|
|
@ -790,6 +999,17 @@ export function emitReceiverBoundCalls(
|
|||
}
|
||||
if (languageResolution?.kind === 'resolved') {
|
||||
const memberDef = languageResolution.definition;
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
|
|
@ -892,6 +1112,17 @@ export function emitReceiverBoundCalls(
|
|||
continue;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
memberDef,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
// For read/write ACCESSES, mirror the legacy DAG's reason
|
||||
// convention so consumers asserting `reason === 'write'`
|
||||
// keep working.
|
||||
|
|
@ -968,6 +1199,17 @@ export function emitReceiverBoundCalls(
|
|||
continue;
|
||||
}
|
||||
if (picked !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
picked,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
// Static-only filter (#1756 / U3): unlike Case 4 there's no
|
||||
// MRO chain to walk here — Case 5 dispatches on a single
|
||||
// owner via `pickOverload`. When the picked candidate is
|
||||
|
|
@ -1152,6 +1394,25 @@ function pickFirstNonStaticOnly(
|
|||
return candidates[0] ?? overloads[0];
|
||||
}
|
||||
|
||||
function suppressDeletedCallTarget(
|
||||
record: ResolutionOutcomeRecorder | undefined,
|
||||
filePath: string,
|
||||
site: ParsedFile['referenceSites'][number],
|
||||
target: SymbolDefinition,
|
||||
): boolean {
|
||||
if (site.kind !== 'call' || target.isDeleted !== true) return false;
|
||||
record?.({
|
||||
kind: 'suppressed',
|
||||
phase: 'receiver-bound-calls',
|
||||
filePath,
|
||||
name: site.name,
|
||||
range: site.atRange,
|
||||
reason: 'selected-callable-deleted',
|
||||
candidateIds: [target.nodeId],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function recordReceiverOverloadSuppression(
|
||||
record: ResolutionOutcomeRecorder | undefined,
|
||||
filePath: string,
|
||||
|
|
|
|||
|
|
@ -84,13 +84,41 @@ export function reconcileOwnership(
|
|||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
const ownerId = (def as { ownerId?: string }).ownerId;
|
||||
if (ownerId === undefined) continue;
|
||||
const simple = simpleQualifiedName(def);
|
||||
if (simple === undefined) continue;
|
||||
|
||||
if (def.type === 'Method' || def.type === 'Function' || def.type === 'Constructor') {
|
||||
if (ownerId === undefined) {
|
||||
if (def.isDeleted !== true) continue;
|
||||
const existingDef = model.symbols
|
||||
.lookupExactAll(def.filePath, simple)
|
||||
.find((candidate) => callableSignatureMatches(candidate, def));
|
||||
if (existingDef !== undefined) {
|
||||
existingDef.isDeleted = true;
|
||||
skippedAlreadyPresent++;
|
||||
continue;
|
||||
}
|
||||
model.symbols.add(def.filePath, simple, def.nodeId, def.type, {
|
||||
parameterCount: def.parameterCount,
|
||||
requiredParameterCount: def.requiredParameterCount,
|
||||
parameterTypes: def.parameterTypes,
|
||||
parameterTypeClasses: def.parameterTypeClasses,
|
||||
returnType: def.returnType,
|
||||
qualifiedName: def.qualifiedName,
|
||||
isDeleted: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const existing = model.methods.lookupAllByOwner(ownerId, simple);
|
||||
if (existing.some((e) => e.nodeId === def.nodeId)) {
|
||||
const existingDef = existing.find(
|
||||
(candidate) =>
|
||||
candidate.nodeId === def.nodeId ||
|
||||
(def.isDeleted === true && callableSignatureMatches(candidate, def)),
|
||||
);
|
||||
if (existingDef !== undefined) {
|
||||
if (def.isDeleted === true) {
|
||||
existingDef.isDeleted = true;
|
||||
}
|
||||
skippedAlreadyPresent++;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -124,6 +152,24 @@ export function reconcileOwnership(
|
|||
return { methodsRegistered, fieldsRegistered, nestedTypesRegistered, skippedAlreadyPresent };
|
||||
}
|
||||
|
||||
function callableSignatureMatches(
|
||||
left: ParsedFile['localDefs'][number],
|
||||
right: ParsedFile['localDefs'][number],
|
||||
): boolean {
|
||||
if (left.filePath !== right.filePath) return false;
|
||||
if (left.parameterCount !== right.parameterCount) return false;
|
||||
if (left.requiredParameterCount !== right.requiredParameterCount) return false;
|
||||
const leftTypes = left.parameterTypes;
|
||||
const rightTypes = right.parameterTypes;
|
||||
if (leftTypes === undefined || rightTypes === undefined) {
|
||||
return leftTypes === rightTypes;
|
||||
}
|
||||
return (
|
||||
leftTypes.length === rightTypes.length &&
|
||||
leftTypes.every((parameterType, index) => parameterType === rightTypes[index])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-mode parity validator. Runs only when
|
||||
* `VALIDATE_SEMANTIC_MODEL !== '0'` AND `NODE_ENV !== 'production'`.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export type ResolutionSuppressionReason =
|
|||
| 'conversion-rank-tied'
|
||||
| 'inline-ns-ambiguous'
|
||||
| 'member-lookup-ambiguous'
|
||||
| 'selected-callable-deleted'
|
||||
| 'overload-ambiguous'
|
||||
| 'overload-ambiguous-normalization';
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ export function buildMethodProps(info: MethodInfo): Record<string, unknown> {
|
|||
...(info.isAsync ? { isAsync: info.isAsync } : {}),
|
||||
...(info.isPartial ? { isPartial: info.isPartial } : {}),
|
||||
...(info.isConst ? { isConst: info.isConst } : {}),
|
||||
...(info.isDeleted ? { isDeleted: info.isDeleted } : {}),
|
||||
...(info.annotations.length > 0 ? { annotations: info.annotations } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ interface ParsedSymbol {
|
|||
isReadonly?: boolean;
|
||||
isAbstract?: boolean;
|
||||
isFinal?: boolean;
|
||||
isDeleted?: boolean;
|
||||
annotations?: string[];
|
||||
}
|
||||
|
||||
|
|
@ -2207,6 +2208,9 @@ const processFileGroup = (
|
|||
isReadonly: methodProps.isReadonly as boolean | undefined,
|
||||
isAbstract: methodProps.isAbstract as boolean | undefined,
|
||||
isFinal: methodProps.isFinal as boolean | undefined,
|
||||
...(methodProps.isDeleted !== undefined
|
||||
? { isDeleted: methodProps.isDeleted as boolean }
|
||||
: {}),
|
||||
...(methodProps.isVirtual !== undefined
|
||||
? { isVirtual: methodProps.isVirtual as boolean }
|
||||
: {}),
|
||||
|
|
|
|||
84
gitnexus/test/fixtures/lang-resolution/cpp-deleted-overload/main.cpp
vendored
Normal file
84
gitnexus/test/fixtures/lang-resolution/cpp-deleted-overload/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
void choose(double) {}
|
||||
void choose(int) = delete;
|
||||
|
||||
void call_live_free() {
|
||||
choose(1.5);
|
||||
}
|
||||
|
||||
void call_deleted_free() {
|
||||
choose(1);
|
||||
}
|
||||
|
||||
struct Gadget {
|
||||
Gadget() = default;
|
||||
|
||||
void touch(int) {}
|
||||
void touch(double) = delete;
|
||||
};
|
||||
|
||||
struct BaseChoice {
|
||||
void select(double) = delete;
|
||||
void select(int) {}
|
||||
};
|
||||
|
||||
struct DerivedChoice : BaseChoice {
|
||||
void call_base_qualified_live() {
|
||||
BaseChoice::select(1);
|
||||
}
|
||||
};
|
||||
|
||||
struct StaticChoice {
|
||||
static void select(double) = delete;
|
||||
static void select(int) {}
|
||||
};
|
||||
|
||||
struct DefaultedChoice {
|
||||
DefaultedChoice(const DefaultedChoice&) = default;
|
||||
DefaultedChoice(int) {}
|
||||
};
|
||||
|
||||
namespace choices {
|
||||
void select(double) = delete;
|
||||
void select(int);
|
||||
}
|
||||
|
||||
void call_live_member(Gadget& gadget) {
|
||||
gadget.touch(1);
|
||||
}
|
||||
|
||||
void call_deleted_member(Gadget& gadget) {
|
||||
gadget.touch(1.5);
|
||||
}
|
||||
|
||||
void call_defaulted_constructor() {
|
||||
auto gadget = Gadget();
|
||||
}
|
||||
|
||||
void call_same_arity_defaulted_constructor() {
|
||||
DefaultedChoice source(1);
|
||||
auto copy = DefaultedChoice(source);
|
||||
}
|
||||
|
||||
void call_inherited_live(DerivedChoice& choice) {
|
||||
choice.select(1);
|
||||
}
|
||||
|
||||
void call_inherited_deleted(DerivedChoice& choice) {
|
||||
choice.select(1.5);
|
||||
}
|
||||
|
||||
void call_static_live() {
|
||||
StaticChoice::select(1);
|
||||
}
|
||||
|
||||
void call_static_deleted() {
|
||||
StaticChoice::select(1.5);
|
||||
}
|
||||
|
||||
void call_namespace_live() {
|
||||
choices::select(1);
|
||||
}
|
||||
|
||||
void call_namespace_deleted() {
|
||||
choices::select(1.5);
|
||||
}
|
||||
|
|
@ -4385,3 +4385,78 @@ describe('C++ root-anchored base ignores enclosing-relative type (issue #1982)',
|
|||
expect(e!.rel.targetId).not.toContain('Wrap');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ deleted overload selection (#1893 A2)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-deleted-overload'), () => {});
|
||||
}, 60000);
|
||||
|
||||
const callsFrom = (source: string, target: string) =>
|
||||
getRelationships(result, 'CALLS').filter(
|
||||
(edge) => edge.source === source && edge.target === target,
|
||||
);
|
||||
const targetParameterTypes = (source: string, target: string) => {
|
||||
const edge = callsFrom(source, target);
|
||||
expect(edge).toHaveLength(1);
|
||||
return result.graph.getNode(edge[0]!.rel.targetId)?.properties.parameterTypes;
|
||||
};
|
||||
|
||||
it('keeps a live free-function winner callable', () => {
|
||||
expect(callsFrom('call_live_free', 'choose')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('suppresses a deleted best free-function match instead of rerouting', () => {
|
||||
expect(callsFrom('call_deleted_free', 'choose')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps a live member winner callable', () => {
|
||||
expect(targetParameterTypes('call_live_member', 'touch')).toEqual(['int']);
|
||||
});
|
||||
|
||||
it('suppresses a deleted best member match instead of rerouting', () => {
|
||||
expect(callsFrom('call_deleted_member', 'touch')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps a defaulted constructor callable', () => {
|
||||
expect(callsFrom('call_defaulted_constructor', 'Gadget')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('ranks a live base-qualified overload declared after a deleted sibling', () => {
|
||||
expect(targetParameterTypes('call_base_qualified_live', 'select')).toEqual(['int']);
|
||||
});
|
||||
|
||||
it('ranks inherited overloads before applying deleted suppression', () => {
|
||||
expect(targetParameterTypes('call_inherited_live', 'select')).toEqual(['int']);
|
||||
expect(callsFrom('call_inherited_deleted', 'select')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ranks class-qualified static overloads before applying deleted suppression', () => {
|
||||
expect(targetParameterTypes('call_static_live', 'select')).toEqual(['int']);
|
||||
expect(callsFrom('call_static_deleted', 'select')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ranks namespace-qualified overloads before applying deleted suppression', () => {
|
||||
expect(targetParameterTypes('call_namespace_live', 'select')).toEqual(['int']);
|
||||
expect(callsFrom('call_namespace_deleted', 'select')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps a same-arity defaulted copy constructor callable', () => {
|
||||
expect(callsFrom('call_same_arity_defaulted_constructor', 'DefaultedChoice')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('records every deleted-winner suppression explicitly', () => {
|
||||
const outcomes = getResolutionOutcomes(result).filter(
|
||||
(outcome) => outcome.kind === 'suppressed' && outcome.reason === 'selected-callable-deleted',
|
||||
);
|
||||
expect(outcomes).toHaveLength(5);
|
||||
expect(outcomes.map((outcome) => outcome.name).sort()).toEqual([
|
||||
'choose',
|
||||
'select',
|
||||
'select',
|
||||
'select',
|
||||
'touch',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2399,7 +2399,7 @@ describe('C++ MethodExtractor', () => {
|
|||
expect(result!.methods[1].visibility).toBe('public');
|
||||
});
|
||||
|
||||
it('suppresses = delete special members from extraction', () => {
|
||||
it('retains = delete special members and marks them unavailable', () => {
|
||||
const tree = parseCPP(`
|
||||
class NonCopyable {
|
||||
public:
|
||||
|
|
@ -2411,11 +2411,12 @@ describe('C++ MethodExtractor', () => {
|
|||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, cppCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
expect(result!.methods[0].name).toBe('doWork');
|
||||
expect(result!.methods).toHaveLength(3);
|
||||
expect(result!.methods.filter((method) => method.isDeleted)).toHaveLength(2);
|
||||
expect(result!.methods.find((method) => method.name === 'doWork')?.isDeleted).toBeUndefined();
|
||||
});
|
||||
|
||||
it('suppresses = default special members from extraction', () => {
|
||||
it('retains = default special members as callable', () => {
|
||||
const tree = parseCPP(`
|
||||
class Widget {
|
||||
public:
|
||||
|
|
@ -2427,8 +2428,9 @@ describe('C++ MethodExtractor', () => {
|
|||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, cppCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
expect(result!.methods[0].name).toBe('paint');
|
||||
expect(result!.methods).toHaveLength(3);
|
||||
expect(result!.methods.map((method) => method.name)).toEqual(['Widget', '~Widget', 'paint']);
|
||||
expect(result!.methods.every((method) => method.isDeleted !== true)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not suppress = 0 (pure virtual) as deleted/defaulted', () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { emitCppScopeCaptures } from '../../../../src/core/ingestion/languages/cpp/captures.js';
|
||||
import { cppProvider } from '../../../../src/core/ingestion/languages/c-cpp.js';
|
||||
import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js';
|
||||
import {
|
||||
clearFileLocalNames,
|
||||
isFileLocal,
|
||||
|
|
@ -424,6 +426,72 @@ describe('emitCppScopeCaptures — arity enrichment', () => {
|
|||
expect(m!['@declaration.parameter-count'].text).toBe('2');
|
||||
});
|
||||
|
||||
it('tags deleted declarations but not defaulted declarations', () => {
|
||||
const deleted = findMatch('void foo(int) = delete;', (tags) =>
|
||||
tags.includes('@declaration.is-deleted'),
|
||||
);
|
||||
const defaulted = emitCppScopeCaptures('struct S { S() = default; };', 'test.cpp').find(
|
||||
(match) => Object.values(match).some((capture) => capture.text.includes('= default')),
|
||||
);
|
||||
|
||||
expect(deleted?.['@declaration.is-deleted'].text).toBe('true');
|
||||
expect(defaulted).toBeDefined();
|
||||
expect(defaulted?.['@declaration.is-deleted']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('tags deleted free operators', () => {
|
||||
const deleted = findMatch(
|
||||
'struct S {}; bool operator==(const S&, const S&) = delete;',
|
||||
(tags) => tags.includes('@declaration.is-deleted'),
|
||||
);
|
||||
|
||||
expect(deleted?.['@declaration.name'].text).toBe('operator==');
|
||||
expect(deleted?.['@declaration.is-deleted'].text).toBe('true');
|
||||
});
|
||||
|
||||
it('tags deleted pointer-return free functions', () => {
|
||||
const deleted = findMatch('int* lookup(int) = delete;', (tags) =>
|
||||
tags.includes('@declaration.is-deleted'),
|
||||
);
|
||||
|
||||
expect(deleted?.['@declaration.name'].text).toBe('lookup');
|
||||
expect(deleted?.['@declaration.is-deleted'].text).toBe('true');
|
||||
});
|
||||
|
||||
it('does not borrow a deleted initializer from another declarator', () => {
|
||||
const declarations = allMatches('void f(int), g = delete(new int);', (tags) =>
|
||||
tags.includes('@declaration.function'),
|
||||
);
|
||||
const f = declarations.find((match) => match['@declaration.name']?.text === 'f');
|
||||
|
||||
expect(f).toBeDefined();
|
||||
expect(f?.['@declaration.is-deleted']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves deleted-callable metadata in parsed local definitions', () => {
|
||||
const parsed = extractParsedFile(
|
||||
cppProvider,
|
||||
`
|
||||
void choose(int) = delete;
|
||||
struct S {
|
||||
S() = default;
|
||||
void touch(double) = delete;
|
||||
};
|
||||
`,
|
||||
'test.cpp',
|
||||
);
|
||||
|
||||
const choose = parsed?.localDefs.find((def) => def.qualifiedName === 'choose');
|
||||
const touch = parsed?.localDefs.find((def) => def.qualifiedName === 'touch');
|
||||
const constructor = parsed?.localDefs.find(
|
||||
(def) => def.type === 'Constructor' && def.qualifiedName === 'S',
|
||||
);
|
||||
|
||||
expect(choose?.isDeleted).toBe(true);
|
||||
expect(touch?.isDeleted).toBe(true);
|
||||
expect(constructor?.isDeleted).not.toBe(true);
|
||||
});
|
||||
|
||||
it('enriches call reference with arity', () => {
|
||||
const src = 'void f() { foo(1, 2, 3); }';
|
||||
const m = findMatch(src, (t) => t.includes('@reference.arity'));
|
||||
|
|
|
|||
|
|
@ -212,6 +212,62 @@ describe('reconcileOwnership', () => {
|
|||
expect(model.methods.lookupAllByOwner('def:User', 'save')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('preserves deleted-callable metadata when the worker def is already registered', () => {
|
||||
const model = createSemanticModel();
|
||||
model.symbols.add('models.cpp', 'touch', 'Method:models.cpp:Gadget.touch', 'Method', {
|
||||
ownerId: 'def:Gadget',
|
||||
qualifiedName: 'Gadget.touch',
|
||||
parameterCount: 1,
|
||||
requiredParameterCount: 1,
|
||||
parameterTypes: ['double'],
|
||||
});
|
||||
const deleted = {
|
||||
...mkMethod({
|
||||
nodeId: 'def:models.cpp#3:2:Method:touch',
|
||||
filePath: 'models.cpp',
|
||||
name: 'touch',
|
||||
ownerId: 'def:Gadget',
|
||||
}),
|
||||
parameterCount: 1,
|
||||
requiredParameterCount: 1,
|
||||
parameterTypes: ['double'],
|
||||
isDeleted: true,
|
||||
};
|
||||
|
||||
const stats = reconcileOwnership([mkFile('models.cpp', [deleted])], model);
|
||||
const registered = model.methods.lookupAllByOwner('def:Gadget', 'touch');
|
||||
|
||||
expect(stats.skippedAlreadyPresent).toBe(1);
|
||||
expect(registered).toHaveLength(1);
|
||||
expect(registered[0].isDeleted).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves deleted metadata for unowned free-function overloads', () => {
|
||||
const model = createSemanticModel();
|
||||
model.symbols.add('helpers.cpp', 'choose', 'Function:helpers.cpp:choose#int', 'Function', {
|
||||
parameterCount: 1,
|
||||
requiredParameterCount: 1,
|
||||
parameterTypes: ['int'],
|
||||
});
|
||||
const deleted: SymbolDefinition = {
|
||||
nodeId: 'def:helpers.cpp#4:0:Function:choose',
|
||||
filePath: 'helpers.cpp',
|
||||
type: 'Function',
|
||||
qualifiedName: 'choose',
|
||||
parameterCount: 1,
|
||||
requiredParameterCount: 1,
|
||||
parameterTypes: ['int'],
|
||||
isDeleted: true,
|
||||
};
|
||||
|
||||
const stats = reconcileOwnership([mkFile('helpers.cpp', [deleted])], model);
|
||||
const registered = model.symbols.lookupCallableByName('choose');
|
||||
|
||||
expect(stats.skippedAlreadyPresent).toBe(1);
|
||||
expect(registered).toHaveLength(1);
|
||||
expect(registered[0].isDeleted).toBe(true);
|
||||
});
|
||||
|
||||
it('registers nested class-like types (Class/Enum/Interface) into TypeRegistry by owner', () => {
|
||||
const model = createSemanticModel();
|
||||
const inner: SymbolDefinition = {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,24 @@ describe('SymbolTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('callable availability metadata', () => {
|
||||
it('preserves isDeleted in file, callable, and owner indexes', () => {
|
||||
table.add('src/example.cpp', 'choose', 'func:choose:int', 'Function', {
|
||||
parameterTypes: ['int'],
|
||||
isDeleted: true,
|
||||
});
|
||||
table.add('src/example.cpp', 'touch', 'method:touch:double', 'Method', {
|
||||
ownerId: 'class:Widget',
|
||||
parameterTypes: ['double'],
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
expect(table.lookupExactAll('src/example.cpp', 'choose')[0]?.isDeleted).toBe(true);
|
||||
expect(table.lookupCallableByName('choose')[0]?.isDeleted).toBe(true);
|
||||
expect(model.methods.lookupAllByOwner('class:Widget', 'touch')[0]?.isDeleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Property exclusion from callable index', () => {
|
||||
it('Property with ownerId is NOT in callable index', () => {
|
||||
table.add('src/models.ts', 'name', 'prop:name', 'Property', {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue