mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-10 09:13:53 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user