From e4379180263392034c8959fa35f84487fa3a3ba4 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Thu, 16 Jul 2026 15:10:55 -0500 Subject: [PATCH] fix(cpp): compose namespace prefix into out-of-line method qualified names (#1310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An out-of-line member definition inside a namespace block takes its qualifiedName from the declarator's receiver, which is spelled RELATIVE to the enclosing namespace — so `namespace simulator { ManifestStartup::Output ManifestStartup::Apply(...) {} }` indexed as ManifestStartup::Apply while the class node carried simulator::ManifestStartup. Fully-qualified call sites (simulator::ManifestStartup::Apply(...)) never resolved; callers and file impact came up empty (#1291). The receiver-based qualifiedName now composes the active namespace prefix, anchored at the first prefix segment the receiver re-spells (so `namespace sim { void sim::M::f() {} }` doesn't double-prefix). namespacePrefix is only ever non-empty for C++ — Go/Rust/Kotlin/Lua receivers pass through unchanged. leveldb re-index: node count byte-stable (3,044), calls edges +6, namespace-qualified method names 947 -> 1,252. Fixes #1291 Co-authored-by: Claude Fable 5 --- CHANGELOG.md | 1 + __tests__/extraction.test.ts | 41 +++++++++++++++++++++++ __tests__/resolution.test.ts | 63 +++++++++++++++++++++++++++++++++++ src/extraction/tree-sitter.ts | 28 +++++++++++++++- 4 files changed, 132 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5dde8..d89beae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- C++ methods defined out-of-line inside a namespace (`namespace sim { Output MyClass::Apply(...) { ... } }`) now carry the namespace in their qualified name, matching their class. Fully-qualified call sites from other files (`sim::MyClass::Apply(...)`) resolve to the definition again, so `codegraph callers` and file impact no longer come up empty for this pattern. (#1291) - C++ methods defined out-of-line on a template class (`template T Box::get() { ... }`) no longer keep the template parameter list in their qualified name. They now index as `Box::get` — identical to an inline definition of the same method — so they link to their class and resolve from call sites again, and pathological multi-line template parameter lists can no longer blow the qualified name past filesystem name limits. (#1286) - Go route detection no longer misidentifies ordinary method calls that share HTTP verb names — `cache.Put("key", value)`, `store.Get("config", out)`, `bus.Handle("user.created", handler)` and the like were being indexed as HTTP routes, polluting route listings in cache-heavy codebases. A registration now has to look like one: its first argument must be a `/`-prefixed path (all routers) or a Go 1.22 `"METHOD /path"` pattern on `Handle`/`HandleFunc`, which now also extracts the method instead of listing the route as `ANY`. (#1259) - Progress output on Windows no longer mixes ASCII `|` rails with the Unicode `│ ◆ ●` frame around them. In terminals that render Unicode (Windows Terminal, VS Code, ConEmu/Cmder, JetBrains, Alacritty), the whole `codegraph init` / `index` / `sync` block now draws with matching box-drawing characters; unrecognized legacy consoles keep the safe all-ASCII output that avoids garbled characters. `CODEGRAPH_ASCII=1` / `CODEGRAPH_UNICODE=1` still override in either direction. (#398) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 6498d42..4a1a5bc 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -3704,6 +3704,47 @@ int f() { return 1; } const result = extractFromSource('nested.cpp', code); expect(result.nodes.find((n) => n.name === 'f')?.qualifiedName).toBe('a::b::f'); }); + + // Out-of-line member definitions take their qualifiedName from the + // declarator's receiver (`ManifestStartup::Apply`), which is spelled + // RELATIVE to the enclosing namespace — the namespace prefix must compose + // in, or the method's qualifiedName diverges from its own class node's + // and `ns::Class::Method(...)` call sites never resolve (#1291). + it('out-of-line method definitions inside a namespace carry the namespace prefix', () => { + const code = `namespace simulator { +class ManifestStartup { +public: + struct Input { int x; }; + struct Output { int y; }; + static Output Apply(const Input& input); +}; +ManifestStartup::Output ManifestStartup::Apply(const Input& input) { return {}; } +} +`; + const result = extractFromSource('manifest_startup.cpp', code); + const apply = result.nodes.filter((n) => n.name === 'Apply'); + // The out-of-line definition's QN matches the class node's prefix. + expect(apply.map((n) => n.qualifiedName)).toContain('simulator::ManifestStartup::Apply'); + expect(result.nodes.find((n) => n.kind === 'class')?.qualifiedName).toBe( + 'simulator::ManifestStartup' + ); + }); + + it('a receiver that re-spells the namespace path is not double-prefixed', () => { + const code = `namespace sim { +class M { public: static void f(); static void g(); }; +void sim::M::f() {} +void M::g() {} +} +void sim::M::f2() {} +`; + const result = extractFromSource('m.cpp', code); + const qns = result.nodes.filter((n) => n.kind === 'method').map((n) => n.qualifiedName); + expect(qns).toContain('sim::M::f'); // fully re-spelled inside the namespace + expect(qns).toContain('sim::M::g'); // relative form + expect(qns).toContain('sim::M::f2'); // global scope, spelled absolute + expect(qns.find((q) => q?.includes('sim::sim'))).toBeUndefined(); + }); }); describe('C++ forward declarations do not mint phantom class nodes (#1093)', () => { diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index aa1bd72..3749388 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -2541,6 +2541,69 @@ func main() { }); }); + describe('C++ namespace-qualified static method calls to out-of-line definitions (#1291)', () => { + // The issue's exact shape: nested types + out-of-line static method + // definition inside `namespace simulator { }` in the .cpp, called via the + // fully-qualified path from a different file. The definition's + // qualifiedName previously dropped the namespace (`ManifestStartup::Apply` + // vs the class's `simulator::ManifestStartup`), so `callers` came up empty. + it('resolves simulator::ManifestStartup::Apply(...) from another file', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1291-')); + try { + fs.writeFileSync( + path.join(tmpDir, 'manifest_startup.h'), + `#pragma once +namespace simulator { +class ManifestStartup { +public: + struct Input { int a; }; + struct Output { int b; }; + static Output Apply(const Input& input); +}; +} +` + ); + fs.writeFileSync( + path.join(tmpDir, 'manifest_startup.cpp'), + `#include "manifest_startup.h" +namespace simulator { +ManifestStartup::Output ManifestStartup::Apply(const Input& input) { + return Output{input.a}; +} +} +` + ); + fs.writeFileSync( + path.join(tmpDir, 'main.cpp'), + `#include "manifest_startup.h" +int run() { + const auto manifest_result = simulator::ManifestStartup::Apply({1}); + return manifest_result.b; +} +` + ); + + const cg = CodeGraph.initSync(tmpDir); + await cg.indexAll(); + + const applyDefs = (await cg.searchNodes('Apply', { limit: 20 })).filter( + (r) => r.node.name === 'Apply' && r.node.kind === 'method' + ); + expect(applyDefs.length).toBeGreaterThan(0); + const def = applyDefs.find((r) => r.node.filePath.endsWith('manifest_startup.cpp')); + expect(def).toBeDefined(); + expect(def!.node.qualifiedName).toBe('simulator::ManifestStartup::Apply'); + + // The qualified cross-file call resolves: run() is a caller of Apply. + const callers = await cg.getCallers(def!.node.id); + expect(callers.map((c) => c.node.name)).toContain('run'); + cg.close(); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }, 30000); + }); + describe('C/C++ Import Resolution', () => { afterEach(() => { clearCppIncludeDirCache(); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index fe3664a..a94d3ed 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -1379,6 +1379,32 @@ export class TreeSitterExtractor { return ns?.id ?? null; } + /** + * Qualified name for a method defined out-of-line via a receiver qualifier + * (`Type::method() {}`). The declarator spells the receiver RELATIVE to the + * enclosing namespace, so the active C++ namespace prefix must be composed + * in — `namespace sim { Output ManifestStartup::Apply() {} }` previously + * indexed as `ManifestStartup::Apply` while the class node carried + * `sim::ManifestStartup`, so qualified call sites + * (`sim::ManifestStartup::Apply(...)`) never resolved (#1291). + * + * The source may also re-spell part or all of the namespace path + * (`namespace sim { void sim::M::f() {} }` is legal), so the receiver is + * anchored at the first prefix segment it names: everything before that + * anchor is taken from the prefix, the receiver supplies the rest. A + * receiver naming no prefix segment gets the whole prefix prepended. + * `namespacePrefix` is only ever non-empty for C++, so every other + * receiver language (Go, Rust, Kotlin, Lua) passes through unchanged. + */ + private composeReceiverQualifiedName(receiverType: string, name: string): string { + const base = `${receiverType}::${name}`; + if (this.namespacePrefix.length === 0) return base; + const receiverHead = receiverType.split('::')[0]; + const anchor = this.namespacePrefix.indexOf(receiverHead!); + const prefix = anchor === -1 ? this.namespacePrefix : this.namespacePrefix.slice(0, anchor); + return prefix.length > 0 ? `${prefix.join('::')}::${base}` : base; + } + /** * Build qualified name from node stack */ @@ -1726,7 +1752,7 @@ export class TreeSitterExtractor { returnType, }; if (receiverType) { - extraProps.qualifiedName = `${receiverType}::${name}`; + extraProps.qualifiedName = this.composeReceiverQualifiedName(receiverType, name); } const methodNode = this.createNode('method', name, node, extraProps);