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:
co-authored by
Claude Opus 4.8
parent
4c0c87ff6e
commit
f4e03e9cdc
@@ -11,6 +11,7 @@ import * as os from 'os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource, scanDirectory, buildDefaultIgnore } from '../src/extraction';
|
||||
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
|
||||
import { stripCppTemplateArgs } from '../src/extraction/languages/c-cpp';
|
||||
import { normalizePath } from '../src/utils';
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -2721,6 +2722,47 @@ class Plain : public Base { public: int y; };
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ templated base-class inheritance (#1043)', () => {
|
||||
// Inheriting from a template (`class D : public Base<int>`) recorded the base
|
||||
// ref as the full instantiation `Base<int>`, which never name-matched the
|
||||
// template indexed as the bare node `Base`. The `<…>` args are stripped so the
|
||||
// `extends` reference matches.
|
||||
it('strips template args from a templated base so the extends ref is the bare name', () => {
|
||||
const code = `
|
||||
template<typename T> class Base {};
|
||||
template<typename D> class CRTPBase {};
|
||||
namespace ns { template<typename T> class Tpl {}; }
|
||||
class Plain {};
|
||||
|
||||
class Widget : public Base<int> {};
|
||||
class App : public CRTPBase<App> {};
|
||||
class Q : public ns::Tpl<int> {};
|
||||
class Both : public Base<char>, public Plain {};
|
||||
`;
|
||||
const extendsRefs = extractFromSource('f.cpp', code).unresolvedReferences.filter(
|
||||
(r) => r.referenceKind === 'extends'
|
||||
);
|
||||
const names = extendsRefs.map((r) => r.referenceName);
|
||||
|
||||
// Templated bases carry the bare name, NOT the `<…>` instantiation.
|
||||
expect(names).toContain('Base'); // from Base<int> / Base<char>
|
||||
expect(names).toContain('CRTPBase'); // from CRTPBase<App> (CRTP)
|
||||
expect(names).toContain('ns::Tpl'); // qualified head preserved, args dropped
|
||||
expect(names).toContain('Plain'); // non-templated base unchanged
|
||||
// No reference still carries angle brackets.
|
||||
expect(names.find((n) => n.includes('<'))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('stripCppTemplateArgs removes balanced <…> at any depth and is a no-op without them', () => {
|
||||
expect(stripCppTemplateArgs('Base<int>')).toBe('Base');
|
||||
expect(stripCppTemplateArgs('ns::Tpl<int>')).toBe('ns::Tpl');
|
||||
expect(stripCppTemplateArgs('ns::Tpl<Foo<int>>')).toBe('ns::Tpl'); // nested
|
||||
expect(stripCppTemplateArgs('Outer<int>::Inner')).toBe('Outer::Inner'); // mid-name
|
||||
expect(stripCppTemplateArgs('Base')).toBe('Base'); // no-op
|
||||
expect(stripCppTemplateArgs('ns::Plain')).toBe('ns::Plain'); // no-op qualified
|
||||
});
|
||||
});
|
||||
|
||||
describe('C/C++ imports', () => {
|
||||
it('should extract system include', () => {
|
||||
const code = `#include <iostream>`;
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
Reference in New Issue
Block a user