From eca1bd61e8925ee217e535ea8b98b3012463670b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 11:10:51 +0000 Subject: [PATCH 1/3] Initial plan From 4b77de0d6f504906ddd465ee50d12b587bcdc7fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 11:31:02 +0000 Subject: [PATCH 2/3] fix(cpp): workspace-wide dependent-base name resolution (cross-file support) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace per-file `populateCppDependentBases(parsed)` with a workspace-wide `populateCppDependentBases(parsedFiles)` that builds a cross-file class index - Use qualified-name prefix for namespace disambiguation when multiple classes share a simple name (e.g. `Box` in two namespaces) - Move the call from `populateOwners` (per-file) to the new `populateWorkspaceOwners` hook so all files are processed before resolution runs - Add `cpp-two-phase-dependent-base-ns` fixture: Base in a namespace in a separate file from Derived, plus a namespace-free function with the same name — exercises the path where the class-owned filter does not apply - Add two integration tests for the new fixture" Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/d78fae8b-cd32-45d8-a815-2b27d7d89e62 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> --- .../ingestion/languages/cpp/scope-resolver.ts | 13 +- .../languages/cpp/two-phase-lookup.ts | 123 ++++++++++++++---- .../cpp-two-phase-dependent-base-ns/base.h | 19 +++ .../cpp-two-phase-dependent-base-ns/derived.h | 21 +++ .../test/integration/resolvers/cpp.test.ts | 31 +++++ 5 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/base.h create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/derived.h diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index a85e5b113c..02aaff3bf0 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -101,16 +101,21 @@ export const cppScopeResolver: ScopeResolver = { // fallback and wildcard expansion can suppress them as unqualified // cross-file callables. populateCppNonGloballyVisible(parsed); - // Resolve recorded template-class → dependent-base simple names to - // class nodeIds for two-phase template lookup (U3 of plan - // 2026-05-13-001). - populateCppDependentBases(parsed); // Build the class-def → enclosing-namespace-qualified-name map used // by ADL (U2 of plan 2026-05-13-001) to identify each argument type's // associated namespace for Koenig lookup. populateCppAssociatedNamespaces(parsed); }, + // Resolve recorded template-class → dependent-base simple names to + // class nodeIds for two-phase template lookup (U3 of plan + // 2026-05-13-001). Runs AFTER all files have had `populateOwners` + // applied so that cross-file base classes (e.g. Base in base.h, + // Derived in derived.h) are reachable in the workspace index. + populateWorkspaceOwners: (parsedFiles: readonly ParsedFile[]) => { + populateCppDependentBases(parsedFiles); + }, + // Simple `isSuperReceiver` returns false for C++. Real super // classification is caller-context-dependent and lives in // `isSuperReceiverInContext` below — without scope context the diff --git a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts index 7840ed81ae..6c42f22ca4 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts @@ -11,8 +11,20 @@ * This module records — during `emitCppScopeCaptures` — which template * class declarations have which dependent base class names (per file). * `populateCppDependentBases` then resolves those names to class nodeIds - * using the workspace registry, building the per-class set the - * `isDependentBaseMember` predicate consumes. + * using a workspace-wide registry, building the per-class set the + * `isCppDependentBaseMember` predicate consumes. + * + * Cross-file resolution: `Base` may be declared in a different header + * than `Derived`. `populateCppDependentBases` therefore runs as a + * workspace-wide pass (`populateWorkspaceOwners` hook) after every file + * has had `populateOwners` applied, so all class defs are reachable. + * + * Namespace disambiguation: when multiple classes share a simple name + * (e.g., `Box` in two namespaces), the resolver prefers the candidate + * whose qualified-name prefix (namespace path) matches the deriving + * class's prefix. If no namespace match is found, a unique simple-name + * match is accepted; ambiguous matches (multiple candidates, no + * namespace winner) are skipped conservatively. * * NOTE: module-level state, single-process-single-repo use only. * `clearFileLocalNames()` clears this state alongside file-local linkage @@ -69,37 +81,92 @@ export function clearCppDependentBases(): void { } /** - * Resolve recorded dependent-base simple names to class nodeIds using - * the parsed file's localDefs. Run as part of `populateOwners` so the - * resolved set is available before any resolution pass consults it. + * Resolve recorded dependent-base simple names to class nodeIds using a + * workspace-wide index. Run as `populateWorkspaceOwners` after every + * file has had `populateOwners` applied, so class defs from ALL files + * are reachable. * - * Matches by simple name within the same file (the template class and - * its base are typically declared in the same TU; cross-file template - * bases are an edge case deferred to V2). + * Disambiguation strategy (multiple classes sharing a simple name): + * 1. Prefer the candidate whose qualified-name namespace prefix matches + * the deriving class's namespace prefix (same-namespace bias). + * 2. Fall back to accepting a unique simple-name match. + * 3. Skip when multiple candidates exist and no namespace match is + * found (conservative: avoids false associations). */ -export function populateCppDependentBases(parsed: ParsedFile): void { - const perFile = dependentBasesByFile.get(parsed.filePath); - if (perFile === undefined) return; - - // Build simple-name → nodeId index for this file's class-like defs. - const classByName = new Map(); - for (const def of parsed.localDefs) { - if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue; - const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? ''; - if (simple !== '') classByName.set(simple, def.nodeId); +export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): void { + if (dependentBasesByFile.size === 0) return; + + // Build workspace-wide index: simpleName → {nodeId, nsPrefix}[] + // nsPrefix is the dot-joined namespace path (qualifiedName without the + // last segment). Classes at global scope have nsPrefix = ''. + const classesBySimpleName = new Map(); + for (const parsed of parsedFiles) { + for (const def of parsed.localDefs) { + if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue; + const qn = def.qualifiedName ?? ''; + const lastDot = qn.lastIndexOf('.'); + const simple = lastDot >= 0 ? qn.slice(lastDot + 1) : qn; + if (simple === '') continue; + const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : ''; + let entries = classesBySimpleName.get(simple); + if (entries === undefined) { + entries = []; + classesBySimpleName.set(simple, entries); + } + entries.push({ nodeId: def.nodeId, nsPrefix }); + } } - for (const [className, baseNames] of perFile) { - const classNodeId = classByName.get(className); - if (classNodeId === undefined) continue; - let bases = dependentBaseNodeIds.get(classNodeId); - if (bases === undefined) { - bases = new Set(); - dependentBaseNodeIds.set(classNodeId, bases); + // Build a filePath → ParsedFile lookup for fast per-file access. + const parsedByFile = new Map(); + for (const parsed of parsedFiles) parsedByFile.set(parsed.filePath, parsed); + + for (const [filePath, perFile] of dependentBasesByFile) { + const parsed = parsedByFile.get(filePath); + if (parsed === undefined) continue; + + // Build a simple-name → {nodeId, nsPrefix} map for THIS file's + // class-like defs so we can identify each template class precisely + // (avoids cross-file name collisions for the deriving class itself). + const localClassByName = new Map(); + for (const def of parsed.localDefs) { + if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue; + const qn = def.qualifiedName ?? ''; + const lastDot = qn.lastIndexOf('.'); + const simple = lastDot >= 0 ? qn.slice(lastDot + 1) : qn; + if (simple === '') continue; + const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : ''; + localClassByName.set(simple, { nodeId: def.nodeId, nsPrefix }); } - for (const baseName of baseNames) { - const baseNodeId = classByName.get(baseName); - if (baseNodeId !== undefined) bases.add(baseNodeId); + + for (const [className, baseNames] of perFile) { + const classEntry = localClassByName.get(className); + if (classEntry === undefined) continue; + + let bases = dependentBaseNodeIds.get(classEntry.nodeId); + if (bases === undefined) { + bases = new Set(); + dependentBaseNodeIds.set(classEntry.nodeId, bases); + } + + for (const baseName of baseNames) { + const candidates = classesBySimpleName.get(baseName); + if (candidates === undefined || candidates.length === 0) continue; + + if (candidates.length === 1) { + // Unique simple-name match — accept regardless of namespace. + bases.add(candidates[0].nodeId); + continue; + } + + // Multiple classes share the same simple name — prefer the one + // whose namespace matches the deriving class's namespace. + const nsMatch = candidates.find((c) => c.nsPrefix === classEntry.nsPrefix); + if (nsMatch !== undefined) { + bases.add(nsMatch.nodeId); + } + // else: ambiguous (multiple candidates, no namespace match) → skip. + } } } } diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/base.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/base.h new file mode 100644 index 0000000000..c9ecf5c229 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/base.h @@ -0,0 +1,19 @@ +#pragma once + +namespace geom { + +template +struct Base { + void compute(); + int area; +}; + +// Free function inside the same namespace — no ownerId, so the +// class-owned filter does NOT apply to this candidate. It is instead +// suppressed by the namespace-nesting filter (isCppDefGloballyVisible). +// The test therefore exercises a candidate path that is orthogonal to +// the class-owned filter, proving the overall suppression stack is +// robust even when ownerId-based blocking is absent. +void compute(); + +} // namespace geom diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/derived.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/derived.h new file mode 100644 index 0000000000..9596bcc81e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-dependent-base-ns/derived.h @@ -0,0 +1,21 @@ +#pragma once + +#include "base.h" + +namespace geom { + +template +struct Derived : Base { + // Unqualified call to compute() inside a template body whose base is + // dependent. Two-phase lookup: the compiler does NOT look into + // Base for this name — so GitNexus must also suppress the edge. + void g() { + compute(); + } + // Unqualified field access — same reasoning applies. + int h() { + return area; + } +}; + +} // namespace geom diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index bcc90350ab..26fb02d364 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -1903,6 +1903,37 @@ describe('C++ two-phase template lookup — dependent base suppression', () => { // and template-body member-lookup work tracked separately. See plan // 2026-05-13-001 follow-ups. +// --------------------------------------------------------------------------- +// U3 cross-file namespace variant: Base lives in a different file AND +// inside a namespace. The fixture also contains a free function with the +// same name inside the namespace — that candidate has no ownerId, so the +// class-owned filter does NOT apply to it; it is instead suppressed by the +// namespace-nesting filter. Both candidates must still yield zero edges. +// --------------------------------------------------------------------------- + +describe('C++ two-phase template lookup — cross-file namespace variant', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-two-phase-dependent-base-ns'), + () => {}, + ); + }, 60000); + + it('geom::Derived::g() -> compute() does NOT bind to geom::Base::compute (cross-file dependent base, class-owned)', () => { + const calls = getRelationships(result, 'CALLS'); + const leaks = calls.filter((c) => c.source === 'g' && c.target === 'compute'); + expect(leaks.length).toBe(0); + }); + + it('geom::Derived::h() -> area does NOT bind to geom::Base::area (cross-file dependent base, class-owned)', () => { + const accesses = getRelationships(result, 'ACCESSES'); + const leaks = accesses.filter((c) => c.source === 'h' && c.target === 'area'); + expect(leaks.length).toBe(0); + }); +}); + // --------------------------------------------------------------------------- // U2 (follow-up plan 2026-05-13-001): argument-dependent (Koenig) lookup. // Free-function calls with class-typed arguments must consider candidates From 1da1e74863cd3f7729e89e81b9b490e9adff83e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 11:34:17 +0000 Subject: [PATCH 3/3] fix(cpp): clarify V1 conservative exact-prefix namespace match in two-phase-lookup Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/d78fae8b-cd32-45d8-a815-2b27d7d89e62 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> --- .../src/core/ingestion/languages/cpp/two-phase-lookup.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts index 6c42f22ca4..8d0050eb14 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts @@ -161,6 +161,11 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v // Multiple classes share the same simple name — prefer the one // whose namespace matches the deriving class's namespace. + // V1: exact dot-prefix match only. Cross-namespace inheritance + // (e.g., `ns::outer::Derived` extending bare `Inner` defined in + // `ns::outer::inner`) and inline-namespace cases are deferred to + // V2; the conservative skip-on-ambiguity below avoids false + // associations in those edge cases. const nsMatch = candidates.find((c) => c.nsPrefix === classEntry.nsPrefix); if (nsMatch !== undefined) { bases.add(nsMatch.nodeId);