fix(extraction): resolve C++ inheritance from templated base classes (#1043) (#1048)

A C++ class deriving from a template — `class Derived : public Base<int>`,
a CRTP base `class App : public CRTPBase<App>`, a struct inheriting a
template, or a templated base mixed into a multi-base clause — recorded its
base as the full instantiation text (`Base<int>`). That never name-matched
the template, which is indexed as the bare node `Base`, so the `extends`
edge never resolved and the derived class looked like it inherited from
nothing — callers/impact analysis stopped at the boundary.

Strip the template arguments from the base-type reference name in the
`base_class_clause` handler via a new `stripCppTemplateArgs` helper: it
removes every balanced `<…>` group (any nesting/position), so `Base<int>`
→ `Base` and `ns::Tpl<int>` → `ns::Tpl`. The remaining qualified head is
exactly what the non-templated base case already produces, so resolution
treats templated and non-templated bases identically; a name with no
template args passes through unchanged.

Covers same-file and same-namespace bases (the dominant real-world
patterns). A base in a different namespace referenced with its qualifier
(`other_ns::Tpl<int>`) still doesn't resolve, but that's a pre-existing,
orthogonal namespace-resolution gap — the non-templated `other_ns::Plain`
fails identically — not a template issue.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-28 20:43:48 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 4c0c87ff6e
commit f4e03e9cdc
5 changed files with 122 additions and 2 deletions
+45
View File
@@ -2173,6 +2173,51 @@ func main() {
});
});
describe('C++ templated base-class inheritance (#1043)', () => {
// A class deriving from a TEMPLATE — `class D : public Base<int>` (or a CRTP
// `class W : public CRTPBase<W>`, or a qualified `class Q : public ns::Tpl<int>`)
// recorded its base as the full instantiation text (`Base<int>`), which never
// name-matched the template, indexed as the bare node `Base`. The `<…>` args
// are now stripped so the `extends` edge resolves end-to-end.
it('resolves an extends edge to a templated base (plain, CRTP, struct, multi-base)', async () => {
fs.writeFileSync(
path.join(tempDir, 'lib.hpp'),
`#pragma once
template<typename T> class Base { public: void foo(); };
template<typename Derived> class CRTPBase {};
class Plain {};
class Widget : public Base<int> {}; // plain template base
class App : public CRTPBase<App> {}; // CRTP (curiously-recurring)
struct Node : public Base<double> {}; // struct inheriting a template
class Both : public Base<char>, public Plain {}; // templated + plain in one clause
`
);
cg = await CodeGraph.init(tempDir, { index: true });
const db = DatabaseConnection.open(path.join(tempDir, '.codegraph', 'codegraph.db'));
const edges = db
.getDb()
.prepare(
`select src.name as fromName, dst.name as toName
from edges e
join nodes src on e.source = src.id
join nodes dst on e.target = dst.id
where e.kind = 'extends'`
)
.all() as Array<{ fromName: string; toName: string }>;
const has = (from: string, to: string) =>
edges.some((r) => r.fromName === from && r.toName === to);
// Every templated base now resolves to the bare template node.
expect(has('Widget', 'Base'), 'Widget : Base<int>').toBe(true);
expect(has('App', 'CRTPBase'), 'App : CRTPBase<App> (CRTP)').toBe(true);
expect(has('Node', 'Base'), 'struct Node : Base<double>').toBe(true);
// A mixed clause resolves BOTH the templated and the plain base.
expect(has('Both', 'Base'), 'Both : Base<char>').toBe(true);
expect(has('Both', 'Plain'), 'Both : Plain (non-templated, regression guard)').toBe(true);
});
});
describe('PHP Include Resolution', () => {
it('isPhpIncludePathRef distinguishes include paths from namespace use (#660)', () => {
const mk = (name: string, over: Partial<UnresolvedRef> = {}): UnresolvedRef => ({