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:
@@ -496,9 +496,40 @@ export function detectLanguage(filePath: string, source?: string, overrides?: Re
|
||||
return lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class/struct BASE CLAUSE — `struct Derived : Base {`, `class Foo final :
|
||||
* public Bar, private Baz {`, `struct D : ns::B<T> {` — which is never valid
|
||||
* C. In C the only thing that can follow `struct <tag>` is `{`, `;`, `*`, an
|
||||
* identifier (declarator), or a closing `)`: a bit-field's `:` sits after a
|
||||
* member NAME inside the body (`unsigned a : 3;`), a ternary's `:` is
|
||||
* separated from the tag by `)` / `*` / a declarator (`sizeof(struct foo) :
|
||||
* 0`), and a label such as `struct_end:` has no whitespace after `struct`. An
|
||||
* optional access specifier / `virtual` after the colon and an optional
|
||||
* `final` before it cover the spelled-out forms; the base may be scoped
|
||||
* (`ns::Base`) and carry template arguments, and must be followed by the
|
||||
* body's `{` or a `,` introducing the next base — prose like
|
||||
* `struct timeval: seconds and microseconds` inside a string never has that
|
||||
* terminator. Comments are stripped before the scan (see `looksLikeCpp`).
|
||||
*/
|
||||
const CPP_BASE_CLAUSE_RE =
|
||||
/\b(?:class|struct)\s+\w+\s*(?:final\s*)?:\s*(?:(?:public|protected|private|virtual)\s+)*[A-Za-z_][\w:]*(?:\s*<[^{};]*>)?\s*[{,]/;
|
||||
|
||||
/** Block and line comments, for a code-only scan. Lazy block match → linear. */
|
||||
const C_COMMENT_RE = /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
|
||||
|
||||
/**
|
||||
* Heuristic: does a .h file contain C++ constructs?
|
||||
* Checks the first ~8KB for patterns that are unique to C++ and never valid C.
|
||||
*
|
||||
* Two passes. The first checks the first ~8KB for patterns that are unique to
|
||||
* C++ and never valid C. The second scans the FULL source for a class/struct
|
||||
* base clause (`CPP_BASE_CLAUSE_RE`): a large header with a long C-compatible
|
||||
* preamble — include guards, `#define`s, plain C typedefs — can put its only
|
||||
* C++ signal past the sample, and the cost of that miss is the C extractor
|
||||
* (classTypes: []) dropping the derived type entirely and minting a phantom
|
||||
* `function Base` from the base clause instead (#1592). The base-clause regex
|
||||
* is anchored on a `struct`/`class` keyword followed by a tag and a colon, a
|
||||
* shape with no C reading, so widening it to the whole file cannot drag a C
|
||||
* header over to C++.
|
||||
*/
|
||||
function looksLikeCpp(source: string): boolean {
|
||||
const sample = source.substring(0, 8192);
|
||||
@@ -511,7 +542,15 @@ function looksLikeCpp(source: string): boolean {
|
||||
// routed through the C extractor (which extracts no classes), and its class
|
||||
// definition silently vanishes. The two-token shape (`<KW> <MACRO> <Name>`
|
||||
// before a `[:{]`) never occurs in valid C, so this can't misclassify C headers.
|
||||
return /\bnamespace\b|\bclass\s+\w+\s*[:{]|\b(?:class|struct)\s+[A-Z][A-Z0-9_]+\s+\w+\s*(?:final\s*)?[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample);
|
||||
if (/\bnamespace\b|\bclass\s+\w+\s*[:{]|\b(?:class|struct)\s+[A-Z][A-Z0-9_]+\s+\w+\s*(?:final\s*)?[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample)) {
|
||||
return true;
|
||||
}
|
||||
// Plain `struct Derived : Base` (no export macro, no `class` keyword, no
|
||||
// explicit access section) — the #1159 branch above only recognizes the
|
||||
// macro-annotated form. Scanned over the whole file, not the sample, with
|
||||
// comments removed so a doc comment's prose (`struct foo: x, y`) can't
|
||||
// flip a C header.
|
||||
return CPP_BASE_CLAUSE_RE.test(source.replace(C_COMMENT_RE, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user