From 712cf285bc41dddcfc97b23707928115df0b0942 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 22 Aug 2026 10:48:11 -0700 Subject: [PATCH] fix(extraction): detect a plain `struct Derived : Base` base clause in .h headers as C++ (#1592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `.h` file whose only C++ syntax is a derived type without an export macro (`struct Derived : Base {};`) fell through the C++ heuristic: the #1159 branch only recognizes the macro-annotated form (`struct ENGINE_API Derived : Base`), and the remaining signals (`class`, `namespace`, `template`, access sections, `virtual`) are all absent from such a header. Routed through the C extractor, the derived struct vanished from the index and a phantom `function Base` with `returnType=Derived` was minted from the base clause instead. `looksLikeCpp()` now runs a second pass for a class/struct base clause — keyword + tag + `:` + optional access specifier/`virtual` + a base name (scoped, possibly templated) followed by the body's `{` or a `,` — a shape with no valid C reading (bit-field colons follow a member name inside the body, ternary colons are separated from the tag by `)`/`*`/a declarator, and `struct_end:` has no whitespace after the keyword). The scan covers the whole file with comments stripped, not the 8 KB sample, so a long C-compatible preamble can no longer hide the one signal. The existing sample-based first pass is unchanged. Tests: plain / `public` / scoped / templated / `final` / multi-base / `virtual` forms detect as cpp, a base clause past 8192 chars detects as cpp, and bit-field, ternary, `struct_`-prefixed identifier, doc-comment prose and the two existing C headers all stay c; an end-to-end extraction of the issue's header yields the `Derived` struct and no phantom function. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK --- CHANGELOG.md | 1 + __tests__/extraction.test.ts | 47 ++++++++++++++++++++++++++++++++++++ src/extraction/grammars.ts | 43 +++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b2d07fb3..0d1816b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Files skipped because they are too large or repeatedly fail to parse are now recorded with the reason, so unchanged rejected files are no longer rediscovered and retried on every status check and sync — and a later successful parse of such a file replaces the record with its real symbols. Thanks @netbrah for the exceptional failure analysis behind this batch, and @danusha2345 for the fixes. (#1557) - C/C++ function-pointer analysis now bounds its compiled-pattern caches, so very large repositories can no longer exhaust the JavaScript engine's regular-expression code space during indexing. (#1559) - JSX rendering analysis now runs only on JavaScript-family files, so JSX-looking strings in C/C++ (or any other language) no longer create impossible call edges — in pure-C projects and in mixed-language monorepos alike. (#1560) +- A C++ `.h` header whose only C++ construct is a plain derived type — `struct Derived : Base` with no export macro, `class` keyword, or access section — is now recognized as C++ (previously only the export-macro form was). Such a header was read as C, so the derived struct vanished from the index and a phantom function named after the base type appeared in its place. The check now also covers the whole file rather than its first few kilobytes, so a long C-compatible preamble no longer hides the signal. Re-index after upgrading to pick up affected headers. Thanks @Jaysenpeng. (#1592) ## [1.5.0] - 2026-07-21 diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 6bc48032e..0616f351c 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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> {};\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'); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index d4127631d..84647c3e4 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -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 {` — which is never valid + * C. In C the only thing that can follow `struct ` 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 (` ` // 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, ' ')); } /**