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>
This commit is contained in:
Martin Vogel
2026-10-02 00:53:42 +02:00
parent 6b25fff3a1
commit 426676b60b
14 changed files with 1065 additions and 59 deletions
+4
View File
@@ -418,6 +418,10 @@ typedef struct {
typedef enum {
CBM_RESOLVED_INVOCATION = 0,
CBM_RESOLVED_CALL_REFERENCE,
/* C-family member access `a.b` / `a->b`: callee_qn is the Field `b` of the
* type of `a`. One row per (caller, field), not per occurrence, and no site
* span: the join is by enclosing function and member name. */
CBM_RESOLVED_FIELD_REFERENCE,
} CBMResolvedKind;
// LSP-resolved invocation/reference: high-confidence type-aware resolution.
+21
View File
@@ -4708,6 +4708,27 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
break;
}
case CBM_LANG_C:
case CBM_LANG_CPP: { // `typedef struct { … } Name;`: the aggregate is
// anonymous and the typedef's declarator names it.
// A pointer/array/function declarator names another
// type, not the aggregate, so only a plain
// type_identifier counts.
if (strcmp(kind, "struct_specifier") != 0 && strcmp(kind, "union_specifier") != 0 &&
strcmp(kind, "enum_specifier") != 0) {
break;
}
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "type_definition") != 0) {
break;
}
TSNode declarator = ts_node_child_by_field_name(parent, TS_FIELD("declarator"));
if (!ts_node_is_null(declarator) &&
strcmp(ts_node_type(declarator), "type_identifier") == 0) {
name_node = declarator;
}
break;
}
default:
break;
}
+236 -46
View File
@@ -2651,12 +2651,14 @@ static uint64_t c_neg_memo_hash(const char *type_qn, const char *member_name) {
return h ? h : 1ULL;
}
static bool c_neg_memo_contains(const CLSPContext *ctx, uint64_t h) {
if (!ctx->neg_memo || ctx->neg_memo_cap == 0)
// Open-addressing uint64 hash set, 0 = empty slot. Shared by the negative-lookup
// memo and the emitted-field-reference set; both are malloc-owned by the context.
static bool c_u64_set_contains(const uint64_t *table, int cap, uint64_t h) {
if (!table || cap == 0)
return false;
uint64_t mask = (uint64_t)ctx->neg_memo_cap - 1;
uint64_t mask = (uint64_t)cap - 1;
for (uint64_t i = h & mask;; i = (i + 1) & mask) {
uint64_t slot = ctx->neg_memo[i];
uint64_t slot = table[i];
if (slot == 0)
return false; // empty slot → not present
if (slot == h)
@@ -2664,23 +2666,24 @@ static bool c_neg_memo_contains(const CLSPContext *ctx, uint64_t h) {
}
}
static void c_neg_memo_insert(CLSPContext *ctx, uint64_t h) {
// Lazy alloc / grow-by-rehash at 70% load. On OOM, silently disable the memo
// (correctness is unaffected — the full cascade still runs).
if (ctx->neg_memo == NULL) {
// Returns true when h was not in the set before this call. Lazy alloc /
// grow-by-rehash at 70% load. On OOM the set stays as it is and h counts as new:
// every caller is correct without the set, it only saves repeated work.
static bool c_u64_set_insert(uint64_t **table, int *cap, int *count, uint64_t h) {
if (*table == NULL) {
uint64_t *nm = (uint64_t *)calloc(1024, sizeof(uint64_t));
if (!nm)
return;
ctx->neg_memo = nm;
ctx->neg_memo_cap = 1024;
ctx->neg_memo_count = 0;
} else if ((ctx->neg_memo_count + 1) * 10 >= ctx->neg_memo_cap * 7) {
int new_cap = ctx->neg_memo_cap * 2;
return true;
*table = nm;
*cap = 1024;
*count = 0;
} else if ((*count + 1) * 10 >= *cap * 7) {
int new_cap = *cap * 2;
uint64_t *nm = (uint64_t *)calloc((size_t)new_cap, sizeof(uint64_t));
if (nm) {
uint64_t nmask = (uint64_t)new_cap - 1;
for (int j = 0; j < ctx->neg_memo_cap; j++) {
uint64_t v = ctx->neg_memo[j];
for (int j = 0; j < *cap; j++) {
uint64_t v = (*table)[j];
if (v == 0)
continue;
uint64_t k = v & nmask;
@@ -2688,30 +2691,44 @@ static void c_neg_memo_insert(CLSPContext *ctx, uint64_t h) {
k = (k + 1) & nmask;
nm[k] = v;
}
free(ctx->neg_memo);
ctx->neg_memo = nm;
ctx->neg_memo_cap = new_cap;
free(*table);
*table = nm;
*cap = new_cap;
}
// if calloc failed: keep the existing table (load may exceed 70%, still correct)
}
uint64_t mask = (uint64_t)ctx->neg_memo_cap - 1;
uint64_t mask = (uint64_t)*cap - 1;
for (uint64_t i = h & mask;; i = (i + 1) & mask) {
if (ctx->neg_memo[i] == 0) {
ctx->neg_memo[i] = h;
ctx->neg_memo_count++;
return;
if ((*table)[i] == 0) {
(*table)[i] = h;
(*count)++;
return true;
}
if (ctx->neg_memo[i] == h)
return; // already recorded
if ((*table)[i] == h)
return false; // already recorded
}
}
static bool c_neg_memo_contains(const CLSPContext *ctx, uint64_t h) {
return c_u64_set_contains(ctx->neg_memo, ctx->neg_memo_cap, h);
}
static void c_neg_memo_insert(CLSPContext *ctx, uint64_t h) {
// On OOM the memo is silently disabled (correctness is unaffected — the full
// cascade still runs).
(void)c_u64_set_insert(&ctx->neg_memo, &ctx->neg_memo_cap, &ctx->neg_memo_count, h);
}
static void c_u64_set_free(uint64_t **table, int *cap, int *count) {
free(*table);
*table = NULL;
*cap = 0;
*count = 0;
}
static void c_neg_memo_free(CLSPContext *ctx) {
if (ctx->neg_memo)
free(ctx->neg_memo);
ctx->neg_memo = NULL;
ctx->neg_memo_cap = 0;
ctx->neg_memo_count = 0;
c_u64_set_free(&ctx->neg_memo, &ctx->neg_memo_cap, &ctx->neg_memo_count);
c_u64_set_free(&ctx->field_ref_seen, &ctx->field_ref_seen_cap, &ctx->field_ref_seen_count);
}
static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const char *type_qn,
@@ -2934,6 +2951,100 @@ static const CBMType *c_lookup_field_type(CLSPContext *ctx, const char *type_qn,
return NULL;
}
// True when qn is `prefix` followed by a '.' segment boundary.
static bool c_qn_in_module(const char *qn, const char *prefix) {
size_t plen = prefix ? strlen(prefix) : 0;
return plen > 0 && strncmp(qn, prefix, plen) == 0 && qn[plen] == '.';
}
// The registered type a member access is made on: as written, then
// module-qualified, then by short name -- a C use site names `struct Alpha`
// while the struct is registered under the header that declares it.
//
// A short name several modules declare is taken from this module, else from a
// directly included header, else only when it is the single candidate. Two
// same-named structs with no evidence between them resolve to NOTHING: binding
// a field of the wrong struct is the defect this lookup exists to remove.
static const CBMRegisteredType *c_resolve_field_owner_type(CLSPContext *ctx, const char *type_qn) {
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (!rt && ctx->module_qn) {
char sbuf[1024];
const char *prefixed = c_build_module_prefixed(ctx, type_qn, sbuf, sizeof(sbuf));
if (!prefixed)
prefixed = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, type_qn);
rt = cbm_registry_lookup_type(ctx->registry, prefixed);
}
if (rt || !ctx->registry)
return rt;
const char *dot = strrchr(type_qn, '.');
const char *shortn = dot ? dot + 1 : type_qn;
size_t slen = strlen(shortn);
const CBMRegisteredType *included = NULL;
const CBMRegisteredType *only = NULL;
bool included_ambiguous = false;
bool only_ambiguous = false;
CBMTypeShortIter it;
cbm_registry_types_by_short_name_chain(ctx->registry, shortn, &it);
int i;
while ((i = cbm_type_short_iter_next(&it)) >= 0) {
const CBMRegisteredType *cand = &it.reg->types[i];
const char *q = cand->qualified_name;
size_t qlen = q ? strlen(q) : 0;
if (qlen <= slen + 1 || q[qlen - slen - 1] != '.' || strcmp(q + qlen - slen, shortn) != 0)
continue;
if (c_qn_in_module(q, ctx->module_qn))
return cand;
if (only && strcmp(only->qualified_name, q) != 0)
only_ambiguous = true;
only = cand;
for (int inc = 0; inc < ctx->include_count; inc++) {
if (!c_qn_in_module(q, ctx->include_ns_qns[inc]))
continue;
if (included && strcmp(included->qualified_name, q) != 0)
included_ambiguous = true;
included = cand;
break;
}
}
if (included)
return included_ambiguous ? NULL : included;
return only_ambiguous ? NULL : only;
}
// The registered type that DECLARES field_name, reached from type_qn through
// aliases and base classes as c_lookup_field_type walks them. A field whose
// type text did not parse still has an owner, so only the name decides.
static const CBMRegisteredType *c_lookup_field_owner(CLSPContext *ctx, const char *type_qn,
const char *field_name, int depth) {
if (!type_qn || !field_name || depth > 5)
return NULL;
const CBMRegisteredType *rt = c_resolve_field_owner_type(ctx, type_qn);
if (!rt)
return NULL;
if (rt->alias_of)
return c_lookup_field_owner(ctx, rt->alias_of, field_name, depth + 1);
if (rt->field_names) {
for (int i = 0; rt->field_names[i]; i++) {
if (strcmp(rt->field_names[i], field_name) == 0)
return rt;
}
}
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
const CBMRegisteredType *owner =
c_lookup_field_owner(ctx, rt->embedded_types[i], field_name, depth + 1);
if (owner)
return owner;
}
}
return NULL;
}
// ============================================================================
// c_process_statement: bind variables from statements
// ============================================================================
@@ -3871,6 +3982,79 @@ static void c_emit_resolved_reference_at(CLSPContext *ctx, const char *callee_qn
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, resolved);
}
// FNV-1a over (caller, type, field) with 0xff separators; 0 is remapped to 1
// because it is the set's empty-slot sentinel.
static uint64_t c_field_ref_hash(const char *caller_qn, const char *type_qn,
const char *field_name) {
const char *parts[] = {caller_qn, type_qn, field_name};
uint64_t h = 1469598103934665603ULL;
for (int part = 0; part < 3; part++) {
for (const unsigned char *p = (const unsigned char *)parts[part]; *p; p++) {
h ^= (uint64_t)*p;
h *= 1099511628211ULL;
}
h ^= (uint64_t)0xffu;
h *= 1099511628211ULL;
}
return h ? h : 1ULL;
}
/* `a.b` / `a->b`: only the type of `a` says WHICH struct's `b` is meant. The
* usage and read/write resolvers see the member name alone, and a bare-name
* lookup of `b` returns whichever Field happens to be called `b`. Publish the
* owner here so they bind the member through its object's type.
*
* One row per (enclosing function, object type, field): the pipeline edge is
* function -> Field, so repeating the row for every occurrence would add a
* record per member access in the corpus and no edge. An object whose type is
* unknown, or whose type declares no such field, publishes nothing. */
static void c_emit_field_reference(CLSPContext *ctx, TSNode node) {
if (!ctx->resolved_calls || !ctx->enclosing_func_qn)
return;
TSNode arg_node = ts_node_child_by_field_name(node, "argument", 8);
TSNode field_node = ts_node_child_by_field_name(node, "field", 5);
if (ts_node_is_null(arg_node) || ts_node_is_null(field_node) ||
strcmp(ts_node_type(field_node), "field_identifier") != 0)
return;
char *field_name = c_node_text(ctx, field_node);
if (!field_name || !field_name[0])
return;
const CBMType *obj_type = c_eval_expr_type(ctx, arg_node);
if (cbm_type_is_unknown(obj_type))
return;
bool is_arrow = false;
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc && !is_arrow; i++) {
TSNode child = ts_node_child(node, i);
is_arrow = !ts_node_is_named(child) && strcmp(ts_node_type(child), "->") == 0;
}
const char *type_qn = type_to_qn(c_simplify_type(ctx, obj_type, is_arrow));
if (!type_qn)
return;
/* Decided once per (function, type, field), hit or miss: the owner lookup
* below formats a module-qualified name on its way. */
if (!c_u64_set_insert(&ctx->field_ref_seen, &ctx->field_ref_seen_cap,
&ctx->field_ref_seen_count,
c_field_ref_hash(ctx->enclosing_func_qn, type_qn, field_name)))
return;
const CBMRegisteredType *owner = c_lookup_field_owner(ctx, type_qn, field_name, 0);
if (!owner || !owner->qualified_name)
return;
CBMResolvedCall resolved = {0};
resolved.caller_qn = ctx->enclosing_func_qn;
resolved.callee_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", owner->qualified_name, field_name);
resolved.strategy = "lsp_field_access";
resolved.confidence = 0.95f;
resolved.kind = CBM_RESOLVED_FIELD_REFERENCE;
resolved.source_origin = ctx->source_origin;
if (resolved.callee_qn)
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, resolved);
}
/* A direct identifier argument denotes a precise callable value only when its
* nearest lexical binding is a tracked function pointer target, or when no
* lexical value shadows one exact registered function. Conditional/composite
@@ -3945,6 +4129,11 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) {
// Process statements for scope building
c_process_statement(ctx, node);
// --- Member access: publish which type owns the field ---
if (strcmp(kind, "field_expression") == 0) {
c_emit_field_reference(ctx, node);
}
// --- Resolve call expressions ---
if (strcmp(kind, "call_expression") == 0) {
c_resolve_callable_argument_references(ctx, node);
@@ -5881,21 +6070,30 @@ static void c_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const cha
}
}
// Field defs
// Field defs. Sized from the list itself: a fixed cap here would
// silently drop the later fields of a large struct, and with them
// every member-access edge onto those fields.
if (d->field_defs) {
int fcap = 1;
for (const char *c = d->field_defs; *c; c++) {
if (*c == '|')
fcap++;
}
const char **fnarr =
(const char **)cbm_arena_alloc(arena, (fcap + 1) * sizeof(const char *));
const CBMType **ftarr =
(const CBMType **)cbm_arena_alloc(arena, (fcap + 1) * sizeof(const CBMType *));
const char *fsrc = d->field_defs;
const char *fnames[64];
const CBMType *ftypes[64];
int fcount = 0;
while (*fsrc && fcount < 63) {
while (fnarr && ftarr && *fsrc && fcount < fcap) {
const char *sep = strchr(fsrc, '|');
const char *end = sep ? sep : fsrc + strlen(fsrc);
char *pair = cbm_arena_strndup(arena, fsrc, end - fsrc);
char *colon = strchr(pair, ':');
char *colon = pair ? strchr(pair, ':') : NULL;
if (colon) {
*colon = '\0';
fnames[fcount] = pair;
ftypes[fcount] = c_parse_return_type_text(
fnarr[fcount] = pair;
ftarr[fcount] = c_parse_return_type_text(
arena, colon + 1, d->def_module_qn ? d->def_module_qn : module_qn);
fcount++;
}
@@ -5904,14 +6102,6 @@ static void c_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const cha
fsrc = sep + 1;
}
if (fcount > 0) {
const char **fnarr =
(const char **)cbm_arena_alloc(arena, (fcount + 1) * sizeof(const char *));
const CBMType **ftarr = (const CBMType **)cbm_arena_alloc(
arena, (fcount + 1) * sizeof(const CBMType *));
for (int j = 0; j < fcount; j++) {
fnarr[j] = fnames[j];
ftarr[j] = ftypes[j];
}
fnarr[fcount] = NULL;
ftarr[fcount] = NULL;
rt.field_names = fnarr;
+9
View File
@@ -63,6 +63,15 @@ typedef struct {
int neg_memo_cap; // power-of-two; 0 until first insert
int neg_memo_count; // live entries (grow by rehash at 70% load)
// Field references already emitted for this file, keyed by (enclosing
// function, owner type, field). The function -> Field edge needs one row
// per pair; a row per occurrence would cost a record for every member
// access in the corpus. Same uint64 hash-set shape and ownership as
// neg_memo; freed at end of c_lsp_process_file.
uint64_t *field_ref_seen;
int field_ref_seen_cap;
int field_ref_seen_count;
// Output
CBMResolvedCallArray *resolved_calls;
CBMSourceOrigin source_origin; // source buffer represented by emitted occurrence spans
+161
View File
@@ -22,6 +22,7 @@
#define CBM_PIPELINE_LSP_RESOLVE_H
#include "cbm.h"
#include "foundation/mem_core.h" /* field-owner index storage */
#include "graph_buffer/graph_buffer.h"
#include "foundation/constants.h"
@@ -882,6 +883,166 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference(
return cbm_pipeline_find_lsp_reference_in_graph(arr, usage, allow_tail_match, NULL, NULL);
}
/* ── C member access: bind a field through its owner ─────────────────
*
* The member half of `a.b` / `a->b` is not a name in any scope: only the type
* of `a` gives `b` a meaning. The C LSP publishes that as FIELD_REFERENCE rows,
* one per (enclosing function, owner field). The usage and read/write
* resolvers join them by enclosing function and member name, which is exactly
* the granularity of the function -> Field edge they emit. */
/* FIELD_REFERENCE rows sorted by (caller, member name, target). A NULL `rows`
* with a nonzero source array means the allocation failed and lookups scan the
* array instead, so no typed edge is ever lost to an index. */
typedef struct {
const CBMResolvedCall **rows;
int count;
} cbm_pipeline_lsp_field_index_t;
static inline bool cbm_pipeline_lsp_field_row_usable(const CBMResolvedCall *row) {
return row->kind == CBM_RESOLVED_FIELD_REFERENCE && row->caller_qn && row->callee_qn &&
row->confidence >= CBM_LSP_CONFIDENCE_FLOOR;
}
static inline int cbm_pipeline_lsp_field_key_cmp(const CBMResolvedCall *row, const char *caller_qn,
const char *member) {
int by_caller = strcmp(row->caller_qn, caller_qn);
return by_caller != 0 ? by_caller : strcmp(cbm_lsp_bare_segment(row->callee_qn), member);
}
static inline int cbm_pipeline_lsp_field_row_cmp(const void *left_ptr, const void *right_ptr) {
const CBMResolvedCall *left = *(const CBMResolvedCall *const *)left_ptr;
const CBMResolvedCall *right = *(const CBMResolvedCall *const *)right_ptr;
int by_key = cbm_pipeline_lsp_field_key_cmp(left, right->caller_qn,
cbm_lsp_bare_segment(right->callee_qn));
return by_key != 0 ? by_key : strcmp(left->callee_qn, right->callee_qn);
}
/* False means only allocation failure; lookups then take the linear path. */
static inline bool cbm_pipeline_lsp_field_index_build(const CBMResolvedCallArray *arr,
cbm_pipeline_lsp_field_index_t *index) {
index->rows = NULL;
index->count = 0;
int field_count = 0;
for (int i = 0; arr && i < arr->count; i++) {
if (cbm_pipeline_lsp_field_row_usable(&arr->items[i])) {
field_count++;
}
}
if (field_count == 0) {
return true;
}
const CBMResolvedCall **rows = (const CBMResolvedCall **)cbm_alloc(
CBM_MEM_CLASS_EXTRACT, (size_t)field_count * sizeof(*rows));
if (!rows) {
return false;
}
int next = 0;
for (int i = 0; i < arr->count; i++) {
if (cbm_pipeline_lsp_field_row_usable(&arr->items[i])) {
rows[next++] = &arr->items[i];
}
}
qsort(rows, (size_t)field_count, sizeof(*rows), cbm_pipeline_lsp_field_row_cmp);
index->rows = rows;
index->count = field_count;
return true;
}
static inline void cbm_pipeline_lsp_field_index_free(cbm_pipeline_lsp_field_index_t *index) {
cbm_free(CBM_MEM_CLASS_EXTRACT, (void *)index->rows);
index->rows = NULL;
index->count = 0;
}
/* Walks the owner rows of one (function, member name). `index` NULL selects
* the linear scan over `arr`. */
typedef struct {
const cbm_pipeline_lsp_field_index_t *index;
const CBMResolvedCallArray *arr;
const char *caller_qn;
const char *member;
int next;
} cbm_pipeline_lsp_field_cursor_t;
static inline cbm_pipeline_lsp_field_cursor_t cbm_pipeline_lsp_field_cursor(
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_field_index_t *index,
const char *caller_qn, const char *member) {
cbm_pipeline_lsp_field_cursor_t cursor = {index, arr, caller_qn, member, 0};
if (index && caller_qn && member) {
int low = 0;
int high = index->count;
while (low < high) {
int middle = low + (high - low) / 2;
if (cbm_pipeline_lsp_field_key_cmp(index->rows[middle], caller_qn, member) < 0) {
low = middle + 1;
} else {
high = middle;
}
}
cursor.next = low;
}
return cursor;
}
static inline const CBMResolvedCall *cbm_pipeline_lsp_field_next(
cbm_pipeline_lsp_field_cursor_t *cursor) {
if (!cursor->caller_qn || !cursor->member) {
return NULL;
}
if (cursor->index) {
if (cursor->next >= cursor->index->count) {
return NULL;
}
const CBMResolvedCall *row = cursor->index->rows[cursor->next];
if (cbm_pipeline_lsp_field_key_cmp(row, cursor->caller_qn, cursor->member) != 0) {
return NULL;
}
cursor->next++;
return row;
}
while (cursor->arr && cursor->next < cursor->arr->count) {
const CBMResolvedCall *row = &cursor->arr->items[cursor->next++];
if (cbm_pipeline_lsp_field_row_usable(row) &&
cbm_pipeline_lsp_field_key_cmp(row, cursor->caller_qn, cursor->member) == 0) {
return row;
}
}
return NULL;
}
/* The graph node of an owner row, or NULL when the owner is not a project
* Field (a libc struct, say). Exact QN only: a name fallback here would undo
* the typing the row carries. */
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_field_node(const cbm_gbuf_t *gbuf,
const char *project_name,
const CBMResolvedCall *row) {
const cbm_gbuf_node_t *node =
cbm_pipeline_lsp_target_node_policy(gbuf, project_name, row->callee_qn, false, false);
return node && node->label && strcmp(node->label, "Field") == 0 ? node : NULL;
}
/* The Field a C member call (`cb.close(ctx)`) lands on, decided by the type of
* its object: among the owner rows of (enclosing function, member name) that
* name a project Field, the one with the smallest QN, so the sequential scan
* and the sorted index agree. NULL when no owner is known. */
static inline const cbm_gbuf_node_t *cbm_pipeline_c_member_call_field(
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_field_index_t *index,
const cbm_gbuf_t *gbuf, const char *project_name, const char *enclosing_func_qn,
const char *callee_text) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
arr, index, enclosing_func_qn, cbm_lsp_bare_segment(callee_text));
const cbm_gbuf_node_t *best = NULL;
const CBMResolvedCall *owner;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
const cbm_gbuf_node_t *field = cbm_pipeline_lsp_field_node(gbuf, project_name, owner);
if (field && (!best || strcmp(field->qualified_name, best->qualified_name) < 0)) {
best = field;
}
}
return best;
}
/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
*
* Per-file LSPs sometimes emit `callee_qn` as the raw package-shaped
+20
View File
@@ -873,6 +873,26 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
if (!target_node || source_node->id == target_node->id) {
return 0;
}
/* A call that resolved onto a struct Field: refused across languages and
* for a bare C call; a C/C++ member call takes the Field its object's type
* names instead of the one the member name happened to find. */
switch (cbm_call_onto_field_policy(lang, call->callee_name, target_node->label,
target_node->file_path)) {
case CBM_FIELD_CALL_DROP:
return 0;
case CBM_FIELD_CALL_BY_OWNER:
target_node =
cbm_pipeline_c_member_call_field(lsp_calls, NULL, ctx->gbuf, ctx->project_name,
call->enclosing_func_qn, call->callee_name);
if (!target_node || source_node->id == target_node->id) {
return 0;
}
res.qualified_name = target_node->qualified_name;
res.strategy = "lsp_field_access";
break;
case CBM_FIELD_CALL_KEEP:
break;
}
/* #725: suffix_match is language-agnostic and will attach a Python
* Store.commit() call to a JS function named commit (or a Bash main
* to a Python main). Drop that weak cross-language edge. */
+18 -10
View File
@@ -462,24 +462,28 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
return 0;
}
/* Go: fold per-field "Field" definitions into their owning struct's
/* Go and C: fold per-field "Field" definitions into their owning type's
* field_defs. extract_defs.c emits one flat CBMDefinition per struct field
* (label "Field", parent_class = owning struct QN, name = field name,
* return_type = raw type text). Those rows are dropped by pxc_build_lsp_def
* (pxc_map_label excludes "Field"), so without this fold every Go struct
* registers with zero fields and field-chain calls (h.svc.Handle) can
* never resolve. Fields are always declared in the same file as their struct,
* so scanning the file's own defs covers every case. Runs inside
* (pxc_map_label excludes "Field"), so without this fold every struct
* registers with zero fields: Go field-chain calls (h.svc.Handle) can never
* resolve, and a C member access on a struct from another file (every header)
* has no owner to bind. Fields are always declared in the same file as their
* struct, so scanning the file's own defs covers every case. Runs inside
* cbm_pxc_collect_all_defs — one site covers both the prebuilt-registry path
* and the per-file fallback, since both consume all_defs. */
static void pxc_fold_go_struct_fields(CBMArena *arena, const CBMFileResult *result, CBMLSPDef *defs,
int start, int end) {
* and the per-file fallback, since both consume all_defs.
*
* type_label is the label the language's aggregate carries: "Struct" for Go,
* "Class" for a C struct or union. */
static void pxc_fold_struct_fields(CBMArena *arena, const CBMFileResult *result, CBMLSPDef *defs,
int start, int end, const char *type_label) {
if (!arena || !result || !defs || start >= end) {
return;
}
for (int si = start; si < end; si++) {
CBMLSPDef *dst = &defs[si];
if (!dst->label || strcmp(dst->label, "Struct") != 0 || !dst->qualified_name) {
if (!dst->label || strcmp(dst->label, type_label) != 0 || !dst->qualified_name) {
continue;
}
int count = 0;
@@ -636,7 +640,11 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMArena *are
}
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */
if (files[fi].language == CBM_LANG_GO) {
pxc_fold_go_struct_fields(arena, fr, defs, file_start, idx);
pxc_fold_struct_fields(arena, fr, defs, file_start, idx, "Struct");
} else if (files[fi].language == CBM_LANG_C || files[fi].language == CBM_LANG_CPP) {
/* Both: a `.h` file is classified C++, and that is where C declares
* the structs its `.c` files access. */
pxc_fold_struct_fields(arena, fr, defs, file_start, idx, "Class");
}
if (files[fi].language == CBM_LANG_RUST) {
for (int ii = 0; ii < fr->impl_traits.count; ii++) {
+133 -3
View File
@@ -2893,6 +2893,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
}
}
/* Field-owner rows, indexed on the first C member call that needs them. */
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_built = false;
bool field_index_ready = false;
for (int c = 0; c < result->calls.count; c++) {
CBMCall *call = &result->calls.items[c];
if (!call->callee_name) {
@@ -3137,6 +3141,31 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
}
atomic_fetch_add_explicit(&rc->time_ns_rc_target, extract_now_ns() - _rc_t0,
memory_order_relaxed);
/* A call that resolved onto a struct Field: refused across languages
* and for a bare C call; a C/C++ member call takes the Field its
* object's type names. Mirrors the sequential twin (pass_calls.c). */
if (target_node && source_node->id != target_node->id) {
cbm_field_call_policy_t field_policy = cbm_call_onto_field_policy(
lang, call->callee_name, target_node->label, target_node->file_path);
if (field_policy == CBM_FIELD_CALL_DROP) {
continue;
}
if (field_policy == CBM_FIELD_CALL_BY_OWNER) {
if (!field_index_built) {
field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
field_index_built = true;
}
target_node = cbm_pipeline_c_member_call_field(
&result->resolved_calls, field_index_ready ? &field_index : NULL, rc->main_gbuf,
rc->project_name, call->enclosing_func_qn, call->callee_name);
if (!target_node || source_node->id == target_node->id) {
continue;
}
res.qualified_name = target_node->qualified_name;
res.strategy = "lsp_field_access";
}
}
if (target_node && source_node->id != target_node->id &&
cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) {
/* #725: same guard as pass_calls.c — do not emit a suffix_match
@@ -3180,6 +3209,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
cbm_ht_foreach(lsp_legacy_idx, lsp_idx_free_key, NULL);
cbm_ht_free(lsp_legacy_idx);
}
cbm_pipeline_lsp_field_index_free(&field_index);
}
/* Resolve usages for one file. */
@@ -3190,6 +3220,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
cbm_pipeline_lsp_reference_index_t reference_index = {0};
bool reference_index_ready =
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
for (int u = 0; u < result->usages.count; u++) {
CBMUsage *usage = &result->usages.items[u];
if (!usage->ref_name) {
@@ -3200,6 +3233,30 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (!src) {
continue;
}
/* A C member name binds through the type of its object, published by
* the C LSP as field-owner rows, and through nothing else. Must mirror
* the sequential twin (pass_usages.c) exactly. */
if (usage->kind == CBM_USAGE_VALUE &&
cbm_c_member_binds_by_owner(lang == CBM_LANG_C, usage->is_member_access)) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, field_index_ready ? &field_index : NULL,
usage->enclosing_func_qn, usage->ref_name);
const CBMResolvedCall *owner;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
const cbm_gbuf_node_t *field =
cbm_pipeline_lsp_field_node(rc->main_gbuf, rc->project_name, owner);
if (!field || field->id == src->id) {
continue;
}
char member_esc[CBM_SZ_256];
cbm_json_escape(member_esc, sizeof(member_esc), usage->ref_name);
char member_props[CBM_SZ_512];
snprintf(member_props, sizeof(member_props), "{\"callee\":\"%s\"}", member_esc);
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, "USAGE", member_props);
ws->usages_resolved++;
}
continue;
}
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
@@ -3278,6 +3335,7 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
ws->usages_resolved++;
}
cbm_pipeline_lsp_reference_index_free(&reference_index);
cbm_pipeline_lsp_field_index_free(&field_index);
}
/* Resolve throws/raises for one file. */
@@ -3315,6 +3373,9 @@ static void resolve_file_throws(resolve_ctx_t *rc, resolve_worker_state_t *ws,
static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang) {
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
if (!rw->var_name) {
@@ -3325,6 +3386,24 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
if (!src) {
continue;
}
/* C member read/write: same owner join as resolve_file_usages. Mirrors
* the sequential twin (pass_usages.c). */
if (cbm_c_member_binds_by_owner(lang == CBM_LANG_C, rw->is_member_access)) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, field_index_ready ? &field_index : NULL,
rw->enclosing_func_qn, rw->var_name);
const CBMResolvedCall *owner;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
const cbm_gbuf_node_t *field =
cbm_pipeline_lsp_field_node(rc->main_gbuf, rc->project_name, owner);
if (!field || field->id == src->id) {
continue;
}
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id,
rw->is_write ? "WRITES" : "READS", "{}");
}
continue;
}
cbm_resolution_t res = cbm_registry_resolve(rc->registry, rw->var_name, module_qn, imp_keys,
imp_vals, imp_count);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
@@ -3348,6 +3427,7 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
const char *etype = rw->is_write ? "WRITES" : "READS";
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}");
}
cbm_pipeline_lsp_field_index_free(&field_index);
}
/* Resolve base_classes → INHERITS edges for one definition. */
@@ -3564,6 +3644,54 @@ static bool pp_has_pending_lsp_site(const CBMFileResult *result) {
return false;
}
/* A C member access binds only through a field-owner row. The per-file walk
* publishes owners for the structs its own file declares; a struct from a
* header is known only to the cross-file pass. A member access that still has
* no owner is therefore a request for that pass, exactly like an unqualified
* call site -- without it a file with no pending call would keep none of its
* edges onto header-declared fields. */
static bool pp_has_unowned_c_member(const CBMFileResult *result, CBMLanguage lang) {
if (lang != CBM_LANG_C) {
return false;
}
cbm_pipeline_lsp_field_index_t field_index = {0};
bool ready = cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
bool unowned = false;
for (int i = 0; i < result->usages.count && !unowned; i++) {
const CBMUsage *usage = &result->usages.items[i];
if (usage->kind != CBM_USAGE_VALUE || !usage->is_member_access || !usage->ref_name) {
continue;
}
cbm_pipeline_lsp_field_cursor_t owners =
cbm_pipeline_lsp_field_cursor(&result->resolved_calls, ready ? &field_index : NULL,
usage->enclosing_func_qn, usage->ref_name);
unowned = cbm_pipeline_lsp_field_next(&owners) == NULL;
}
for (int i = 0; i < result->rw.count && !unowned; i++) {
const CBMReadWrite *rw = &result->rw.items[i];
if (!rw->is_member_access || !rw->var_name) {
continue;
}
cbm_pipeline_lsp_field_cursor_t owners =
cbm_pipeline_lsp_field_cursor(&result->resolved_calls, ready ? &field_index : NULL,
rw->enclosing_func_qn, rw->var_name);
unowned = cbm_pipeline_lsp_field_next(&owners) == NULL;
}
for (int i = 0; i < result->calls.count && !unowned; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->callee_name ||
(!strchr(call->callee_name, '.') && !strstr(call->callee_name, "->"))) {
continue;
}
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, ready ? &field_index : NULL, call->enclosing_func_qn,
cbm_lsp_bare_segment(call->callee_name));
unowned = cbm_pipeline_lsp_field_next(&owners) == NULL;
}
cbm_pipeline_lsp_field_index_free(&field_index);
return unowned;
}
static void resolve_worker(int worker_id, void *ctx_ptr) {
resolve_ctx_t *rc = ctx_ptr;
resolve_worker_state_t *ws = &rc->workers[worker_id];
@@ -3651,11 +3779,13 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
int semantic_sites = result->calls.count + call_reference_sites;
int qualified_lsp_sites = pp_qualified_lsp_site_count(result);
bool pending_lsp_site = pp_has_pending_lsp_site(result);
bool unowned_c_member = pp_has_unowned_c_member(result, lang);
bool cross_lsp_eligible =
(rc->all_defs && rc->def_count > 0 && cbm_pxc_has_cross_lsp(lang) &&
(semantic_sites > 0 || pending_lsp_site) &&
(jvm_cross_lsp || rust_workspace_cross_lsp || pending_lsp_site ||
qualified_lsp_sites < semantic_sites) &&
(((semantic_sites > 0 || pending_lsp_site) &&
(jvm_cross_lsp || rust_workspace_cross_lsp || pending_lsp_site ||
qualified_lsp_sites < semantic_sites)) ||
unowned_c_member) &&
!is_generated);
/* Skip files with nothing else to resolve and no cross-LSP work. */
+52
View File
@@ -151,6 +151,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
cbm_pipeline_lsp_reference_index_t reference_index = {0};
bool reference_index_ready =
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
for (int u = 0; u < result->usages.count; u++) {
CBMUsage *usage = &result->usages.items[u];
if (!usage->ref_name) {
@@ -161,6 +164,31 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
continue;
}
/* A C member name binds through the type of its object, published by
* the C LSP as field-owner rows, and through nothing else: the bare
* member name must never reach the short-name registry below. */
if (usage->kind == CBM_USAGE_VALUE &&
cbm_c_member_binds_by_owner(lang == CBM_LANG_C, usage->is_member_access)) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, field_index_ready ? &field_index : NULL,
usage->enclosing_func_qn, usage->ref_name);
const CBMResolvedCall *owner;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
const cbm_gbuf_node_t *field =
cbm_pipeline_lsp_field_node(ctx->gbuf, ctx->project_name, owner);
if (!field || field->id == src->id) {
continue;
}
char member_esc[CBM_SZ_256];
cbm_json_escape(member_esc, sizeof(member_esc), usage->ref_name);
char member_props[CBM_SZ_512];
snprintf(member_props, sizeof(member_props), "{\"callee\":\"%s\"}", member_esc);
cbm_gbuf_insert_edge(ctx->gbuf, src->id, field->id, "USAGE", member_props);
resolved++;
}
continue;
}
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
@@ -242,6 +270,7 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
resolved++;
}
cbm_pipeline_lsp_reference_index_free(&reference_index);
cbm_pipeline_lsp_field_index_free(&field_index);
return resolved;
}
@@ -284,6 +313,9 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
const char *module_qn, const char **imp_keys, const char **imp_vals,
int imp_count, CBMLanguage lang) {
int resolved = 0;
cbm_pipeline_lsp_field_index_t field_index = {0};
bool field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
if (!rw->var_name) {
@@ -295,6 +327,25 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
continue;
}
/* C member read/write: same owner join as resolve_usage_edges. */
if (cbm_c_member_binds_by_owner(lang == CBM_LANG_C, rw->is_member_access)) {
cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor(
&result->resolved_calls, field_index_ready ? &field_index : NULL,
rw->enclosing_func_qn, rw->var_name);
const CBMResolvedCall *owner;
while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) {
const cbm_gbuf_node_t *field =
cbm_pipeline_lsp_field_node(ctx->gbuf, ctx->project_name, owner);
if (!field || field->id == src->id) {
continue;
}
cbm_gbuf_insert_edge(ctx->gbuf, src->id, field->id,
rw->is_write ? "WRITES" : "READS", "{}");
resolved++;
}
continue;
}
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, rw->var_name, module_qn,
imp_keys, imp_vals, imp_count);
if (!res.qualified_name || res.qualified_name[0] == '\0') {
@@ -320,6 +371,7 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}");
resolved++;
}
cbm_pipeline_lsp_field_index_free(&field_index);
return resolved;
}
+17
View File
@@ -358,6 +358,23 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
* unit-tested in test_registry.c. */
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label);
/* What to do with a call whose registry resolution landed on a Field node.
* Pure; unit-tested in test_registry.c. */
typedef enum {
CBM_FIELD_CALL_KEEP = 0, /* not a Field target, or a language this rule leaves alone */
CBM_FIELD_CALL_DROP, /* a Field can never be this call's target */
CBM_FIELD_CALL_BY_OWNER, /* C/C++ member call: the object's type names the Field */
} cbm_field_call_policy_t;
cbm_field_call_policy_t cbm_call_onto_field_policy(CBMLanguage caller_lang, const char *callee_text,
const char *target_label,
const char *target_file_path);
/* True when a reference is the member half of a C `a.b` / `a->b`. Such a
* reference binds only through the C LSP's field-owner rows (the type of `a`),
* never through a bare-name lookup of `b`. C only. Pure; unit-tested in
* test_registry.c. */
bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access);
/* Get the label of a qualified name, or NULL if not found. */
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
+46
View File
@@ -811,6 +811,52 @@ bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const cha
return strcmp(target_label, "Field") == 0;
}
cbm_field_call_policy_t cbm_call_onto_field_policy(CBMLanguage caller_lang, const char *callee_text,
const char *target_label,
const char *target_file_path) {
/* The short-name registry holds Field nodes, so a call spelled like some
* struct member can resolve onto that Field. Three shapes are decided here:
* - a caller in another language: nothing but the spelling connects a
* Bash `command` or a Python `sorted()` to a C struct member. The
* unique_name exemption of the CALLS guard (#1572) is about functions;
* for a Field the #1928 reference rule applies, with its JS/TS and
* C/C++ family exemptions;
* - a bare C call (`socket(...)`, a function-pointer parameter
* `want(x)`): a bare C name is never a struct member;
* - a C or C++ member call (`cb.close(ctx)`, `text.size()`): the type of
* the object decides which struct's member is called, never the
* member name alone. The C LSP publishes owners in both modes.
* A bare C++ call keeps its resolution (a method body reaches its own
* members without a selector), and so does every other language. */
if (!target_label || strcmp(target_label, "Field") != 0) {
return CBM_FIELD_CALL_KEEP;
}
if (cbm_suppress_cross_language_ref(caller_lang, target_file_path)) {
return CBM_FIELD_CALL_DROP;
}
if (caller_lang != CBM_LANG_C && caller_lang != CBM_LANG_CPP) {
return CBM_FIELD_CALL_KEEP;
}
if (callee_text && (strchr(callee_text, '.') || strstr(callee_text, "->"))) {
return CBM_FIELD_CALL_BY_OWNER;
}
return caller_lang == CBM_LANG_C ? CBM_FIELD_CALL_DROP : CBM_FIELD_CALL_KEEP;
}
bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access) {
/* The member half of `a.b` / `a->b` is not a name in any C scope: only the
* type of `a` gives `b` a meaning. Handing the bare `b` to the short-name
* registry bound whichever Field (or function, or variable) was called
* `b` -- on this repository 9,150 USAGE and 2,810 WRITES edges onto
* Fields, 3,976 of the USAGE edges on a name several structs share. Such a
* reference binds only through the C LSP's field-owner rows.
*
* C-gated like the Go rule above: a C++ or Objective-C method body
* reaches its own members without a selector, and their member accesses
* keep the existing resolution until their LSPs publish owners too. */
return is_c && is_member_access;
}
/* ── Lifecycle ──────────────────────────────────────────────────── */
cbm_registry_t *cbm_registry_new(void) {
+47
View File
@@ -1069,6 +1069,52 @@ TEST(c_struct) {
PASS();
}
/* `typedef struct { … } Name;` is the dominant C spelling of a type: the
* aggregate itself is anonymous and the typedef's declarator names it. Without
* that name the struct had no Class node and none of its members a Field node
* (37 of the 38 structs in internal/cbm/cbm.h), so a member access could only
* bind some OTHER struct's same-named field. */
TEST(extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef) {
CBMFileResult *r = extract("typedef struct {\n"
" int count;\n"
" const char *label;\n"
"} Tally;\n"
"\n"
"typedef union {\n"
" int as_int;\n"
" float as_float;\n"
"} Slot;\n"
"\n"
"typedef enum { SHADE_RED, SHADE_GREEN } Shade;\n"
"\n"
"typedef struct Named {\n"
" int id;\n"
"} Named;\n"
"\n"
"typedef struct {\n"
" int raw;\n"
"} *TallyRef;\n",
CBM_LANG_C, "t", "tally.h");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* RED before the fix: none of the three anonymous aggregates has a def. */
ASSERT_TRUE(has_def(r, "Class", "Tally"));
ASSERT_TRUE(has_def_qn(r, "t.tally.Tally.count"));
ASSERT_TRUE(has_def_qn(r, "t.tally.Tally.label"));
ASSERT_TRUE(has_def(r, "Class", "Slot"));
ASSERT_TRUE(has_def_qn(r, "t.tally.Slot.as_int"));
ASSERT_TRUE(has_def_qn(r, "t.tally.Slot.as_float"));
ASSERT_TRUE(has_def_any(r, "Shade"));
/* A struct that names itself keeps exactly one def, under its own name. */
ASSERT_EQ(count_defs_named(r, "Class", "Named"), 1);
ASSERT_TRUE(has_def_qn(r, "t.tally.Named.id"));
/* A pointer typedef names a pointer type, not the aggregate: no Class. */
ASSERT_FALSE(has_def(r, "Class", "TallyRef"));
ASSERT_FALSE(has_def(r, "Field", "raw"));
cbm_free_result(r);
PASS();
}
/* return_type of the first definition named `name`; NULL when there is no such
* definition or it carries no return type. */
static const char *def_return_type(CBMFileResult *r, const char *name) {
@@ -8934,6 +8980,7 @@ SUITE(extraction) {
RUN_TEST(c_function_return_type_preserves_pointer_and_qualifier);
RUN_TEST(c_function_return_type_plain_unchanged);
RUN_TEST(c_struct);
RUN_TEST(extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef);
RUN_TEST(cpp_class);
RUN_TEST(cpp_method_return_type_preserves_pointer_and_qualifier);
+252
View File
@@ -6745,6 +6745,256 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) {
PASS();
}
/* True when a node named src_name has an edge of edge_type to the Field
* `field` OWNED BY `owner` (qualified name ends in ".owner.field"). The
* name-only cross_file_edge_exists cannot tell two same-named fields apart,
* which is exactly what the C member-access probes below must do. */
static bool field_edge_exists(cbm_store_t *s, const char *project, const char *src_name,
const char *owner, const char *field, const char *edge_type) {
char suffix[256];
snprintf(suffix, sizeof(suffix), ".%s.%s", owner, field);
size_t suffix_len = strlen(suffix);
cbm_node_t *srcs = NULL;
cbm_node_t *tgts = NULL;
int sc = 0;
int tc = 0;
cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc);
cbm_store_find_nodes_by_name(s, project, field, &tgts, &tc);
bool found = false;
for (int i = 0; i < sc && !found; i++) {
cbm_edge_t *edges = NULL;
int ec = 0;
cbm_store_find_edges_by_source_type(s, srcs[i].id, edge_type, &edges, &ec);
for (int j = 0; j < ec && !found; j++) {
for (int k = 0; k < tc && !found; k++) {
const char *qn = tgts[k].qualified_name;
size_t qn_len = qn ? strlen(qn) : 0;
found = edges[j].target_id == tgts[k].id && tgts[k].label &&
strcmp(tgts[k].label, "Field") == 0 && qn_len >= suffix_len &&
strcmp(qn + qn_len - suffix_len, suffix) == 0;
}
}
if (edges) {
cbm_store_free_edges(edges, ec);
}
}
if (srcs) {
cbm_store_free_nodes(srcs, sc);
}
if (tgts) {
cbm_store_free_nodes(tgts, tc);
}
return found;
}
/* Number of edges of edge_type that END on the Field `field` owned by `owner`,
* whatever their source: the call probes below must hold for a caller in any
* language, including a shell script whose source node has no stable name. */
static int field_inbound_edge_count(cbm_store_t *s, const char *project, const char *owner,
const char *field, const char *edge_type) {
char suffix[256];
snprintf(suffix, sizeof(suffix), ".%s.%s", owner, field);
size_t suffix_len = strlen(suffix);
cbm_node_t *tgts = NULL;
int tc = 0;
cbm_store_find_nodes_by_name(s, project, field, &tgts, &tc);
int total = 0;
for (int k = 0; k < tc; k++) {
const char *qn = tgts[k].qualified_name;
size_t qn_len = qn ? strlen(qn) : 0;
if (!tgts[k].label || strcmp(tgts[k].label, "Field") != 0 || qn_len < suffix_len ||
strcmp(qn + qn_len - suffix_len, suffix) != 0) {
continue;
}
cbm_edge_t *edges = NULL;
int ec = 0;
cbm_store_find_edges_by_target_type(s, tgts[k].id, edge_type, &edges, &ec);
total += ec;
if (edges) {
cbm_store_free_edges(edges, ec);
}
}
if (tgts) {
cbm_store_free_nodes(tgts, tc);
}
return total;
}
/* Fixture for the C member-access probes: two structs share the field name
* `count`, and each function touches exactly one of them. A bare-name lookup
* of `count` can only pick one Field for both functions; only the type of the
* object expression says which struct is meant. `use_opaque` reaches `count`
* through a type the project never defines, so no Field may be bound at all.
*
* Calls follow the same rule. The structs carry a function-pointer member
* `notify` (declared through a typedef: a member spelled `void (*notify)(int)`
* has no Field node today, a separate extraction gap). `use_remote` calls
* Beta's `notify` from a file that declares its own struct with a `notify`
* member -- the candidate a bare-name lookup prefers, being in the same
* module. `call_param` calls a PARAMETER named like the field `alpha_only`,
* and `tool.sh` runs a command of that name: neither calls a struct member. */
static void write_c_member_field_fixture(const char *tmp, int pad_files) {
write_temp_file(tmp, "alpha.h",
"#ifndef ALPHA_H\n"
"#define ALPHA_H\n"
"typedef void (*alpha_notify_fn)(int);\n"
"struct Alpha {\n"
" int count;\n"
" int alpha_only;\n"
" alpha_notify_fn notify;\n"
"};\n"
"#endif\n");
write_temp_file(tmp, "beta.h",
"#ifndef BETA_H\n"
"#define BETA_H\n"
"typedef void (*beta_notify_fn)(int);\n"
"typedef struct {\n"
" int count;\n"
" int beta_only;\n"
" beta_notify_fn notify;\n"
"} Beta;\n"
"#endif\n");
write_temp_file(tmp, "use_alpha.c",
"#include \"alpha.h\"\n"
"\n"
"int use_alpha(struct Alpha *a) {\n"
" a->count = 1;\n"
" return a->count + a->alpha_only;\n"
"}\n"
"\n"
"int call_param(int (*alpha_only)(void)) {\n"
" return alpha_only();\n"
"}\n");
write_temp_file(tmp, "mix.c",
"#include \"beta.h\"\n"
"\n"
"typedef void (*local_notify_fn)(int);\n"
"struct Local {\n"
" local_notify_fn notify;\n"
"};\n"
"\n"
"void use_remote(Beta b) {\n"
" b.notify(3);\n"
"}\n");
write_temp_file(tmp, "tool.sh",
"#!/bin/sh\n"
"alpha_only --check\n");
write_temp_file(tmp, "use_beta.c",
"#include \"beta.h\"\n"
"\n"
"int use_beta(Beta b) {\n"
" b.count = 2;\n"
" return b.count + b.beta_only;\n"
"}\n");
write_temp_file(tmp, "use_opaque.c",
"struct Opaque;\n"
"\n"
"int use_opaque(struct Opaque *o) {\n"
" return o->count;\n"
"}\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "pad/filler%d.c", i);
snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i);
write_temp_file(tmp, name, body);
}
}
/* Shared assertions for the sequential and the parallel twin. */
static int assert_c_member_fields_bind_by_type(cbm_store_t *s, const char *project) {
/* The probe means nothing unless both same-named fields exist. */
ASSERT_GTE(fixture_node_count(s, project, "alpha.h", "count", "Field"), 1);
ASSERT_GTE(fixture_node_count(s, project, "beta.h", "count", "Field"), 1);
/* Reproduce-first: RED while `count` resolves by bare name — both
* functions then bind the SAME Field, so one of each pair is wrong. */
ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "use_alpha", "Beta", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "use_beta", "Alpha", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "count", "WRITES"));
ASSERT_FALSE(field_edge_exists(s, project, "use_alpha", "Beta", "count", "WRITES"));
ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "count", "WRITES"));
ASSERT_FALSE(field_edge_exists(s, project, "use_beta", "Alpha", "count", "WRITES"));
/* A field whose name is unique keeps its edge. */
ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "alpha_only", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "beta_only", "USAGE"));
/* An object whose type the project never defines binds no Field. */
ASSERT_FALSE(field_edge_exists(s, project, "use_opaque", "Alpha", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "use_opaque", "Beta", "count", "USAGE"));
/* A call through a function-pointer member lands on the member of its
* object's type, not on the same-named member its own file declares. */
ASSERT_GTE(fixture_node_count(s, project, "mix.c", "notify", "Field"), 1);
ASSERT_TRUE(field_edge_exists(s, project, "use_remote", "Beta", "notify", "CALLS"));
ASSERT_FALSE(field_edge_exists(s, project, "use_remote", "Local", "notify", "CALLS"));
/* A bare C call of a parameter named `alpha_only` and a shell command of
* that name are not calls of the struct member, however unique the name. */
ASSERT_EQ(field_inbound_edge_count(s, project, "Alpha", "alpha_only", "CALLS"), 0);
return 0;
}
TEST(pipeline_c_member_access_binds_field_by_object_type) {
/* `x.f` / `x->f` handed only the member name `f` to the short-name
* registry, which returned whichever Field was called `f`: 9,150 USAGE
* and 2,810 WRITES edges onto Fields on this repository, 3,976 of the
* USAGE edges onto a name several structs share. Sequential-path twin. */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_field_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_c_member_field_fixture(tmp, 0);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/c_field.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = assert_c_member_fields_bind_by_type(s, project);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
if (rc != 0) {
return rc;
}
PASS();
}
TEST(pipeline_c_member_access_binds_field_by_object_type_parallel) {
/* Parallel twin: resolve_file_usages / resolve_file_rw are independent
* resolvers and must take the same typed join (#1928's lesson). */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_fieldp_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_c_member_field_fixture(tmp, 52);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/c_fieldp.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = assert_c_member_fields_bind_by_type(s, project);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
if (rc != 0) {
return rc;
}
PASS();
}
/* Count nodes with the given exact name in the project (e.g. a Route path). */
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) {
cbm_node_t *ns = NULL;
@@ -16264,6 +16514,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
RUN_TEST(pipeline_c_member_access_binds_field_by_object_type);
RUN_TEST(pipeline_c_member_access_binds_field_by_object_type_parallel);
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
+49
View File
@@ -1037,6 +1037,53 @@ TEST(go_bare_ref_never_binds_field) {
PASS();
}
TEST(call_onto_field_follows_language_and_shape) {
/* A call the registry resolved onto a Field node. */
/* Another language: only the spelling connects them. */
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_BASH, "command", "Field", "src/probe.h"),
CBM_FIELD_CALL_DROP);
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_PYTHON, "sorted", "Field", "src/pool.c"),
CBM_FIELD_CALL_DROP);
/* A bare C call is never a struct member. */
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "socket", "Field", "tests/t.c"),
CBM_FIELD_CALL_DROP);
/* A C member call is decided by its object's type, in a .c or a .h. */
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "cb.close", "Field", "src/cb.h"),
CBM_FIELD_CALL_BY_OWNER);
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "w->svc->app.cancel", "Field", "src/a.c"),
CBM_FIELD_CALL_BY_OWNER);
/* C++ reaches its own members bare, so a bare call keeps its resolution;
* a member call is typed like C's (`text.size()` is not some struct's
* `size` member). */
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_CPP, "handler", "Field", "src/a.h"),
CBM_FIELD_CALL_KEEP);
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_CPP, "text.size", "Field", "src/a.h"),
CBM_FIELD_CALL_BY_OWNER);
/* The same language as the Field keeps its resolution too. */
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_GO, "s.fn", "Field", "pkg/s.go"),
CBM_FIELD_CALL_KEEP);
/* Not a Field target: not this rule's business. */
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_BASH, "main", "Function", "src/main.c"),
CBM_FIELD_CALL_KEEP);
ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "socket", NULL, "tests/t.c"),
CBM_FIELD_CALL_KEEP);
PASS();
}
TEST(c_member_access_binds_by_owner_only) {
/* The member half of a C `a.b` / `a->b` means nothing without the type of
* `a`: it binds through the C LSP's field-owner rows and never through a
* bare-name lookup. The selector shape arrives as is_member_access. */
ASSERT_TRUE(cbm_c_member_binds_by_owner(true, true));
/* A bare C identifier keeps the ordinary resolution. */
ASSERT_FALSE(cbm_c_member_binds_by_owner(true, false));
/* Other languages keep theirs: a C++ or Objective-C method body reaches its
* own members without a selector, and Go has its own rule above. */
ASSERT_FALSE(cbm_c_member_binds_by_owner(false, true));
ASSERT_FALSE(cbm_c_member_binds_by_owner(false, false));
PASS();
}
TEST(dynamic_suppress_drops_weak_method_matches) {
/* #592/#606/#1276: a member call whose receiver the LSP could not type, that
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
@@ -1296,6 +1343,8 @@ SUITE(registry) {
RUN_TEST(registry_tie_break_is_independent_of_registration_order);
RUN_TEST(cross_language_ref_drops_go_vs_c);
RUN_TEST(go_bare_ref_never_binds_field);
RUN_TEST(call_onto_field_follows_language_and_shape);
RUN_TEST(c_member_access_binds_by_owner_only);
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
RUN_TEST(python_builtin_member_table_matches_builtin_type_methods);