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 59b6c323dc..ef1d4a0ebf 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 @@ -501,6 +501,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 e33fa66329..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 () => { @@ -3054,14 +3054,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); }); }); 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