diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index bfeebe721..4922fd5d8 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -944,9 +944,9 @@ static void cbm_attr_collect(TSNode node, const char *source, CBMAttrWords *word return; } bool container = cbm_attr_is_container(ts_node_type(node)); - bool unterminated = false; TSTreeCursor cursor = ts_tree_cursor_new(node); if (ts_tree_cursor_goto_first_child(&cursor)) { + bool unterminated = false; do { TSNode child = ts_tree_cursor_current_node(&cursor); if (!ts_node_is_named(child)) { @@ -1048,6 +1048,64 @@ typedef struct { TSRange directive; } CBMAttrConditional; +/* Past spaces and comments, on the same line only when `same_line` asks for it. */ +static uint32_t cbm_attr_text_skip(const char *source, uint32_t length, uint32_t at, + bool same_line) { + while (at < length) { + char ch = source[at]; + if (ch == ' ' || ch == '\t' || ch == '\r' || (!same_line && ch == '\n')) { + at++; + } else if (ch == '/' && at + 1 < length && source[at + 1] == '*') { + for (at += 2; at + 1 < length && !(source[at] == '*' && source[at + 1] == '/'); at++) + ; + at = at + 2 <= length ? at + 2 : length; + } else if (ch == '/' && at + 1 < length && source[at + 1] == '/') { + while (at < length && source[at] != '\n') + at++; + } else { + break; + } + } + return at; +} + +/* `void f(void)` with no `;` or brace of its own: a function's head, its body after the branch. */ +static bool cbm_attr_text_is_head(const char *source, uint32_t from, uint32_t to) { + char last = 0; + for (uint32_t at = cbm_attr_text_skip(source, to, from, false); at < to; + at = cbm_attr_text_skip(source, to, at + 1, false)) { + char ch = source[at]; + if (ch == ';' || ch == '{' || ch == '}' || ch == '"' || ch == '\'') + return false; + last = ch; + } + return last == ')'; +} + +/* The `#endif` that closes the conditional whose `#else` line ended at `from`, or `length`. */ +static uint32_t cbm_attr_text_endif(const char *source, uint32_t length, uint32_t from) { + int nesting = 0; + for (uint32_t at = from; at < length; at++) { + if (source[at] != '\n') + continue; + uint32_t word = cbm_attr_text_skip(source, length, at + 1, true); + if (word >= length || source[word] != '#') + continue; + word = cbm_attr_text_skip(source, length, word + 1, true); + if (word + 2 < length && !memcmp(source + word, "if", 2)) { + nesting++; + } else if (word + 5 < length && !memcmp(source + word, "endif", 5)) { + if (!nesting) + return at + 1; + nesting--; + } else if (!nesting && word + 4 < length && + (!memcmp(source + word, "else", 4) || !memcmp(source + word, "elif", 4))) { + return length; /* one alternative is all this reads */ + } + } + return length; +} + static void cbm_attr_expression_branches(const char *source, uint32_t length, CBMAttrWords *words) { CBMAttrConditional stack[CBM_ATTR_MAX_CONDITIONALS]; int top = 0, skipping = 0, depth = 0, overflow = 0; @@ -1056,7 +1114,7 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB /* Braces opened by `= {`. An initializer holds no `;`, so one closes the count whatever two * branches of a conditional did to it. */ uint32_t initializer = 0; - bool after_assign = false; + bool after_assign = false, after_string = false; while (at < length) { char ch = source[at]; if (ch == '\n') { @@ -1162,6 +1220,28 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB frame->skip_start = at; frame->skip_point = begin_point; skipping++; + } else if (!frame->in_expression && !skipping && depth == frame->depth_at_open && + cbm_attr_text_is_head(source, frame->directive.end_byte, at)) { + /* `#ifndef X / void f(void) / #else / void g(void) / #endif / {`: two heads and + * one body, and no grammar reads both. The first is read, as it is in a split + * expression, and the body is the same in either configuration. */ + uint32_t close = cbm_attr_text_endif(source, length, end); + uint32_t after = close; + if (close < length) { + after = cbm_attr_text_skip(source, length, close, true); + while (after < length && source[after] != '\n') + after++; + after = cbm_attr_text_skip(source, length, after, false); + } + if (close < length && after < length && source[after] == '{' && + cbm_attr_text_is_head(source, end, close)) { + frame->in_expression = true; + frame->skipping = true; + frame->skip_start = at; + frame->skip_point = begin_point; + skipping++; + cbm_attr_range_add(words, frame->directive); + } } else if (!frame->in_expression && !skipping) { /* `#if A / f(x, / #else / g(x, / #endif`: the branch leaves a parenthesis open * that the code after the conditional closes, so each branch is half of a @@ -1205,8 +1285,52 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB } if (at < length && source[at] == quote) at++; + after_string = quote == '"'; continue; } + if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_') { + /* `"x" LUA_QL("=") " or "`: a macro call standing among string literals has to expand + * to a string, so the concatenation holds without it, and no grammar reads a call + * there. */ + uint32_t word_end = at; + bool caps = true; + while (word_end < length && ((source[word_end] >= 'A' && source[word_end] <= 'Z') || + (source[word_end] >= '0' && source[word_end] <= '9') || + (source[word_end] >= 'a' && source[word_end] <= 'z') || + source[word_end] == '_')) { + caps &= !(source[word_end] >= 'a' && source[word_end] <= 'z'); + word_end++; + } + uint32_t open = cbm_attr_text_skip(source, length, word_end, true); + uint32_t quote = open < length && source[open] == '(' + ? cbm_attr_text_skip(source, length, open + 1, true) + : length; + if (!skipping && caps && word_end - at >= CBM_ATTR_MIN_CAPS && quote < length && + source[quote] == '"') { + uint32_t close = quote + 1; + while (close < length && source[close] != '"' && source[close] != '\n') + close += source[close] == '\\' ? 2 : 1; + uint32_t paren = close < length && source[close] == '"' + ? cbm_attr_text_skip(source, length, close + 1, true) + : length; + if (paren < length && source[paren] == ')') { + uint32_t next = cbm_attr_text_skip(source, length, paren + 1, false); + if (after_string || (next < length && source[next] == '"')) { + cbm_attr_range_add(words, (TSRange){{row, at - line_start}, + {row, paren + 1 - line_start}, + at, + paren + 1}); + at = paren + 1; + continue; + } + } + } + after_string = false; + after_assign = false; + at = word_end; + continue; + } + after_string = false; if (!skipping) { if (ch == '(') { depth++; diff --git a/internal/cbm/extract_usages.c b/internal/cbm/extract_usages.c index a1c21a136..0b4d915e0 100644 --- a/internal/cbm/extract_usages.c +++ b/internal/cbm/extract_usages.c @@ -1154,6 +1154,7 @@ static bool is_binding_occurrence(CBMExtractCtx *ctx, TSNode node, const CBMLang TSNode current = node; TSTreeCursor *cursor = reset_occurrence_cursor(state, node); + bool bodyless_tag = false; while (!ts_node_is_null(current)) { TSNode parent; const char *field; @@ -1163,6 +1164,15 @@ static bool is_binding_occurrence(CBMExtractCtx *ctx, TSNode node, const CBMLang if (is_value_field(field)) { return false; } + /* `struct X;` at file scope or in a body has no node of its own around the specifier. */ + if (bodyless_tag && !field && + (strcmp(ts_node_type(parent), "translation_unit") == 0 || + strcmp(ts_node_type(parent), "compound_statement") == 0 || + strcmp(ts_node_type(parent), "declaration_list") == 0 || + strcmp(ts_node_type(parent), "field_declaration_list") == 0 || + strcmp(ts_node_type(parent), "template_declaration") == 0)) { + return true; + } /* A declaration container binds its name/pattern, not the symbols used * by its type annotation. Both TypeScript (`cfg: Config`) and Go * (`cfg Config`) expose that annotation through the exact `type` field; @@ -1170,10 +1180,29 @@ static bool is_binding_occurrence(CBMExtractCtx *ctx, TSNode node, const CBMLang * The emitted occurrence remains an ordinary USAGE, never a callable * reference merely because the target happens to be a type. */ if (field && strcmp(field, "type") == 0) { + /* `struct X;` declares the tag: nothing here uses the type. */ + if (bodyless_tag && + (strcmp(ts_node_type(parent), "declaration") == 0 || + strcmp(ts_node_type(parent), "field_declaration") == 0) && + ts_node_is_null(ts_node_child_by_field_name(parent, TS_FIELD("declarator")))) { + return true; + } return false; } const char *kind = ts_node_type(parent); + /* `struct X *p` names a tag declared elsewhere. Only a body or a lone `struct X;` makes + * this the tag's own declaration. */ + if ((ctx->language == CBM_LANG_C || ctx->language == CBM_LANG_CPP || + ctx->language == CBM_LANG_CUDA) && + field && strcmp(field, "name") == 0 && + (strcmp(kind, "struct_specifier") == 0 || strcmp(kind, "union_specifier") == 0 || + strcmp(kind, "enum_specifier") == 0 || strcmp(kind, "class_specifier") == 0) && + ts_node_is_null(ts_node_child_by_field_name(parent, TS_FIELD("body")))) { + bodyless_tag = true; + current = parent; + continue; + } /* PL/SQL: `parameter` is a ref_call ARGUMENT wrapper (upstream grammar * naming), not a declaration; definition-side bindings use the distinct * parameter_declaration kind. Skip it so call arguments stay ordinary diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 334df077c..84f16b382 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -1264,6 +1264,28 @@ static int c_proof_include_compare(const void *left, const void *right) { return order ? order : strcmp(a->module, b->module); } +static int c_proof_offset_compare(const void *left, const void *right) { + uint32_t a = *(const uint32_t *)left, b = *(const uint32_t *)right; + return (a > b) - (a < b); +} + +static void c_proof_add_unknown_include(CLSPContext *ctx, uint32_t at) { + if (ctx->proof_unknown_include_count == ctx->proof_unknown_include_cap) { + int cap = ctx->proof_unknown_include_cap ? ctx->proof_unknown_include_cap * 2 : 8; + uint32_t *next = cbm_arena_alloc(ctx->arena, (size_t)cap * sizeof(*next)); + if (!next) { + ctx->binding_failed = true; + return; + } + if (ctx->proof_unknown_include_count) + memcpy(next, ctx->proof_unknown_includes, + (size_t)ctx->proof_unknown_include_count * sizeof(*next)); + ctx->proof_unknown_includes = next; + ctx->proof_unknown_include_cap = cap; + } + ctx->proof_unknown_includes[ctx->proof_unknown_include_count++] = at; +} + static void c_proof_include_site(CLSPContext *ctx, TSNode node) { TSNode path = ts_node_child_by_field_name(node, "path", 4); if (ts_node_is_null(path)) @@ -1273,6 +1295,7 @@ static void c_proof_include_site(CLSPContext *ctx, TSNode node) { if (len < 3 || !((text[0] == '"' && text[len - 1] == '"') || (text[0] == '<' && text[len - 1] == '>'))) return; + bool quoted = text[0] == '"'; text[len - 1] = '\0'; text++; int lo = 0, hi = ctx->include_count; @@ -1283,10 +1306,16 @@ static void c_proof_include_site(CLSPContext *ctx, TSNode node) { else hi = mid; } + bool known = false; for (int i = lo; i < ctx->include_count && !strcmp(ctx->proof_includes[i].path, text); i++) { if (ts_node_start_byte(node) < ctx->proof_includes[i].at) ctx->proof_includes[i].at = ts_node_start_byte(node); + known |= ctx->proof_includes[i].file_qn || + (ctx->proof_includes[i].module[0] && ctx->c_decls && + cbm_c_file_facts(ctx->c_decls, ctx->proof_includes[i].module)); } + if (quoted && !known) + c_proof_add_unknown_include(ctx, ts_node_start_byte(node)); } static TSNode c_proof_function_name(TSNode declaration) { @@ -1457,6 +1486,14 @@ static void c_proof_collect(CLSPContext *ctx, TSNode node, const char *owner, in text ? cbm_arena_sprintf(ctx->arena, "$type:%s.%s", owner, text) : NULL; if (qn) c_proof_add_declaration(ctx, qn, ts_node_end_byte(alias)); + /* `typedef struct {...} Name;` gives no tag of that name. */ + if (text && !ts_node_is_null(type) && + ts_node_is_null(ts_node_child_by_field_name(type, "name", 4)) && + !ts_node_is_null(ts_node_child_by_field_name(type, "body", 4))) { + const char *key = cbm_arena_sprintf(ctx->arena, "$no-tag:%s", text); + if (key) + c_proof_add_declaration(ctx, key, ts_node_end_byte(alias)); + } break; } alias = ts_node_child_by_field_name(alias, "declarator", 10); @@ -1756,15 +1793,8 @@ static bool c_tu_add_closure(CLSPContext *ctx, const char *file, const cbm_resol * what that includer had in view ahead of the `#include` was in view for this file too. A source * file with includes of its own is another matter: Lua's lbaselib.c also sits in one amalgamation * and is compiled alone. Each hop goes up one file, and a file seen twice ends it. */ -static void c_proof_add_includer_context(CLSPContext *ctx) { - const char *current = c_own_file_key(ctx); - if (!ctx->c_decls || !current) - return; - const CBMCFileFacts *own_facts = cbm_c_file_facts(ctx->c_decls, current); - if (!own_facts || (own_facts->source_file && own_facts->include_count > 0)) - return; +static void c_proof_walk_includers(CLSPContext *ctx, const char *current) { const cbm_resolve_proof_t *via = NULL; - ctx->tu_adding_includer_context = true; for (uint32_t hop = 0; hop < 64; hop++) { const CBMCFileFacts *includer = cbm_c_sole_includer(ctx->c_decls, current); const char *key = @@ -1800,6 +1830,17 @@ static void c_proof_add_includer_context(CLSPContext *ctx) { } current = key; } +} + +static void c_proof_add_includer_context(CLSPContext *ctx) { + const char *current = c_own_file_key(ctx); + if (!ctx->c_decls || !current) + return; + const CBMCFileFacts *own_facts = cbm_c_file_facts(ctx->c_decls, current); + if (!own_facts || (own_facts->source_file && own_facts->include_count > 0)) + return; + ctx->tu_adding_includer_context = true; + c_proof_walk_includers(ctx, current); ctx->tu_adding_includer_context = false; } @@ -1846,6 +1887,8 @@ static void c_proof_build_translation_unit(CLSPContext *ctx) { facts = cbm_c_file_facts(ctx->c_decls, queue[head++]); if (!facts) continue; + if (facts->has_unknown_include) + c_proof_add_unknown_include(ctx, include->at); for (int g = 0; g < facts->generated_count; g++) { const char *source = facts->generated_for[g]; if (!c_tu_find(ctx, source, strlen(source)) && strcmp(source, own_key) && @@ -1943,6 +1986,9 @@ static void c_proof_prepare(CLSPContext *ctx, TSNode root) { sizeof(*ctx->proof_declarations), c_proof_declaration_compare); c_proof_build_translation_unit(ctx); c_proof_add_includer_context(ctx); + if (ctx->proof_unknown_include_count > 1) + qsort(ctx->proof_unknown_includes, (size_t)ctx->proof_unknown_include_count, + sizeof(*ctx->proof_unknown_includes), c_proof_offset_compare); } static void c_proof_record_using(CLSPContext *ctx, TSNode node) { @@ -2435,7 +2481,6 @@ typedef struct { const CBMCDecl **items; uint32_t *from; /* first byte of this file where the definition is in force */ int count; - int8_t beside_function; /* c_macro_only_beside_function, once known: 0 unknown, 1 yes, -1 no */ } CMacroDefs; /* `#if defined(REDISMODULE_CORE) / #define RedisModuleString robj / #endif`: server.h defines the @@ -2462,7 +2507,6 @@ static bool c_macro_branch_is_off(CLSPContext *ctx, const CBMCDecl *decl) { } static CMacroDefs *c_macro_defs(CLSPContext *ctx, const char *name) { - /* Written only through a real entry: a count of zero returns before beside_function is set. */ static CMacroDefs none = {0}; const CBMCDeclLink *first = cbm_c_decl_lookup(ctx->c_decls, name); if (!first) @@ -2522,34 +2566,46 @@ static bool c_proof_macro_visible(CLSPContext *ctx, const char *name, uint32_t a return false; } -/* `#if COMPAT / #define getn(L) objlen(L) / #else / int (getn)(State *L); / #endif`: a function - * stands in the branch beside every definition of the macro this unit sees, so the macro does not - * decide the name. Every branch of a conditional is read, and the function's is one of them. */ -static bool c_macro_only_beside_function(CLSPContext *ctx, const char *name) { - CMacroDefs *defs = c_macro_defs(ctx, name); - if (!defs->count) +/* `#define CALL() f()`, `#include "gen.h"` with nothing in the index behind it, then the use: that + * text may `#undef` and redefine CALL, so the definition read here is not shown to be the one in + * force. An unknown header before an unconditional definition changes nothing, since the later + * definition wins. Before a conditional one it may have decided the branch. Inside an included file + * the order is not known here, so an unknown header anywhere under the same include counts. `<...>` + * is the toolchain, which defines its own names. */ +static bool c_macro_definition_unproven(CLSPContext *ctx, const CBMCDecl *decl, uint32_t at) { + if (!ctx->proof_unknown_include_count) return false; - if (defs->beside_function) - return defs->beside_function > 0; - const CBMCDeclLink *first = cbm_c_decl_lookup(ctx->c_decls, name); - defs->beside_function = -1; - for (int i = 0; i < defs->count; i++) { - const CBMCDecl *macro = defs->items[i]; - if (!macro->branch_group) - return false; - bool beside = false; - for (const CBMCDeclLink *link = first; link && !beside; link = link->next) { - const CBMCDecl *other = link->decl; - beside = (other->kind == CBM_C_DECL_PROTOTYPE || - other->kind == CBM_C_DECL_DEFINED_IN_BRANCH) && - other->branch_group == macro->branch_group && other->branch != macro->branch && - !strcmp(other->name, name) && !strcmp(other->file, macro->file); + uint32_t from = 0; /* the first offset where an unknown header could still matter */ + if (!decl->branch_group) { + if (!strcmp(decl->file, c_own_file_key(ctx))) { + const char *key = cbm_arena_sprintf(ctx->arena, "$macro:%s", decl->name); + int low = 0, high = ctx->proof_declaration_count; + while (key && low < high) { + int mid = low + (high - low) / 2; + if (strcmp(ctx->proof_declarations[mid].qn, key) < 0) + low = mid + 1; + else + high = mid; + } + for (; key && low < ctx->proof_declaration_count && + !strcmp(ctx->proof_declarations[low].qn, key) && + ctx->proof_declarations[low].at < at; + low++) + from = ctx->proof_declarations[low].at + 1; + } else { + const CTUModule *entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + from = entry ? entry->at : 0; } - if (!beside) - return false; } - defs->beside_function = 1; - return true; + int low = 0, high = ctx->proof_unknown_include_count; + while (low < high) { + int mid = low + (high - low) / 2; + if (ctx->proof_unknown_includes[mid] < from) + low = mid + 1; + else + high = mid; + } + return low < ctx->proof_unknown_include_count && ctx->proof_unknown_includes[low] < at; } static bool c_proof_using_visible(CLSPContext *ctx, const CProofUsing *using, uint32_t at) { @@ -2597,7 +2653,7 @@ static cbm_resolve_result_t c_proof_name_lookup(CLSPContext *ctx, const char *te } if (cbm_scope_contains(ctx->current_scope, name)) return c_proof_missing("c_name_shadowed"); - if (c_proof_macro_visible(ctx, name, at) && !c_macro_only_beside_function(ctx, name)) + if (c_proof_macro_visible(ctx, name, at)) return c_proof_missing("c_macro_may_rewrite_name"); cbm_resolve_result_t result = {.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE}; const char *relative = @@ -3217,8 +3273,9 @@ const CBMType *c_parse_type_node(CLSPContext *ctx, TSNode node) { return cbm_type_func(ctx->arena, NULL, NULL, NULL); } - // struct_specifier / class_specifier (inline struct/class type) - if (strcmp(kind, "struct_specifier") == 0 || strcmp(kind, "class_specifier") == 0) { + // struct_specifier / class_specifier / union_specifier (inline type) + if (strcmp(kind, "struct_specifier") == 0 || strcmp(kind, "class_specifier") == 0 || + strcmp(kind, "union_specifier") == 0) { TSNode name_node = ts_node_child_by_field_name(node, "name", 4); if (!ts_node_is_null(name_node)) { char *name = c_node_text(ctx, name_node); @@ -5328,9 +5385,34 @@ static bool c_nested_type_in_scope(const CLSPContext *ctx, const char *scope) { (klass->embedded_types && klass->embedded_types[0]); } +/* A scope named from a module of `module_len` bytes: the top level of that file, a namespace around + * this code, or one a visible `using namespace` brings here. */ +static bool c_scope_reached(CLSPContext *ctx, const char *written, const char *scope, + size_t module_len, uint32_t at) { + if (!scope[module_len]) + return true; + if (written[0] == ':') + return false; + const char *from = scope + module_len + 1; + size_t from_len = strlen(from); + const char *around = ctx->current_namespace; + if (around && ctx->module_qn && !strncmp(around, ctx->module_qn, ctx->module_qn_len) && + around[ctx->module_qn_len] == '.') { + around += ctx->module_qn_len + 1; + if (!strncmp(around, from, from_len) && (!around[from_len] || around[from_len] == '.')) + return true; + } + for (int i = 0; i < ctx->proof_using_count; i++) { + const CProofUsing *using = &ctx->proof_usings[i]; + if (using->namespace && using->supported && !strcmp(using->target, from) && + c_proof_using_visible(ctx, using, at)) + return true; + } + return false; +} + /* `ns::Name`: what the first segment means depends on where lookup starts. The declaration's scope, - * with the written qualifier taken off, has to be a place that lookup reaches: the top level of - * its file, a namespace around this code, or one a visible `using namespace` brings here. */ + * with the written qualifier taken off, has to be a place that lookup reaches. */ static bool c_qualifier_starts_here(CLSPContext *ctx, const char *written, const char *file_qn, const char *scope, uint32_t at) { /* `p.dir.t.hpp.__file__` is the file whose declarations are named `p.dir.t.Name`. */ @@ -5347,26 +5429,7 @@ static bool c_qualifier_starts_here(CLSPContext *ctx, const char *written, const file_len--; if (strncmp(scope, file_qn, file_len) || (scope[file_len] && scope[file_len] != '.')) return false; - if (!scope[file_len]) - return true; - if (written[0] == ':') - return false; - const char *from = scope + file_len + 1; - size_t from_len = strlen(from); - const char *around = ctx->current_namespace; - if (around && ctx->module_qn && !strncmp(around, ctx->module_qn, ctx->module_qn_len) && - around[ctx->module_qn_len] == '.') { - around += ctx->module_qn_len + 1; - if (!strncmp(around, from, from_len) && (!around[from_len] || around[from_len] == '.')) - return true; - } - for (int i = 0; i < ctx->proof_using_count; i++) { - const CProofUsing *using = &ctx->proof_usings[i]; - if (using->namespace && using->supported && !strcmp(using->target, from) && - c_proof_using_visible(ctx, using, at)) - return true; - } - return false; + return c_scope_reached(ctx, written, scope, file_len, at); } /* Every declaration of this type name the translation unit sees. A name may be @@ -5501,6 +5564,60 @@ static bool c_base_may_depend(CLSPContext *ctx, const char *name) { return ctx->in_template && name && c_name_is_declared_as(ctx, name, CBM_C_DECL_CLASS_TEMPLATE); } +/* `struct Name` is looked up among tags, and where lookup finds none it declares a new one. So it + * is not a typedef of something else (`typedef struct impl Name;`), not the typedef of an unnamed + * aggregate (`typedef struct {...} Name;` gave no tag), and in C++ not a namesake in a namespace + * that lookup does not reach from here. A plain `Name` had to be declared to compile. This need + * not. */ +static bool c_tag_is_other_than(CLSPContext *ctx, const CBMRegisteredType *type, const char *name, + bool qualified, uint32_t at) { + if (type->alias_of && strcmp(type->alias_of, name)) + return true; + /* This file's own typedefs are known on every pass, an included file's only where facts are. */ + const char *no_tag = cbm_arena_sprintf(ctx->arena, "$no-tag:%s", name); + if (no_tag && c_proof_declared(ctx, no_tag, at)) + return true; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind == CBM_C_DECL_UNNAMED_TYPEDEF && !strcmp(decl->name, name) && + c_tu_find(ctx, decl->file, strlen(decl->file))) + return true; + } + if (!ctx->cpp_mode || ctx->plain_header || qualified) + return false; + const char *qn = type->qualified_name; + const char *dot = qn ? strrchr(qn, '.') : NULL; + const char *scope = dot ? cbm_arena_strndup(ctx->arena, qn, (size_t)(dot - qn)) : NULL; + if (!scope) + return true; + /* A type nested in a class passed the scope check of that class on its way here. */ + if (cbm_registry_lookup_type(ctx->proof_registry, scope)) + return false; + if (type->source_file_qn) + return !c_qualifier_starts_here(ctx, name, type->source_file_qn, scope, at); + /* The pass over one file alone has no facts, and every project type in it is this file's own. + */ + size_t module_len = ctx->module_qn_len; + return strncmp(scope, ctx->module_qn, module_len) || + (scope[module_len] && scope[module_len] != '.') || + !c_scope_reached(ctx, name, scope, module_len, at); +} + +/* `struct S { ... }; struct S x;` in a block: x has the block's S, whatever an outer S declares. + * Bound under a key of its own, because `struct config config;` is an ordinary variable that hides + * no tag: C keeps the two names apart. */ +static bool c_name_bound_locally(CLSPContext *ctx, const char *name); +static const char *c_local_tag_key(CLSPContext *ctx, const char *name) { + return cbm_arena_sprintf(ctx->arena, "struct %s", name); +} +static bool c_local_tag_hides(CLSPContext *ctx, const char *name) { + if (!ctx->local_tag_declared || strstr(name, "::")) + return false; + const char *key = c_local_tag_key(ctx, name); + return key && c_name_bound_locally(ctx, key); +} + static const CBMRegisteredType *c_annotation_owner(CLSPContext *ctx, TSNode site) { TSNode annotation = ts_node_child_by_field_name(site, "type", 4); if (ts_node_is_null(annotation)) { @@ -5508,6 +5625,7 @@ static const CBMRegisteredType *c_annotation_owner(CLSPContext *ctx, TSNode site if (!ts_node_is_null(declaration)) annotation = ts_node_child_by_field_name(declaration, "type", 4); } + bool elaborated = false; for (int depth = 0; depth < 8 && !ts_node_is_null(annotation); depth++) { const char *kind = ts_node_type(annotation); if (!strcmp(kind, "type_descriptor")) { @@ -5516,6 +5634,7 @@ static const CBMRegisteredType *c_annotation_owner(CLSPContext *ctx, TSNode site } if (!strcmp(kind, "struct_specifier") || !strcmp(kind, "union_specifier") || !strcmp(kind, "class_specifier")) { + elaborated = true; annotation = ts_node_child_by_field_name(annotation, "name", 4); continue; } @@ -5558,8 +5677,15 @@ static const CBMRegisteredType *c_annotation_owner(CLSPContext *ctx, TSNode site return NULL; } } - return c_proof_follow_alias( - ctx, c_proof_type_by_name(ctx, name, ts_node_start_byte(annotation))); + if (c_local_tag_hides(ctx, name)) + return NULL; + uint32_t at = ts_node_start_byte(annotation); + const CBMRegisteredType *named = c_proof_type_by_name(ctx, name, at); + /* That tag says nothing about the type its namesake is. */ + if (elaborated && named && + c_tag_is_other_than(ctx, named, name, strstr(name, "::") != NULL, at)) + return NULL; + return c_proof_follow_alias(ctx, named); } return NULL; } @@ -7376,6 +7502,67 @@ static const CBMRegisteredType *c_proof_written_cast(CLSPContext *ctx, TSNode si return owner; } +static uint32_t c_macro_use_arguments(TSNode call, TSNode *out, uint32_t cap); + +/* `hl_table[i].x` with `#define hl_table ((HlGroup *)(ga.ga_data))`, `SDS_HDR(8, s)->len` with + * `#define SDS_HDR(T, s) ((struct sdshdr##T *)...)`: whatever the operand is, the replacement + * writes the type. Every definition in force has to write the same one. */ +static const CBMRegisteredType *c_macro_cast_owner(CLSPContext *ctx, const char *macro, TSNode site, + TSNode call, const cbm_resolve_proof_t **proof, + const CBMType **shape) { + uint32_t at = ts_node_start_byte(site); + if (!ctx->c_decls || !macro || c_name_bound_locally(ctx, macro) || + !c_proof_macro_visible(ctx, macro, at)) + return NULL; + const CMacroDefs *defs = c_macro_defs(ctx, macro); + const char *own = c_own_file_key(ctx); + const CBMCMacroShape *agreed = NULL; + for (int d = 0; d < defs->count; d++) { + const CBMCDecl *decl = defs->items[d]; + if (defs->from[d] >= at && strcmp(decl->file, own)) + continue; + const CBMCMacroShape *written = decl->macro; + if (c_macro_definition_unproven(ctx, decl, at) || !written || !written->cast_type || + written->object_like != ts_node_is_null(call)) + return NULL; + if (agreed && (strcmp(agreed->cast_type, written->cast_type) || + agreed->cast_pointers != written->cast_pointers || + agreed->cast_pastes != written->cast_pastes || + agreed->cast_param != written->cast_param)) + return NULL; + agreed = written; + } + if (!agreed) + return NULL; + const char *name = agreed->cast_type; + if (agreed->cast_pastes) { + TSNode arguments[32]; + uint32_t count = c_macro_use_arguments(call, arguments, 32); + const char *argument = + agreed->cast_param < count ? c_node_text(ctx, arguments[agreed->cast_param]) : NULL; + for (const char *c = argument; c && *c; c++) + if (!isalnum((unsigned char)*c) && *c != '_') + argument = NULL; + name = argument ? cbm_arena_sprintf(ctx->arena, "%s%s", name, argument) : NULL; + } + const CBMRegisteredType *owner = name ? c_proof_named_class(ctx, name, at) : NULL; + if (!owner) + return NULL; + cbm_resolve_result_t typed = cbm_lsp_proof_type_lookup( + &ctx->field_types, macro, site, "c-macro-replacement-cast", owner->qualified_name, + owner->declaration_identity, owner->graph_qualified_name, owner->source_flags, + "cast-written-in-the-replacement"); + typed = c_proof_type_import(ctx, owner->qualified_name, at, typed); + if (!cbm_resolve_admit(&typed)) + return NULL; + const CBMType *type = cbm_type_named(ctx->arena, owner->qualified_name); + for (int i = 0; i < agreed->cast_pointers; i++) + type = cbm_type_pointer(ctx->arena, type); + *proof = typed.proof; + *shape = type; + return owner; +} + /* The struct an expression denotes, as the written declarations along it establish * it: a typed name, an extern object, a member of a known struct, or any of those * under `*`, `&`, `[]` and parentheses. No initializer is consulted and nothing @@ -7390,6 +7577,20 @@ static const CBMRegisteredType *c_proof_expression_owner(CLSPContext *ctx, TSNod uint32_t at = ts_node_start_byte(node); if (!strcmp(kind, "parenthesized_expression") && ts_node_named_child_count(node) == 1) return c_proof_expression_owner(ctx, ts_node_named_child(node, 0), depth + 1, proof, shape); + if (!strcmp(kind, "identifier")) { + const CBMRegisteredType *cast = + c_macro_cast_owner(ctx, c_node_text(ctx, node), node, (TSNode){0}, proof, shape); + if (cast) + return cast; + } else if (!strcmp(kind, "call_expression")) { + TSNode callee = ts_node_child_by_field_name(node, "function", 8); + const CBMRegisteredType *cast = + !ts_node_is_null(callee) && !strcmp(ts_node_type(callee), "identifier") + ? c_macro_cast_owner(ctx, c_node_text(ctx, callee), node, node, proof, shape) + : NULL; + if (cast) + return cast; + } if (!strcmp(kind, "subscript_expression") || !strcmp(kind, "pointer_expression")) { const CBMRegisteredType *base = c_proof_expression_owner( ctx, ts_node_child_by_field_name(node, "argument", 8), depth + 1, proof, shape); @@ -7558,6 +7759,8 @@ static bool c_field_is_macro_path(CLSPContext *ctx, TSNode argument, TSNode fiel const CBMCDecl *decl = defs->items[d]; if (defs->from[d] >= at && strcmp(decl->file, own)) continue; + if (c_macro_definition_unproven(ctx, decl, at)) + return true; const char *written = decl->macro ? decl->macro->field_path : NULL; if (!written || (path && strcmp(path, written))) return false; @@ -8146,41 +8349,89 @@ static int c_macro_parameter(const CBMCMacroShape *shape, int position) { return shape->variadic ? shape->param_count - 1 : -1; } +/* A macro may hand an argument on several times, to macros that do the same, so following every + * hand-off doubles with each hop. A walk reads each (macro, position) once. Meeting one again adds + * nothing: its answer is already part of this one, and a macro is not expanded again inside its + * own expansion. */ +typedef struct { + const char *macro; + int position; +} CMacroArgument; + +static const char *c_macro_argument_key(CLSPContext *ctx, const char *macro, int position) { + return cbm_arena_sprintf(ctx->arena, "%d#%s", position, macro); +} + /* Does this written name, as argument `position` of the macro, reach the expansion as itself in * every definition the unit sees? The name then means what it means at the use, because * substitution puts it there. A definition that pastes it, reads it as a member, declares it or - * declares a local of that name says otherwise, and so does one this does not understand. */ + * declares a local of that name says otherwise, and so does one this does not understand. One + * definition's answer does not depend on another's, so the order of the walk cannot change this. */ static bool c_macro_argument_is_plain(CLSPContext *ctx, const char *macro, int position, - const char *written, uint32_t at, int depth) { - if (depth > 16) - return false; - bool seen = false; - for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, macro); link; - link = link->next) { - const CBMCDecl *decl = link->decl; - if (decl->kind != CBM_C_DECL_MACRO || strcmp(decl->name, macro) || - !c_macro_definition_visible(ctx, decl, at)) - continue; - seen = true; - const CBMCMacroShape *shape = decl->macro; - int param = shape ? c_macro_parameter(shape, position) : -1; - if (param < 0 || !(shape->plain & (1u << param))) - return false; - for (int i = 0; i < shape->local_count; i++) - if (!strcmp(shape->locals[i], written)) - return false; - for (int i = 0; i < shape->handoff_count; i++) { - const CBMCMacroHandoff *handoff = &shape->handoffs[i]; - if (handoff->param != param || !c_proof_macro_visible(ctx, handoff->callee, at)) + const char *written, uint32_t at) { + CMacroArgument top = {macro, position}; + CMacroArgument *stack = ⊤ + int count = 1, cap = 1; + CBMHashTable *seen = NULL; + bool plain = true; + while (plain && count) { + CMacroArgument use = stack[--count]; + bool defined = false; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, use.macro); link && plain; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_MACRO || strcmp(decl->name, use.macro) || + !c_macro_definition_visible(ctx, decl, at)) continue; - /* `__VA_ARGS__` handed on stands for several arguments at several positions. */ - if ((shape->variadic && param == shape->param_count - 1) || - !c_macro_argument_is_plain(ctx, handoff->callee, handoff->position, written, at, - depth + 1)) - return false; + CBM_LSP_WORK(1); + defined = true; + const CBMCMacroShape *shape = decl->macro; + int param = shape ? c_macro_parameter(shape, use.position) : -1; + if (param < 0 || !(shape->plain & (1u << param))) { + plain = false; + break; + } + for (int i = 0; plain && i < shape->local_count; i++) + plain = strcmp(shape->locals[i], written) != 0; + for (int i = 0; plain && i < shape->handoff_count; i++) { + const CBMCMacroHandoff *handoff = &shape->handoffs[i]; + if (handoff->param != param || !c_proof_macro_visible(ctx, handoff->callee, at)) + continue; + /* `__VA_ARGS__` handed on stands for several arguments at several positions. */ + if (shape->variadic && param == shape->param_count - 1) { + plain = false; + break; + } + const char *key = c_macro_argument_key(ctx, handoff->callee, handoff->position); + if (!seen) + seen = cbm_ht_create(8); + if (!key || !seen) { + plain = false; + break; + } + if (cbm_ht_get(seen, key)) + continue; + cbm_ht_set(seen, key, (void *)key); + if (count == cap) { + int grown = cap * 2 + 6; + CMacroArgument *next = + cbm_arena_alloc(ctx->arena, (size_t)grown * sizeof(*next)); + if (!next) { + plain = false; + break; + } + memcpy(next, stack, (size_t)count * sizeof(*next)); + stack = next; + cap = grown; + } + stack[count++] = (CMacroArgument){handoff->callee, handoff->position}; + } } + plain = plain && defined; } - return seen; + if (seen) + cbm_ht_free(seen); + return plain; } static void c_macro_args_load(CLSPContext *ctx, TSNode list) { @@ -8211,7 +8462,7 @@ static void c_macro_args_load(CLSPContext *ctx, TSNode list) { continue; if (!strcmp(ts_node_type(argument), "identifier")) { const char *written = c_node_text(ctx, argument); - if (written && c_macro_argument_is_plain(ctx, name, position, written, at, 0)) + if (written && c_macro_argument_is_plain(ctx, name, position, written, at)) plain[count++] = ts_node_start_byte(argument); } position++; @@ -8302,9 +8553,47 @@ static bool c_bare_argument_of_macro(CLSPContext *ctx, TSNode node) { * variable in every definition the unit sees, directly or by handing it to a macro that does: * `#define FOR_ALL_TABS(tp) for (tabpage_T *(tp) = first_tabpage; ...)`. */ static const char *c_macro_declared_type(CLSPContext *ctx, const char *macro, int position, - uint32_t at, int depth, int *pointers) { - if (depth > 16) + uint32_t at, int depth, int *pointers, + CBMHashTable **answers); + +typedef struct { + const char *type; + int pointers; +} CMacroDeclared; + +/* What the macro a hand-off reaches declares, read once per walk. Reached again while being read, + * in a cycle, the answer is still none: a macro is not expanded inside its own expansion. The depth + * only keeps a hostile chain off the stack: going to it from 16 changed no edge in redis, neovim, + * or the LLVM and kernel subtrees measured. */ +#define C_MACRO_HANDOFF_MAX_DEPTH 64 +static const char *c_macro_handed_type(CLSPContext *ctx, const CBMCMacroHandoff *handoff, + uint32_t at, int depth, int *pointers, + CBMHashTable **answers) { + if (depth > C_MACRO_HANDOFF_MAX_DEPTH) return NULL; + const char *key = c_macro_argument_key(ctx, handoff->callee, handoff->position); + if (!*answers) + *answers = cbm_ht_create(8); + CMacroDeclared *answer = key && *answers ? cbm_ht_get(*answers, key) : NULL; + if (answer) { + *pointers = answer->pointers; + return answer->type; + } + answer = key && *answers ? cbm_arena_alloc(ctx->arena, sizeof(*answer)) : NULL; + if (!answer) + return NULL; + *answer = (CMacroDeclared){0}; + cbm_ht_set(*answers, key, answer); + const char *type = c_macro_declared_type(ctx, handoff->callee, handoff->position, at, depth, + pointers, answers); + answer->type = type; + answer->pointers = *pointers; + return type; +} + +static const char *c_macro_declared_type(CLSPContext *ctx, const char *macro, int position, + uint32_t at, int depth, int *pointers, + CBMHashTable **answers) { const char *found = NULL; for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, macro); link; link = link->next) { @@ -8312,6 +8601,9 @@ static const char *c_macro_declared_type(CLSPContext *ctx, const char *macro, in if (decl->kind != CBM_C_DECL_MACRO || strcmp(decl->name, macro) || !c_macro_definition_visible(ctx, decl, at)) continue; + CBM_LSP_WORK(1); + if (c_macro_definition_unproven(ctx, decl, at)) + return NULL; const CBMCMacroShape *shape = decl->macro; int param = shape ? c_macro_parameter(shape, position) : -1; if (param < 0) @@ -8325,8 +8617,7 @@ static const char *c_macro_declared_type(CLSPContext *ctx, const char *macro, in for (int i = 0; i < shape->handoff_count && !type; i++) { const CBMCMacroHandoff *handoff = &shape->handoffs[i]; if (handoff->param == param && c_proof_macro_visible(ctx, handoff->callee, at)) - type = c_macro_declared_type(ctx, handoff->callee, handoff->position, at, - depth + 1, &depth_found); + type = c_macro_handed_type(ctx, handoff, at, depth + 1, &depth_found, answers); } } if (!type || (found && (strcmp(found, type) || *pointers != depth_found))) @@ -8343,9 +8634,13 @@ static void c_bind_macro_declared(CLSPContext *ctx, const char *macro, int posit uint32_t at = ts_node_start_byte(name); const char *written = c_node_text(ctx, name); int pointers = 0; - const char *type_name = written && ctx->c_decls && c_proof_macro_visible(ctx, macro, at) - ? c_macro_declared_type(ctx, macro, position, at, 0, &pointers) - : NULL; + CBMHashTable *answers = NULL; + const char *type_name = + written && ctx->c_decls && c_proof_macro_visible(ctx, macro, at) + ? c_macro_declared_type(ctx, macro, position, at, 0, &pointers, &answers) + : NULL; + if (answers) + cbm_ht_free(answers); if (!type_name) return; const CBMRegisteredType *owner = @@ -8518,7 +8813,7 @@ static bool c_macro_word_only_handed_on(CLSPContext *ctx, const CBMCMacroShape * if (strcmp(argument->name, name)) continue; if (c_proof_macro_visible(ctx, argument->callee, at)) { - if (!c_macro_argument_is_plain(ctx, argument->callee, argument->position, name, at, 0)) + if (!c_macro_argument_is_plain(ctx, argument->callee, argument->position, name, at)) return true; continue; } @@ -8599,8 +8894,8 @@ static void c_macro_body_expand(CLSPContext *ctx, CMacroWalk *walk, const char * in_force[d] = defs->from[d] < at || !strcmp(decl->file, own); if (!in_force[d]) continue; - /* A definition this cannot read may say anything. */ - if (!decl->macro) + /* A definition this cannot read may say anything, and so may a header it cannot read. */ + if (!decl->macro || c_macro_definition_unproven(ctx, decl, at)) return; first = first ? first : decl->macro; visible++; @@ -8626,9 +8921,6 @@ static void c_macro_body_expand(CLSPContext *ctx, CMacroWalk *walk, const char * continue; if (c_proof_macro_visible(ctx, name, at)) { c_macro_walk_push(ctx, walk, name); - /* Lua: `#define error(S,s)` in one branch and `static void error(...)` in the other. */ - if (c_macro_only_beside_function(ctx, name)) - c_macro_body_name(ctx, walk, name, true); } else { c_macro_body_name(ctx, walk, name, true); } @@ -8748,10 +9040,25 @@ static bool c_is_sizeof_operand(TSNode node) { } static void c_emit_value_reference(CLSPContext *ctx, TSNode node) { - if (!ctx->c_decls || !ctx->enclosing_func_qn || c_bare_argument_of_macro(ctx, node)) + if (!ctx->c_decls || !ctx->enclosing_func_qn) return; TSNode parent = ts_node_parent(node); const char *parent_kind = ts_node_is_null(parent) ? "" : ts_node_type(parent); + /* `container_of(p, struct X, m)`: no grammar has a type among call arguments, and the keyword + * lands in an error just before the name. That name is a tag and no value. */ + if (!strcmp(parent_kind, "argument_list") && ts_node_has_error(parent)) { + TSNode before = ts_node_prev_sibling(node); + const char *keyword = !ts_node_is_null(before) && !strcmp(ts_node_type(before), "ERROR") + ? c_node_text(ctx, before) + : NULL; + if (keyword && + (!strcmp(keyword, "struct") || !strcmp(keyword, "union") || !strcmp(keyword, "enum"))) { + c_emit_type_reference(ctx, node); + return; + } + } + if (c_bare_argument_of_macro(ctx, node)) + return; if (!strcmp(parent_kind, "qualified_identifier")) { c_emit_scoped_enumerator(ctx, parent, node); return; @@ -8845,18 +9152,31 @@ static void c_emit_value_reference(CLSPContext *ctx, TSNode node) { } /* A function can declare its own `enum { LIMIT = 8 }` or `struct node { ... }`. Those - * names are in block scope and hide any file-scope namesake from here on. */ + * names are in block scope and hide any file-scope namesake from here on. A lone `struct node;` + * does the same: it declares a new tag here, where `struct node *p;` would use the outer one. */ static void c_bind_local_tag_and_enumerators(CLSPContext *ctx, TSNode node, const char *kind) { if (strcmp(kind, "struct_specifier") && strcmp(kind, "union_specifier") && - strcmp(kind, "enum_specifier")) + strcmp(kind, "enum_specifier") && strcmp(kind, "class_specifier")) return; TSNode body = ts_node_child_by_field_name(node, "body", 4); - if (ts_node_is_null(body)) - return; + if (ts_node_is_null(body)) { + TSNode parent = ts_node_parent(node); + const char *around = ts_node_is_null(parent) ? "" : ts_node_type(parent); + if (strcmp(around, "compound_statement") && + (strcmp(around, "declaration") || + !ts_node_is_null(ts_node_child_by_field_name(parent, "declarator", 10)))) + return; + } TSNode tag = ts_node_child_by_field_name(node, "name", 4); const char *tag_name = ts_node_is_null(tag) ? NULL : c_node_text(ctx, tag); - if (tag_name) + if (tag_name) { c_scope_bind(ctx, tag_name, cbm_type_unknown()); + const char *tag_key = c_local_tag_key(ctx, tag_name); + if (tag_key) { + c_scope_bind(ctx, tag_key, cbm_type_unknown()); + ctx->local_tag_declared = true; + } + } if (strcmp(kind, "enum_specifier")) return; uint32_t count = ts_node_named_child_count(body); @@ -8946,13 +9266,18 @@ static void c_emit_type_reference(CLSPContext *ctx, TSNode node) { return; } /* `struct node {...}` defines a tag here rather than using one. */ - if ((!strcmp(parent_kind, "struct_specifier") || !strcmp(parent_kind, "union_specifier") || - !strcmp(parent_kind, "enum_specifier")) && - !ts_node_is_null(ts_node_child_by_field_name(parent, "body", 4))) + bool in_specifier = + !strcmp(parent_kind, "struct_specifier") || !strcmp(parent_kind, "union_specifier") || + !strcmp(parent_kind, "class_specifier") || !strcmp(parent_kind, "enum_specifier"); + if (in_specifier && !ts_node_is_null(ts_node_child_by_field_name(parent, "body", 4))) return; + /* `struct X` and `container_of(p, struct X, m)` ask among tags, which a variable named X does + * not hide. */ + bool is_tag = in_specifier || !strcmp(parent_kind, "argument_list"); const char *name = c_node_text(ctx, node); if (!name || !ctx->module_qn || - (!qualified && (c_is_template_parameter(ctx, name) || c_name_bound_locally(ctx, name)))) + (!qualified && (c_is_template_parameter(ctx, name) || + (is_tag ? c_local_tag_hides(ctx, name) : c_name_bound_locally(ctx, name))))) return; /* `#define sds hisds`, `#define RedisModuleString robj`: the type is what the macro names. */ if (!qualified && c_proof_macro_visible(ctx, name, ts_node_start_byte(node))) { @@ -8961,6 +9286,9 @@ static void c_emit_type_reference(CLSPContext *ctx, TSNode node) { } uint32_t at = ts_node_start_byte(node); const CBMRegisteredType *type = c_proof_type_by_name(ctx, qualified ? qualified : name, at); + /* A tag, which may not be the type found for that spelling. */ + if (type && is_tag && c_tag_is_other_than(ctx, type, name, qualified != NULL, at)) + return; /* A member type of the class around this code hides a namesake outside the class. */ if (type && !qualified && ctx->cpp_mode && c_enclosing_class_declares_other(ctx, name, type)) return; @@ -10600,7 +10928,8 @@ static void c_process_body_child(CLSPContext *ctx, TSNode child) { c_process_function(ctx, child); } else if (strcmp(ck, "namespace_definition") == 0) { c_process_namespace(ctx, child); - } else if (strcmp(ck, "class_specifier") == 0 || strcmp(ck, "struct_specifier") == 0) { + } else if (strcmp(ck, "class_specifier") == 0 || strcmp(ck, "struct_specifier") == 0 || + strcmp(ck, "union_specifier") == 0) { c_process_class(ctx, child); } else if (strcmp(ck, "declaration") == 0) { // declaration may contain class/struct specifier: class Foo { ... }; @@ -10610,7 +10939,8 @@ static void c_process_body_child(CLSPContext *ctx, TSNode child) { for (uint32_t di = 0; di < dnc; di++) { TSNode dch = ts_node_named_child(child, di); const char *dk = ts_node_type(dch); - if (strcmp(dk, "class_specifier") == 0 || strcmp(dk, "struct_specifier") == 0) { + if (strcmp(dk, "class_specifier") == 0 || strcmp(dk, "struct_specifier") == 0 || + strcmp(dk, "union_specifier") == 0) { c_process_class(ctx, dch); has_class = true; } diff --git a/internal/cbm/lsp/c_lsp.h b/internal/cbm/lsp/c_lsp.h index b4b74d06c..b7312759f 100644 --- a/internal/cbm/lsp/c_lsp.h +++ b/internal/cbm/lsp/c_lsp.h @@ -24,11 +24,15 @@ typedef struct { struct CProofDeclaration *proof_declarations; int proof_declaration_count, proof_declaration_cap; struct CProofInclude *proof_includes; + /* Where this file includes a quoted header the index holds nothing for, in file order. */ + uint32_t *proof_unknown_includes; + int proof_unknown_include_count, proof_unknown_include_cap; /* The translation unit: every file this one includes, through other files * too, with the point in this file where it became visible. */ struct CTUModule *tu_modules; uint32_t tu_cap, tu_count; bool tu_adding_includer_context; + bool local_tag_declared; /* a block of this file declared a tag of its own */ const struct CBMCDeclIndex *c_decls; const char *own_file_qn; /* NULL when only the module identifies this file */ /* C++: the class whose member function this is, inline or out of line. Its members diff --git a/internal/cbm/lsp/c_proof.c b/internal/cbm/lsp/c_proof.c index 43cb2f4af..6b143a6c1 100644 --- a/internal/cbm/lsp/c_proof.c +++ b/internal/cbm/lsp/c_proof.c @@ -698,8 +698,152 @@ static bool csp_append_member(CBMArena *arena, CBMCMacroMember **items, int *cou return true; } +/* The end of the `(` at `open`, or 0 when it does not close. Comments and literals have been left + * to the caller: a replacement with a string in its type part reads as no type at all. */ +static uint32_t csp_macro_close(const char *text, uint32_t length, uint32_t open) { + int depth = 0; + for (uint32_t at = open; at < length; at++) { + if (text[at] == '"' || text[at] == '\'') { + char quote = text[at]; + for (at++; at < length && text[at] != quote; at++) + if (text[at] == '\\') + at++; + } else if (text[at] == '(') { + depth++; + } else if (text[at] == ')' && --depth == 0) { + return at + 1; + } + } + return 0; +} + +static uint32_t csp_macro_skip_space(const char *text, uint32_t length, uint32_t at) { + while (at < length) { + if (text[at] == ' ' || text[at] == '\t' || text[at] == '\r' || text[at] == '\n' || + (text[at] == '\\' && at + 1 < length && text[at + 1] == '\n')) { + at++; + } else if (text[at] == '/' && at + 1 < length && text[at + 1] == '*') { + for (at += 2; at + 1 < length && !(text[at] == '*' && text[at + 1] == '/'); at++) + ; + at = at + 2 <= length ? at + 2 : length; + } else if (text[at] == '/' && at + 1 < length && text[at + 1] == '/') { + return length; + } else { + break; + } + } + return at; +} + +/* `((T *)operand)`. The parentheses around it have to close at its end: bare `(T *)p` used as + * `X->y` would cast p->y instead. The operand may be followed by nothing that takes the value out + * of the pointer type, which rules out comparisons, `?:` and `,`, and by no `+` or `-`, which would + * need the other operand read. A value the replacement's own parameter names as the type is not + * written. */ +static void csp_macro_cast(csp_context_t *ctx, CBMCMacroShape *shape, const char *text, + uint32_t length, const csp_macro_params_t *params) { + uint32_t begin = csp_macro_skip_space(text, length, 0), end = length; + /* The end, past trailing space and comments. */ + for (uint32_t at = begin; at < length;) { + uint32_t next = csp_macro_skip_space(text, length, at); + if (next >= length) + break; + at = next + 1; + end = at; + } + bool wrapped = false; + while (begin < end && text[begin] == '(' && csp_macro_close(text, end, begin) == end) { + wrapped = true; + begin = csp_macro_skip_space(text, end, begin + 1); + end--; + while (end > begin && + (text[end - 1] == ' ' || text[end - 1] == '\t' || text[end - 1] == '\r' || + text[end - 1] == '\n' || text[end - 1] == '\\')) + end--; + } + if (!wrapped || begin >= end || text[begin] != '(') + return; + uint32_t close = csp_macro_close(text, end, begin); + if (!close || close >= end) + return; + /* The type part: qualifiers and keywords, one name, `*`s. */ + const char *word = NULL; + size_t word_length = 0; + uint32_t pointers = 0; + int paste_param = -1; + for (uint32_t at = csp_macro_skip_space(text, close - 1, begin + 1); at < close - 1; + at = csp_macro_skip_space(text, close - 1, at)) { + if (text[at] == '*') { + pointers++; + at++; + continue; + } + if (!isalpha((unsigned char)text[at]) && text[at] != '_') + return; + uint32_t start = at; + while (at < close - 1 && (isalnum((unsigned char)text[at]) || text[at] == '_')) + at++; + size_t size = at - start; + static const char *const skipped[] = {"const", "volatile", "struct", "union", "enum"}; + bool keyword = false; + for (size_t k = 0; k < sizeof(skipped) / sizeof(skipped[0]); k++) + keyword |= strlen(skipped[k]) == size && !memcmp(text + start, skipped[k], size); + if (keyword) + continue; + if (word || pointers) + return; + word = text + start; + word_length = size; + if (at + 1 < close - 1 && text[at] == '#' && text[at + 1] == '#') { + at += 2; + uint32_t param_start = at; + while (at < close - 1 && (isalnum((unsigned char)text[at]) || text[at] == '_')) + at++; + for (int i = 0; i < params->count; i++) + if (strlen(params->names[i]) == at - param_start && + !memcmp(params->names[i], text + param_start, at - param_start)) + paste_param = i; + if (paste_param < 0) + return; + } + } + if (!word || pointers > 8) + return; + if (paste_param < 0) + for (int i = 0; i < params->count; i++) + if (strlen(params->names[i]) == word_length && + !memcmp(params->names[i], word, word_length)) + return; + /* The operand. */ + uint32_t operand = csp_macro_skip_space(text, end, close); + if (operand >= end || text[operand] == '.' || text[operand] == '[' || + (text[operand] == '-' && operand + 1 < end && text[operand + 1] == '>')) + return; + int depth = 0; + for (uint32_t at = operand; at < end; at++) { + char ch = text[at]; + if (ch == '"' || ch == '\'') { + for (at++; at < end && text[at] != ch; at++) + if (text[at] == '\\') + at++; + } else if (ch == '(' || ch == '[') { + depth++; + } else if (ch == ')' || ch == ']') { + depth--; + } else if (depth == 0 && at > operand && strchr("+*/%&|^<>=!?:,", ch)) { + return; + } else if (depth == 0 && ch == '-' && !(at + 1 < end && text[at + 1] == '>')) { + return; + } + } + shape->cast_type = cbm_arena_strndup(ctx->arena, word, word_length); + shape->cast_pointers = (uint8_t)pointers; + shape->cast_pastes = paste_param >= 0; + shape->cast_param = paste_param >= 0 ? (uint8_t)paste_param : 0; +} + static const CBMCMacroShape *csp_macro_shape_keep(csp_context_t *ctx, const CBMCMacroShape *shape) { - if (shape->object_like && !shape->call_count && !shape->value_count) + if (shape->object_like && !shape->call_count && !shape->value_count && !shape->cast_type) return NULL; CBMCMacroShape *kept = cbm_arena_alloc(ctx->arena, sizeof(*kept)); if (!kept) { @@ -733,6 +877,7 @@ static const CBMCMacroShape *csp_macro_shape(csp_context_t *ctx, TSNode node) { return csp_macro_shape_keep(ctx, shape); const char *text = ctx->source + begin; uint32_t length = end - begin; + csp_macro_cast(ctx, shape, text, length, ¶ms); if (shape->object_like) { /* Up to a comment or the end of the line: names joined by single dots, nothing else. */ uint32_t last = 0; @@ -822,9 +967,8 @@ static const CBMCMacroShape *csp_macro_shape(csp_context_t *ctx, TSNode node) { bool after_star = csp_token_is(&back1, "*") || parenthesized; const csp_token_t *before_star = parenthesized ? &back3 : &back2; csp_token_t following = next; - uint32_t peek = at; if (parenthesized && csp_token_is(&following, ")")) - (void)csp_macro_token(text, length, peek, &following); + (void)csp_macro_token(text, length, at, &following); bool star_declares = after_star && before_star->identifier && !csp_token_precedes_expression(before_star) && csp_token_ends_declarator(&following); @@ -919,9 +1063,10 @@ static const CBMCMacroShape *csp_macro_shape(csp_context_t *ctx, TSNode node) { * and stays unbound for it, which costs a value and proves nothing false. */ (after_star && before_star->identifier && csp_token_is(&next, ")") && !csp_token_precedes_expression(before_star))) { - const char *local = cbm_arena_strndup(ctx->arena, current.text, current.length); - if (!local || - !csp_append_file(ctx->arena, &locals, &local_count, &local_cap, local)) { + const char *declared_name = + cbm_arena_strndup(ctx->arena, current.text, current.length); + if (!declared_name || !csp_append_file(ctx->arena, &locals, &local_count, + &local_cap, declared_name)) { ctx->failed = true; return NULL; } @@ -1041,6 +1186,24 @@ static void csp_note_macro(csp_context_t *ctx, TSNode node) { } } +static void csp_note_quoted_include(csp_context_t *ctx, TSNode node) { + TSNode path = csp_field(node, "path"); + if (!ctx->facts || !csp_is(path, "string_literal")) + return; + const char *text = csp_text(ctx, path); + size_t length = text ? strlen(text) : 0; + const char *spelled = length >= 3 ? cbm_arena_strndup(ctx->arena, text + 1, length - 2) : NULL; + if (spelled && !csp_append_file(ctx->arena, &ctx->facts->quoted_includes, + &ctx->facts->quoted_include_count, + &ctx->facts->quoted_include_cap, spelled)) + ctx->failed = true; +} + +static bool csp_text_starts_with(const char *at, const char *end, const char *word) { + size_t length = strlen(word); + return (size_t)(end - at) >= length && !memcmp(at, word, length); +} + /* A `#define` inside a function body stays defined to the end of the file: malloc_vsnprintf's * APPEND_C, do_sub's ADJUST_SUB_FIRSTLNUM. Most bodies hold none, and the text says so. */ bool cbm_c_text_defines_macro(const char *text, uint32_t length) { @@ -1049,7 +1212,7 @@ bool cbm_c_text_defines_macro(const char *text, uint32_t length) { const char *word = at + 1; while (word < end && (*word == ' ' || *word == '\t')) word++; - if (end - word >= 6 && !memcmp(word, "define", 6)) + if (csp_text_starts_with(word, end, "define")) return true; at = word; } @@ -1091,8 +1254,6 @@ static void csp_free_function(csp_context_t *ctx, TSNode node, const char *owner const char *qn = name ? cbm_arena_sprintf(ctx->arena, "%s.%s", owner, name) : NULL; if (csp_is(node, "declaration") && name) csp_note_prototype(ctx, node, decl, owner, name); - else if (name && ctx->branch_group) - (void)csp_add_decl(ctx, CBM_C_DECL_DEFINED_IN_BRANCH, csp_relative(ctx, owner, name), 0); csp_entry_t *entry = csp_entry(ctx, qn, ts_node_start_point(node).row + 1); /* A macro over a builtin type word (`#define int double`) falsifies what the * signature below would say, so the definition's linkage facts stay unknown. */ @@ -1344,7 +1505,7 @@ static void csp_class(csp_context_t *ctx, TSNode node, const char *parent, return; bool complete = true, body_complete = true; uint32_t access = - csp_is(node, "struct_specifier") ? CBM_SOURCE_DECL_PUBLIC : CBM_SOURCE_DECL_PRIVATE; + csp_is(node, "class_specifier") ? CBM_SOURCE_DECL_PRIVATE : CBM_SOURCE_DECL_PUBLIC; TSTreeCursor header = ts_tree_cursor_new(node); if (ts_tree_cursor_goto_first_child(&header)) do { @@ -1399,10 +1560,10 @@ static void csp_class(csp_context_t *ctx, TSNode node, const char *parent, * an ERROR. Definition extraction recovers that function from the expanded source, so it has an * entry here to find: the signature standing in the ERROR says how it is linked. */ static void csp_note_stranded_definitions(csp_context_t *ctx, TSNode error, const char *owner) { - bool is_static = false; - uint32_t first_line = 0; TSTreeCursor cursor = ts_tree_cursor_new(error); - if (ts_tree_cursor_goto_first_child(&cursor)) + if (ts_tree_cursor_goto_first_child(&cursor)) { + bool is_static = false; + uint32_t first_line = 0; do { TSNode child = ts_tree_cursor_current_node(&cursor); if (!ts_node_is_named(child) || csp_is(child, "comment")) @@ -1410,6 +1571,7 @@ static void csp_note_stranded_definitions(csp_context_t *ctx, TSNode error, cons if (csp_is(child, "storage_class_specifier")) { const char *word = csp_text(ctx, child); is_static |= word && !strcmp(word, "static"); + continue; } else if (csp_is(child, "primitive_type") || csp_is(child, "type_identifier") || csp_is(child, "sized_type_specifier") || csp_is(child, "type_qualifier") || csp_is(child, "struct_specifier")) { @@ -1442,6 +1604,7 @@ static void csp_note_stranded_definitions(csp_context_t *ctx, TSNode error, cons is_static = false; first_line = 0; } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } ts_tree_cursor_delete(&cursor); } @@ -1528,6 +1691,10 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne csp_note_macro(ctx, node); return; } + if (csp_is(node, "preproc_include")) { + csp_note_quoted_include(ctx, node); + return; + } if (csp_preprocessor_branch(node)) { bool saved = ctx->conditional; uint32_t saved_group = ctx->branch_group, saved_branch = ctx->branch; @@ -1559,6 +1726,9 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne if (csp_is(node, "type_definition")) { TSNode type = csp_field(node, "type"), declarator = csp_field(node, "declarator"); csp_note_enumerators(ctx, type, owner); + if (csp_is(declarator, "type_identifier") && !ts_node_is_null(csp_field(type, "body")) && + ts_node_is_null(csp_field(type, "name"))) + (void)csp_add_decl(ctx, CBM_C_DECL_UNNAMED_TYPEDEF, csp_text(ctx, declarator), 0); /* An error among the members does not change which struct this is. */ if (csp_is(type, "class_specifier") || csp_is(type, "struct_specifier") || csp_is(type, "union_specifier")) diff --git a/internal/cbm/lsp/c_proof.h b/internal/cbm/lsp/c_proof.h index 71d98bc5c..8222a98d0 100644 --- a/internal/cbm/lsp/c_proof.h +++ b/internal/cbm/lsp/c_proof.h @@ -23,7 +23,7 @@ typedef enum { CBM_C_DECL_CLASS_TEMPLATE, /* `template class Name`: a base of this name can depend */ CBM_C_DECL_TEMPLATE_PARAMETER, /* `template class Pair`: name is Value, type_name * is Pair. What it means comes with each instantiation. */ - CBM_C_DECL_DEFINED_IN_BRANCH, /* a function defined inside a conditional */ + CBM_C_DECL_UNNAMED_TYPEDEF, /* `typedef struct {...} Name;`: no tag is called Name */ } CBMCDeclKind; enum { @@ -95,6 +95,12 @@ typedef struct CBMCMacroShape { /* `#define w_p_list w_onebuf_opt.wo_list`: an object-like replacement that is nothing but * dotted names, which can stand after `->` and name members there. */ const char *field_path; + /* `#define hl_table ((HlGroup *)(ga.ga_data))`, `#define SDS_HDR(T, s) ((struct sdshdr##T + * *)...)`: a replacement that is one cast as a whole writes the type of what it gives. */ + const char *cast_type; + uint8_t cast_pointers; + bool cast_pastes; /* the type's name is cast_type, then the argument cast_param */ + uint8_t cast_param; } CBMCMacroShape; typedef struct CBMCDecl { @@ -135,6 +141,9 @@ typedef struct CBMCFileFacts { int include_count, include_cap; const char **generated_for; /* source files declared through a generated header */ int generated_count, generated_cap; + const char **quoted_includes; /* every `#include "x"`, as spelled */ + int quoted_include_count, quoted_include_cap; + bool has_unknown_include; /* one of them has nothing in the index behind it */ } CBMCFileFacts; /* As cbm_c_collect_source_facts, also filling facts when it is not NULL. */ diff --git a/internal/cbm/resolve/capabilities.c b/internal/cbm/resolve/capabilities.c index 55ae6dabe..46da89020 100644 --- a/internal/cbm/resolve/capabilities.c +++ b/internal/cbm/resolve/capabilities.c @@ -299,7 +299,10 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t * out->limitation = "Exact visibility and owner proofs; overloads of one parameter count " "with separate nodes, a template's dependent names, specialized class " "templates, ADL with a second visible function, callable-field " - "selection and what only some definitions of a macro name abstain."; + "selection, what only some definitions of a macro name, a name that is " + "a macro in one preprocessor branch and a function in another, and a " + "macro that an included header the index cannot read may redefine " + "abstain."; break; case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 17d77435e..b58c117f2 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -1205,6 +1205,17 @@ static void pxc_collect_c_file(const cbm_pipeline_ctx_t *ctx, CBMArena *arena, c (void)cbm_c_facts_add_generated(arena, facts, cbm_arena_strdup(arena, generated)); free(generated); } + /* A quoted include nothing proves is text this index cannot read. */ + for (int q = 0; q < facts->quoted_include_count && !facts->has_unknown_include; q++) { + bool known = false; + for (size_t i = 0; snapshot && i < snapshot->count && !known; i++) { + const cbm_resolve_import_fact_t *fact = &snapshot->items[i]; + known = fact->module_path && !strcmp(fact->module_path, facts->quoted_includes[q]) && + cbm_resolve_admit(&fact->dependency) && cbm_resolve_admit(&fact->binding) && + fact->dependency.target; + } + facts->has_unknown_include = !known; + } CBMLSPDef *record = &defs[count]; memset(record, 0, sizeof(*record)); record->qualified_name = cbm_arena_sprintf(arena, "%s.@c-file-facts", module); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 0ad950bb2..fc9837b00 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -16954,7 +16954,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " void (*open)(int fd);"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -16967,7 +16967,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 6, .declaration_text = " int (*shut)(int fd);"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -16981,7 +16981,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 4, .declaration_text = " void (*open)(int fd);"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -16994,7 +16994,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 15, .declaration_text = " struct Dev fixed[2];"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17007,7 +17007,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 10, .declaration_text = " struct Ops *ops;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17020,7 +17020,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 16, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17036,7 +17036,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 11, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17052,7 +17052,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 11, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17100,7 +17100,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 6, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17116,7 +17116,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 6, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-DESIGNATOR-OWNER", .why = c_field_designator_why, .tried = c_field_designator_tried}, @@ -17129,7 +17129,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Function", .declaration_line = 3, .declaration_text = "static void my_open(int fd) { (void)fd; }"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-INITIALIZER-VALUE", .why = c_field_global_value_why, .tried = c_field_global_value_tried}, @@ -17142,7 +17142,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Function", .declaration_line = 4, .declaration_text = "static int my_shut(int fd) { return fd; }"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-INITIALIZER-VALUE", .why = c_field_global_value_why, .tried = c_field_global_value_tried}, @@ -17156,10 +17156,56 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Function", .declaration_line = 3, .declaration_text = "static void their_open(int fd) { (void)fd; }"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-INITIALIZER-VALUE", .why = c_field_global_value_why, .tried = c_field_global_value_tried}, +#define C_FIELD_TAG(id, file_, caller_, begin, end, target_file, target_suffix, line, written) \ + {.case_id = id, \ + .site = {.file = file_, .caller = caller_, .start_byte = begin, .end_byte = end}, \ + .relation = "USAGE", \ + .target = {.file = target_file, \ + .suffix = target_suffix, \ + .label = "Class", \ + .declaration_line = line, \ + .declaration_text = written}, \ + .mode = RQ_REQUIRED} + /* `struct Dev` in a signature, a body or a compound literal names a tag this file sees. */ + C_FIELD_TAG("c.field.tag-use-alpha", "use_alpha.c", "use_alpha.use_alpha", 41, 46, "alpha.h", + "alpha.Alpha", 4, "struct Alpha {"), + C_FIELD_TAG("c.field.tag-guarded-alpha", "guarded.c", "guarded.guarded_alpha", 82, 87, "alpha.h", + "alpha.Alpha", 4, "struct Alpha {"), + C_FIELD_TAG("c.field.tag-hdr-alpha", "inline.h", "inline.hdr_alpha", 106, 111, "alpha.h", + "alpha.Alpha", 4, "struct Alpha {"), + C_FIELD_TAG("c.field.tag-hdr-own-struct", "inline.h", "inline.hdr_own_struct", 484, 491, "inline.h", + "inline.HdrNode", 17, "struct HdrNode {"), + C_FIELD_TAG("c.field.tag-arrow-ops", "use_ops.c", "use_ops.arrow_ops", 63, 66, "ops.h", + "ops.Ops", 4, "struct Ops {"), + C_FIELD_TAG("c.field.tag-arrow-other", "use_ops.c", "use_ops.arrow_other", 116, 124, "other_ops.h", + "other_ops.OtherOps", 3, "struct OtherOps {"), + C_FIELD_TAG("c.field.tag-arrow-chain", "use_ops.c", "use_ops.arrow_chain", 173, 176, "ops.h", + "ops.Dev", 9, "struct Dev {"), + C_FIELD_TAG("c.field.tag-arrow-ret", "use_ops.c", "use_ops.arrow_ret", 235, 238, "ops.h", + "ops.Ops", 4, "struct Ops {"), + C_FIELD_TAG("c.field.tag-index-ptr", "use_ops.c", "use_ops.index_ptr", 297, 301, "ops.h", + "ops.Bank", 13, "struct Bank {"), + C_FIELD_TAG("c.field.tag-index-arr", "use_ops.c", "use_ops.index_arr", 358, 362, "ops.h", + "ops.Bank", 13, "struct Bank {"), + C_FIELD_TAG("c.field.tag-second-decl", "use_ops.c", "use_ops.second_decl", 422, 426, "ops.h", + "ops.Bank", 13, "struct Bank {"), + C_FIELD_TAG("c.field.tag-init-block", "init_local.c", "init_local.init_block", 75, 78, "ops.h", + "ops.Dev", 9, "struct Dev {"), + C_FIELD_TAG("c.field.tag-init-literal-param", "init_local.c", "init_local.init_literal", 141, 145, "ops.h", + "ops.Bank", 13, "struct Bank {"), + C_FIELD_TAG("c.field.tag-init-literal-cast", "init_local.c", "init_local.init_literal", 175, 178, "ops.h", + "ops.Dev", 9, "struct Dev {"), + C_FIELD_TAG("c.field.tag-init-array", "init_local.c", "init_local.init_array", 230, 238, "other_ops.h", + "other_ops.OtherOps", 3, "struct OtherOps {"), + C_FIELD_TAG("c.field.tag-exact-local", "field_control.c", "field_control.exact_local_field", 91, 101, "field_control.c", + "field_control.ExactLocal", 2, "struct ExactLocal { int count; };"), + C_FIELD_TAG("c.field.tag-exact-external", "field_control.c", "field_control.exact_external_field", 159, 172, "field_external.h", + "field_external.ExactExternal", 1, "struct ExactExternal { int count; };"), +#undef C_FIELD_TAG {.case_id = "c.field.initializer-table-address", /* Literal source: &table_ops */ .site = {.file = "table_ops.c", .caller = "table_ops", .start_byte = 245, .end_byte = 254}, @@ -27608,17 +27654,14 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { .tried = "The original qualified/unqualified source and declaration membership checks " "remain; the method/type bridge cannot authorize an enum value."}, {.case_id = "cpp.enum.round", - .site = {.file = "use.cpp", - .caller = "use.pick", - .needle = "gfx::Brush::ROUND", - .occurrence_index = 0}, + .site = {.file = "use.cpp", .caller = "use.pick", .needle = "ROUND", .occurrence_index = 0}, .relation = "USAGE", .target = {.file = "shapes.hpp", .suffix = "shapes.gfx.Brush.ROUND", .label = "Variable", .declaration_line = 6, .declaration_text = " enum Shape { ROUND, SQUARE };"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-ENUM-LEXICAL-VALUE", .why = "The enumerator has an exact declaration but no admitted enum-value lexical " "binding proof.", @@ -27708,6 +27751,15 @@ TEST(pipeline_c_enum_inside_struct_reference_resolves_c1) { const RQ_Source sources[] = {{.path = files[0], .text = contents[0]}}; const RQ_Expected expected[] = { + {.case_id = "c.enum.tag-of-the-parameter", + .site = {.file = "link.c", .caller = "link.busy", .start_byte = 119, .end_byte = 123}, + .relation = "USAGE", + .target = {.file = "link.c", + .suffix = "link.conn", + .label = "Class", + .declaration_line = 1, + .declaration_text = "struct conn {"}, + .mode = RQ_REQUIRED}, {.case_id = "c.enum.named-field", .site = {.file = "link.c", .caller = "link.busy", .start_byte = 144, .end_byte = 146}, .relation = "USAGE", diff --git a/tests/test_resolver_c.c b/tests/test_resolver_c.c index beb8f1904..c08721465 100644 --- a/tests/test_resolver_c.c +++ b/tests/test_resolver_c.c @@ -10,6 +10,7 @@ #include "pipeline/pipeline.h" #include "test_helpers.h" #include "resolver_quality.h" +#include "lsp/lsp_work.h" extern const TSLanguage *tree_sitter_c(void); extern const TSLanguage *tree_sitter_cpp(void); @@ -810,6 +811,14 @@ static const char C_PP_PERSIST_GUESSED[] = static const char C_PP_PERSIST_MISSING[] = "#define CALL() missing_target()\n#include \"missing.h\"\n" "void missing_target(void) {}\nvoid missing_run(void) { CALL(); }\n"; +/* The same header ahead of the definition can do nothing to it, unless the definition is + * conditional: then the header may be what decides the branch. */ +static const char C_PP_PERSIST_AFTER[] = + "#include \"missing.h\"\n#define CALL() after_target()\n" + "void after_target(void) {}\nvoid after_run(void) { CALL(); }\n"; +static const char C_PP_PERSIST_GUARDED[] = + "#include \"missing.h\"\n#ifndef CALL\n#define CALL() guarded_target()\n#endif\n" + "void guarded_target(void) {}\nvoid guarded_run(void) { CALL(); }\n"; TEST(resolver_c_preprocessed_proofs_require_complete_explicit_configuration) { const char *defined[] = {"LIB_API=1", NULL}; @@ -918,6 +927,8 @@ TEST(resolver_c_preprocessed_proofs_require_complete_explicit_configuration) { TEST(resolver_c_unverified_preprocessing_cannot_persist_relationships) { const RQ_Source sources[] = {{.path = "guessed.c", .text = C_PP_PERSIST_GUESSED}, {.path = "missing.c", .text = C_PP_PERSIST_MISSING}, + {.path = "after.c", .text = C_PP_PERSIST_AFTER}, + {.path = "guarded.c", .text = C_PP_PERSIST_GUARDED}, {.path = "raw.c", .text = C_PP_RAW}}; const RQ_Expected expected[] = { {.case_id = "raw-direct-positive", @@ -928,19 +939,17 @@ TEST(resolver_c_unverified_preprocessing_cannot_persist_relationships) { .label = "Function", .declaration_line = 1, .declaration_text = "void raw_target(void) {}"}}, - /* No preprocessed text speaks here. The raw #define does, the way a direct call of this - * function in this file would bind in spite of the same missing header. */ - {.case_id = "raw-define-beside-a-missing-header", - .site = {.file = "missing.c", - .caller = "missing.missing_run", + {.case_id = "raw-define-after-a-missing-header", + .site = {.file = "after.c", + .caller = "after.after_run", .needle = "CALL()", .occurrence_index = 1}, .relation = "CALLS", - .target = {.file = "missing.c", - .suffix = "missing.missing_target", + .target = {.file = "after.c", + .suffix = "after.after_target", .label = "Function", .declaration_line = 3, - .declaration_text = "void missing_target(void) {}"}}}; + .declaration_text = "void after_target(void) {}"}}}; const RQ_Forbidden forbidden[] = { {.case_id = "guessed-config-negative", .site = {.file = "guessed.c", @@ -949,6 +958,21 @@ TEST(resolver_c_unverified_preprocessing_cannot_persist_relationships) { .occurrence_index = 2}, .relation = "CALLS", .any_target = true}, + /* `missing.h` stands between the definition and the use, and may redefine CALL. */ + {.case_id = "missing-header-negative", + .site = {.file = "missing.c", + .caller = "missing.missing_run", + .needle = "CALL()", + .occurrence_index = 1}, + .relation = "CALLS", + .any_target = true}, + {.case_id = "a-missing-header-may-decide-the-branch", + .site = {.file = "guarded.c", + .caller = "guarded.guarded_run", + .needle = "CALL()", + .occurrence_index = 1}, + .relation = "CALLS", + .any_target = true}, }; for (int driver = 0; driver < 2; driver++) { const char *root = th_mktempdir("cbm_c_pp_persist"); @@ -1981,6 +2005,14 @@ TEST(resolver_c_known_gaps_stay_honest) { .label = "Field", .declaration_line = 4, .declaration_text = " struct tabpage_S *tp_next;"}}, + {.case_id = "the-tag-in-the-signature", + .site = C_TU_WORD("use_gaps.c", use, "use_gaps.use_union", "struct item *o", "item"), + .relation = "USAGE", + .target = {.file = "gaps.h", + .suffix = "gaps.item", + .label = "Class", + .declaration_line = 10, + .declaration_text = "struct item {"}}, {.case_id = "anonymous-aggregate-member", .site = C_TU_WORD("use_gaps.c", use, "use_gaps.use_union", "o->value", "value"), .relation = "USAGE", @@ -3129,6 +3161,18 @@ TEST(resolver_c_struct_shapes_real_headers_use) { .site = C_TU_WORD("list.c", impl, "list.read_opaque", "(opaque *o", "opaque"), .relation = "USAGE", .target = opaque_class}, + {.case_id = "a-tag-in-its-own-file", + .site = C_TU_WORD("list.c", impl, "list.read_tag", "struct opaque *o", "opaque"), + .relation = "USAGE", + .target = opaque_class}, + {.case_id = "the-name-of-a-struct-with-two-layouts", + .site = C_TU_WORD("use_shapes.c", use, "use_shapes.use_swapped", "struct swapped *s", "swapped"), + .relation = "USAGE", + .target = {.file = "swap.h", + .suffix = "swap.swapped", + .label = "Class", + .declaration_line = 6, + .declaration_text = "struct swapped { int size; };"}}, {.case_id = "member-through-the-typedef-name-in-its-own-file", .site = C_TU_WORD("list.c", impl, "list.read_node", "n->", "sz"), .relation = "USAGE", @@ -3568,6 +3612,11 @@ TEST(resolver_c_fragment_sees_what_its_one_includer_saw) { "#define ONE_BRANCH(x)\n" \ "#define BOTH_BRANCHES(x) report_failure(\"b\")\n" \ "#endif\n" \ + "#ifdef FEATURE\n" \ + "#define LATER_BRANCH(x)\n" \ + "#else\n" \ + "#define LATER_BRANCH(x) report_failure(\"x\")\n" \ + "#endif\n" \ "#endif\n" #define C_TU_BODY_API_C \ "void report_failure(const char *what) { (void)what; }\n" \ @@ -3594,6 +3643,7 @@ TEST(resolver_c_fragment_sees_what_its_one_includer_saw) { "struct holder { int report_failure; };\n" \ "int member(struct holder *h) { return MEMBER_OF(h); }\n" \ "void one_branch(int v) { ONE_BRANCH(v); }\n" \ + "void later_branch(int v) { LATER_BRANCH(v); }\n" \ "void both_branches(int v) { BOTH_BRANCHES(v); }\n" \ "int read_len(list_T *l) { return LIST_LEN(l); }\n" \ "int read_next(list_T *l) { return NEXT_LEN(l); }\n" \ @@ -3644,6 +3694,15 @@ static const RQ_Source C_TU_BODY[] = {{.path = "body_api.h", .text = C_TU_BODY_A TEST(resolver_c_macro_replacement_compiles_at_its_use) { const char *body_use = C_TU_BODY_USE; + const RQ_Expected holder_row = { + .case_id = "the-tag-in-the-signature", + .site = C_TU_WORD("body_use.c", body_use, "body_use.member", "struct holder *h", "holder"), + .relation = "USAGE", + .target = {.file = "body_use.c", + .suffix = "body_use.holder", + .label = "Class", + .declaration_line = 14, + .declaration_text = "struct holder { int report_failure; };"}}; const RQ_Target list_type = {.file = "body_api.h", .suffix = "body_api.list_T", .label = "Type", @@ -3668,6 +3727,7 @@ TEST(resolver_c_macro_replacement_compiles_at_its_use) { .declaration_line = 2, .declaration_text = "int counter;"}; const RQ_Expected expected[] = { + holder_row, {.case_id = "call-in-a-replacement", .site = {.file = "body_use.c", .caller = "body_use.checked", .needle = "CHECK(v)"}, .relation = "CALLS", @@ -3852,6 +3912,10 @@ TEST(resolver_c_macro_replacement_compiles_at_its_use) { .site = {.file = "body_use.c", .caller = "body_use.one_branch", .needle = "ONE_BRANCH(v)"}, .relation = "CALLS", .any_target = true}, + {.case_id = "only-the-other-definition-names-it", + .site = {.file = "body_use.c", .caller = "body_use.later_branch", .needle = "LATER_BRANCH(v)"}, + .relation = "CALLS", + .any_target = true}, {.case_id = "local-at-the-use-takes-the-name", .site = {.file = "body_use.c", .caller = "body_use.shadowed", .needle = "READ_COUNTER()"}, .relation = "USAGE", @@ -3909,41 +3973,47 @@ TEST(resolver_c_macro_replacement_compiles_at_its_use) { "#define sometimes(x) (x)\n" \ "#endif\n" \ "int sometimes(int x);\n" \ + "int plain_control(int x);\n" \ "#endif\n" #define C_TU_ALT_C \ "int (get_len)(int x) { return x; }\n" \ "int (always)(int x) { return x; }\n" \ - "int (sometimes)(int x) { return x; }\n" + "int (sometimes)(int x) { return x; }\n" \ + "int (plain_control)(int x) { return x; }\n" #define C_TU_ALT_USE \ "#include \"alt.h\"\n" \ "int use_len(int v) { return get_len(v); }\n" \ "int use_swap(int v) { return swap_bytes(v); }\n" \ "int use_always(int v) { return always(v); }\n" \ - "int use_sometimes(int v) { return sometimes(v); }\n" + "int use_sometimes(int v) { return sometimes(v); }\n" \ + "int use_plain(int v) { return plain_control(v); }\n" static const RQ_Source C_TU_ALT[] = {{.path = "alt.h", .text = C_TU_ALT_H}, {.path = "alt.c", .text = C_TU_ALT_C}, {.path = "alt_use.c", .text = C_TU_ALT_USE}}; -TEST(resolver_c_macro_in_one_branch_leaves_the_function_in_another) { +/* A macro in one branch and the function in the other: in one configuration this is a call and in the + * other it may be no call at all, so with no compile configuration the name is unknown. An edge here + * would hold only by chance. See also pipeline_c_call_targets_definition_before_macro_c1. */ +TEST(resolver_c_macro_in_one_branch_leaves_the_name_unknown) { const RQ_Expected expected[] = { + {.case_id = "a-name-no-macro-touches", + .site = {.file = "alt_use.c", .caller = "alt_use.use_plain", .needle = "plain_control(v)"}, + .relation = "CALLS", + .target = {.file = "alt.c", + .suffix = "alt.plain_control", + .label = "Function", + .declaration_line = 4, + .declaration_text = "int (plain_control)(int x) { return x; }"}}, + }; + const RQ_Forbidden forbidden[] = { {.case_id = "prototype-in-the-other-branch", .site = {.file = "alt_use.c", .caller = "alt_use.use_len", .needle = "get_len(v)"}, .relation = "CALLS", - .target = {.file = "alt.c", - .suffix = "alt.get_len", - .label = "Function", - .declaration_line = 1, - .declaration_text = "int (get_len)(int x) { return x; }"}}, + .any_target = true}, {.case_id = "definition-in-the-other-branch", .site = {.file = "alt_use.c", .caller = "alt_use.use_swap", .needle = "swap_bytes(v)"}, .relation = "CALLS", - .target = {.file = "alt.h", - .suffix = "alt.swap_bytes", - .label = "Function", - .declaration_line = 11, - .declaration_text = "static int swap_bytes(int v) { return v; }"}}, - }; - const RQ_Forbidden forbidden[] = { + .any_target = true}, {.case_id = "macro-outside-any-conditional", .site = {.file = "alt_use.c", .caller = "alt_use.use_always", .needle = "always(v)"}, .relation = "CALLS", @@ -4622,11 +4692,14 @@ TEST(resolver_cpp_member_template_is_a_method_of_its_class) { "#endif\n" #define C_TU_SW_TABLE \ "#include \"sw.h\"\n" \ - "static const int table[] = {\n" \ - "#define OP(n) n,\n" \ + "typedef struct { const char *a; int b; } node_t;\n" \ + "static const char *NAME(const char *n) { return n; }\n" \ + "static int CTL(int v) { return v; }\n" \ + "static const node_t table[] = {\n" \ + "#define OP(n) {NAME(#n), CHILD(named, stats_##n)},\n" \ "LIST\n" \ "#undef OP\n" \ - " 0\n" \ + " {NAME(\"reset\"), CTL(1)}\n" \ "};\n" \ "static int after_table(int v) { return v; }\n" \ "int call_after(int v) { return after_table(v); }\n" @@ -4637,7 +4710,32 @@ TEST(resolver_cpp_member_template_is_a_method_of_its_class) { "static int inside(int v) { return v; }\n" \ "int call_inside(int v) { return inside(v); }\n" \ "}\n" +#define C_TU_SW_HEADS \ + "#include \"sw.h\"\n" \ + "static int helper(int v) { return v; }\n" \ + "#ifndef CONFIG_ONE\n" \ + "void prefork(void)\n" \ + "#else\n" \ + "void other_prefork(void)\n" \ + "#endif\n" \ + "{\n" \ + " helper(1);\n" \ + "}\n" \ + "void caller(void) { prefork(); }\n" +#define C_TU_SW_LUA \ + "#include \"sw.h\"\n" \ + "#define QUOTED(x) \"'\" x \"'\"\n" \ + "static void report(const char *m) { (void)m; }\n" \ + "static void before(int c) {\n" \ + " switch (c) {\n" \ + " case 1: report(QUOTED(\"=\") \" or \" QUOTED(\"in\") \" expected\"); break;\n" \ + " default: report(\"no \" QUOTED(\"x\")); break;\n" \ + " }\n" \ + "}\n" \ + "static void after_it(void) { report(\"x\"); before(1); }\n" static const RQ_Source C_TU_SW[] = {{.path = "sw.h", .text = C_TU_SW_H}, + {.path = "sw_heads.c", .text = C_TU_SW_HEADS}, + {.path = "sw_lua.c", .text = C_TU_SW_LUA}, {.path = "sw_table.c", .text = C_TU_SW_TABLE}, {.path = "sw_ns.cpp", .text = C_TU_SW_NS}}; @@ -4649,8 +4747,76 @@ TEST(resolver_c_a_broken_head_does_not_swallow_the_functions_after_it) { .target = {.file = "sw_table.c", .suffix = "sw_table.after_table", .label = "Function", - .declaration_line = 8, + .declaration_line = 11, .declaration_text = "static int after_table(int v) { return v; }"}}, + {.case_id = "a-call-in-the-table-belongs-to-the-file", + .site = {.file = "sw_table.c", .caller = "sw_table", .needle = "NAME(\"reset\")"}, + .relation = "CALLS", + .target = {.file = "sw_table.c", + .suffix = "sw_table.NAME", + .label = "Function", + .declaration_line = 3, + .declaration_text = "static const char *NAME(const char *n) { return n; }"}}, + {.case_id = "and-so-does-the-next-one", + .site = {.file = "sw_table.c", .caller = "sw_table", .needle = "CTL(1)"}, + .relation = "CALLS", + .target = {.file = "sw_table.c", + .suffix = "sw_table.CTL", + .label = "Function", + .declaration_line = 4, + .declaration_text = "static int CTL(int v) { return v; }"}}, + {.case_id = "the-body-belongs-to-the-first-head", + .site = {.file = "sw_heads.c", .caller = "sw_heads.prefork", .needle = "helper(1)"}, + .relation = "CALLS", + .target = {.file = "sw_heads.c", + .suffix = "sw_heads.helper", + .label = "Function", + .declaration_line = 2, + .declaration_text = "static int helper(int v) { return v; }"}}, + {.case_id = "a-call-of-the-first-head", + .site = {.file = "sw_heads.c", .caller = "sw_heads.caller", .needle = "prefork()"}, + .relation = "CALLS", + .target = {.file = "sw_heads.c", + .suffix = "sw_heads.prefork", + .label = "Function", + .declaration_line = 4, + .declaration_text = "void prefork(void)"}}, + {.case_id = "a-call-with-a-macro-among-its-strings", + .site = {.file = "sw_lua.c", + .caller = "sw_lua.before", + .needle = "report(QUOTED(\"=\") \" or \" QUOTED(\"in\") \" expected\")"}, + .relation = "CALLS", + .target = {.file = "sw_lua.c", + .suffix = "sw_lua.report", + .label = "Function", + .declaration_line = 3, + .declaration_text = "static void report(const char *m) { (void)m; }"}}, + {.case_id = "a-macro-after-a-string", + .site = {.file = "sw_lua.c", + .caller = "sw_lua.before", + .needle = "report(\"no \" QUOTED(\"x\"))"}, + .relation = "CALLS", + .target = {.file = "sw_lua.c", + .suffix = "sw_lua.report", + .label = "Function", + .declaration_line = 3, + .declaration_text = "static void report(const char *m) { (void)m; }"}}, + {.case_id = "the-call-after-those-strings", + .site = {.file = "sw_lua.c", .caller = "sw_lua.after_it", .needle = "report(\"x\")"}, + .relation = "CALLS", + .target = {.file = "sw_lua.c", + .suffix = "sw_lua.report", + .label = "Function", + .declaration_line = 3, + .declaration_text = "static void report(const char *m) { (void)m; }"}}, + {.case_id = "the-function-before-them", + .site = {.file = "sw_lua.c", .caller = "sw_lua.after_it", .needle = "before(1)"}, + .relation = "CALLS", + .target = {.file = "sw_lua.c", + .suffix = "sw_lua.before", + .label = "Function", + .declaration_line = 4, + .declaration_text = "static void before(int c) {"}}, {.case_id = "a-function-after-a-word-before-namespace", .site = {.file = "sw_ns.cpp", .caller = "sw_ns.lib.call_inside", .needle = "inside(v)"}, .relation = "CALLS", @@ -4810,7 +4976,27 @@ static const RQ_Source C_TU_KV[] = {{.path = "kv.h", .text = C_TU_KV_H}, TEST(resolver_c_macro_typed_member_keeps_its_own_name) { const char *use = C_TU_KV_USE; + const RQ_Target buffer = {.file = "kv.h", + .suffix = "kv.buffer", + .label = "Class", + .declaration_line = 5, + .declaration_text = "struct buffer {"}; const RQ_Expected expected[] = { + {.case_id = "the-tag-of-the-first-reader", + .site = C_TU_WORD("kv_use.c", use, "kv_use.read_channels", + "buffer *b) { return &b->update_channels", "buffer"), + .relation = "USAGE", + .target = buffer}, + {.case_id = "the-tag-of-the-second-reader", + .site = C_TU_WORD("kv_use.c", use, "kv_use.read_callbacks", + "buffer *b) { return &b->update_callbacks", "buffer"), + .relation = "USAGE", + .target = buffer}, + {.case_id = "the-tag-of-the-third-reader", + .site = C_TU_WORD("kv_use.c", use, "kv_use.read_pointers", "buffer *b) { return &b->pointer_vec", + "buffer"), + .relation = "USAGE", + .target = buffer}, {.case_id = "a-member-whose-name-the-grammar-split-off", .site = C_TU_WORD("kv_use.c", use, "kv_use.read_channels", "update_channels; }", "update_channels"), @@ -4972,6 +5158,795 @@ TEST(resolver_cpp_template_field_reads_the_template_and_not_its_parameter_spelli PASS(); } +/* `struct X *p` had no occurrence at all: the specifier's name counted as a declaration, so a tag in a + * signature, a body, a cast or sizeof had no edge. Only a body or a lone `struct X;` declares. A tag is + * not a typedef: `struct Thing *t` beside `typedef struct impl Thing;` is a tag Thing of its own. */ +#define C_TU_TAG_H \ + "#ifndef TAG_H\n" \ + "#define TAG_H\n" \ + "struct header_tag { int size; };\n" \ + "union shared_u { int a; };\n" \ + "enum shared_e { E_ONE };\n" \ + "struct real_impl { int r; };\n" \ + "typedef struct real_impl Thing;\n" \ + "typedef struct { int n; } NoTag;\n" \ + "typedef union { int n; } NoTagU;\n" \ + "typedef enum { NT_ONE } NoTagE;\n" \ + "struct second_tag { int size; };\n" \ + "struct fourth_tag { int size; };\n" \ + "struct fifth_tag { int size; };\n" \ + "#endif\n" +#define C_TU_TAG_USE \ + "#include \"tag.h\"\n" \ + "struct local_tag { int a; };\n" \ + "struct local_tag;\n" \ + "int signature(struct header_tag *h) { return h != 0; }\n" \ + "int in_body(void) { union shared_u u; enum shared_e e; (void)u; (void)e; return 0; }\n" \ + "unsigned long sized(void) { return sizeof(struct header_tag); }\n" \ + "int cast_of(void *v) { return ((struct header_tag *)v) != 0; }\n" \ + "int own_file(struct local_tag *l) { return l != 0; }\n" \ + "int opaque(struct only_here *o) { return o != 0; }\n" \ + "int through_alias_tag(struct Thing *t) { return t->r; }\n" \ + "int shadowed(void) { struct header_tag { int other; }; struct header_tag mine; return 0; }\n" \ + "void *in_macro_argument(void *p) { return container_of(p, struct header_tag, size); }\n" \ + "void *alias_in_macro_argument(void *p) { return container_of(p, struct Thing, r); }\n" \ + "int lone_in_body(void) { struct second_tag; struct second_tag *mine = 0; return mine != 0; }\n"\ + "int after_the_block(struct second_tag *s) { return s != 0; }\n" \ + "int no_tag(struct NoTag *t) { return t != 0; }\n" \ + "int no_tag_member(struct NoTag *u) { return u->n; }\n" \ + "int no_tag_union(union NoTagU *x) { return x != 0; }\n" \ + "int no_tag_enum(enum NoTagE *y) { return y != 0; }\n" \ + "int typedef_name(NoTag *v) { return v->n; }\n" \ + "int hidden_member(void) { struct fourth_tag { int size; }; struct fourth_tag w; return w.size; }\n" \ + "int variable_of_that_spelling(void) { struct fifth_tag fifth_tag; return fifth_tag.size; }\n" \ + "typedef struct { int n; } OwnNoTag;\n" \ + "typedef union { int n; } OwnNoTagU;\n" \ + "int own_no_tag(struct OwnNoTag *o) { return o != 0; }\n" \ + "int own_no_tag_member(struct OwnNoTag *p) { return p->n; }\n" \ + "int own_no_tag_union(union OwnNoTagU *q) { return q != 0; }\n" +static const RQ_Source C_TU_TAG[] = {{.path = "tag.h", .text = C_TU_TAG_H}, + {.path = "tag_use.c", .text = C_TU_TAG_USE}}; + +TEST(resolver_c_struct_tag_is_a_use_unless_it_declares) { + const char *use = C_TU_TAG_USE; + const RQ_Target header_tag = {.file = "tag.h", + .suffix = "tag.header_tag", + .label = "Class", + .declaration_line = 3, + .declaration_text = "struct header_tag { int size; };"}; +#define TAG_USE(id, caller, context, word, target_) \ + {.case_id = id, \ + .site = C_TU_WORD("tag_use.c", use, "tag_use." caller, context, word), \ + .relation = "USAGE", \ + .target = target_} + const RQ_Expected expected[] = { + TAG_USE("a-tag-in-a-signature", "signature", "header_tag *h", "header_tag", header_tag), + TAG_USE("a-union-in-a-body", "in_body", "shared_u u", "shared_u", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.shared_u", + .label = "Class", + .declaration_line = 4, + .declaration_text = "union shared_u { int a; };"})), + TAG_USE("an-enum-in-a-body", "in_body", "shared_e e", "shared_e", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.shared_e", + .label = "Enum", + .declaration_line = 5, + .declaration_text = "enum shared_e { E_ONE };"})), + TAG_USE("a-tag-in-sizeof", "sized", "sizeof(struct header_tag", "header_tag", header_tag), + TAG_USE("a-tag-in-a-cast", "cast_of", "(struct header_tag *)v", "header_tag", header_tag), + TAG_USE("a-tag-among-call-arguments", "in_macro_argument", "struct header_tag, size", + "header_tag", header_tag), + TAG_USE("a-tag-of-this-file", "own_file", "local_tag *l", "local_tag", + ((RQ_Target){.file = "tag_use.c", + .suffix = "tag_use.local_tag", + .label = "Class", + .declaration_line = 2, + .declaration_text = "struct local_tag { int a; };"})), + TAG_USE("a-lone-tag-hides-only-in-its-block", "after_the_block", "second_tag *s", "second_tag", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.second_tag", + .label = "Class", + .declaration_line = 11, + .declaration_text = "struct second_tag { int size; };"})), + TAG_USE("a-typedef-name-is-the-type", "typedef_name", "NoTag *v", "NoTag", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.NoTag", + .label = "Class", + .declaration_line = 8, + .declaration_text = "typedef struct { int n; } NoTag;"})), + TAG_USE("a-member-through-the-typedef-name", "typedef_name", "v->n", "n", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.NoTag.n", + .label = "Field", + .declaration_line = 8, + .declaration_text = "typedef struct { int n; } NoTag;"})), + TAG_USE("a-tag-beside-a-variable-of-its-spelling", "variable_of_that_spelling", + "struct fifth_tag fifth_tag", "fifth_tag", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.fifth_tag", + .label = "Class", + .declaration_line = 13, + .declaration_text = "struct fifth_tag { int size; };"})), + TAG_USE("that-variable-is-no-tag-of-its-own", "variable_of_that_spelling", "fifth_tag.size", + "size", + ((RQ_Target){.file = "tag.h", + .suffix = "tag.fifth_tag.size", + .label = "Field", + .declaration_line = 13, + .declaration_text = "struct fifth_tag { int size; };"})), + }; +#undef TAG_USE +#define TAG_NEVER(id, caller, context, word) \ + {.case_id = id, \ + .site = C_TU_WORD("tag_use.c", use, caller, context, word), \ + .relation = "USAGE", \ + .any_target = true} + const RQ_Forbidden forbidden[] = { + TAG_NEVER("a-lone-tag-declares-it", "tag_use", "struct local_tag;", "local_tag"), + TAG_NEVER("a-tag-this-unit-does-not-declare", "tag_use.opaque", "only_here *o", "only_here"), + TAG_NEVER("a-tag-is-not-the-typedef-of-its-spelling", "tag_use.through_alias_tag", "Thing *t", + "Thing"), + TAG_NEVER("nothing-through-that-tag", "tag_use.through_alias_tag", "t->r", "r"), + TAG_NEVER("nor-among-call-arguments", "tag_use.alias_in_macro_argument", "struct Thing, r", + "Thing"), + TAG_NEVER("a-tag-defined-in-the-function", "tag_use.shadowed", "struct header_tag mine", + "header_tag"), + TAG_NEVER("a-lone-tag-in-a-block-is-a-new-one", "tag_use.lone_in_body", "second_tag *mine", + "second_tag"), + TAG_NEVER("an-unnamed-struct-has-no-tag", "tag_use.no_tag", "NoTag *t", "NoTag"), + TAG_NEVER("nor-through-it", "tag_use.no_tag_member", "NoTag *u", "NoTag"), + TAG_NEVER("nor-a-member-through-it", "tag_use.no_tag_member", "u->n", "n"), + TAG_NEVER("an-unnamed-union-has-no-tag", "tag_use.no_tag_union", "NoTagU *x", "NoTagU"), + TAG_NEVER("an-unnamed-enum-has-no-tag", "tag_use.no_tag_enum", "NoTagE *y", "NoTagE"), + TAG_NEVER("a-member-of-a-local-struct-is-not-the-outer-one", "tag_use.hidden_member", "w.size", + "size"), + TAG_NEVER("nor-does-this-file-s-own-typedef-give-a-tag", "tag_use.own_no_tag", "OwnNoTag *o", + "OwnNoTag"), + TAG_NEVER("nor-through-its-own", "tag_use.own_no_tag_member", "OwnNoTag *p", "OwnNoTag"), + TAG_NEVER("nor-a-member-through-its-own", "tag_use.own_no_tag_member", "p->n", "n"), + TAG_NEVER("nor-its-own-union", "tag_use.own_no_tag_union", "OwnNoTagU *q", "OwnNoTagU"), + }; +#undef TAG_NEVER + const CTUCase fixture = {.suite = "c-tu-struct-tags", + .sources = C_TU_TAG, + .source_count = sizeof(C_TU_TAG) / sizeof(C_TU_TAG[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* `static int f(int x)` with an #ifdef splitting its braces is left loose in an ERROR, and the + * signature there says how it is linked. It must still read as static, or a caller in another file + * binds to it: through a prototype with no other definition, and in place of the real definition. */ +#define C_TU_STRANDED_BODY \ + "\n" \ + "{\n" \ + "#ifdef A\n" \ + " if (x) {\n" \ + "#else\n" \ + " if (!x) {\n" \ + "#endif\n" \ + " return 1;\n" \ + " }\n" \ + " return 0;\n" \ + "}\n" +#define C_TU_STRANDED_CALLER \ + "#include \"sapi.h\"\n" \ + "int run_shared(void) { return shared(1); }\n" \ + "int run_alone(void) { return alone(2); }\n" +static const RQ_Source C_TU_STRANDED[] = { + {.path = "sapi.h", + .text = "#ifndef SAPI_H\n#define SAPI_H\nint shared(int x);\nint alone(int x);\n#endif\n"}, + {.path = "split_shared.c", .text = "static int shared(int x)" C_TU_STRANDED_BODY}, + {.path = "split_alone.c", .text = "static int alone(int x)" C_TU_STRANDED_BODY}, + {.path = "real.c", .text = "int shared(int x) { return x; }\n"}, + {.path = "scaller.c", .text = C_TU_STRANDED_CALLER}, +}; + +TEST(resolver_c_a_static_left_in_an_error_stays_static) { + const char *caller = C_TU_STRANDED_CALLER; + const RQ_Expected expected[] = { + {.case_id = "the-real-definition-not-the-static-one", + .site = C_TU_WORD("scaller.c", caller, "scaller.run_shared", "shared(1)", "shared(1)"), + .relation = "CALLS", + .target = {.file = "real.c", + .suffix = "real.shared", + .label = "Function", + .declaration_line = 1, + .declaration_text = "int shared(int x) { return x; }"}}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "a-static-in-another-file-is-not-the-definition", + .site = C_TU_WORD("scaller.c", caller, "scaller.run_alone", "alone(2)", "alone(2)"), + .relation = "CALLS", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-stranded-static", + .sources = C_TU_STRANDED, + .source_count = sizeof(C_TU_STRANDED) / sizeof(C_TU_STRANDED[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Whether a body holds a `#define`, to its last byte: a `#` with nothing after it, a word cut off by the + * end of the text, a directive with space after the `#`, one after another directive. */ +TEST(resolver_c_text_defines_macro_reads_to_the_end_of_the_text) { + static const struct { + const char *text; + bool defines; + } cases[] = { + {"", false}, {"#", false}, {"# ", false}, {"#defin", false}, + {"#define", true}, {"# \tdefine X", true}, {"x = 1;\n#define Y 2", true}, + {"#include \n#define Z", true}, {"#include \n", false}, + {"# # define", true}, {"a # b", false}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + bool found = cbm_c_text_defines_macro(cases[i].text, (uint32_t)strlen(cases[i].text)); + ASSERT_EQ(found, cases[i].defines); + } + PASS(); +} + +/* A header runs on the pass over one file alone, with no source file on its types and in C++ mode: + * `struct other *p;` as a member and in a prototype name the header's own tag. One in a namespace is + * out of reach of code outside it there as well. */ +#define C_TU_TAG_HEADER \ + "struct other { int a; };\n" \ + "namespace within { struct nested { int b; }; }\n" \ + "struct holder {\n" \ + " struct other *member;\n" \ + " struct nested *wrong_member;\n" \ + "};\n" \ + "int prototype(struct other *q);\n" \ + "namespace within { struct inner_holder { struct nested *right_member; }; }\n" +static const RQ_Source C_TU_TAG_HEADERS[] = {{.path = "taghdr.hpp", .text = C_TU_TAG_HEADER}}; + +TEST(resolver_cpp_tag_in_a_header_is_looked_up_from_its_own_module) { + const char *source = C_TU_TAG_HEADER; + const RQ_Target other = {.file = "taghdr.hpp", + .suffix = "taghdr.other", + .label = "Class", + .declaration_line = 1, + .declaration_text = "struct other { int a; };"}; + const RQ_Expected expected[] = { + {.case_id = "a-member-names-the-header-s-own-tag", + .site = C_TU_WORD("taghdr.hpp", source, "taghdr", "other *member", "other"), + .relation = "USAGE", + .target = other}, + {.case_id = "a-prototype-names-it-too", + .site = C_TU_WORD("taghdr.hpp", source, "taghdr", "other *q", "other"), + .relation = "USAGE", + .target = other}, + {.case_id = "a-namespace-reaches-its-own-tag", + .site = C_TU_WORD("taghdr.hpp", source, "taghdr", "nested *right_member", "nested"), + .relation = "USAGE", + .target = {.file = "taghdr.hpp", + .suffix = "taghdr.within.nested", + .label = "Class", + .declaration_line = 2, + .declaration_text = "namespace within { struct nested { int b; }; }"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "C-NAMESPACED-CLASS-MEMBER-TYPE", + .why = "A member declared in a class inside a namespace has no type row. At the top level of " + "a file it has one.", + .tried = "Read on the binaries before and after the tag rule: the row is absent on both, so " + "it is not the lookup. Sized and left for a later rule."}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "outside-the-namespace-it-is-another-tag", + .site = C_TU_WORD("taghdr.hpp", source, "taghdr", "nested *wrong_member", "nested"), + .relation = "USAGE", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-tag-in-header", + .sources = C_TU_TAG_HEADERS, + .source_count = 1, + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .known_gaps = 1, + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* A lone tag declares it, wherever it stands. Nothing resolves there, so the decoys above stay + * quiet with or without this: what it changes is an occurrence with no answer, counted as unresolved. */ +static int c_usage_rows_named(const CBMFileResult *result, const char *name) { + int count = 0; + for (int i = 0; i < result->usages.count; i++) + count += result->usages.items[i].ref_name && !strcmp(result->usages.items[i].ref_name, name); + return count; +} + +TEST(resolver_c_lone_tag_is_no_occurrence) { + static const char source[] = "struct lone_file;\n" + "struct used_file *a;\n" + "int f(void) { struct lone_body; struct used_body *b; return b != 0; }\n" + "struct holder { struct lone_member; struct used_member *c; };\n"; + CBMFileResult *c_file = + cbm_extract_file(source, (int)strlen(source), CBM_LANG_C, "p", "tag.c", 0, NULL, NULL); + ASSERT(c_file != NULL); + ASSERT_EQ(c_usage_rows_named(c_file, "lone_file"), 0); + ASSERT_EQ(c_usage_rows_named(c_file, "lone_body"), 0); + ASSERT_EQ(c_usage_rows_named(c_file, "lone_member"), 0); + ASSERT_EQ(c_usage_rows_named(c_file, "used_file"), 1); + ASSERT_EQ(c_usage_rows_named(c_file, "used_body"), 1); + ASSERT_EQ(c_usage_rows_named(c_file, "used_member"), 1); + cbm_free_result(c_file); + static const char cxx_source[] = "class lone_class;\n" + "template struct lone_template;\n" + "namespace n { struct lone_in_namespace; }\n" + "class used_class *d;\n"; + CBMFileResult *cxx_file = cbm_extract_file(cxx_source, (int)strlen(cxx_source), CBM_LANG_CPP, "p", + "tag.cpp", 0, NULL, NULL); + ASSERT(cxx_file != NULL); + ASSERT_EQ(c_usage_rows_named(cxx_file, "lone_class"), 0); + ASSERT_EQ(c_usage_rows_named(cxx_file, "lone_template"), 0); + ASSERT_EQ(c_usage_rows_named(cxx_file, "lone_in_namespace"), 0); + ASSERT_EQ(c_usage_rows_named(cxx_file, "used_class"), 1); + cbm_free_result(cxx_file); + PASS(); +} + +/* C++ looks a tag up as it looks up anything: a class in a block hides the outer one from there on, and + * one in a namespace is out of reach of code outside it. `struct Name` needs no earlier declaration, so + * it compiles where a plain `Name` would not, and it means a new type there. */ +#define C_TU_TL \ + "struct Outer { int o; };\n" \ + "class Klass { public: int k; };\n" \ + "namespace ns {\n" \ + "struct InNs { int n; };\n" \ + "int inside(struct InNs *a) { return a->n; }\n" \ + "}\n" \ + "namespace used { struct Used { int u; }; }\n" \ + "using namespace used;\n" \ + "int via_using(struct Used *b) { return b->u; }\n" \ + "int outside(struct InNs *c) { return c != nullptr; }\n" \ + "int outside_member(struct InNs *d) { return d->n; }\n" \ + "int local_class() { class Klass { public: int k; }; Klass e; return e.k; }\n" \ + "int lone_class() { class Klass; Klass *f = nullptr; return f != nullptr; }\n" \ + "int lone_struct() { struct Outer; Outer *g = nullptr; return g != nullptr; }\n" \ + "int plain_class() { Klass h; return h.k; }\n" \ + "int elaborated_class() { class Klass *i = nullptr; return i->k; }\n" +static const RQ_Source C_TU_TAG_LOOKUP[] = {{.path = "tl.cpp", .text = C_TU_TL}}; + +TEST(resolver_cpp_tag_lookup_stops_where_the_language_does) { + const char *source = C_TU_TL; + const RQ_Target klass = {.file = "tl.cpp", + .suffix = "tl.Klass", + .label = "Class", + .declaration_line = 2, + .declaration_text = "class Klass { public: int k; };"}; + const RQ_Target klass_k = {.file = "tl.cpp", + .suffix = "tl.Klass.k", + .label = "Field", + .declaration_line = 2, + .declaration_text = "class Klass { public: int k; };"}; +#define TL_USE(id, caller, context, word, target_) \ + {.case_id = id, \ + .site = C_TU_WORD("tl.cpp", source, "tl." caller, context, word), \ + .relation = "USAGE", \ + .target = target_} +#define TL_NEVER(id, caller, context, word) \ + {.case_id = id, \ + .site = C_TU_WORD("tl.cpp", source, "tl." caller, context, word), \ + .relation = "USAGE", \ + .any_target = true} + const RQ_Expected expected[] = { + TL_USE("a-tag-in-the-namespace-around", "ns.inside", "InNs *a", "InNs", + ((RQ_Target){.file = "tl.cpp", + .suffix = "tl.ns.InNs", + .label = "Class", + .declaration_line = 4, + .declaration_text = "struct InNs { int n; };"})), + TL_USE("a-member-through-it", "ns.inside", "a->n", "n", + ((RQ_Target){.file = "tl.cpp", + .suffix = "tl.ns.InNs.n", + .label = "Field", + .declaration_line = 4, + .declaration_text = "struct InNs { int n; };"})), + TL_USE("a-tag-by-a-using-directive", "via_using", "Used *b", "Used", + ((RQ_Target){.file = "tl.cpp", + .suffix = "tl.used.Used", + .label = "Class", + .declaration_line = 7, + .declaration_text = "namespace used { struct Used { int u; }; }"})), + TL_USE("a-member-by-a-using-directive", "via_using", "b->u", "u", + ((RQ_Target){.file = "tl.cpp", + .suffix = "tl.used.Used.u", + .label = "Field", + .declaration_line = 7, + .declaration_text = "namespace used { struct Used { int u; }; }"})), + TL_USE("the-outer-class-by-name", "plain_class", "Klass h", "Klass", klass), + TL_USE("a-member-of-it", "plain_class", "h.k", "k", klass_k), + TL_USE("the-outer-class-by-its-tag", "elaborated_class", "Klass *i", "Klass", klass), + TL_USE("a-member-through-the-tag", "elaborated_class", "i->k", "k", klass_k), + }; + const RQ_Forbidden forbidden[] = { + TL_NEVER("a-tag-out-of-reach-is-a-new-one", "outside", "InNs *c", "InNs"), + TL_NEVER("and-through-the-other-route", "outside_member", "InNs *d", "InNs"), + TL_NEVER("with-no-member-through-it", "outside_member", "d->n", "n"), + TL_NEVER("a-local-class-hides-the-outer-one", "local_class", "Klass e", "Klass"), + TL_NEVER("and-its-members-with-it", "local_class", "e.k", "k"), + TL_NEVER("a-lone-class-in-a-block-too", "lone_class", "Klass *f", "Klass"), + TL_NEVER("a-lone-struct-in-a-block-too", "lone_struct", "Outer *g", "Outer"), + }; +#undef TL_USE +#undef TL_NEVER + const CTUCase fixture = {.suite = "c-tu-tag-lookup", + .sources = C_TU_TAG_LOOKUP, + .source_count = sizeof(C_TU_TAG_LOOKUP) / sizeof(C_TU_TAG_LOOKUP[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* `hl_table[i].x` with `#define hl_table ((HlGroup *)(ga.ga_data))`: the replacement writes the type, + * and with a paste `SDS_HDR(8, s)->len` writes it through the argument. It has to be one cast as a whole + * and in parentheses: `(T *)p` used as `X->y` casts p->y. Every definition in force has to agree. */ +#define C_TU_CAST_H \ + "#ifndef C_H\n" \ + "#define C_H\n" \ + "typedef struct { int a; } Group;\n" \ + "struct sdshdr8 { int len; };\n" \ + "struct sdshdr16 { int len; };\n" \ + "typedef struct { void *data; } GA;\n" \ + "extern GA ga;\n" \ + "typedef struct { int a; } T;\n" \ + "typedef struct { int a; } Other;\n" \ + "#define table ((Group *)(ga.data))\n" \ + "#define ITEMS(g) ((Group *)((g)->data))\n" \ + "#define HDR(T, s) ((struct sdshdr##T *)((s) - sizeof(struct sdshdr##T)))\n" \ + "#define BARE (Group *)ga.data\n" \ + "#define PARAM_TYPE(T, p) ((T *)(p))\n" \ + "#define SUM(p) ((Group *)(p) + 1)\n" \ + "#ifdef FEATURE\n" \ + "#define BRANCHED ((Group *)ga.data)\n" \ + "#else\n" \ + "#define BRANCHED ((Other *)ga.data)\n" \ + "#endif\n" \ + "#endif\n" +#define C_TU_CAST_USE \ + "#include \"cast.h\"\n" \ + "int from_table(int i) { return table[i].a; }\n" \ + "int from_call(GA *g, int i) { return ITEMS(g)[i].a; }\n" \ + "int pasted8(char *s) { return HDR(8, s)->len; }\n" \ + "int pasted16(char *s) { return HDR(16, s)->len; }\n" \ + "int bare(void) { return BARE->a; }\n" \ + "int param(GA *g) { return PARAM_TYPE(Group, g)->a; }\n" \ + "int summed(void) { return SUM(ga.data)->a; }\n" \ + "int branched(void) { return BRANCHED->a; }\n" +static const RQ_Source C_TU_CAST[] = {{.path = "cast.h", .text = C_TU_CAST_H}, + {.path = "cast_use.c", .text = C_TU_CAST_USE}}; + +TEST(resolver_c_macro_that_is_one_cast_writes_the_receiver_type) { + const char *use = C_TU_CAST_USE; + const RQ_Target group = {.file = "cast.h", + .suffix = "cast.Group", + .label = "Class", + .declaration_line = 3, + .declaration_text = "typedef struct { int a; } Group;"}; + const RQ_Target member_a = {.file = "cast.h", + .suffix = "cast.Group.a", + .label = "Field", + .declaration_line = 3, + .declaration_text = "typedef struct { int a; } Group;"}; + const RQ_Target ga_type = {.file = "cast.h", + .suffix = "cast.GA", + .label = "Class", + .declaration_line = 6, + .declaration_text = "typedef struct { void *data; } GA;"}; + const RQ_Target ga_data = {.file = "cast.h", + .suffix = "cast.GA.data", + .label = "Field", + .declaration_line = 6, + .declaration_text = "typedef struct { void *data; } GA;"}; +#define CAST_USE(id, caller, context, word, target_) \ + {.case_id = id, \ + .site = C_TU_WORD("cast_use.c", use, "cast_use." caller, context, word), \ + .relation = "USAGE", \ + .target = target_} + const RQ_Expected expected[] = { + CAST_USE("the-type-an-object-macro-casts-to", "from_table", "table[i]", "table", group), + CAST_USE("a-member-through-an-object-macro", "from_table", "a; }\nint from_call", "a", member_a), + CAST_USE("the-parameter-type", "from_call", "GA *g", "GA", ga_type), + {.case_id = "the-type-a-function-macro-casts-to", + .site = {.file = "cast_use.c", .caller = "cast_use.from_call", .needle = "ITEMS(g)"}, + .relation = "USAGE", + .dispatch_group = "what-items-reads", + .target = group}, + {.case_id = "the-member-a-function-macro-reads", + .site = {.file = "cast_use.c", .caller = "cast_use.from_call", .needle = "ITEMS(g)"}, + .relation = "USAGE", + .dispatch_group = "what-items-reads", + .target = ga_data}, + CAST_USE("a-member-through-a-function-macro", "from_call", "a; }\nint pasted8", "a", member_a), + CAST_USE("a-paste-of-one-argument", "pasted8", "HDR(8, s)->len", "len", + ((RQ_Target){.file = "cast.h", + .suffix = "cast.sdshdr8.len", + .label = "Field", + .declaration_line = 4, + .declaration_text = "struct sdshdr8 { int len; };"})), + CAST_USE("a-paste-of-another", "pasted16", "HDR(16, s)->len", "len", + ((RQ_Target){.file = "cast.h", + .suffix = "cast.sdshdr16.len", + .label = "Field", + .declaration_line = 5, + .declaration_text = "struct sdshdr16 { int len; };"})), + CAST_USE("the-type-in-a-bare-cast", "bare", "BARE->a", "BARE", group), + CAST_USE("the-parameter-of-the-parameter-case", "param", "GA *g) { return PARAM", "GA", ga_type), + {.case_id = "the-type-in-a-sum", + .site = {.file = "cast_use.c", .caller = "cast_use.summed", .needle = "SUM(ga.data)"}, + .relation = "USAGE", + .target = group}, + CAST_USE("the-argument-of-the-sum", "summed", "ga.data)->a", "data", ga_data), + }; +#undef CAST_USE +#define CAST_NEVER(id, caller, context) \ + {.case_id = id, \ + .site = C_TU_WORD("cast_use.c", use, "cast_use." caller, context, "a; }"), \ + .relation = "USAGE", \ + .any_target = true} + const RQ_Forbidden forbidden[] = { + {.case_id = "no-parentheses-means-the-member-is-cast", + .site = C_TU_WORD("cast_use.c", use, "cast_use.bare", "BARE->a", "a"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "a-parameter-as-the-type", + .site = C_TU_WORD("cast_use.c", use, "cast_use.param", "g)->a", "a"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "a-sum-is-not-read", + .site = C_TU_WORD("cast_use.c", use, "cast_use.summed", ")->a", "a"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "definitions-that-cast-differently", + .site = C_TU_WORD("cast_use.c", use, "cast_use.branched", "BRANCHED->a", "a"), + .relation = "USAGE", + .any_target = true}, + }; +#undef CAST_NEVER + const CTUCase fixture = {.suite = "c-tu-cast-macros", + .sources = C_TU_CAST, + .source_count = sizeof(C_TU_CAST) / sizeof(C_TU_CAST[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* `#define M0(x) M1(x) M1(x)` and so on, down `levels` macros to `leaf`. */ +static size_t c_handoff_chain(char *out, size_t cap, const char *name, int levels, int fan, + const char *leaf) { + size_t at = 0; + for (int level = 0; level < levels; level++) { + at += (size_t)snprintf(out + at, cap - at, "#define %s%d(x)", name, level); + for (int i = 0; i < fan; i++) + at += (size_t)snprintf(out + at, cap - at, " %s%d(x)", name, level + 1); + at += (size_t)snprintf(out + at, cap - at, "\n"); + } + at += (size_t)snprintf(out + at, cap - at, "#define %s%d(x) %s\n", name, levels, leaf); + return at; +} + +/* The work of one use at the top of a chain whose every macro hands its argument on twice. */ +static uint64_t c_handoff_work(int levels, const char *use) { + char source[8192]; + size_t at = c_handoff_chain(source, sizeof(source), "M", levels, 2, "sink(x)"); + snprintf(source + at, sizeof(source) - at, + "void sink(int v);\nint value;\nvoid run(void) { %s }\n", use); + CBMArena arena; + cbm_arena_init(&arena); + CBMLSPDef defs[] = {{.qualified_name = "test.main.@c-file-facts", + .short_name = "@c-file-facts", + .label = CBM_C_FILE_FACTS_LABEL, + .def_module_qn = "test.main"}}; + CBMCFileFacts facts = {0}; + bool collected = cbm_c_collect_file_facts(&arena, source, (int)strlen(source), "test.main", + CBM_LANG_C, defs, 1, &facts); + defs[0].c_file_facts = &facts; + CBMResolvedCallArray rows = {0}; + (void)cbm_lsp_work_take(); + if (collected) + c_cross(&arena, source, false, defs, 1, NULL, NULL, 0, &rows); + uint64_t work = cbm_lsp_work_take(); + cbm_arena_destroy(&arena); + return collected ? work : 0; +} + +/* Following every hand-off doubles with each hop: 2^16 reads of a declaration bucket for one use, + * under the old limit. Each (macro, position) is read once now, so the work follows the chain's + * length, on both walks. Counted, not timed. */ +TEST(resolver_c_macro_handoffs_are_read_once_per_macro) { + const char *const uses[] = {"M0(value);", "M0(item) { sink(1); }"}; + for (size_t i = 0; i < sizeof(uses) / sizeof(uses[0]); i++) { + uint64_t shorter = c_handoff_work(6, uses[i]); + uint64_t longer = c_handoff_work(12, uses[i]); + printf(" %s work: 6 levels=%llu 12 levels=%llu\n", uses[i], + (unsigned long long)shorter, (unsigned long long)longer); + ASSERT_GT(shorter, 0); + ASSERT(longer < shorter * 3); + } + PASS(); +} + +/* What a hand-off proves does not depend on how many there are: the old limit of 16 was a work bound + * that decided the answer. A cycle ends where the preprocessor ends it, since a macro is not expanded + * inside its own expansion, and what a definition in the cycle does to its argument still counts. */ +TEST(resolver_c_macro_handoff_chain_has_no_length_limit) { + static char source[16384]; + size_t at = (size_t)snprintf(source, sizeof(source), + "struct Item { int a; };\n" + "int value;\n" + "int cycled;\n" + "int broken;\n" + "void sink(int v);\n"); + at += c_handoff_chain(source + at, sizeof(source) - at, "P", 40, 1, "sink(x)"); + at += c_handoff_chain(source + at, sizeof(source) - at, "L", 40, 1, + "for (struct Item *(x) = 0; (x); (x) = 0)"); + snprintf(source + at, sizeof(source) - at, + "#define A0(x) A1(x)\n" + "#define A1(x) A0(x)\n" + "#define B0(x) B1(x)\n" + "#define B1(x) B0(x) x##_y\n" + "void read_value(void) { P0(value); }\n" + "void read_member(void) { L0(it) { sink(it->a); } }\n" + "void read_cycled(void) { A0(cycled); }\n" + "void read_broken(void) { B0(broken); }\n"); + const RQ_Source sources[] = {{.path = "chain.c", .text = source}}; +#define CHAIN_USE(id, caller, context, word, suffix_, label_, line, declaration) \ + {.case_id = id, \ + .site = C_TU_WORD("chain.c", source, "chain." caller, context, word), \ + .relation = "USAGE", \ + .target = {.file = "chain.c", \ + .suffix = suffix_, \ + .label = label_, \ + .declaration_line = line, \ + .declaration_text = declaration}} + const RQ_Expected expected[] = { + CHAIN_USE("forty-hops-reach-the-value", "read_value", "P0(value)", "value", "chain.value", + "Variable", 2, "int value;"), + CHAIN_USE("forty-hops-reach-the-declaration", "read_member", "it->a", "a", "chain.Item.a", + "Field", 1, "struct Item { int a; };"), + CHAIN_USE("a-cycle-ends-at-the-argument", "read_cycled", "A0(cycled)", "cycled", + "chain.cycled", "Variable", 3, "int cycled;"), + }; +#undef CHAIN_USE + const RQ_Forbidden forbidden[] = { + {.case_id = "a-paste-in-the-cycle-still-counts", + .site = C_TU_WORD("chain.c", source, "chain.read_broken", "B0(broken)", "broken"), + .relation = "USAGE", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-macro-handoff-chains", + .sources = sources, + .source_count = 1, + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* The includes of a sole includer are marked while that step adds them, and only then. The step's + * usual exit is the top of the chain, in the middle of its loop. */ +TEST(resolver_c_includer_context_ends_with_its_step) { + const char *source = "int f(void) { return 0; }\n"; + TSParser *parser = ts_parser_new(); + ASSERT_NOT_NULL(parser); + ts_parser_set_language(parser, tree_sitter_c()); + TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source)); + ts_parser_delete(parser); + ASSERT_NOT_NULL(tree); + CBMArena arena; + cbm_arena_init(&arena); + CBMLSPDef defs[] = {{.qualified_name = "test.main.@c-file-facts", + .short_name = "@c-file-facts", + .label = CBM_C_FILE_FACTS_LABEL, + .def_module_qn = "test.main"}}; + CBMCFileFacts facts = {0}; + bool collected = cbm_c_collect_file_facts(&arena, source, (int)strlen(source), "test.main", + CBM_LANG_C, defs, 1, &facts); + defs[0].c_file_facts = &facts; + CBMTypeRegistry registry; + cbm_registry_init(®istry, &arena); + registry.c_decls = cbm_c_decl_index_build(&arena, defs, 1); + cbm_registry_finalize(®istry); + CBMResolvedCallArray out = {0}; + CLSPContext ctx; + c_lsp_init(&ctx, &arena, source, (int)strlen(source), ®istry, "test.main", false, &out); + c_lsp_process_file(&ctx, ts_tree_root_node(tree)); + bool facts_found = cbm_c_file_facts(ctx.c_decls, "test.main") != NULL; + bool still_marking = ctx.tu_adding_includer_context; + cbm_arena_destroy(&arena); + ts_tree_delete(tree); + ASSERT(collected); + ASSERT(facts_found); + ASSERT(!still_marking); + PASS(); +} + +/* A C++ union with member functions is a class: its methods read its members, and its members are + * public by default. The class path of the LSP listed only `class` and `struct`. */ +#define C_TU_UN \ + "union Number {\n" \ + " int i;\n" \ + " int helper() { return i; }\n" \ + " int get() { return helper(); }\n" \ + "};\n" \ + "int caller(Number &n) { return n.get(); }\n" +static const RQ_Source C_TU_UNION[] = {{.path = "un.cpp", .text = C_TU_UN}}; + +TEST(resolver_cpp_union_is_a_class_with_public_members) { + const char *source = C_TU_UN; + const RQ_Expected expected[] = { + {.case_id = "a-member-from-a-method", + .site = C_TU_WORD("un.cpp", source, "un.Number.helper", "return i;", "i"), + .relation = "USAGE", + .target = {.file = "un.cpp", + .suffix = "un.Number.i", + .label = "Field", + .declaration_line = 2, + .declaration_text = " int i;"}}, + {.case_id = "a-sibling-from-a-method", + .site = {.file = "un.cpp", + .caller = "un.Number.get", + .needle = "helper()", + .occurrence_index = 1}, + .relation = "CALLS", + .target = {.file = "un.cpp", + .suffix = "un.Number.helper", + .label = "Method", + .declaration_line = 3, + .declaration_text = " int helper() { return i; }"}}, + {.case_id = "a-method-from-outside", + .site = {.file = "un.cpp", .caller = "un.caller", .needle = "n.get()"}, + .relation = "CALLS", + .target = {.file = "un.cpp", + .suffix = "un.Number.get", + .label = "Method", + .declaration_line = 4, + .declaration_text = " int get() { return helper(); }"}}, + {.case_id = "the-union-in-a-signature", + .site = C_TU_WORD("un.cpp", source, "un.caller", "Number &n", "Number"), + .relation = "USAGE", + .target = {.file = "un.cpp", + .suffix = "un.Number", + .label = "Class", + .declaration_line = 1, + .declaration_text = "union Number {"}}, + }; + const CTUCase fixture = {.suite = "c-tu-cxx-unions", + .sources = C_TU_UNION, + .source_count = sizeof(C_TU_UNION) / sizeof(C_TU_UNION[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + /* A class in an anonymous namespace is declared in the file around it. Each .cpp has its own, and * another file's class of the same name stays out of it. */ #define C_TU_ANON_ONE \ @@ -5655,7 +6630,7 @@ SUITE(resolver_c) { RUN_TEST(resolver_c_struct_shapes_real_headers_use); RUN_TEST(resolver_c_parse_repairs_keep_declarations); RUN_TEST(resolver_c_macro_replacement_compiles_at_its_use); - RUN_TEST(resolver_c_macro_in_one_branch_leaves_the_function_in_another); + RUN_TEST(resolver_c_macro_in_one_branch_leaves_the_name_unknown); RUN_TEST(resolver_c_macro_after_the_parameters_keeps_what_follows); RUN_TEST(resolver_c_designated_member_binds_through_the_type_being_initialized); RUN_TEST(resolver_cpp_anonymous_namespace_declares_in_its_file); @@ -5668,6 +6643,17 @@ SUITE(resolver_c) { RUN_TEST(resolver_c_macro_after_an_arrow_reads_the_members_it_names); RUN_TEST(resolver_c_macro_typed_member_keeps_its_own_name); RUN_TEST(resolver_cpp_template_field_reads_the_template_and_not_its_parameter_spelling); + RUN_TEST(resolver_c_struct_tag_is_a_use_unless_it_declares); + RUN_TEST(resolver_c_lone_tag_is_no_occurrence); + RUN_TEST(resolver_cpp_tag_in_a_header_is_looked_up_from_its_own_module); + RUN_TEST(resolver_c_a_static_left_in_an_error_stays_static); + RUN_TEST(resolver_c_text_defines_macro_reads_to_the_end_of_the_text); + RUN_TEST(resolver_c_macro_handoffs_are_read_once_per_macro); + RUN_TEST(resolver_c_macro_handoff_chain_has_no_length_limit); + RUN_TEST(resolver_c_includer_context_ends_with_its_step); + RUN_TEST(resolver_cpp_tag_lookup_stops_where_the_language_does); + RUN_TEST(resolver_c_macro_that_is_one_cast_writes_the_receiver_type); + RUN_TEST(resolver_cpp_union_is_a_class_with_public_members); RUN_TEST(resolver_c_macro_behind_a_switch_this_unit_leaves_off); RUN_TEST(resolver_cpp_templates_bind_what_is_not_dependent); RUN_TEST(resolver_c_fragment_sees_what_its_one_includer_saw);