wip(graph): checkpoint staged member-resolution changes

Preserve the original staged graph and LSP work separately from the
smart-CI continuation. This is an unfinished checkpoint; final
integration and cross-platform verification remain outstanding.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-03 23:53:16 +02:00
1 parent 193b0a4d06
commit e37274f088
13 files changed
+1178 -86

No files matched your search

+206 -4
View File
@@ -40,6 +40,9 @@ enum {
NIX_HEADER_HOP_MAX = 8,
DECORATOR_SCAN_LIMIT = 3,
C_RETURN_WALK_DEPTH = 5,
/* Declarator nesting around a member name: `char *(*(*tbl[4])(int))(void)`
* is pointer, function, parens, pointer, function, parens, pointer, array. */
C_FIELD_DECL_WALK_DEPTH = 16,
VAR_RECURSION_LIMIT = 8,
NESTED_CLASS_STACK_CAP = 128,
FP_HASH_MUL = 31, /* FNV-like hash multiplier for identifier dedup */
@@ -3576,9 +3579,10 @@ static bool is_c_declarator_lang(CBMLanguage lang) {
* O(depth) with no recursion and needs no depth cap. It stops at the first node
* that is neither a pointer nor a reference declarator: for a function returning
* a function pointer (`int (*f(void))(int)`) that is the outer
* function_declarator, which leaves the base type as it was. */
* function_declarator, which leaves the base type as it was. That node is
* handed back through `rest` when the caller asks for it. */
static size_t c_rt_render(c_rt_out_t *out, TSNode func_node, TSNode type_node, TSNode declarator,
const char *source) {
const char *source, TSNode *rest) {
size_t added = 0;
uint32_t decl_start = ts_node_start_byte(declarator);
uint32_t nc = ts_node_named_child_count(func_node);
@@ -3632,6 +3636,9 @@ static size_t c_rt_render(c_rt_out_t *out, TSNode func_node, TSNode type_node, T
}
decl = inner;
}
if (rest) {
*rest = decl; /* the declarator the markers wrap; null when none is left */
}
return added;
}
@@ -3645,7 +3652,7 @@ static char *c_declared_return_type(CBMExtractCtx *ctx, TSNode func_node, TSNode
return cbm_node_text(a, type_node, ctx->source);
}
c_rt_out_t out = {NULL, 0};
if (c_rt_render(&out, func_node, type_node, declarator, ctx->source) == 0) {
if (c_rt_render(&out, func_node, type_node, declarator, ctx->source, NULL) == 0) {
return cbm_node_text(a, type_node, ctx->source);
}
out.buf = (char *)cbm_arena_alloc(a, out.len + NULL_TERM);
@@ -3653,7 +3660,7 @@ static char *c_declared_return_type(CBMExtractCtx *ctx, TSNode func_node, TSNode
return cbm_node_text(a, type_node, ctx->source);
}
out.len = 0;
(void)c_rt_render(&out, func_node, type_node, declarator, ctx->source);
(void)c_rt_render(&out, func_node, type_node, declarator, ctx->source, NULL);
out.buf[out.len] = '\0';
return out.buf;
}
@@ -6927,6 +6934,196 @@ static bool is_func_ptr_field(TSNode field) {
return false;
}
/* ── C-family member declarations ───────────────────────────────────
* `type declarator, declarator;` in a struct/union/class body. Each declarator
* is one member: its name is the identifier at the bottom of the declarator,
* its type everything around that identifier. */
/* The name of one member declarator, or a null node when the declarator is not
* a data member. A member FUNCTION declaration has the same field_declaration
* + function_declarator shape as a pointer-to-function member; the two differ
* in what the function_declarator wraps: `(*open)` is a parenthesized pointer
* declarator, `resize` (or `(resize)`) a name. */
static TSNode c_member_declarator_name(TSNode decl) {
TSNode null_node = {0};
bool need_pointer = false;
for (int depth = 0; depth < C_FIELD_DECL_WALK_DEPTH && !ts_node_is_null(decl); depth++) {
const char *kind = ts_node_type(decl);
if (strcmp(kind, "field_identifier") == 0 || strcmp(kind, "identifier") == 0) {
return need_pointer ? null_node : decl;
}
TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
bool first_child = false;
if (strcmp(kind, "function_declarator") == 0) {
if (ts_node_is_null(inner) ||
strcmp(ts_node_type(inner), "parenthesized_declarator") != 0) {
return null_node; /* a member function declaration */
}
need_pointer = true;
} else if (strstr(kind, "pointer_declarator") != NULL ||
strcmp(kind, "reference_declarator") == 0) {
need_pointer = false;
} else if (strcmp(kind, "attributed_declarator") == 0) {
first_child = true; /* the declarator, then its attributes */
} else if (strcmp(kind, "array_declarator") != 0 &&
strcmp(kind, "parenthesized_declarator") != 0) {
return null_node; /* operator, destructor, qualified name, ... */
}
if (ts_node_is_null(inner)) {
/* No `declarator` field: a parenthesized declarator holds it last
* (after an optional calling-convention modifier), a reference
* declarator as its only named child. */
uint32_t named = ts_node_named_child_count(decl);
for (uint32_t k = 0; k < named; k++) {
TSNode cand = ts_node_named_child(decl, first_child ? k : named - SKIP_ONE - k);
if (strcmp(ts_node_type(cand), "comment") != 0) {
inner = cand;
break;
}
}
}
decl = inner;
}
return null_node;
}
/* Type of one member declarator: the canonical prefix a return type gets from
* c_rt_render (cv-qualifiers, base type, pointer/reference markers), then the
* declarator that is left with the member name cut out, which is how C spells
* an abstract declarator: `int [8]`, `char *[4]`, `void (*)(int fd)`.
* Whitespace runs in that remainder collapse to one space; a `|` in it (the
* separator of the field list the cross-file registry is fed) drops it. */
static char *c_member_type_text(CBMExtractCtx *ctx, TSNode field, TSNode type_node, TSNode decl,
TSNode name_node) {
CBMArena *a = ctx->arena;
const char *src = ctx->source;
TSNode rest = decl;
c_rt_out_t out = {NULL, 0};
(void)c_rt_render(&out, field, type_node, decl, src, &rest);
uint32_t rest_start = ts_node_is_null(rest) ? 0 : ts_node_start_byte(rest);
uint32_t rest_end = ts_node_is_null(rest) ? 0 : ts_node_end_byte(rest);
uint32_t cut_start = ts_node_start_byte(name_node);
uint32_t cut_end = ts_node_end_byte(name_node);
/* prefix + one separating space + remainder + NUL */
char *buf = (char *)cbm_arena_alloc(a, out.len + (size_t)(rest_end - rest_start) + PAIR_LEN);
if (!buf) {
return cbm_node_text(a, type_node, src);
}
out.buf = buf;
out.len = 0;
(void)c_rt_render(&out, field, type_node, decl, src, NULL);
size_t prefix_len = out.len;
size_t n = prefix_len;
bool pending_space = n > 0 && buf[n - SKIP_ONE] != '*' && buf[n - SKIP_ONE] != '&';
bool wrote_rest = false;
for (uint32_t i = rest_start; i < rest_end; i++) {
if (i >= cut_start && i < cut_end) {
continue;
}
char c = src[i];
if (c == '|') {
n = prefix_len;
break;
}
if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
pending_space = pending_space || wrote_rest;
continue;
}
if (pending_space) {
buf[n++] = ' ';
pending_space = false;
}
buf[n++] = c;
wrote_rest = true;
}
buf[n] = '\0';
return buf;
}
/* True when `field` is a C-family member declaration: a type and at least one
* declarator. An anonymous struct/union member has no declarator and is left
* to the caller. */
static bool is_c_member_declaration(CBMExtractCtx *ctx, TSNode field) {
if (!is_c_declarator_lang(ctx->language) ||
strcmp(ts_node_type(field), "field_declaration") != 0) {
return false;
}
return !ts_node_is_null(ts_node_child_by_field_name(field, TS_FIELD("type"))) &&
!ts_node_is_null(ts_node_child_by_field_name(field, TS_FIELD("declarator")));
}
/* A member whose type is a macro invocation (`ql_head(tcache_slow_t)
* tcache_ql;`). The C grammar reads the type as a macro_type_specifier; the
* C++ grammar, which every header gets, reads `(tcache_slow_t)` as a
* parenthesized declarator and leaves the member's real name in an ERROR node
* behind it. Returns that name, or a null node for any other shape. */
static TSNode c_macro_typed_member_name(TSNode decl) {
TSNode null_node = {0};
if (strcmp(ts_node_type(decl), "parenthesized_declarator") != 0) {
return null_node;
}
TSNode err = ts_node_next_sibling(decl);
if (ts_node_is_null(err) || strcmp(ts_node_type(err), "ERROR") != 0 ||
ts_node_named_child_count(err) != SKIP_ONE) {
return null_node;
}
TSNode name = ts_node_named_child(err, 0);
const char *kind = ts_node_type(name);
if (strcmp(kind, "identifier") != 0 && strcmp(kind, "field_identifier") != 0) {
return null_node;
}
return name;
}
/* One Field per data-member declarator of a C-family member declaration. */
static void extract_c_member_fields(CBMExtractCtx *ctx, TSNode field, const char *class_qn,
const CBMLangSpec *spec) {
CBMArena *a = ctx->arena;
TSNode type_node = ts_node_child_by_field_name(field, TS_FIELD("type"));
uint32_t nc = ts_node_child_count(field);
for (uint32_t i = 0; i < nc; i++) {
const char *field_name = ts_node_field_name_for_child(field, i);
if (!field_name || strcmp(field_name, "declarator") != 0) {
continue;
}
TSNode decl = ts_node_child(field, i);
char *name = NULL;
char *type_text = NULL;
TSNode macro_name = c_macro_typed_member_name(decl);
if (!ts_node_is_null(macro_name)) {
/* The type is the macro invocation: the `type` node through the
* misread declarator. */
uint32_t start = ts_node_start_byte(type_node);
uint32_t end = ts_node_end_byte(decl);
name = cbm_node_text(a, macro_name, ctx->source);
type_text = end > start ? cbm_arena_strndup(a, ctx->source + start, end - start) : NULL;
} else {
TSNode name_node = c_member_declarator_name(decl);
if (ts_node_is_null(name_node)) {
continue;
}
name = cbm_node_text(a, name_node, ctx->source);
type_text = c_member_type_text(ctx, field, type_node, decl, name_node);
}
if (!name || !name[0] || !type_text || !type_text[0]) {
continue;
}
CBMDefinition def;
memset(&def, 0, sizeof(def));
def.name = name;
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, name);
def.label = "Field";
def.file_path = ctx->rel_path;
def.parent_class = class_qn;
def.return_type = type_text;
def.start_line = ts_node_start_point(field).row + TS_LINE_OFFSET;
def.end_line = ts_node_end_point(field).row + TS_LINE_OFFSET;
def.is_exported = cbm_is_exported(name, ctx->language);
def.decorators = extract_decorators(a, field, ctx->source, ctx->language, spec);
cbm_defs_push(&ctx->result->defs, a, def);
}
}
// Resolve the name node for a field declaration, unwrapping C pointer/array declarators.
static TSNode resolve_field_name_node(TSNode child) {
TSNode name_node = ts_node_child_by_field_name(child, TS_FIELD("declarator"));
@@ -7031,6 +7228,11 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
continue;
}
if (is_c_member_declaration(ctx, child)) {
extract_c_member_fields(ctx, child, class_qn, spec);
continue;
}
if (is_func_ptr_field(child)) {
continue;
}
+23 -2
View File
@@ -13,6 +13,8 @@
enum {
USE_PREFIX_LEN = 4, /* strlen("use ") */
MIN_WOLFRAM_CHILDREN = 2,
/* Nested `#if` branches followed while collecting C includes. */
C_INCLUDE_NEST_LIMIT = 64,
SECOND_IDX = 1,
};
@@ -663,10 +665,22 @@ static char *strip_angle_brackets(CBMArena *a, char *path) {
return path;
}
static void parse_c_imports(CBMExtractCtx *ctx) {
/* Node kinds that only group file-level items: the branches of `#if` /
* `#ifdef` (an include guard is one) and the body of `extern "C" { ... }`. */
static bool is_c_include_container(const char *kind) {
return strcmp(kind, "preproc_if") == 0 || strcmp(kind, "preproc_ifdef") == 0 ||
strcmp(kind, "preproc_else") == 0 || strcmp(kind, "preproc_elif") == 0 ||
strcmp(kind, "preproc_elifdef") == 0 || strcmp(kind, "linkage_specification") == 0 ||
strcmp(kind, "declaration_list") == 0;
}
/* Collect the includes among `parent`'s children, descending into
* preprocessor branches: every header keeps its includes inside its include
* guard, so the top level alone gave a guarded header no imports at all. */
static void parse_c_imports_in(CBMExtractCtx *ctx, TSNode parent, int depth) {
CBMArena *a = ctx->arena;
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
TSTreeCursor cursor = ts_tree_cursor_new(parent);
if (!ts_tree_cursor_goto_first_child(&cursor)) {
ts_tree_cursor_delete(&cursor);
return;
@@ -675,6 +689,9 @@ static void parse_c_imports(CBMExtractCtx *ctx) {
TSNode node = ts_tree_cursor_current_node(&cursor);
const char *kind = ts_node_type(node);
if (strcmp(kind, "preproc_include") != 0 && strcmp(kind, "preproc_import") != 0) {
if (depth < C_INCLUDE_NEST_LIMIT && is_c_include_container(kind)) {
parse_c_imports_in(ctx, node, depth + SKIP_ONE);
}
continue;
}
@@ -695,6 +712,10 @@ static void parse_c_imports(CBMExtractCtx *ctx) {
ts_tree_cursor_delete(&cursor);
}
static void parse_c_imports(CBMExtractCtx *ctx) {
parse_c_imports_in(ctx, ctx->root, 0);
}
// --- Ruby imports ---
// call nodes: require("X"), require_relative("X")
+331 -46
View File
@@ -19,6 +19,7 @@ static void c_emit_resolved_call(CLSPContext *ctx, const char *callee_qn, const
static void c_emit_unresolved_call(CLSPContext *ctx, const char *expr_text, const char *reason);
static const CBMType *c_lookup_field_type(CLSPContext *ctx, const char *type_qn,
const char *field_name, int depth);
static const CBMRegisteredType *c_resolve_field_owner_type(CLSPContext *ctx, const char *type_qn);
static void c_process_function(CLSPContext *ctx, TSNode func_node);
static void c_process_namespace(CLSPContext *ctx, TSNode ns_node);
static void c_process_class(CLSPContext *ctx, TSNode class_node);
@@ -2920,11 +2921,11 @@ static const CBMType *c_lookup_field_type(CLSPContext *ctx, const char *type_qn,
if (!type_qn || !field_name || depth > 5)
return NULL;
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (!rt && ctx->module_qn) {
rt = cbm_registry_lookup_type(
ctx->registry, cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, type_qn));
}
/* The same type lookup the field-owner rows use: a use site names the
* struct under its own module, the registry holds it under the header
* that declares it. Without it `d->ops->shut` loses its type at the first
* step whenever `d`'s struct comes from a header. */
const CBMRegisteredType *rt = c_resolve_field_owner_type(ctx, type_qn);
if (!rt)
return NULL;
@@ -2965,20 +2966,21 @@ static bool c_qn_in_module(const char *qn, const char *prefix) {
// 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;
//
// Only a type that HAS members can own one. `struct Ops *ops;` inside another
// struct, a forward declaration and a parameter type each register a memberless
// `Ops` of their own scope; counted as candidates they made every such name
// ambiguous and left the object untyped. They are considered only when no
// candidate with members exists (the "typed, declares no such field" answer).
static bool c_type_has_members(const CBMRegisteredType *t) {
return (t->field_names && t->field_names[0]) || (t->embedded_types && t->embedded_types[0]) ||
t->alias_of;
}
const char *dot = strrchr(type_qn, '.');
const char *shortn = dot ? dot + 1 : type_qn;
// One short-name scan of c_resolve_field_owner_type. *found says whether the
// scan decided anything, including "ambiguous" (NULL with *found set).
static const CBMRegisteredType *c_field_owner_by_short_name(CLSPContext *ctx, const char *shortn,
bool members_only, bool *found) {
size_t slen = strlen(shortn);
const CBMRegisteredType *included = NULL;
const CBMRegisteredType *only = NULL;
@@ -2987,12 +2989,16 @@ static const CBMRegisteredType *c_resolve_field_owner_type(CLSPContext *ctx, con
CBMTypeShortIter it;
cbm_registry_types_by_short_name_chain(ctx->registry, shortn, &it);
int i;
*found = false;
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 (members_only && !c_type_has_members(cand))
continue;
*found = true;
if (c_qn_in_module(q, ctx->module_qn))
return cand;
if (only && strcmp(only->qualified_name, q) != 0)
@@ -3012,6 +3018,29 @@ static const CBMRegisteredType *c_resolve_field_owner_type(CLSPContext *ctx, con
return only_ambiguous ? NULL : only;
}
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 (!ctx->registry || (rt && c_type_has_members(rt)))
return rt;
const char *dot = strrchr(type_qn, '.');
const char *shortn = dot ? dot + 1 : type_qn;
bool found = false;
const CBMRegisteredType *owner = c_field_owner_by_short_name(ctx, shortn, true, &found);
if (found)
return owner;
if (rt)
return rt;
return c_field_owner_by_short_name(ctx, shortn, false, &found);
}
// 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.
@@ -3537,6 +3566,12 @@ void c_process_statement(CLSPContext *ctx, TSNode node) {
target = c_parse_type_node(ctx, child);
found_type = true;
} else if (found_type && strcmp(ck, "type_identifier") == 0) {
/* `typedef struct { ... } Name;`: the aggregate has no name of
* its own, so `Name` is not an alias of anything -- it IS the
* type, registered with its members under that name. Aliasing
* it to the nameless struct made every `Name *p` untyped. */
if (target && target->kind == CBM_TYPE_STRUCT)
continue;
char *alias_name = c_node_text(ctx, child);
if (alias_name) {
const char *alias_qn =
@@ -4008,6 +4043,8 @@ static uint64_t c_field_ref_hash(const char *caller_qn, const char *type_qn,
* 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_publish_field_owner(CLSPContext *ctx, const char *type_qn, const char *field_name);
static void c_emit_field_reference(CLSPContext *ctx, TSNode node) {
if (!ctx->resolved_calls || !ctx->enclosing_func_qn)
return;
@@ -4029,8 +4066,14 @@ static void c_emit_field_reference(CLSPContext *ctx, TSNode node) {
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)
c_publish_field_owner(ctx, type_to_qn(c_simplify_type(ctx, obj_type, is_arrow)), field_name);
}
/* Publish the owner of member `field_name` of the type `type_qn` for the
* enclosing function (the module, at file scope). */
static void c_publish_field_owner(CLSPContext *ctx, const char *type_qn, const char *field_name) {
if (!ctx->resolved_calls || !ctx->enclosing_func_qn || !type_qn || !field_name ||
!field_name[0])
return;
/* Decided once per (function, type, field), hit or miss: the owner lookup
@@ -4064,6 +4107,133 @@ static void c_emit_field_reference(CLSPContext *ctx, TSNode node) {
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, resolved);
}
/* ── Designated initializers ─────────────────────────────────────────
* `.open = my_open` names a member with no object expression: the type being
* initialized says whose member it is. That type is known where the list
* starts (a declarator, a compound literal, an enclosing list) and is handed
* down the walk; nothing here looks upward, so a table of ten thousand entries
* costs ten thousand steps. */
/* Hand `type` to the walk of `list`, when `list` is an initializer list. */
static void c_hint_initializer_list(CLSPContext *ctx, TSNode list, const CBMType *type) {
if (ts_node_is_null(list) || !type || cbm_type_is_unknown(type) ||
strcmp(ts_node_type(list), "initializer_list") != 0)
return;
ctx->init_list_type = type;
ctx->init_list_id = list.id;
}
/* `Type name[N] = {...}` / `Type *name = ...`: the declared type of one
* init_declarator, its declarator's pointer and array levels applied to the
* declaration's base type. */
static void c_hint_declared_initializer(CLSPContext *ctx, TSNode init_declarator) {
TSNode value = ts_node_child_by_field_name(init_declarator, "value", 5);
if (ts_node_is_null(value) || strcmp(ts_node_type(value), "initializer_list") != 0)
return;
TSNode declaration = ts_node_parent(init_declarator);
if (ts_node_is_null(declaration) || strcmp(ts_node_type(declaration), "declaration") != 0)
return;
const CBMType *type = c_parse_declaration_type(ctx, declaration);
TSNode decl = ts_node_child_by_field_name(init_declarator, "declarator", 10);
for (int depth = 0; depth < 8 && !ts_node_is_null(decl); depth++) {
const char *dk = ts_node_type(decl);
if (strcmp(dk, "identifier") == 0) {
c_hint_initializer_list(ctx, value, type);
return;
}
if (strcmp(dk, "pointer_declarator") == 0)
type = cbm_type_pointer(ctx->arena, type);
else if (strcmp(dk, "array_declarator") == 0)
type = cbm_type_slice(ctx->arena, type);
else
return; /* a parenthesized or function declarator: not followed */
decl = ts_node_child_by_field_name(decl, "declarator", 10);
}
}
/* One `.name` step of a designator on a value of type `type`: publish the
* member's owner and return the member's type (NULL when unknown). */
static const CBMType *c_designate_member(CLSPContext *ctx, const CBMType *type, TSNode name_node) {
if (!type || ts_node_is_null(name_node))
return NULL;
const char *type_qn = type_to_qn(c_simplify_type(ctx, type, false));
char *name = c_node_text(ctx, name_node);
if (!type_qn || !name || !name[0])
return NULL;
c_publish_field_owner(ctx, type_qn, name);
return c_lookup_field_type(ctx, type_qn, name, 0);
}
/* The type of one element of an array being initialized, NULL otherwise. */
static const CBMType *c_initialized_element_type(CLSPContext *ctx, const CBMType *type) {
if (!type)
return NULL;
const CBMType *base = c_simplify_type(ctx, type, false);
return base && base->kind == CBM_TYPE_SLICE ? base->data.slice.elem : NULL;
}
static void c_walk_initializer_pair(CLSPContext *ctx, TSNode pair, const CBMType *list_type) {
TSNode value = ts_node_child_by_field_name(pair, "value", 5);
const CBMType *type = list_type;
uint32_t nc = ts_node_named_child_count(pair);
for (uint32_t i = 0; i < nc; i++) {
TSNode part = ts_node_named_child(pair, i);
if (!ts_node_is_null(value) && part.id == value.id)
continue;
const char *pk = ts_node_type(part);
if (strcmp(pk, "field_designator") == 0) {
type = c_designate_member(ctx, type, ts_node_named_child(part, 0));
} else if (strcmp(pk, "field_identifier") == 0) {
type = c_designate_member(ctx, type, part); /* GNU `name: value` */
} else if (strcmp(pk, "subscript_designator") == 0 ||
strcmp(pk, "subscript_range_designator") == 0) {
type = c_initialized_element_type(ctx, type);
c_resolve_calls_in_node(ctx, part); /* the index is an expression */
}
}
if (!ts_node_is_null(value)) {
c_hint_initializer_list(ctx, value, type);
c_resolve_calls_in_node(ctx, value);
}
}
static void c_walk_initializer_list(CLSPContext *ctx, TSNode list) {
const CBMType *list_type = ctx->init_list_id == list.id ? ctx->init_list_type : NULL;
ctx->init_list_type = NULL;
ctx->init_list_id = NULL;
const CBMType *elem_type = c_initialized_element_type(ctx, list_type);
/* A positional nested list initializes the member at its position, as
* long as no designator has moved the position. */
const CBMRegisteredType *record = NULL;
if (list_type && !elem_type) {
const char *record_qn = type_to_qn(c_simplify_type(ctx, list_type, false));
record = record_qn ? c_resolve_field_owner_type(ctx, record_qn) : NULL;
for (int hop = 0; record && record->alias_of && hop < 5; hop++)
record = c_resolve_field_owner_type(ctx, record->alias_of);
}
int position = 0;
uint32_t nc = ts_node_named_child_count(list);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_named_child(list, i);
const char *ck = ts_node_type(child);
if (strcmp(ck, "comment") == 0)
continue;
if (strcmp(ck, "initializer_pair") == 0) {
record = NULL;
c_walk_initializer_pair(ctx, child, list_type);
continue;
}
if (strcmp(ck, "initializer_list") == 0) {
c_hint_initializer_list(ctx, child,
elem_type ? elem_type
: c_struct_field_type_at(record, position));
}
position++;
c_resolve_calls_in_node(ctx, child);
}
}
/* 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
@@ -4138,6 +4308,20 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) {
// Process statements for scope building
c_process_statement(ctx, node);
// --- Initializers: hand the initialized type down to the list ---
if (strcmp(kind, "initializer_list") == 0) {
c_walk_initializer_list(ctx, node);
return;
}
if (strcmp(kind, "init_declarator") == 0) {
c_hint_declared_initializer(ctx, node);
} else if (strcmp(kind, "compound_literal_expression") == 0) {
TSNode literal_type = ts_node_child_by_field_name(node, "type", 4);
if (!ts_node_is_null(literal_type))
c_hint_initializer_list(ctx, ts_node_child_by_field_name(node, "value", 5),
c_parse_type_node(ctx, literal_type));
}
// --- Member access: publish which type owns the field ---
if (strcmp(kind, "field_expression") == 0) {
c_emit_field_reference(ctx, node);
@@ -5168,6 +5352,15 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) {
// Process namespace
// ============================================================================
// Node kinds that only group top-level items: the branches of `#if` / `#ifdef`
// (an include guard is one) and the body of `extern "C" { ... }`.
static bool c_is_body_container(const char *kind) {
return strcmp(kind, "preproc_if") == 0 || strcmp(kind, "preproc_ifdef") == 0 ||
strcmp(kind, "preproc_else") == 0 || strcmp(kind, "preproc_elif") == 0 ||
strcmp(kind, "preproc_elifdef") == 0 || strcmp(kind, "linkage_specification") == 0 ||
strcmp(kind, "declaration_list") == 0;
}
// Process a top-level or nested declaration within a namespace/class body,
// handling template_declaration wrapping.
static void c_process_body_child(CLSPContext *ctx, TSNode child) {
@@ -5283,6 +5476,15 @@ static void c_process_body_child(CLSPContext *ctx, TSNode child) {
}
if (!has_class) {
c_resolve_calls_in_node(ctx, child);
} else {
/* The specifier was handled as a class above; what the declaration
* DECLARES is still code. `static struct Ops table = { .open = f };`
* names a struct and never opened its initializer. */
for (uint32_t di = 0; di < dnc; di++) {
TSNode dch = ts_node_named_child(child, di);
if (strcmp(ts_node_type(dch), "init_declarator") == 0)
c_resolve_calls_in_node(ctx, dch);
}
}
} else if (strcmp(ck, "concept_definition") == 0) {
// C++20 concept: concept Sortable = requires(T a) { a.sort(); };
@@ -5309,6 +5511,22 @@ static void c_process_body_child(CLSPContext *ctx, TSNode child) {
ctx->template_param_names = saved_tpn;
ctx->template_param_defaults = saved_tpd;
ctx->template_param_count = saved_tpc;
} else if (c_is_body_container(ck)) {
/* A preprocessor branch or an `extern "C"` block holds top-level
* items, not statements: its functions are functions. Walked as an
* expression they kept the FILE as their enclosing scope, so every
* typed row of a function behind an include guard or an `#ifdef` was
* attributed to the module and matched no call or member site.
* Cursor walk (O(n)): an include guard holds the whole header. */
TSTreeCursor cursor = ts_tree_cursor_new(child);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode inner = ts_tree_cursor_current_node(&cursor);
if (ts_node_is_named(inner))
c_process_body_child(ctx, inner);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
} else {
c_resolve_calls_in_node(ctx, child);
}
@@ -5623,6 +5841,41 @@ static void c_process_class(CLSPContext *ctx, TSNode class_node) {
// Process file: top-level walk
// ============================================================================
/* Pass 1 for one file-scope item: bind using declarations, typedefs and global
* variables before any function body is walked. Preprocessor branches and
* `extern "C"` blocks are descended: everything in a header sits inside its
* include guard, and a global behind an `#ifdef` is a global. */
static void c_bind_file_scope_item(CLSPContext *ctx, TSNode item, int depth) {
const char *ck = ts_node_type(item);
if (strcmp(ck, "using_declaration") == 0 || strcmp(ck, "alias_declaration") == 0 ||
strcmp(ck, "type_definition") == 0 || strcmp(ck, "namespace_alias_definition") == 0 ||
strcmp(ck, "declaration") == 0) {
c_process_statement(ctx, item);
} else if (strcmp(ck, "template_declaration") == 0) {
// template<class T> using Vec = std::vector<T>;
// Unwrap template_declaration to process inner alias/typedef
uint32_t tnc = ts_node_named_child_count(item);
for (uint32_t ti = 0; ti < tnc; ti++) {
TSNode inner = ts_node_named_child(item, ti);
const char *ik = ts_node_type(inner);
if (strcmp(ik, "alias_declaration") == 0 || strcmp(ik, "type_definition") == 0) {
c_process_statement(ctx, inner);
}
}
} else if (c_is_body_container(ck) && depth < C_LSP_MAX_WALK_DEPTH) {
// Cursor walk (O(n)): an include guard holds the whole header.
TSTreeCursor cursor = ts_tree_cursor_new(item);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode inner = ts_tree_cursor_current_node(&cursor);
if (ts_node_is_named(inner))
c_bind_file_scope_item(ctx, inner, depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
}
void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
if (ts_node_is_null(root))
return;
@@ -5632,30 +5885,11 @@ void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
uint32_t kn = 0;
TSNode *kids = cbm_lsp_collect_children(ctx->arena, root, &kn);
TSNode child; // Hoisted: prevents ASan stack-use-after-scope between passes
TSNode inner;
// Pass 1: process using declarations and global variables
for (uint32_t i = 0; i < kn; i++) {
child = kids[i];
const char *ck = ts_node_type(child);
if (strcmp(ck, "using_declaration") == 0 || strcmp(ck, "alias_declaration") == 0 ||
strcmp(ck, "type_definition") == 0 || strcmp(ck, "namespace_alias_definition") == 0) {
c_process_statement(ctx, child);
} else if (strcmp(ck, "declaration") == 0) {
c_process_statement(ctx, child);
} else if (strcmp(ck, "template_declaration") == 0) {
// template<class T> using Vec = std::vector<T>;
// Unwrap template_declaration to process inner alias/typedef
uint32_t tnc = ts_node_named_child_count(child);
for (uint32_t ti = 0; ti < tnc; ti++) {
inner = ts_node_named_child(child, ti);
const char *ik = ts_node_type(inner);
if (strcmp(ik, "alias_declaration") == 0 || strcmp(ik, "type_definition") == 0) {
c_process_statement(ctx, inner);
}
}
}
c_bind_file_scope_item(ctx, child, 0);
}
// Pass 2: process functions, namespaces, classes, templates
@@ -5678,14 +5912,65 @@ static const CBMType *c_parse_return_type_text(CBMArena *a, const char *text,
if (!text || !text[0])
return cbm_type_unknown();
// Skip const/volatile qualifiers
while (strncmp(text, "const ", 6) == 0)
text += 6;
while (strncmp(text, "volatile ", 9) == 0)
text += 9;
// Skip const/volatile qualifiers and the elaborated-type keyword: a member
// declared `struct Ops *ops;` has the type text `struct Ops`, and the
// struct is registered as `Ops`.
static const char *const skipped[] = {"const ", "volatile ", "struct ", "union ", "enum "};
for (bool again = true; again;) {
again = false;
for (size_t k = 0; k < sizeof(skipped) / sizeof(skipped[0]); k++) {
size_t klen = strlen(skipped[k]);
if (strncmp(text, skipped[k], klen) == 0) {
text += klen;
again = true;
}
}
}
// Pointer to function, the way the extractor spells the type of an inline
// pointer-to-function member: `ret (*)(params)`, `ret (*[4])(params)`. A
// call through it yields `ret`.
size_t len = strlen(text);
const char *fp_open = strstr(text, "(*");
if (fp_open && fp_open > text && len > 0 && text[len - 1] == ')' &&
strncmp(text, "decltype(", 9) != 0) {
char *ret_text = cbm_arena_strndup(a, text, (size_t)(fp_open - text));
size_t rlen = ret_text ? strlen(ret_text) : 0;
while (rlen > 0 && ret_text[rlen - 1] == ' ')
ret_text[--rlen] = '\0';
const CBMType *rets[2] = {
rlen > 0 ? c_parse_return_type_text(a, ret_text, module_qn) : cbm_type_unknown(), NULL};
return cbm_type_func(a, NULL, NULL, rets);
}
// A qualifier on the outermost pointer level (`char *const`) says nothing
// about the type's shape.
static const char *const trailing[] = {"const", "volatile", "restrict", "__restrict"};
for (size_t k = 0; k < sizeof(trailing) / sizeof(trailing[0]); k++) {
size_t klen = strlen(trailing[k]);
if (len > klen && strcmp(text + len - klen, trailing[k]) == 0 &&
(text[len - klen - 1] == '*' || text[len - klen - 1] == ' ')) {
char *inner = cbm_arena_strndup(a, text, len - klen);
size_t ilen = inner ? strlen(inner) : 0;
while (ilen > 0 && inner[ilen - 1] == ' ')
inner[--ilen] = '\0';
return c_parse_return_type_text(a, inner, module_qn);
}
}
// Array member (`int [8]`, `char *[4]`): indexing it yields the element.
if (len > 0 && text[len - 1] == ']') {
const char *open = strrchr(text, '[');
if (open && open > text) {
char *inner = cbm_arena_strndup(a, text, (size_t)(open - text));
size_t ilen = inner ? strlen(inner) : 0;
while (ilen > 0 && inner[ilen - 1] == ' ')
inner[--ilen] = '\0';
return cbm_type_slice(a, c_parse_return_type_text(a, inner, module_qn));
}
}
// Pointer
size_t len = strlen(text);
if (len > 0 && text[len - 1] == '*') {
char *inner = cbm_arena_strndup(a, text, len - 1);
// Trim trailing space
+7
View File
@@ -72,6 +72,13 @@ typedef struct {
int field_ref_seen_cap;
int field_ref_seen_count;
// The type an initializer_list is about to initialize, handed from the
// declarator / compound literal / enclosing list that knows it to the walk
// of that list. Bound to the list's node id: a hint nothing consumed must
// never type a later, unrelated list.
const CBMType *init_list_type;
const void *init_list_id;
// Output
CBMResolvedCallArray *resolved_calls;
CBMSourceOrigin source_origin; // source buffer represented by emitted occurrence spans
+35
View File
@@ -25,6 +25,7 @@
#include "foundation/mem_core.h" /* field-owner index storage */
#include "graph_buffer/graph_buffer.h"
#include "foundation/constants.h"
#include "pipeline/pipeline.h" /* cbm_registry_unique_field_qn, cbm_resolution_t */
#include <ctype.h>
#include <stdio.h>
@@ -1047,6 +1048,40 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_c_member_call_field(
return best;
}
/* Confidence of a member call bound by the type of its object, and of one
* bound because the project holds exactly one Field of that name. */
#define CBM_CONF_MEMBER_TYPED 0.95
#define CBM_CONF_MEMBER_UNIQUE 0.75
/* The complete rule for a C/C++ member call: the Field the object's type
* names; for an object the LSP could not type, the one Field of that name in
* the project, where allow_unique_name says the file follows the C member rule
* (cbm_c_member_rule_file) -- in other C++ files an untyped `u->size()` is a
* method of a type the project does not hold far more often than a call
* through some struct's member; otherwise NULL. On a hit *res is rewritten to
* that Field. */
static inline const cbm_gbuf_node_t *cbm_pipeline_c_member_call_resolve(
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_field_index_t *index,
const cbm_gbuf_t *gbuf, const cbm_registry_t *registry, const char *project_name,
const char *enclosing_func_qn, const char *callee_text, bool allow_unique_name,
cbm_resolution_t *res) {
bool typed = false;
const cbm_gbuf_node_t *field = cbm_pipeline_c_member_call_field(
arr, index, gbuf, project_name, enclosing_func_qn, callee_text, &typed);
if (!field && !typed && allow_unique_name) {
const char *only =
cbm_registry_unique_field_qn(registry, cbm_lsp_bare_segment(callee_text));
field = only ? cbm_gbuf_find_by_qn(gbuf, only) : NULL;
}
if (field) {
res->qualified_name = field->qualified_name;
res->strategy = typed ? "lsp_field_access" : "unique_name";
res->confidence = typed ? CBM_CONF_MEMBER_TYPED : CBM_CONF_MEMBER_UNIQUE;
res->candidate_count = SKIP_ONE;
}
return field;
}
/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
*
* Per-file LSPs sometimes emit `callee_qn` as the raw package-shaped
+19 -14
View File
@@ -743,6 +743,18 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, imp_keys, imp_vals,
imp_count);
}
/* `p->open(fd)`: the registry does not split a callee on the arrow, and a
* member is not a name it could resolve anyway. The object's type decides,
* exactly as for the dot call handled further down. MUST match
* pass_parallel.c. */
bool arrow_bound = false;
if ((!res.qualified_name || res.qualified_name[0] == '\0') &&
cbm_c_arrow_member_call(lang, call->callee_name)) {
arrow_bound = cbm_pipeline_c_member_call_resolve(
lsp_calls, NULL, ctx->gbuf, ctx->registry, ctx->project_name,
call->enclosing_func_qn, call->callee_name,
cbm_c_member_rule_file(lang, rel), &res) != NULL;
}
if (!res.qualified_name || res.qualified_name[0] == '\0') {
/* Resolution is empty when the callee belongs to an EXTERNAL client
* library whose source is not in the indexed tree (e.g. `requests.get`,
@@ -880,25 +892,18 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
target_node->file_path)) {
case CBM_FIELD_CALL_DROP:
return 0;
case CBM_FIELD_CALL_BY_OWNER: {
bool typed = false;
target_node =
cbm_pipeline_c_member_call_field(lsp_calls, NULL, ctx->gbuf, ctx->project_name,
call->enclosing_func_qn, call->callee_name, &typed);
if (!target_node && !typed) {
/* Untyped object: only a member name the project holds exactly
* once may still bind. */
const char *only = cbm_registry_unique_field_qn(
ctx->registry, cbm_lsp_bare_segment(call->callee_name));
target_node = only ? cbm_gbuf_find_by_qn(ctx->gbuf, only) : NULL;
case CBM_FIELD_CALL_BY_OWNER:
/* An arrow call was already bound this way above. */
if (!arrow_bound) {
target_node = cbm_pipeline_c_member_call_resolve(
lsp_calls, NULL, ctx->gbuf, ctx->registry, ctx->project_name,
call->enclosing_func_qn, call->callee_name, cbm_c_member_rule_file(lang, rel),
&res);
}
if (!target_node || source_node->id == target_node->id) {
return 0;
}
res.qualified_name = target_node->qualified_name;
res.strategy = typed ? "lsp_field_access" : "unique_name";
break;
}
case CBM_FIELD_CALL_KEEP:
break;
}
+28 -18
View File
@@ -2998,6 +2998,23 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, imp_keys,
imp_vals, imp_count);
}
/* `p->open(fd)`: the registry does not split a callee on the arrow.
* The object's type decides, as for the dot call handled further
* down. Mirrors pass_calls.c. */
bool arrow_bound = false;
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution &&
cbm_c_arrow_member_call(lang, call->callee_name)) {
if (!field_index_built) {
field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
field_index_built = true;
}
arrow_bound =
cbm_pipeline_c_member_call_resolve(
&result->resolved_calls, field_index_ready ? &field_index : NULL, rc->main_gbuf,
rc->registry, rc->project_name, call->enclosing_func_qn, call->callee_name,
cbm_c_member_rule_file(lang, rel), &res) != NULL;
}
atomic_fetch_add_explicit(&rc->time_ns_rc_resolve, extract_now_ns() - _rc_t0,
memory_order_relaxed);
@@ -3150,28 +3167,19 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
if (field_policy == CBM_FIELD_CALL_DROP) {
continue;
}
if (field_policy == CBM_FIELD_CALL_BY_OWNER) {
if (field_policy == CBM_FIELD_CALL_BY_OWNER && !arrow_bound) {
if (!field_index_built) {
field_index_ready =
cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index);
field_index_built = true;
}
bool typed = false;
target_node = cbm_pipeline_c_member_call_field(
target_node = cbm_pipeline_c_member_call_resolve(
&result->resolved_calls, field_index_ready ? &field_index : NULL, rc->main_gbuf,
rc->project_name, call->enclosing_func_qn, call->callee_name, &typed);
if (!target_node && !typed) {
/* Untyped object: only a member name the project holds
* exactly once may still bind. */
const char *only = cbm_registry_unique_field_qn(
rc->registry, cbm_lsp_bare_segment(call->callee_name));
target_node = only ? cbm_gbuf_find_by_qn(rc->main_gbuf, only) : NULL;
}
rc->registry, rc->project_name, call->enclosing_func_qn, call->callee_name,
cbm_c_member_rule_file(lang, rel), &res);
if (!target_node || source_node->id == target_node->id) {
continue;
}
res.qualified_name = target_node->qualified_name;
res.strategy = typed ? "lsp_field_access" : "unique_name";
}
}
if (target_node && source_node->id != target_node->id &&
@@ -3245,7 +3253,8 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
* 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_c_member_binds_by_owner(cbm_c_member_rule_file(lang, rel),
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);
@@ -3411,7 +3420,7 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
}
/* 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)) {
if (cbm_c_member_binds_by_owner(cbm_c_member_rule_file(lang, rel), 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);
@@ -3684,8 +3693,9 @@ static bool pp_has_pending_lsp_site(const CBMFileResult *result) {
* 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) {
static bool pp_has_unowned_c_member(const CBMFileResult *result, CBMLanguage lang,
const char *rel) {
if (!cbm_c_member_rule_file(lang, rel)) {
return false;
}
cbm_pipeline_lsp_field_index_t field_index = {0};
@@ -3813,7 +3823,7 @@ 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 unowned_c_member = pp_has_unowned_c_member(result, lang, rel);
bool cross_lsp_eligible =
(rc->all_defs && rc->def_count > 0 && cbm_pxc_has_cross_lsp(lang) &&
(((semantic_sites > 0 || pending_lsp_site) &&
+3 -2
View File
@@ -168,7 +168,8 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
* 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_c_member_binds_by_owner(cbm_c_member_rule_file(lang, rel),
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);
@@ -341,7 +342,7 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
}
/* 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)) {
if (cbm_c_member_binds_by_owner(cbm_c_member_rule_file(lang, rel), 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);
+14
View File
@@ -369,12 +369,26 @@ cbm_field_call_policy_t cbm_call_onto_field_policy(CBMLanguage caller_lang, cons
const char *target_label,
const char *target_file_path);
/* True for a C/C++ call spelled through an arrow (`p->open(fd)`). The
* short-name registry splits a callee on `.` and `::` only, so such a call
* comes back from it unresolved; the caller then asks the object's type for
* the member, as for a dot call. Pure; unit-tested in test_registry.c. */
bool cbm_c_arrow_member_call(CBMLanguage caller_lang, const char *callee_text);
/* True when a reference is the member half of a C `a.b` / `a->b`. Such a
* reference binds through the C LSP's field-owner rows (the type of `a`); with
* no typed owner it binds only cbm_registry_unique_field_qn(). C only. Pure;
* unit-tested in test_registry.c. */
bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access);
/* True when a file's member accesses follow the C member rule above: every C
* file, and every `.h`. A `.h` is parsed as C++ because it may be either, and
* `a.b` / `a->b` means the same in both: the type of `a` names `b`. Without it
* the inline functions of a C header kept binding members by bare name. Other
* C++ files are left to their own resolution. Pure; unit-tested in
* test_registry.c. */
bool cbm_c_member_rule_file(CBMLanguage lang, const char *rel_path);
/* The QN of the Field `member_name` names when exactly one Field in the
* registry carries that name (symbols of other labels are not candidates for
* a member); NULL when none or several do. */
+18
View File
@@ -843,6 +843,24 @@ cbm_field_call_policy_t cbm_call_onto_field_policy(CBMLanguage caller_lang, cons
return caller_lang == CBM_LANG_C ? CBM_FIELD_CALL_DROP : CBM_FIELD_CALL_KEEP;
}
bool cbm_c_arrow_member_call(CBMLanguage caller_lang, const char *callee_text) {
if (caller_lang != CBM_LANG_C && caller_lang != CBM_LANG_CPP) {
return false;
}
return callee_text != NULL && strstr(callee_text, "->") != NULL;
}
bool cbm_c_member_rule_file(CBMLanguage lang, const char *rel_path) {
if (lang == CBM_LANG_C) {
return true;
}
if (lang != CBM_LANG_CPP || !rel_path) {
return false;
}
size_t len = strlen(rel_path);
return len > PAIR_LEN && strcmp(rel_path + len - PAIR_LEN, ".h") == 0;
}
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
+195
View File
@@ -1115,6 +1115,57 @@ TEST(extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef) {
PASS();
}
/* A pointer-to-function member (`void (*open)(int);`, the shape of every
* kernel ops table) is a field of its struct. It shares the
* field_declaration + function_declarator shape with a C++ member FUNCTION
* declaration, which the field extractor rightly skips; the two differ in
* where the name sits: `(*open)` is a parenthesized pointer declarator. */
TEST(extract_c_function_pointer_member_is_a_field) {
CBMFileResult *r = extract("struct Ops {\n"
" int flags;\n"
" void (*open)(int fd);\n"
" int (*cmp)(const void *, const void *);\n"
" char *(*name_of)(int id);\n"
" void (*handlers[4])(void);\n"
"};\n",
CBM_LANG_C, "t", "ops.h");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_TRUE(has_def_qn(r, "t.ops.Ops.flags"));
/* RED before the fix: none of the four pointer-to-function members. */
ASSERT_TRUE(has_def_qn(r, "t.ops.Ops.open"));
ASSERT_TRUE(has_def_qn(r, "t.ops.Ops.cmp"));
ASSERT_TRUE(has_def_qn(r, "t.ops.Ops.name_of"));
ASSERT_TRUE(has_def_qn(r, "t.ops.Ops.handlers"));
ASSERT_EQ(count_defs_with_label(r, "Field"), 5);
/* Each carries a type text, which the cross-file registry needs to list
* it among its struct's fields. */
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (d->label && strcmp(d->label, "Field") == 0) {
ASSERT_NOT_NULL(d->return_type);
ASSERT_TRUE(d->return_type[0] != '\0');
}
}
cbm_free_result(r);
/* A C++ member function DECLARATION stays out of the fields; a
* pointer-to-function member beside it is one. */
r = extract("class Widget {\n"
"public:\n"
" void resize(int w);\n"
" int (*hook)(int);\n"
" int width;\n"
"};\n",
CBM_LANG_CPP, "t", "widget.hpp");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(has_def(r, "Field", "hook"));
ASSERT_TRUE(has_def(r, "Field", "width"));
ASSERT_FALSE(has_def(r, "Field", "resize"));
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) {
@@ -1126,6 +1177,103 @@ static const char *def_return_type(CBMFileResult *r, const char *name) {
return NULL;
}
/* A member's name is the identifier at the bottom of its declarator, and its
* type is everything around that identifier, in the canonical spelling of the
* function return types below. Taking the first declarator's first child as
* the name published members called `*argv` and `seen[8]`, which no member
* access can ever match; taking only the `type` node typed `struct Node *next`
* as a struct, so `list->next[i]` had no element type; and of
* `struct Node *next, *prev;` only `next` existed. */
TEST(extract_c_member_declarators_name_and_type) {
CBMFileResult *r = extract("struct Shapes {\n"
" char **argv;\n"
" int seen[8];\n"
" struct Node *next, *prev;\n"
" int a, b;\n"
" const char *label;\n"
" unsigned flags : 3;\n"
" char *const fixed;\n"
" char *names[4];\n"
" int plain;\n"
"};\n",
CBM_LANG_C, "t", "shapes.h");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* RED before the fix: `*argv`, `seen[8]`, `*next`, `*label`, `*names[4]`
* as names, and no `prev` / `b` at all. */
ASSERT_EQ(count_defs_with_label(r, "Field"), 11);
ASSERT_STR_EQ(def_return_type(r, "argv"), "char **");
ASSERT_STR_EQ(def_return_type(r, "seen"), "int [8]");
ASSERT_STR_EQ(def_return_type(r, "next"), "struct Node *");
ASSERT_STR_EQ(def_return_type(r, "prev"), "struct Node *");
ASSERT_STR_EQ(def_return_type(r, "a"), "int");
ASSERT_STR_EQ(def_return_type(r, "b"), "int");
ASSERT_STR_EQ(def_return_type(r, "label"), "const char *");
ASSERT_STR_EQ(def_return_type(r, "flags"), "unsigned");
ASSERT_STR_EQ(def_return_type(r, "fixed"), "char *const");
ASSERT_STR_EQ(def_return_type(r, "names"), "char *[4]");
ASSERT_STR_EQ(def_return_type(r, "plain"), "int");
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (d->label && strcmp(d->label, "Field") == 0) {
ASSERT_NULL(strpbrk(d->name, "*[]() "));
}
}
cbm_free_result(r);
/* Pointer-to-function members spell their type the way C does. */
r = extract("struct Ops {\n"
" void (*open)(int fd);\n"
" char *(*name_of)(int id);\n"
" void (*handlers[4])(void);\n"
" int (*cmp)(const void *,\n"
" const void *);\n"
"};\n",
CBM_LANG_C, "t", "ops.h");
ASSERT_NOT_NULL(r);
ASSERT_STR_EQ(def_return_type(r, "open"), "void (*)(int fd)");
ASSERT_STR_EQ(def_return_type(r, "name_of"), "char *(*)(int id)");
ASSERT_STR_EQ(def_return_type(r, "handlers"), "void (*[4])(void)");
ASSERT_STR_EQ(def_return_type(r, "cmp"), "int (*)(const void *, const void *)");
cbm_free_result(r);
/* C++: a reference member, and member functions stay out. */
r = extract("class Widget {\n"
"public:\n"
" Widget *parent, *child;\n"
" const Text &title;\n"
" int *ids() const;\n"
" void (resize)(int w);\n"
"};\n",
CBM_LANG_CPP, "t", "widget.hpp");
ASSERT_NOT_NULL(r);
ASSERT_EQ(count_defs_with_label(r, "Field"), 3);
ASSERT_STR_EQ(def_return_type(r, "parent"), "Widget *");
ASSERT_STR_EQ(def_return_type(r, "child"), "Widget *");
ASSERT_STR_EQ(def_return_type(r, "title"), "const Text &");
cbm_free_result(r);
/* A member whose type is a macro invocation (jemalloc's
* `ql_head(tcache_slow_t) tcache_ql;`): the member is `tcache_ql`, never
* the macro's argument. Both grammars: a header is parsed as C++. */
static const CBMLanguage macro_langs[] = {CBM_LANG_C, CBM_LANG_CPP};
for (size_t li = 0; li < sizeof(macro_langs) / sizeof(macro_langs[0]); li++) {
r = extract("struct arena_s {\n"
" ql_head(tcache_slow_t) tcache_ql;\n"
" malloc_mutex_t mtx;\n"
"};\n",
macro_langs[li], "t", "arena.h");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(has_def(r, "Field", "tcache_slow_t"));
ASSERT_FALSE(has_def(r, "Field", "(tcache_slow_t)"));
ASSERT_TRUE(has_def(r, "Field", "tcache_ql"));
ASSERT_STR_EQ(def_return_type(r, "tcache_ql"), "ql_head(tcache_slow_t)");
ASSERT_TRUE(has_def(r, "Field", "mtx"));
cbm_free_result(r);
}
PASS();
}
/* PR #1245: the C grammar splits a declared return type across the `type` node,
* sibling type_qualifier nodes and the pointer_declarator chain wrapping the
* function declarator. Taking only the `type` node published `const char *` as
@@ -3645,6 +3793,50 @@ TEST(c_imports) {
PASS();
}
/* Every header keeps its includes inside its include guard, and plenty of
* sources include behind `#ifdef`. Reading only the top level of the file
* left a guarded header with no imports at all. */
TEST(c_imports_inside_preprocessor_blocks) {
static const CBMLanguage langs[] = {CBM_LANG_C, CBM_LANG_CPP};
for (size_t li = 0; li < sizeof(langs) / sizeof(langs[0]); li++) {
CBMFileResult *r = extract("#ifndef GUARD_H\n"
"#define GUARD_H\n"
"#include \"first.h\"\n"
"#include <second.h>\n"
"#if defined(USE_X)\n"
"#include \"x_only.h\"\n"
"#elif defined(USE_Z)\n"
"#include \"z_only.h\"\n"
"#else\n"
"#include \"y_only.h\"\n"
"#endif\n"
"int guarded(void);\n"
"#endif\n",
langs[li], "t", "guard.h");
ASSERT_NOT_NULL(r);
ASSERT(has_import(r, "first.h"));
ASSERT(has_import(r, "second.h"));
ASSERT(has_import(r, "x_only.h"));
ASSERT(has_import(r, "z_only.h"));
ASSERT(has_import(r, "y_only.h"));
ASSERT_EQ(r->imports.count, 5);
cbm_free_result(r);
}
/* Top-level includes come out once, as before. */
CBMFileResult *r = extract("#include \"top.h\"\n"
"#ifdef WITH_EXTRA\n"
"#include \"extra.h\"\n"
"#endif\n"
"int f(void) { return 0; }\n",
CBM_LANG_C, "t", "top.c");
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 2);
ASSERT(has_import(r, "top.h"));
ASSERT(has_import(r, "extra.h"));
cbm_free_result(r);
PASS();
}
TEST(ruby_imports) {
CBMFileResult *r = extract(
"require 'json'\nrequire 'net/http'\nrequire_relative 'helpers'\n\nclass Foo; end\n",
@@ -8981,6 +9173,8 @@ SUITE(extraction) {
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(extract_c_function_pointer_member_is_a_field);
RUN_TEST(extract_c_member_declarators_name_and_type);
RUN_TEST(cpp_class);
RUN_TEST(cpp_method_return_type_preserves_pointer_and_qualifier);
@@ -9158,6 +9352,7 @@ SUITE(extraction) {
RUN_TEST(java_imports);
RUN_TEST(rust_imports);
RUN_TEST(c_imports);
RUN_TEST(c_imports_inside_preprocessor_blocks);
RUN_TEST(ruby_imports);
RUN_TEST(lua_imports);
RUN_TEST(import_stress_go);
+263
View File
@@ -6894,6 +6894,195 @@ static void write_c_member_field_fixture(const char *tmp, int pad_files) {
"int use_opaque(struct Opaque *o) {\n"
" return o->count + o->beta_only;\n"
"}\n");
/* Inline functions in a header. A `.h` is parsed as C++, and its member
* accesses mean what they mean in the .c files that include it. */
write_temp_file(tmp, "inline.h",
"#ifndef INLINE_H\n"
"#define INLINE_H\n"
"#include \"alpha.h\"\n"
"#include \"beta.h\"\n"
"static inline int hdr_alpha(struct Alpha *a) {\n"
" return a->count;\n"
"}\n"
"static inline int hdr_beta(Beta *b) {\n"
" b->count = 1;\n"
" return b->count;\n"
"}\n"
"/* A struct the header itself declares, through a typedef. */\n"
"typedef struct {\n"
" int count;\n"
" char *buf;\n"
"} HdrBuf;\n"
"struct HdrNode {\n"
" int count;\n"
"};\n"
"static inline int hdr_own_typedef(HdrBuf *s) {\n"
" return s->count;\n"
"}\n"
"static inline int hdr_own_struct(struct HdrNode *n) {\n"
" return n->count;\n"
"}\n"
"#endif\n");
/* Functions inside preprocessor branches of a .c file are functions like
* any other. */
write_temp_file(tmp, "guarded.c",
"#include \"alpha.h\"\n"
"#include \"beta.h\"\n"
"\n"
"#ifdef SOME_CONFIG\n"
"int guarded_alpha(struct Alpha *a) {\n"
" return a->count;\n"
"}\n"
"#else\n"
"int guarded_beta(Beta *b) {\n"
" return b->count;\n"
"}\n"
"#endif\n");
/* A member call on an object nothing types. In C the one member of that
* name in the project may still bind; in a C++ source such a call is a
* method of some type the project does not hold, and binds nothing. */
write_temp_file(tmp, "cb.h",
"#ifndef CB_H\n"
"#define CB_H\n"
"struct CbTable {\n"
" int (*only_cb)(int);\n"
"};\n"
"#endif\n");
write_temp_file(tmp, "untyped_c.c",
"struct Hidden;\n"
"\n"
"int c_untyped_call(struct Hidden *h) {\n"
" return h->only_cb(1);\n"
"}\n");
write_temp_file(tmp, "untyped_cpp.cpp",
"template <class T> int cpp_untyped_call(T *h) {\n"
" return h->only_cb(1);\n"
"}\n");
/* A global variable declared behind an `#ifndef` types the accesses made
* through it, whether its type is spelled with `struct` or a typedef. */
write_temp_file(tmp, "globals.c",
"#include \"alpha.h\"\n"
"#include \"beta.h\"\n"
"\n"
"#ifndef NO_GLOBALS\n"
"static struct Alpha g_alpha;\n"
"static Beta g_beta;\n"
"#endif\n"
"\n"
"int read_alpha_global(void) {\n"
" return g_alpha.count;\n"
"}\n"
"\n"
"int read_beta_global(void) {\n"
" return g_beta.count;\n"
"}\n");
/* Ops tables: pointer-to-function members written inline, called through
* `->`. Two tables share both member names, so only the object's type
* says which is called; `arrow_chain` reaches its table through a member
* whose type is spelled `struct Ops *`. */
write_temp_file(tmp, "ops.h",
"#ifndef OPS_H\n"
"#define OPS_H\n"
"struct Dev;\n"
"struct Ops {\n"
" void (*open)(int fd);\n"
" int (*shut)(int fd);\n"
" struct Dev *(*owner)(int fd);\n"
"};\n"
"struct Dev {\n"
" struct Ops *ops;\n"
" int id;\n"
"};\n"
"struct Bank {\n"
" struct Dev *devs, *spare;\n"
" struct Dev fixed[2];\n"
" int id;\n"
"};\n"
"#endif\n");
write_temp_file(tmp, "other_ops.h",
"#ifndef OTHER_OPS_H\n"
"#define OTHER_OPS_H\n"
"struct OtherOps {\n"
" void (*open)(int fd);\n"
" int (*shut)(int fd);\n"
" int id;\n"
"};\n"
"#endif\n");
write_temp_file(tmp, "use_ops.c",
"#include \"ops.h\"\n"
"#include \"other_ops.h\"\n"
"\n"
"void arrow_ops(struct Ops *o) {\n"
" o->open(1);\n"
"}\n"
"\n"
"void arrow_other(struct OtherOps *x) {\n"
" x->open(2);\n"
"}\n"
"\n"
"int arrow_chain(struct Dev *d) {\n"
" return d->ops->shut(3);\n"
"}\n"
"\n"
"int arrow_ret(struct Ops *o) {\n"
" return o->owner(4)->id;\n"
"}\n"
"\n"
"int index_ptr(struct Bank *b) {\n"
" return b->devs[1].id;\n"
"}\n"
"\n"
"int index_arr(struct Bank *b) {\n"
" return b->fixed[0].id;\n"
"}\n"
"\n"
"int second_decl(struct Bank *b) {\n"
" return b->spare->id;\n"
"}\n");
/* Designated initializers name members with no object expression at all:
* the type being initialized says whose member `.open` is. At file scope
* (the usual ops table), nested, through an array, in a block and in a
* compound literal. Each table file initializes ONE of the two tables. */
write_temp_file(tmp, "table_ops.c",
"#include \"ops.h\"\n"
"\n"
"static void my_open(int fd) { (void)fd; }\n"
"static int my_shut(int fd) { return fd; }\n"
"\n"
"static struct Ops table_ops = {\n"
" .open = my_open,\n"
" .shut = my_shut,\n"
"};\n"
"\n"
"static struct Bank bank_init = {\n"
" .fixed = { [0] = { .ops = &table_ops } },\n"
" .id = 3,\n"
"};\n");
write_temp_file(tmp, "table_other.c",
"#include \"other_ops.h\"\n"
"\n"
"static void their_open(int fd) { (void)fd; }\n"
"\n"
"static struct OtherOps other_table = {\n"
" .open = their_open,\n"
"};\n");
write_temp_file(tmp, "init_local.c",
"#include \"ops.h\"\n"
"#include \"other_ops.h\"\n"
"\n"
"int init_block(void) {\n"
" struct Dev d = { .id = 1 };\n"
" return d.id;\n"
"}\n"
"\n"
"void init_literal(struct Bank *b) {\n"
" *b->devs = (struct Dev){ .id = 2 };\n"
"}\n"
"\n"
"void init_array(void) {\n"
" struct OtherOps pair[2] = { { .id = 1 }, { .id = 2 } };\n"
" (void)pair;\n"
"}\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
@@ -6935,6 +7124,80 @@ static int assert_c_member_fields_bind_by_type(cbm_store_t *s, const char *proje
/* 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);
/* `p->f(...)` through an inline pointer-to-function member: the call lands
* on the member of the pointed-to struct, never on the same-named member
* of the other table. */
ASSERT_GTE(fixture_node_count(s, project, "ops.h", "open", "Field"), 1);
ASSERT_GTE(fixture_node_count(s, project, "other_ops.h", "open", "Field"), 1);
ASSERT_TRUE(field_edge_exists(s, project, "arrow_ops", "Ops", "open", "CALLS"));
ASSERT_FALSE(field_edge_exists(s, project, "arrow_ops", "OtherOps", "open", "CALLS"));
ASSERT_TRUE(field_edge_exists(s, project, "arrow_other", "OtherOps", "open", "CALLS"));
ASSERT_FALSE(field_edge_exists(s, project, "arrow_other", "Ops", "open", "CALLS"));
/* The object of the call is itself a member access whose member is typed
* `struct Ops *`: both steps of the chain are typed. */
ASSERT_TRUE(field_edge_exists(s, project, "arrow_chain", "Dev", "ops", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "arrow_chain", "Ops", "shut", "CALLS"));
ASSERT_FALSE(field_edge_exists(s, project, "arrow_chain", "OtherOps", "shut", "CALLS"));
/* A call through a pointer-to-function member has that member's return
* type: `o->owner(4)->id` is the `id` of struct Dev. */
ASSERT_TRUE(field_edge_exists(s, project, "arrow_ret", "Ops", "owner", "CALLS"));
ASSERT_TRUE(field_edge_exists(s, project, "arrow_ret", "Dev", "id", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "arrow_ret", "OtherOps", "id", "USAGE"));
/* A pointer member and an array member have an element type, and the
* second declarator of `struct Dev *devs, *spare;` is a member too. `id`
* is a name three structs share, so each of these needs the full chain. */
ASSERT_TRUE(field_edge_exists(s, project, "index_ptr", "Bank", "devs", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "index_ptr", "Dev", "id", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "index_ptr", "Bank", "id", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "index_arr", "Bank", "fixed", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "index_arr", "Dev", "id", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "second_decl", "Bank", "spare", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "second_decl", "Dev", "id", "USAGE"));
/* The same rule inside a header's inline functions. */
ASSERT_TRUE(field_edge_exists(s, project, "hdr_alpha", "Alpha", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "hdr_alpha", "Beta", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "hdr_beta", "Beta", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "hdr_beta", "Alpha", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "hdr_beta", "Beta", "count", "WRITES"));
ASSERT_FALSE(field_edge_exists(s, project, "hdr_beta", "Alpha", "count", "WRITES"));
/* A struct the header declares itself, by typedef or by tag. */
ASSERT_TRUE(field_edge_exists(s, project, "hdr_own_typedef", "HdrBuf", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "hdr_own_typedef", "Alpha", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "hdr_own_struct", "HdrNode", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "hdr_own_struct", "Beta", "count", "USAGE"));
/* ...and inside `#ifdef` / `#else` branches. */
ASSERT_TRUE(field_edge_exists(s, project, "guarded_alpha", "Alpha", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "guarded_alpha", "Beta", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "guarded_beta", "Beta", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "guarded_beta", "Alpha", "count", "USAGE"));
/* An untyped member call binds the project's one member of that name in
* C, and nothing in a C++ source. */
ASSERT_GTE(fixture_node_count(s, project, "cb.h", "only_cb", "Field"), 1);
ASSERT_TRUE(field_edge_exists(s, project, "c_untyped_call", "CbTable", "only_cb", "CALLS"));
ASSERT_FALSE(field_edge_exists(s, project, "cpp_untyped_call", "CbTable", "only_cb", "CALLS"));
/* A global behind `#ifndef` types the accesses through it. */
ASSERT_TRUE(field_edge_exists(s, project, "read_alpha_global", "Alpha", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "read_alpha_global", "Beta", "count", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "read_beta_global", "Beta", "count", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "read_beta_global", "Alpha", "count", "USAGE"));
/* Designated initializers at file scope: the source is the file. */
ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Ops", "open", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Ops", "shut", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "table_ops.c", "OtherOps", "open", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "table_other.c", "OtherOps", "open", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "table_other.c", "Ops", "open", "USAGE"));
/* Nested: `.fixed = { [0] = { .ops = ... } }` initializes a Dev. */
ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Bank", "fixed", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Dev", "ops", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Bank", "id", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "table_ops.c", "Dev", "id", "USAGE"));
/* In a block, in a compound literal, and per element of an array. */
ASSERT_TRUE(field_edge_exists(s, project, "init_block", "Dev", "id", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "init_block", "Bank", "id", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "init_literal", "Dev", "id", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "init_literal", "OtherOps", "id", "USAGE"));
ASSERT_TRUE(field_edge_exists(s, project, "init_array", "OtherOps", "id", "USAGE"));
ASSERT_FALSE(field_edge_exists(s, project, "init_array", "Dev", "id", "USAGE"));
return 0;
}
+36
View File
@@ -1070,6 +1070,22 @@ TEST(call_onto_field_follows_language_and_shape) {
PASS();
}
TEST(c_arrow_member_call_is_c_family_and_arrow_only) {
/* The registry returns nothing for an arrow callee; these are the calls
* the resolvers then hand to the object's type. */
ASSERT_TRUE(cbm_c_arrow_member_call(CBM_LANG_C, "o->open"));
ASSERT_TRUE(cbm_c_arrow_member_call(CBM_LANG_C, "d->ops->shut"));
ASSERT_TRUE(cbm_c_arrow_member_call(CBM_LANG_CPP, "self->hook"));
/* A dot call resolves through the registry and its Field policy. */
ASSERT_FALSE(cbm_c_arrow_member_call(CBM_LANG_C, "cb.close"));
ASSERT_FALSE(cbm_c_arrow_member_call(CBM_LANG_C, "socket"));
ASSERT_FALSE(cbm_c_arrow_member_call(CBM_LANG_C, NULL));
/* Perl and PHP spell method calls with an arrow too: not this rule. */
ASSERT_FALSE(cbm_c_arrow_member_call(CBM_LANG_PERL, "$obj->method"));
ASSERT_FALSE(cbm_c_arrow_member_call(CBM_LANG_PHP, "$this->save"));
PASS();
}
TEST(unique_field_qn_needs_exactly_one_field_of_that_name) {
/* What an untyped C member access may still bind: a name exactly one
* Field carries. Two Fields would be a choice; a function or variable of
@@ -1117,6 +1133,24 @@ TEST(c_member_access_binds_by_owner_only) {
PASS();
}
TEST(c_member_rule_covers_c_files_and_every_dot_h) {
ASSERT_TRUE(cbm_c_member_rule_file(CBM_LANG_C, "src/a.c"));
ASSERT_TRUE(cbm_c_member_rule_file(CBM_LANG_C, NULL));
/* A header is classified C++ whatever it holds; its inline functions
* access members like the .c files that include it. */
ASSERT_TRUE(cbm_c_member_rule_file(CBM_LANG_CPP, "include/linux/list.h"));
/* Other C++ files keep their own resolution. */
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_CPP, "src/widget.cpp"));
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_CPP, "src/widget.hpp"));
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_CPP, "src/match.hh"));
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_CPP, ".h"));
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_CPP, NULL));
/* No other language, whatever the file is called. */
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_OBJC, "ui/View.h"));
ASSERT_FALSE(cbm_c_member_rule_file(CBM_LANG_GO, "pkg/a.go"));
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.
@@ -1377,8 +1411,10 @@ SUITE(registry) {
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_arrow_member_call_is_c_family_and_arrow_only);
RUN_TEST(unique_field_qn_needs_exactly_one_field_of_that_name);
RUN_TEST(c_member_access_binds_by_owner_only);
RUN_TEST(c_member_rule_covers_c_files_and_every_dot_h);
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);