fix(extraction): detect a plain struct Derived : Base base clause in .h headers as C++ (#1592) (#1593)

Fixes #1592.

## What was wrong

A `.h` header whose only C++ construct is a plain derived type —

```cpp
struct Base {};
struct Derived : Base {};
```

— was classified as C. The `.h` language check (`looksLikeCpp`) recognizes `class`, `namespace`, `template`, access sections, `virtual`, `using`, and — since #1159/#1207 — the export-macro form `struct ENGINE_API Derived : Base`. The plain form has none of those signals. Routed through the C extractor, `Derived` vanished from the index and the base clause was read as a K&R-style declaration, minting a phantom `function Base` with `returnType=Derived` (the exact output in the issue).

A second, independent miss the reporter called out: the check only read the first 8192 characters, so a large header with a long C-compatible preamble (include guards, `#define`s, plain typedefs) hid the signal even when it was there.

## What this does

`looksLikeCpp()` now runs two passes:

1. The existing 8 KB sample regex, unchanged.
2. A scan of the **whole file** (comments stripped) for a class/struct **base clause**: `class`/`struct` + tag + optional `final` + `:` + optional `public`/`protected`/`private`/`virtual` + a base name (scoped, optionally templated) followed by the body's `{` or a `,` introducing the next base.

That shape has no valid C reading, so widening it to the whole file can't drag a C header over to C++:

- a bit-field's `:` follows a member *name* inside the body (`unsigned a : 3;`), not the tag;
- a ternary's `:` is separated from the tag by `)` / `*` / a declarator (`sizeof(struct foo) : 0`);
- a label or identifier like `struct_end:` has no whitespace after `struct`;
- comments are removed before the scan, so doc-comment prose (`/* struct timeval: seconds, microseconds */`) can't match; and the `{`/`,` terminator keeps a string literal's prose from matching too.

Detection only — the C++ extractor already handles the header correctly once it's routed there (renaming to `.hpp`, as the issue notes, already worked).

## Tests

`__tests__/extraction.test.ts`:

- plain / `: public Base` / `: ns::Base` / `: Base<int, Foo<T>>` / `final : Base` / multi-base with `{` on the next line / `: virtual Base` → `cpp`;
- a base clause placed **after** 8192 characters of C-compatible preamble → `cpp`;
- controls that must stay `c`: a bit-field struct, `sizeof(struct foo) : 0` + a cast ternary, a `struct_end:` label and `struct_a` identifiers, doc-comment prose shaped like a base clause, and the two pre-existing C controls;
- end-to-end `extractFromSource('src/min.h', …)` on the issue's header: a `struct` node `Derived` (language `cpp`), exactly one `Base` node and it is a `struct` — no phantom function.

Issue repro re-run against this build: `codegraph init` → `query Derived` returns the `cpp` struct; `query Base` returns only the struct; the files table records `src/min.h` as `cpp`.

Full suite: `npm test` → 174 files passed, 3010 tests passed, 179 skipped.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
This commit is contained in:
Colby Mchenry
2026-08-26 10:38:48 -05:00
committed by GitHub
parent cf1b0e341a
commit d618d94144
3 changed files with 89 additions and 2 deletions
+47
View File
@@ -175,6 +175,53 @@ class ENGINE_API UNetConnectionRepControl : public UObject
expect(detectLanguage('cfoo.h', '#ifndef CFOO_H\nstruct Point { int x; int y; };\nvoid f(struct Point p);\n#endif\n')).toBe('c');
});
it('should detect a .h whose only C++ signal is a plain base clause as cpp (#1592)', () => {
// No export macro, no `class` keyword, no access section, no `virtual`:
// the derived struct's base clause is the only C++ construct, and the
// #1159 branch only knows the macro-annotated form. Misdetected as C, the
// C extractor drops `Derived` and mints a phantom `function Base`.
expect(detectLanguage('min.h', 'struct Base {};\nstruct Derived : Base {};\n')).toBe('cpp');
expect(detectLanguage('pub.h', 'struct Derived : public Base {};\n')).toBe('cpp');
expect(detectLanguage('scoped.h', 'struct Derived : ns::Base {};\n')).toBe('cpp');
expect(detectLanguage('tmpl.h', 'struct Derived : Base<int, Foo<T>> {};\n')).toBe('cpp');
expect(detectLanguage('final.h', 'struct Derived final : Base {};\n')).toBe('cpp');
expect(detectLanguage('multi.h', 'class Derived : public A, private B\n{\n};\n')).toBe('cpp');
expect(detectLanguage('virt.h', 'struct Derived : virtual Base {};\n')).toBe('cpp');
// The base clause sits PAST the 8 KB sample, behind a long C-compatible
// preamble (guards, defines, plain typedefs) — the second pass must scan
// the whole file, not just the sample.
const preamble = '#ifndef BIG_H\n#define BIG_H\n' + '#define VALUE_0 0\n'.repeat(700);
expect(preamble.length).toBeGreaterThan(8192);
expect(detectLanguage('big.h', `${preamble}struct Base {};\nstruct Derived : Base {};\n#endif\n`)).toBe('cpp');
// Controls — all genuine C, none may flip to C++:
// a bit-field (`:` after a member name inside the body),
expect(detectLanguage('bits.h', 'struct S { unsigned int a : 3; unsigned int b : 5; };\n')).toBe('c');
// a ternary whose `:` follows a `sizeof(struct …)` / cast,
expect(detectLanguage('tern.h', 'static inline int sz(int x) { return x ? sizeof(struct foo) : 0; }\n#define P(a,b) ((a) ? (struct foo *)(a) : (b))\n')).toBe('c');
// a label / identifier that merely starts with `struct`,
expect(detectLanguage('label.h', 'static void g(void) {\nstruct_end:\n return;\n}\nint struct_a, struct_b;\n')).toBe('c');
// a doc comment whose prose reads like a base clause,
expect(detectLanguage('doc.h', '/* struct timeval: seconds, microseconds */\nstruct timeval { long tv_sec; long tv_usec; };\n// struct foo: x, y\n')).toBe('c');
// and the two existing controls.
expect(detectLanguage('cfoo.h', '#ifndef CFOO_H\nstruct Point { int x; int y; };\nvoid f(struct Point p);\n#endif\n')).toBe('c');
expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c');
});
it('should extract a derived struct from a plain base-clause .h, with no phantom function (#1592)', () => {
const result = extractFromSource('src/min.h', 'struct Base {};\nstruct Derived : Base {};\n');
const derived = result.nodes.find((n) => n.name === 'Derived');
expect(derived).toBeDefined();
expect(derived?.kind).toBe('struct');
expect(derived?.language).toBe('cpp');
// The C mis-route read `Derived : Base {}` as a K&R-ish function `Base`
// returning `Derived` — that phantom must be gone.
expect(result.nodes.some((n) => n.name === 'Base' && n.kind === 'function')).toBe(false);
expect(result.nodes.filter((n) => n.name === 'Base')).toHaveLength(1);
expect(result.nodes.find((n) => n.name === 'Base')?.kind).toBe('struct');
});
it('should return unknown for unsupported extensions', () => {
expect(detectLanguage('styles.css')).toBe('unknown');
expect(detectLanguage('data.json')).toBe('unknown');