From 7e1f697c6438bb6125440a8e4a5105a58f1c41aa Mon Sep 17 00:00:00 2001 From: Sparsh Date: Mon, 25 May 2026 08:46:29 +0530 Subject: [PATCH 1/2] fix(cpp): thread call-site types into qualified member lookup (#1632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widen Callsite (arity optional, add argumentTypes) and add optional callsite?: Callsite to ScopeResolver.resolveQualifiedReceiverMember. receiver-bound-calls.ts passes the ReferenceSite through structurally; resolveCppQualifiedNamespaceMember forwards it to narrowOverloadCandidates along with cppConversionRank, enabling exact-type and conversion-rank disambiguation across inline-namespace children. Behavior change: - outer::foo(42) where v1 declares foo(int) and v2 declares foo(double) now resolves to v1::foo (was: 0 edges, conservatively suppressed). - Same-name same-normalized-signature (e.g. foo(int) vs foo(long)) still suppresses at 0 edges via isOverloadAmbiguousAfterNormalization. - ADL using-import path (resolveAdlCandidates) unchanged — passes no callsite, narrowing degrades to existing pass-through behavior. Closes #1632. Part of #1564. --- gitnexus-shared/src/scope-resolution/types.ts | 7 ++-- .../languages/cpp/inline-namespaces.ts | 28 ++++++++-------- .../ingestion/languages/cpp/scope-resolver.ts | 10 ++++-- .../contract/scope-resolver.ts | 1 + .../passes/receiver-bound-calls.ts | 1 + .../caller.cpp | 5 +++ .../lib.h | 10 ++++++ .../test/integration/resolvers/cpp.test.ts | 33 +++++++++++++++---- 8 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/caller.cpp create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/lib.h diff --git a/gitnexus-shared/src/scope-resolution/types.ts b/gitnexus-shared/src/scope-resolution/types.ts index c3d8b80fbc..828874a7f1 100644 --- a/gitnexus-shared/src/scope-resolution/types.ts +++ b/gitnexus-shared/src/scope-resolution/types.ts @@ -267,8 +267,11 @@ export interface ScopeLookup { /** Call-site description passed to `arityCompatibility`. */ export interface Callsite { - /** Number of arguments at the call site. */ - readonly arity: number; + /** Number of arguments at the call site, if available. */ + readonly arity?: number; + /** Inferred argument types at the call site, one per argument. + * An empty string entry means the type was not inferred. */ + readonly argumentTypes?: readonly string[]; } // ─── §2.4 ImportEdge ──────────────────────────────────────────────────────── diff --git a/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts b/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts index 604c50c0b6..eec44c68ff 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts @@ -27,12 +27,13 @@ * declaration transparently. */ -import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared'; +import type { Callsite, ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared'; import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js'; +import { cppConversionRank } from './conversion-rank.js'; interface RangeKey { readonly startLine: number; @@ -107,6 +108,7 @@ export function resolveCppQualifiedNamespaceMember( memberName: string, parsedFiles: readonly ParsedFile[], _scopes: ScopeResolutionIndexes, + callsite?: Callsite, ): SymbolDefinition | 'ambiguous' | undefined { const allHits: SymbolDefinition[] = []; const seenNodeId = new Set(); @@ -132,19 +134,17 @@ export function resolveCppQualifiedNamespaceMember( if (allHits.length === 0) return undefined; if (allHits.length === 1) return allHits[0]; - // Multi-candidate: the `resolveQualifiedReceiverMember` hook has no - // access to call-site arity or argument types, so - // `narrowOverloadCandidates` cannot actually narrow here — the call - // with `(allHits, undefined, undefined)` is effectively a pass-through. - // We retain it so that `isOverloadAmbiguousAfterNormalization` can - // still detect int/long-style normalization collisions on this path, - // but for any multi-hit case where candidates have genuinely distinct - // signatures (e.g. `foo(int)` vs `foo(double)` in different inline - // children), we conservatively suppress rather than pick arbitrarily. - // A future enhancement could thread call-site argument info through - // the `resolveQualifiedReceiverMember` contract to enable real - // narrowing here. - const narrowed = narrowOverloadCandidates(allHits, undefined, undefined); + // Multi-candidate: thread call-site arity/argument-types through the + // `resolveQualifiedReceiverMember` contract so `narrowOverloadCandidates` + // can disambiguate via exact-type match and, when available, conversion-rank + // scoring (`cppConversionRank`). Same-signature ambiguity is still detected + // by `isOverloadAmbiguousAfterNormalization` below. + const narrowed = narrowOverloadCandidates( + allHits, + callsite?.arity, + callsite?.argumentTypes, + callsite !== undefined ? { conversionRankFn: cppConversionRank } : undefined, + ); if (narrowed.length === 1) return narrowed[0]; if (narrowed.length === 0) return undefined; if (isOverloadAmbiguousAfterNormalization(narrowed, undefined)) return 'ambiguous'; diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index 4a1f343d18..9a5ac85a55 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -275,6 +275,12 @@ export const cppScopeResolver: ScopeResolver = { // descends transitively through inline-namespace children when // searching for the called member. Returns undefined for non-namespace // receivers so receiver-bound-calls Case 2 still gets a chance. - resolveQualifiedReceiverMember: (receiverName, memberName, _callerScope, scopes, parsedFiles) => - resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, scopes), + resolveQualifiedReceiverMember: ( + receiverName, + memberName, + _callerScope, + scopes, + parsedFiles, + callsite, + ) => resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, scopes, callsite), }; diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index cdfbe3d8e3..2d07336840 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -705,6 +705,7 @@ export interface ScopeResolver { callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], + callsite?: Callsite, ) => SymbolDefinition | 'ambiguous' | undefined; /** diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts index ac7164c70f..5894036283 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts @@ -494,6 +494,7 @@ export function emitReceiverBoundCalls( site.inScope, scopes, parsedFiles, + site, ); if (memberDef === 'ambiguous') { // Same-name ambiguity across inline-namespace children (#1564): diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/caller.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/caller.cpp new file mode 100644 index 0000000000..fcb416b39c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/caller.cpp @@ -0,0 +1,5 @@ +#include "lib.h" + +void run() { + outer::foo(42); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/lib.h b/gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/lib.h new file mode 100644 index 0000000000..703fb15e52 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-inline-namespace-ambiguous-normalized/lib.h @@ -0,0 +1,10 @@ +#pragma once + +namespace outer { + inline namespace v1 { + void foo(int x); + } + inline namespace v2 { + void foo(long y); + } +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 90bc751171..94dab149ba 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -2996,14 +2996,35 @@ describe('C++ inline namespace — ambiguous distinct signatures (conservative s ); }, 60000); - it('outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(double)', () => { + it('outer::foo(42) emits exactly 1 CALLS edge to v1::foo(int) when v1 declares foo(int) and v2 declares foo(double)', () => { const calls = getRelationships(result, 'CALLS'); const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo'); - // Even though the two overloads have distinct signatures and a compiler - // could disambiguate via argument types, the `resolveQualifiedReceiverMember` - // hook lacks call-site arity/argument-type information, so multi-hit cases - // are conservatively suppressed. Documents the limitation noted in - // inline-namespaces.ts (Finding 1 of Claude review on #1600). + // Call-site arity and argument types are now threaded through the + // resolveQualifiedReceiverMember contract (#1632). narrowOverloadCandidates + // matches the exact type 'int' against v1::foo(int), producing exactly 1 edge. + expect(fooCalls).toHaveLength(1); + // Verify it resolved to v1::foo(int) at line 4 (0-indexed), not v2::foo(double) at line 7 + const targetNode = result.graph.getNode(fooCalls[0].rel.targetId); + expect(targetNode?.properties.startLine).toBe(4); + }); +}); + +describe('C++ inline namespace — ambiguous normalized signatures', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-inline-namespace-ambiguous-normalized'), + () => {}, + ); + }, 60000); + + it('outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(long) — both normalize to int', () => { + const calls = getRelationships(result, 'CALLS'); + const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo'); + // int and long both normalize to 'int' via normalizeCppParamType, making + // the two candidates indistinguishable after normalization. The resolver + // must suppress rather than pick arbitrarily (isOverloadAmbiguousAfterNormalization). expect(fooCalls.length).toBe(0); }); }); From f9a40e7c5ddd06b17dbf4ea436ee02533827e532 Mon Sep 17 00:00:00 2001 From: Sparsh Date: Mon, 25 May 2026 11:19:11 +0530 Subject: [PATCH 2/2] fix(cpp): update legacy parity expected-failure list for #1632 - Remove stale expected-failure entry for old diff-sigs test name (test now expects 1 edge; legacy DAG also emits 1 edge) - Add entry for normalized-signature ambiguity (int vs long) test - Rename describe block from 'conservative suppress' to 'distinct signatures resolved via call-site types' Verified both modes: REGISTRY_PRIMARY_CPP=1: 241/241 passed REGISTRY_PRIMARY_CPP=0: 194 passed, 47 skipped, 0 failed --- gitnexus/test/integration/resolvers/cpp.test.ts | 2 +- gitnexus/test/integration/resolvers/helpers.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index aaf709e9f6..0d31b3f186 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -3044,7 +3044,7 @@ describe('C++ inline namespace — ambiguous same-name across inline children (# }); }); -describe('C++ inline namespace — ambiguous distinct signatures (conservative suppress)', () => { +describe('C++ inline namespace — distinct signatures resolved via call-site types', () => { let result: PipelineResult; beforeAll(async () => { diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index f599835b58..486449aa9d 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -332,11 +332,14 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly