diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index d22558c4e..10a7a44e4 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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; } diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index 7d416923c..de06d8843 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -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") diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index aad2c1190..32e813dc0 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -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 using Vec = std::vector; + // 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 using Vec = std::vector; - // 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 diff --git a/internal/cbm/lsp/c_lsp.h b/internal/cbm/lsp/c_lsp.h index 0cff48f49..ee0290cd3 100644 --- a/internal/cbm/lsp/c_lsp.h +++ b/internal/cbm/lsp/c_lsp.h @@ -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 diff --git a/src/pipeline/lsp_resolve.h b/src/pipeline/lsp_resolve.h index e52448dac..693947106 100644 --- a/src/pipeline/lsp_resolve.h +++ b/src/pipeline/lsp_resolve.h @@ -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 #include @@ -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 diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 0c324f466..1f7b9b4f4 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -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; } diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 14644cd51..0aec4e942 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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) && diff --git a/src/pipeline/pass_usages.c b/src/pipeline/pass_usages.c index 31f3b015d..f6c04ece2 100644 --- a/src/pipeline/pass_usages.c +++ b/src/pipeline/pass_usages.c @@ -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); diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index e27750303..8f18a9ae9 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -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. */ diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 1fc8fceb1..dd366f3bd 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -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 diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 4a6d5941d..916fc7a64 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -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 \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); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 91862bfc8..86ad18b6d 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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 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; } diff --git a/tests/test_registry.c b/tests/test_registry.c index 4f9aba696..1057befac 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -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);