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
+27
View File
@@ -84,6 +84,33 @@ export function normalizeCppReturnType(raw: string): string | undefined {
return last;
}
/**
* Strip C++ template arguments from a base-type reference name so it matches the
* bare class/struct the template was DEFINED as. `template<typename T> class
* Base { … }` is indexed as a node named `Base`, but a derived class
* `class D : public Base<int>` records its base as the full `Base<int>` (and
* `class Q : public ns::Tpl<int>` as `ns::Tpl<int>`) — neither name-matches
* `Base` / `ns::Tpl`, so the `extends` edge never resolves and the derived class
* looks like it inherits from nothing (#1043).
*
* Removes every balanced `<…>` group regardless of nesting or position, so
* `Base<int>` → `Base`, `ns::Tpl<Foo<int>>` → `ns::Tpl`, and the rare
* `Outer<int>::Inner` → `Outer::Inner`. The remaining qualified head is exactly
* what the non-templated base case already produces, so resolution treats them
* identically. A name with no template args passes through unchanged.
*/
export function stripCppTemplateArgs(name: string): string {
if (!name.includes('<')) return name;
let out = '';
let depth = 0;
for (const ch of name) {
if (ch === '<') depth++;
else if (ch === '>') { if (depth > 0) depth--; }
else if (depth === 0) out += ch;
}
return out.trim();
}
/**
* A function/method's return type lives in the `function_definition`'s `type`
* field (`Metrics& Metrics::instance()` → `Metrics`). Constructors, destructors,