Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 47 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
43 changes: 41 additions & 2 deletions src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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, ' '));
}

/**
Expand Down