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,
+7 -2
View File
@@ -21,6 +21,7 @@ import { FN_REF_SPECS, captureFnRefCandidates, type FnRefSpec, type FnRefCandida
import { isGeneratedFile } from './generated-detection';
import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
import { EXTRACTORS } from './languages';
import { stripCppTemplateArgs } from './languages/c-cpp';
import { LiquidExtractor } from './liquid-extractor';
import { RazorExtractor } from './razor-extractor';
import { SvelteExtractor } from './svelte-extractor';
@@ -4455,7 +4456,11 @@ export class TreeSitterExtractor {
// C++ base classes: `class Derived : public Base, private Other` →
// base_class_clause holds access specifiers + base type(s). Emit an extends
// ref per base type (skip the public/private/protected keywords).
// ref per base type (skip the public/private/protected keywords). A
// templated base (`Base<int>`, `ns::Tpl<int>`) arrives as a `template_type`
// or a `qualified_identifier` wrapping one; strip the `<…>` args so the ref
// matches the bare class the template was defined as — `Base`, `ns::Tpl` —
// instead of never resolving (#1043).
if (child.type === 'base_class_clause') {
for (const t of child.namedChildren) {
if (
@@ -4465,7 +4470,7 @@ export class TreeSitterExtractor {
) {
this.unresolvedReferences.push({
fromNodeId: classId,
referenceName: getNodeText(t, this.source),
referenceName: stripCppTemplateArgs(getNodeText(t, this.source)),
referenceKind: 'extends',
line: t.startPosition.row + 1,
column: t.startPosition.column,