Skip to content
Merged
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 @@ -73,6 +73,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)
- Editing `codegraph.json`'s `exclude` or `include` (or a `.gitignore`) while the MCP server is running now takes effect immediately. Previously the running file watcher kept the scope it had when it started, so a newly excluded file was removed by `codegraph sync` and then quietly re-added by the watcher seconds later — which looked like `exclude` not working at all — until the server was restarted. A scope change now refreshes the watcher and triggers a full reconcile, and a changed file the watcher hands to sync is re-checked against the current scope first, so the CLI and the live server can no longer disagree about what belongs in the index. Thanks @K1nG11. (#1590)
- Indexing no longer crashes the whole process — a segmentation fault with no message and no partial index — on a C/C++ (or any other) file with extremely deep nesting, such as the parser stress-test fixtures in the clang and gcc test suites or a fuzzer corpus. Such a file is now handed to the fallback parser and recorded with a parse warning while the rest of the repository indexes normally. Thanks @apollo600 for the exact diagnosis. (#1581)
- Calls to the methods of an exported object-literal constant — `export const api = { call() { … } }` used as a module's namespace, a common way to organize a TypeScript API surface — now resolve to the method, both in the defining file and through imports. Previously such a call linked to nothing (or to the constant itself), so `codegraph callers` and impact analysis reported zero callers for methods that are called from everywhere. Re-index after upgrading to pick up the edges. Thanks @IAliceBobI for the precise report and root-cause. (#1573)
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