Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions gitnexus-shared/src/scope-resolution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────
Expand Down
28 changes: 14 additions & 14 deletions gitnexus/src/core/ingestion/languages/cpp/inline-namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string>();
Expand All @@ -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';
Expand Down
10 changes: 8 additions & 2 deletions gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ export interface ScopeResolver {
callerScope: ScopeId,
scopes: ScopeResolutionIndexes,
parsedFiles: readonly ParsedFile[],
callsite?: Callsite,
) => SymbolDefinition | 'ambiguous' | undefined;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ export function emitReceiverBoundCalls(
site.inScope,
scopes,
parsedFiles,
site,
);
if (memberDef === 'ambiguous') {
// Same-name ambiguity across inline-namespace children (#1564):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "lib.h"

void run() {
outer::foo(42);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

namespace outer {
inline namespace v1 {
void foo(int x);
}
inline namespace v2 {
void foo(long y);
}
}
35 changes: 28 additions & 7 deletions gitnexus/test/integration/resolvers/cpp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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)', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 🔥

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);
});
});
Expand Down
13 changes: 8 additions & 5 deletions gitnexus/test/integration/resolvers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,14 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'outer::foo() emits zero CALLS edges when v1 and v2 both declare foo',
'records a structured suppression reason for inline namespace ambiguity',
// Distinct-signature inline-namespace ambiguity: `foo(int)` in v1 and
// `foo(double)` in v2. The scope-resolver conservatively suppresses
// because `resolveQualifiedReceiverMember` lacks call-site argument
// types. Legacy DAG has no inline-namespace resolver. Scope-resolver-
// only correctness win (#1600 / Claude review Finding 1).
'outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(double)',
// `foo(double)` in v2. PR #1810 threads call-site types through the
// resolveQualifiedReceiverMember contract — resolved in both mode paths.
// Legacy DAG emits an edge via the global callable fallback; the test
// now expects 1 edge, so the old expected-failure entry is removed.
// Normalized-signature ambiguity: `foo(int)` vs `foo(long)` both map to
// `int` via normalizeCppParamType. Scope-resolver suppresses via
// isOverloadAmbiguousAfterNormalization; legacy path picks arbitrarily.
'outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(long) — both normalize to int',
// PR #1598: ADL free-function reference arg negative fixtures rely on
// scope-resolver-only correctness. The legacy DAG falls back to
// `pickUniqueGlobalCallable` which resolves the callee by simple-name
Expand Down
Loading