Skip to content

fix(c,cpp): a struct, class, union or enum defined in a variable declaration is indexed - #2446

Merged
colbymchenry merged 7 commits into
mainfrom
claude/clever-burnell-cb4e58
Oct 7, 2026
Merged

colbymchenry merged 7 commits into
mainfrom
claude/clever-burnell-cb4e58

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Problem

A class, struct, union or enum defined in the type of a C or C++ variable declaration was never indexed, by either engine:

// decl.cpp
namespace n {
struct Foo { int a; void f() {} } foo;
class Bar { void g() {} } bar, *pbar;
}
// main: only `variable n::foo` and `variable n::bar`; Foo, Bar, f and g are missing
// decl.c
struct Foo { int a; } foo;
static struct { int b; } anon_cfg;
enum { MODE_A, MODE_B } mode;
// main: nothing at all, not even MODE_A / MODE_B

The walker's declaration branch (variableTypes) hands the declaration to extractVariable and skips its children, so a class-like type with a body is never visited. The kernel's kind == "declaration" && !inside_class_like() branch mirrors it. Only declarations outside function and class bodies are affected, since inside a body the children walk already reaches the type. #2426 (now on main) added this walk for C++ files whose tree has errors, in brace scopes. Clean files, and C, were still open.

Fix

Both engines (src/extraction/tree-sitter.ts + languages/c-cpp.ts, mirrored in codegraph-kernel/src/ccpp/mod.rs):

  • The type is walked before the variables, and the variables keep their nodes (C++: the generic fallback's n::foo and n::bar; C: init, pointer and array declarators, as before). One walk covers every case: C always, and C++ when the tree is clean or the file is walked in fix(cpp): a class keeps its namespaces and enclosing class past code the parser misreads #2426's brace scopes. The only file left out is an erroring C++ file whose braces don't balance (no brace scopes), which fix(cpp): a class keeps its namespaces and enclosing class past code the parser misreads #2426 also leaves alone. There, error recovery can run a class past its own }, and the walked classes would land in the wrong scope (measured below).
  • An unnamed type takes the name of the first variable its declaration declares. static struct { … } SPT; gives struct SPT. This also applies inside function bodies, where such types were indexed as <anonymous> until now. It goes through the c/cpp resolveName hook, mirrored in the kernel's extract_name_raw.
  • The comment above the declaration documents the type, as it already documents the variable (a new c/cpp getDeclarationWrapper, mirrored by class_like_docstring). /** The handler registry. */ struct Registry { … } registry; gives both nodes that docstring.
  • The declaration's function-as-value scan skips the walked type, which captured its own candidates with the type on the stack. So a function value in the type's body (a C++ member default like void (*hs[1])() = {&on_open};) is captured once, by the type, not a second time from the file. Main's brace-scope walk captured those twice.
  • cDeclaratorIdentifier moves from tree-sitter.ts to languages/c-cpp.ts, where the naming rule shares it, and fix(cpp): a class keeps its namespaces and enclosing class past code the parser misreads #2426's CPP_CLASS_LIKE_SPECIFIERS check becomes isClassLikeDefinition there.

Naming an unnamed type

No code can name the type of static struct { … } SPT;; it only ever names SPT. So the struct is indexed under that name, which is the rule typedef struct { … } Name; already follows. Before, an agent searching for SPT (redis's setproctitle.c) or rdbstate (redis-check-rdb.c) found nothing: C doesn't index a variable with a bare declarator (the macro-prototype rule in extractVariable, unchanged here), and <anonymous> isn't searchable. Now codegraph_explore on SPT returns the struct and its fields.

What stays <anonymous>: a type whose declaration declares no variable, like a function's return type (struct { int x; } make(void);), and the unnamed types of class members (field_declaration), which this PR doesn't touch.

Alternatives considered:

  • <anonymous> everywhere: consistent, but unsearchable, which defeats the redis-style idiom.
  • A synthetic name like ctags' __anonNNN: also unsearchable.
  • No node, with the members hoisted to the enclosing scope: loses the type's body, which is what an agent wants to read.

One side effect: in fmt, struct { … operator()(…) … } enter_state; is a local functor, so a call enter_state(state::align) now links to the struct named enter_state. The edge is instantiates, because its target is a type. That is the right code, though a call to its operator() would be the more precise edge. Before, those calls linked to nothing.

Verification

  • New __tests__/c-cpp-declaration-types.test.ts: 29 tests, on the kernel and on wasm, with LF and CRLF, plus an end-to-end index. It covers:

    • the issue's repro in both languages;
    • unnamed types named after their variable (pointer, array and initialized declarators);
    • a function prototype's type staying <anonymous>;
    • the docstring;
    • a function-local type;
    • a single function-value capture;
    • erroring files: C kept, C++ in brace scopes kept, C++ with unbalanced braces not walked;
    • on an indexed project, Registry::size gets its callers, Worker extends Base resolves, and searchNodes('SPT') finds the struct.

    All 29 fail on main's walker and main's kernel (ed199e6).

  • torture.c and torture.cpp gain the same shapes, so the C/C++ kernel-parity suite keeps the two engines in agreement on them.

  • scripts/kernel-parity.mjs --lang c,cpp on jq, leveldb, redis, fmt, protobuf and rocksdb: 0 files with diffs, and deferral counts are unchanged. Under CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT=1, the set of files where the engines disagree is identical for main and the fix in all six repos (5 / 4 / 19 / 9 / 56 / 11).

  • npx tsc --noEmit, npm run build, bash scripts/build-kernel.sh. Full suite: passes. The parallel run on this shared box (100% CPU from other sessions) had 109 failures in 39 files, all timing: sync, git, daemon and MCP tests hitting 5 s timeouts or EBUSY on teardown, plus the timed-out cases of extraction, resolution and cpp-macro-and-constructor-calls. Rerun serially with 120 s timeouts, those 39 files passed 1634 of 1635. The last one, function-ref's Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case, has its own 60 s limit and passed alone in 27 s. Every C/C++ suite passed in the parallel run too, this PR's 29 tests included.

Validation: before and after on real repos (main = ed199e6)

Each repo was indexed with main and with this branch, both engines as shipped (kernel for clean C/C++ files, wasm for the rest). The graphs were diffed by node, and by edge keyed by call site.

repo nodes edges edge sites that changed
jq 1846 → 1846 7449 → 7449 1 contains (rename)
leveldb 3094 → 3094 10116 → 10116 1 contains (rename)
redis 21940 → 21959 76391 → 76410 36 contains retargeted; 1 removed + 1 added (a renamed enum's members)
fmt 8321 → 8326 19057 → 19082 +20 instantiates; contains +3 / −2 / 2 retargeted; 1 confidence change
rocksdb 72562 → 72564 308586 → 308590 +2 calls; contains +1 / 4 retargeted
protobuf 118001 → 118005 376623 → 376629 +2 calls; contains +4 / −3 / 7 retargeted

No calls, extends, instantiates or references edge was removed or retargeted in any repo. Every removed site is a contains edge whose source was renamed from <anonymous>; the same edge reappears under the new name.

Types indexed for the first time:

  • redis (19): sentinelState (sentinel.c), luaCtx and ldbState (eval.c), SPT, rdbstate, redis-cli's config, pref and clusterManager, redis-benchmark's config, RedisModuleForkInfo, redisTest, the ACL tables ACLCategoryItem, ACLUserFlag and ACLSelectorFlags, ExprOptable, and in deps/: lua's priority, native, and tre's tre_macro_struct and tre_ctype_map.
  • fmt: the enum fstat_sim with its three enumerators, and test_mock (posix-mock-test.cc).
  • rocksdb: StaticDestructionTester and its destructor (env_test.cc). Its calls to Env::Default() and GetHostNameString now resolve correctly.
  • protobuf: ForceShutdown and its destructor (googletest.cc), whose two calls to ShutdownProtobufLibrary resolve correctly, and the two utf8_range ftab structs.

Renamed from <anonymous>, all function-local:

  • jq 1 (res) and leveldb 1 (cases);
  • redis 25: 8 structs, 16 unions, and 1 enum whose 37 enumerators are re-qualified;
  • fmt 2 functor structs (enter_state, parse_presentation_type, with their operator());
  • rocksdb 3;
  • protobuf 4: 3 enums with 11 enumerators, and 1 union.

Triage of the rest:

  • fmt's 20 instantiates: the functor calls described above. Each now reaches the struct that holds the operator() that runs; before, nothing.
  • fmt's confidence change: max_size() in max_size_allocator::allocate still links to max_size_allocator::max_size, but at 0.4 instead of 0.9. The newly indexed enum value fstat_sim::max_size (posix-mock-test.cc) shares its name, and exact-match still counts an enum value as a candidate for a call (pre-existing). The target is unchanged.

These site diffs are identical, as sets, to the same A/B run on this branch's original base (d8a7f86), before #2426, #2413 and #2415 landed.

Re-run on main cdb994b (after #2449 changed the C/C++ preParse, and #2430 and #2442 landed): the changed sites are identical, as sets, in all six repos. Parity is still 0 diffs, with fewer files deferred now that more of them parse cleanly: leveldb 9, redis 176, fmt 31, protobuf 292 and rocksdb 214, against 20, 186, 32, 343 and 223 before. This PR's tests, kernel-ccpp-parity, cpp-brace-scopes and #2449's cpp-attribute-macros pass on the merged tree (100/100, with the #2430 and #2442 test files).

Erroring C++ files. Before merging #2426, I measured an unconditional walk with no brace scopes. In leveldb's util/env_posix.cc, error recovery runs class Limiter to the end of the file (an #if inside its constructor's initializer list). That file gained 8 classes and 57 methods, all nested under leveldb::Limiter::. Two protobuf types also landed in a truncated namespace (google::ConstMapIterator). That is why an erroring C++ file without brace scopes is left alone. With #2426, main already indexes those classes in their correct scope, and this branch leaves them unchanged.

Overlap

Follow-ups (not in this PR)

  • Objective-C has the same gap: objc.ts also lists declaration in variableTypes, and the gate here is C/C++ only.
  • Unnamed types of class members (union { … } u; inside a struct) are still <anonymous>. The same naming rule could apply to them, but it renames many existing nodes (89 in redis alone), so it deserves its own change.
  • A call through a functor object (enter_state(x)) could link to the type's operator() as a call, instead of to the type as an instantiation.
  • C still skips a variable with a bare declarator: static struct { … } SPT; has no variable SPT, by the existing macro-prototype rule. The struct now carries the name.

🤖 Generated with Claude Code

colbymchenry and others added 3 commits October 7, 2026 06:43
…aration is indexed

`struct Foo { … } foo;`, `class Bar { … } bar;` and `static struct { … } SPT;`
outside any function or class body went to the variable extractor, which
skipped the declaration's children, so the type, its methods and its
enumerators were never indexed in either extraction engine.

Both engines now walk such a type before the variables (C always; C++ when
the tree has no errors), and the declaration's function-as-value scan skips
the walked type, which captured its own. An unnamed struct, union or enum
takes the name of the first variable its declaration declares (also inside
function bodies, where it was `<anonymous>`), as `typedef struct { … } Name;`
already takes the typedef name, and the comment above the declaration is the
type's docstring. cDeclaratorIdentifier moves to languages/c-cpp.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-cb4e58

# Conflicts:
#	CHANGELOG.md
#	codegraph-kernel/src/ccpp/mod.rs
#	src/extraction/tree-sitter.ts
…copes in the test header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant