Files
codebase-memory-mcp/tests
Martin Vogel 426676b60b fix(graph): typedef'd C aggregates get nodes; member access binds by object type
Two defects that fed each other, fixed together because the first alone would
let the new Field nodes attract bare-name calls.

1. An anonymous aggregate under a typedef had no nodes.
`typedef struct { ... } X;` produced no Class node and none of its members a
Field node: the struct_specifier carries no name of its own, and
extract_class_def gave up on it. That is the dominant C spelling of a type
(37 of the 38 structs in internal/cbm/cbm.h). The enclosing type_definition
now names the aggregate through its declarator; only a plain type_identifier
declarator counts, since a pointer, array or function typedef names another
type. Applies to struct, union and enum specifiers in C and C++ (`.h` is
classified C++).

2. A C member access was resolved by its bare member name.
The member half of `a.b` / `a->b` reached the short-name registry as `b`,
which returned whichever Field (or function, or variable) was called `b`: on
this repository 9,150 USAGE and 2,810 WRITES edges onto Fields from C
sources, 3,976 of the USAGE edges onto a name several structs share.

The C LSP already types the object of a field_expression. It now publishes
the owner as a FIELD_REFERENCE row, one per (enclosing function, object type,
field): the pipeline edge is function -> Field, so a row per occurrence would
only add a record per member access. The usage and read/write resolvers
(sequential and parallel) join those rows by function and member name; a C
member name with no typed owner binds nothing.

Four things had to hold for the owner to be found:
- the cross-file registry folded Field defs into their owning type for Go
  only, so every C/C++ struct from another file (every header) registered
  with zero fields. The fold now covers C and C++;
- c_register_lsp_defs kept the first 63 fields of a type. It is sized from
  the list now: a cap there silently dropped edges onto later fields;
- a struct is registered under the header that declares it while the use
  site knows its short name. The owner type resolves as written, then
  module-qualified, then by short name from this module, a directly included
  header, or as the single candidate. Two same-named structs with no
  evidence between them resolve to nothing;
- the parallel resolver ran the cross-file pass only for files with
  unqualified call sites. A C member access or member call that still has no
  owner now requests it too.

Calls landing on a Field follow the same rule (cbm_call_onto_field_policy):
dropped across languages (the #1928 reference rule; a Bash `command` or a
Python `sorted()` is not a C struct member), dropped for a bare C call
(`socket(...)`, a function-pointer parameter), and bound by owner for a C or
C++ member call. Bare C++ calls and every other language are unchanged.

Measured on this repository, old binary vs new:
  C/H Class nodes                                         155 -> 1000
  C/H Field nodes                                        1047 -> 5478
  enumerators of formerly anonymous enums                +911 Variable nodes
  USAGE onto the new types, enums and constants          +23k edges
  USAGE -> Field from .c, owner type named in the file  58.1% -> 97.6%
  USAGE onto a non-Field, name only used as a member      146 -> 0
  WRITES -> Field from .c                                2981 -> 6534
  CALLS                                       159 removed, 3 added
  index wall time unchanged within noise; peak footprint +2.8%
  full macOS suite: 8309 passed, 0 failed, 10 skipped

Tests: extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef,
pipeline_c_member_access_binds_field_by_object_type (+ _parallel),
call_onto_field_follows_language_and_shape,
c_member_access_binds_by_owner_only. Each was RED on the unchanged tree; the
pipeline tests are RED again with the call policy neutralized.

Not in this change: a function-pointer member declared inline
(`void (*f)(int);`) has no Field node; a `p->f()` call is not resolved (the
registry splits on `.` and `::` only); inline functions in `.h` files keep
the bare-name member binding; designated initializers are not typed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-02 00:53:42 +02:00
..