diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index b83732d64..985b11852 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -1155,8 +1155,17 @@ typedef struct { TSPoint skip_point; /* The `#if` line, left out as well once its branch turns out not to be complete. */ TSRange directive; + bool has_alternative; + /* Braces this block opened less those it closed, outside any block inside it. */ + int brace_net; } CBMAttrConditional; +/* A block with no `#else` that left braces open, waiting at its own depth for one to close them. */ +typedef struct { + int open_braces; + TSRange open, close; +} CBMAttrSplitBlock; + /* 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) { @@ -1215,15 +1224,76 @@ static uint32_t cbm_attr_text_endif(const char *source, uint32_t length, uint32_ return length; } +/* Where the enum body opening here ends, when the grammar cannot hold it: an `#include` beside a + * `#define` or another `#include`. Otherwise 0, and one directive alone stays where it is. An + * enumerator list holds neither `;` nor `{`, and stopping at them keeps the scans of one file + * linear in its length. */ +static uint32_t cbm_attr_enum_directives_end(const char *source, uint32_t length, uint32_t open) { + bool defines = false, line_start = false; + int includes = 0; + for (uint32_t at = open + 1; at < length; at++) { + char ch = source[at]; + if (ch == '\n') { + line_start = true; + } else if (ch == '/' && at + 1 < length && source[at + 1] == '/') { + while (at + 1 < length && source[at + 1] != '\n') + at++; + } else if (ch == '/' && at + 1 < length && source[at + 1] == '*') { + for (at += 2; at + 1 < length && !(source[at] == '*' && source[at + 1] == '/'); at++) {} + at++; + } else if (ch == '#' && line_start) { + uint32_t word = at + 1; + while (word < length && (source[word] == ' ' || source[word] == '\t')) + word++; + defines |= word + 6 <= length && !memcmp(source + word, "define", 6); + includes += word + 7 <= length && !memcmp(source + word, "include", 7); + /* A directive runs to the end of its line, continuations included. */ + while (at + 1 < length && source[at + 1] != '\n') { + at += source[at + 1] == '\\' && at + 2 < length && source[at + 2] == '\n' ? 2 : 1; + } + } else if (ch != ' ' && ch != '\t' && ch != '\r') { + line_start = false; + if (ch == '{' || ch == ';') { + return 0; + } + if (ch == '}') { + return includes > 0 && (defines || includes > 1) ? at : 0; + } + } + } + return 0; +} + +/* `#ifndef X / union { / #endif / ... / #ifndef X / }; / #endif`: a brace opened in one block and + * closed in another. No grammar holds half of a block, and how error recovery reads it depends on + * how much follows: past a dozen members DeclSpec.h lost its whole namespace. With both blocks' + * directives out, all of the text stays and balances, as in the configuration that has the braces. + * A block with an `#else` has two versions of its text and stays as it is. */ +static void cbm_attr_split_block(CBMAttrWords *words, CBMAttrSplitBlock *waiting, + const CBMAttrConditional *frame, TSRange endif) { + if (frame->brace_net > 0) { + *waiting = (CBMAttrSplitBlock){frame->brace_net, frame->directive, endif}; + } else if (frame->brace_net < 0 && waiting->open_braces == -frame->brace_net) { + cbm_attr_range_add(words, waiting->open); + cbm_attr_range_add(words, waiting->close); + cbm_attr_range_add(words, frame->directive); + cbm_attr_range_add(words, endif); + waiting->open_braces = 0; + } +} + static void cbm_attr_expression_branches(const char *source, uint32_t length, CBMAttrWords *words) { CBMAttrConditional stack[CBM_ATTR_MAX_CONDITIONALS]; + CBMAttrSplitBlock split[CBM_ATTR_MAX_CONDITIONALS]; + memset(split, 0, sizeof(split)); int top = 0, skipping = 0, depth = 0, overflow = 0; uint32_t at = 0, line_start = 0, row = 0; bool line_begun = false; /* 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, after_string = false; + uint32_t directive_enum_end = 0; + bool after_assign = false, after_enum = false, after_string = false; while (at < length) { char ch = source[at]; if (ch == '\n') { @@ -1267,8 +1337,11 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB /* `T table[] = { / #define OP(x) {x}, / LIST / #undef OP / };`: no grammar has a * directive inside an initializer, and error recovery read a function named OP that * swallowed the next 1,400 lines of jemalloc's ctl.c. The preprocessor takes these - * lines out before any of it is C, and so does this. */ - bool in_table = initializer > 0 && !skipping && + * lines out before any of it is C, and so does this. An enum's body holding an + * `#include` beside another directive cost Type.h and InstrProf.h the namespace of all + * that follows it; its includes are read back from its text in + * cbm_note_c_includes_at. */ + bool in_table = !skipping && (initializer > 0 || at < directive_enum_end) && (defines || (size == 5 && !memcmp(text, "undef", 5)) || (size == 7 && !memcmp(text, "include", 7))); while (end < length && source[end] != '\n') { @@ -1324,6 +1397,7 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB } } else if (alternative && top && !overflow) { CBMAttrConditional *frame = &stack[top - 1]; + frame->has_alternative = true; if (frame->in_expression && !frame->skipping) { frame->skipping = true; frame->skip_start = at; @@ -1377,6 +1451,9 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB words, (TSRange){frame->skip_point, end_point, frame->skip_start, end}); } else if (frame->in_expression) { cbm_attr_range_add(words, (TSRange){begin_point, end_point, at, end}); + } else if (!frame->has_alternative && !skipping) { + cbm_attr_split_block(words, &split[top], frame, + (TSRange){begin_point, end_point, at, end}); } } } @@ -1436,17 +1513,23 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB } after_string = false; after_assign = false; + after_enum |= word_end - at == 4 && !memcmp(source + at, "enum", 4); at = word_end; continue; } after_string = false; if (!skipping) { + if (top && !overflow && (ch == '{' || ch == '}')) { + stack[top - 1].brace_net += ch == '{' ? 1 : -1; + } if (ch == '(') { depth++; } else if (ch == ')' && depth > 0) { depth--; } else if (ch == '{' && (initializer || after_assign)) { initializer++; + } else if (ch == '{' && after_enum) { + directive_enum_end = cbm_attr_enum_directives_end(source, length, at); } else if (ch == '}' && initializer) { initializer--; } else if (ch == ';') { @@ -1454,6 +1537,9 @@ static void cbm_attr_expression_branches(const char *source, uint32_t length, CB } } after_assign = ch == '='; + /* Until the body, or what shows there is none: `enum E f(void);`, `enum E e = A;`. */ + if (ch == '{' || ch == ';' || ch == '(' || ch == '=') + after_enum = false; at++; } } diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 9eb71d992..c7198d6b6 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -1531,6 +1531,7 @@ void cbm_extract_definitions_without_module(CBMExtractCtx *ctx); // contains a dbt builtin call. Defined in extract_dbt.c. void cbm_extract_dbt(CBMExtractCtx *ctx); void cbm_extract_imports(CBMExtractCtx *ctx); +void cbm_note_c_includes_at(CBMExtractCtx *ctx, TSNode node, uint32_t *body_end); /* Owned inner tree for one fully audited Vue/Svelte inline script. Absolute * source coordinates are preserved; NULL means the host domain is unproved. */ TSTree *cbm_component_binding_tree(CBMExtractCtx *ctx, CBMLanguage *language, TSRange *range); diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index d3e7013ba..5354aacff 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -2085,12 +2085,13 @@ static char *strip_angle_brackets(CBMArena *a, char *path) { } /* Node kinds that only group file-level items: the branches of `#if` / - * `#ifdef` (an include guard is one) and the body of `extern "C" { ... }`. */ + * `#ifdef` (an include guard is one), the body of `extern "C" { ... }`, and a namespace: + * sanitizer_linux.cpp includes its syscall tables inside one. */ static bool is_c_include_container(const char *kind) { return strcmp(kind, "preproc_if") == 0 || strcmp(kind, "preproc_ifdef") == 0 || strcmp(kind, "preproc_else") == 0 || strcmp(kind, "preproc_elif") == 0 || strcmp(kind, "preproc_elifdef") == 0 || strcmp(kind, "linkage_specification") == 0 || - strcmp(kind, "declaration_list") == 0; + strcmp(kind, "declaration_list") == 0 || strcmp(kind, "namespace_definition") == 0; } /* Collect the includes among `parent`'s children, descending into @@ -2139,6 +2140,114 @@ static void parse_c_imports(CBMExtractCtx *ctx) { parse_c_imports_in(ctx, ctx->root, 0); } +/* `"path"` or `` at this byte: what follows an `#include` the grammar did not name. The text + * may run on over a comment, so the path ends at its closing mark. `#include NAME` has no path. */ +static void push_c_include_text(CBMExtractCtx *ctx, uint32_t begin, uint32_t end) { + char open = ctx->source[begin]; + if (open != '"' && open != '<') { + return; + } + char close = open == '"' ? '"' : '>'; + uint32_t stop = begin + SKIP_ONE; + while (stop < end && ctx->source[stop] != close) { + stop++; + } + if (stop >= end || stop == begin + SKIP_ONE) { + return; + } + char *path = + cbm_arena_strndup(ctx->arena, ctx->source + begin + SKIP_ONE, stop - begin - SKIP_ONE); + if (!path) { + return; + } + CBMImport imp = {.local_name = path_last(ctx->arena, path), + .module_path = path, + .kind = close == '"' ? CBM_IMPORT_KIND_INCLUDE_QUOTED + : CBM_IMPORT_KIND_INCLUDE_SYSTEM, + .site_start_byte = begin, + .site_end_byte = stop + SKIP_ONE, + .source_origin = CBM_SOURCE_ORIGIN_RAW}; + cbm_imports_push(&ctx->result->imports, ctx->arena, imp); +} + +/* The grammar has no place for `#include` in an enum's body. A lone one reads as a directive it has + * no name for, and more than one are out of the tree altogether (cbm_attr_enum_directives_end), so + * the body's text is what says what it includes. A block comment is skipped, and a line comment + * starts no directive; an enumerator list holds no string one could hide in. */ +static void note_c_enum_body_includes(CBMExtractCtx *ctx, TSNode list) { + const char *source = ctx->source; + uint32_t end = ts_node_end_byte(list); + bool line_start = false; + for (uint32_t at = ts_node_start_byte(list); at < end; at++) { + char ch = source[at]; + if (ch == '\n') { + line_start = true; + } else if (ch == '/' && at + SKIP_ONE < end && source[at + SKIP_ONE] == '*') { + at += PAIR_LEN; + while (at + SKIP_ONE < end && !(source[at] == '*' && source[at + SKIP_ONE] == '/')) { + at++; + } + at++; + } else if (ch == '#' && line_start) { + uint32_t word = at + SKIP_ONE; + while (word < end && (source[word] == ' ' || source[word] == '\t')) { + word++; + } + uint32_t line_end = word; + while (line_end < end && source[line_end] != '\n') { + bool continues = source[line_end] == '\\' && line_end + SKIP_ONE < end && + source[line_end + SKIP_ONE] == '\n'; + line_end += continues ? PAIR_LEN : SKIP_ONE; + } + size_t size = strlen("include"); + if (line_end - word > size && memcmp(source + word, "include", size) == 0) { + uint32_t argument = word + (uint32_t)size; + while (argument < line_end && + (source[argument] == ' ' || source[argument] == '\t')) { + argument++; + } + if (argument < line_end) { + push_c_include_text(ctx, argument, line_end); + } + } + at = line_end - SKIP_ONE; + } else if (ch != ' ' && ch != '\t' && ch != '\r') { + line_start = false; + } + } +} + +/* Where a C or C++ `#include` stands outside what parse_c_imports reads, this reads it. Called once + * for each node of a file, in order; `body_end` is where the last enum body read ends. */ +void cbm_note_c_includes_at(CBMExtractCtx *ctx, TSNode node, uint32_t *body_end) { + const char *kind = ts_node_type(node); + bool list = strcmp(kind, "enumerator_list") == 0; + if ((!list && strcmp(kind, "preproc_call") != 0) || ts_node_start_byte(node) < *body_end) { + return; + } + if (list) { + *body_end = ts_node_end_byte(node); + note_c_enum_body_includes(ctx, node); + return; + } + /* A struct's body, or an enum's the grammar could not read as a list: `#include` is a directive + * with no name there too. */ + TSNode directive = ts_node_child_by_field_name(node, TS_FIELD("directive")); + TSNode argument = ts_node_child_by_field_name(node, TS_FIELD("argument")); + if (ts_node_is_null(directive) || ts_node_is_null(argument)) { + return; + } + const char *source = ctx->source; + /* The token is `#`, spaces and a word, so the spaces end inside it. */ + uint32_t word = ts_node_start_byte(directive) + SKIP_ONE; + word += (uint32_t)strspn(source + word, " \t"); + uint32_t word_end = ts_node_end_byte(directive); + size_t size = strlen("include"); + if (word_end - word == size && memcmp(source + word, "include", size) == 0) { + push_c_include_text(ctx, ts_node_start_byte(argument), ts_node_end_byte(argument)); + } +} + // --- Ruby imports --- // call nodes: require("X"), require_relative("X") diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index f2aec62c7..40abfb601 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -3054,6 +3054,9 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { const char *ext = ctx->rel_path ? strrchr(ctx->rel_path, '.') : NULL; bool tf_module = ctx->language == CBM_LANG_HCL && ext && (strcmp(ext, ".tf") == 0 || strcmp(ext, ".tofu") == 0); + bool c_includes = ctx->language == CBM_LANG_C || ctx->language == CBM_LANG_CPP || + ctx->language == CBM_LANG_OBJC; + uint32_t enum_body_end = 0; for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); @@ -3096,6 +3099,9 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { handle_string_refs(ctx, node, &state); handle_yaml_nested(ctx, node); scan_infra_bindings(ctx, node); + if (c_includes) { + cbm_note_c_includes_at(ctx, node, &enum_body_end); + } push_boundary_scopes(ctx, node, spec, &state, depth, &invocation); } diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index d12b12fcc..8372264fd 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -5397,22 +5397,20 @@ 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; +/* Does lookup reach this namespace, named from the top of its file? Around this code, written + * around an out-of-line definition (`void ns::Foo::run() { ... }`), or brought here by a visible + * `using namespace`. */ +static bool c_namespace_reached(CLSPContext *ctx, const char *from, uint32_t at) { 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; + const char *const places[] = {ctx->current_namespace, ctx->enclosing_func_qn}; + for (size_t i = 0; i < sizeof(places) / sizeof(places[0]); i++) { + const char *around = places[i]; + 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]; @@ -5423,22 +5421,38 @@ static bool c_scope_reached(CLSPContext *ctx, const char *written, const char *s 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. */ -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`. */ +/* A scope named from a module of `module_len` bytes: the top level of that file, or a namespace + * lookup reaches. */ +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; + return c_namespace_reached(ctx, scope + module_len + 1, at); +} + +/* `p.dir.t.hpp.__file__` is the file whose declarations are named `p.dir.t.Name`. The length of + * that prefix, or 0 when this is no file's name. */ +static size_t c_file_module_len(const char *file_qn) { static const char file_tail[] = ".__file__"; size_t tail_len = sizeof(file_tail) - 1; size_t file_len = file_qn ? strlen(file_qn) : 0; if (file_len <= tail_len || strcmp(file_qn + file_len - tail_len, file_tail)) - return false; + return 0; file_len -= tail_len; while (file_len && file_qn[file_len - 1] != '.') file_len--; + return file_len ? file_len - 1 : 0; +} + +/* `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. */ +static bool c_qualifier_starts_here(CLSPContext *ctx, const char *written, const char *file_qn, + const char *scope, uint32_t at) { + size_t file_len = c_file_module_len(file_qn); if (!file_len) return false; - file_len--; if (strncmp(scope, file_qn, file_len) || (scope[file_len] && scope[file_len] != '.')) return false; return c_scope_reached(ctx, written, scope, file_len, at); @@ -8510,6 +8524,52 @@ static const char *c_proof_class_enumerator(CLSPContext *ctx, const CBMRegistere return NULL; } +/* `Color::RED`, `Mode::FAST`, `gfx::Blend::ADD`: the whole written scope names an enum the unit + * sees, and that enum, in that file, declares the name. */ +static void c_emit_enumerator_of_enum(CLSPContext *ctx, TSNode qualified, TSNode scope, TSNode name, + const char *text, uint32_t at) { + TSNode top = qualified; + for (int hop = 0; hop < 16; hop++) { + TSNode above = ts_node_parent(top); + if (ts_node_is_null(above) || strcmp(ts_node_type(above), "qualified_identifier")) + break; + top = above; + } + const char *whole = c_node_text(ctx, top); + const char *written = whole + ? cbm_arena_strndup(ctx->arena, whole, + ts_node_end_byte(scope) - ts_node_start_byte(top)) + : NULL; + const CBMRegisteredType *type = + written ? c_proof_follow_alias(ctx, c_proof_type_by_name(ctx, written, at)) : NULL; + if (!type || !type->project_source || !type->graph_qualified_name || !type->source_file_qn || + !type->short_name || !c_proof_type_visible(ctx, type->qualified_name, at)) + return; + const char *target = NULL; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, text); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_ENUMERATOR || strcmp(decl->name, text) || !decl->target || + strcmp(decl->type_name, type->short_name) || strcmp(decl->file, type->source_file_qn)) + continue; + if (target && strcmp(target, decl->target)) + return; + target = decl->target; + } + if (!target) + return; + cbm_resolve_result_t typed = cbm_lsp_proof_type_lookup( + &ctx->field_types, written, scope, "c-written-enum-scope", type->qualified_name, + type->declaration_identity, type->graph_qualified_name, type->source_flags, + "enum-named-by-the-scope"); + typed = c_proof_type_import(ctx, type->qualified_name, at, typed); + if (!cbm_resolve_admit(&typed)) + return; + cbm_resolve_result_t member = c_proof_member_lookup(ctx, text, name, target, typed.proof); + if (cbm_resolve_admit(&member)) + c_emit_proven(ctx, &member, text, CBM_RESOLVED_VALUE_REFERENCE, name); +} + /* `Triple::UnknownOS`: the scope names a class the unit sees, and the class declares the name. */ static void c_emit_scoped_enumerator(CLSPContext *ctx, TSNode qualified, TSNode name) { TSNode written_name = ts_node_child_by_field_name(qualified, "name", 4); @@ -8523,8 +8583,11 @@ static void c_emit_scoped_enumerator(CLSPContext *ctx, TSNode qualified, TSNode uint32_t at = ts_node_start_byte(name); const CBMRegisteredType *klass = owner ? c_proof_named_class(ctx, owner, at) : NULL; const char *target = klass && text ? c_proof_class_enumerator(ctx, klass, text, at, 0) : NULL; - if (!target) + if (!target) { + if (text) + c_emit_enumerator_of_enum(ctx, qualified, scope, name, text, at); return; + } cbm_resolve_result_t typed = cbm_lsp_proof_type_lookup( &ctx->field_types, owner, scope, "c-written-class-scope", klass->qualified_name, klass->declaration_identity, klass->graph_qualified_name, klass->source_flags, @@ -9228,6 +9291,158 @@ static bool c_enclosing_class_declares_other(CLSPContext *ctx, const char *name, return false; } +/* The `class Fwd;` in view that this written name means, if exactly one is: the name as written has + * to be the tail of the declared one, and what it leaves off a namespace lookup reaches from here. + */ +static const CBMCDecl *c_forward_class_in_view(CLSPContext *ctx, const char *name, + const char *qualified, uint32_t at, + const CTUModule **via) { + const char *written = qualified ? c_proof_name(ctx, qualified) : name; + if (!written) + return NULL; + bool rooted = qualified && qualified[0] == ':'; + if (qualified) { + /* A namespace alias can change what the first segment means. */ + size_t first = strcspn(written, "."); + for (int i = 0; i < ctx->ns_alias_count; i++) + if (!strncmp(ctx->ns_alias_names[i], written, first) && !ctx->ns_alias_names[i][first]) + return NULL; + } + size_t written_len = strlen(written); + const char *own = c_own_file_key(ctx); + const CBMCDecl *found = NULL; + const CTUModule *found_via = NULL; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + size_t length = decl->kind == CBM_C_DECL_FORWARD_CLASS ? strlen(decl->name) : 0; + if (length < written_len || strcmp(decl->name + length - written_len, written) || + (length > written_len && decl->name[length - written_len - 1] != '.')) + continue; + const CTUModule *entry = NULL; + if (!strcmp(decl->file, own)) { + const char *key = + cbm_arena_sprintf(ctx->arena, "$type:%s.%s", ctx->module_qn, decl->name); + if (!key || !c_proof_declared(ctx, key, at)) + continue; + } else { + entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + const CBMCFileFacts *facts = cbm_c_file_facts(ctx->c_decls, decl->file); + if (!entry || entry->exports_only || entry->at >= at || !facts || + (facts->plain_header && !facts->cpp_syntax)) + continue; + } + if (length > written_len) { + const char *scope = cbm_arena_strndup(ctx->arena, decl->name, length - written_len - 1); + if (rooted || !scope || !c_namespace_reached(ctx, scope, at)) + continue; + } + if (found && strcmp(found->name, decl->name)) + return NULL; + found = decl; + found_via = entry; + } + *via = found_via; + return found; +} + +static bool c_type_has_relative_name(const CLSPContext *ctx, const CBMRegisteredType *type, + const char *relative) { + const char *qn = type->qualified_name; + size_t module_len = + type->source_file_qn ? c_file_module_len(type->source_file_qn) : ctx->module_qn_len; + const char *module = type->source_file_qn ? type->source_file_qn : ctx->module_qn; + return qn && module_len && !strncmp(qn, module, module_len) && qn[module_len] == '.' && + !strcmp(qn + module_len + 1, relative); +} + +/* A type found by its short name, beside a `class Fwd;` in view that is not its declaration. Lookup + * goes from the inside out, so the forward declaration is what the name means unless the type is + * nearer: a member of the class around this code, or in a namespace around it inside the + * declaration's. */ +static bool c_forward_class_is_nearer(CLSPContext *ctx, const CBMRegisteredType *type, + const CBMCDecl *forward, uint32_t at) { + const char *qn = type->qualified_name; + const char *module = type->source_file_qn ? type->source_file_qn : ctx->module_qn; + size_t module_len = + type->source_file_qn ? c_file_module_len(type->source_file_qn) : ctx->module_qn_len; + if (!qn || !module_len || strncmp(qn, module, module_len) || qn[module_len] != '.') + return false; + const char *relative = qn + module_len + 1; + if (!strcmp(relative, forward->name)) + return false; + const char *dot = strrchr(relative, '.'); + if (!dot) + return true; + const char *around = cbm_arena_strndup(ctx->arena, qn, (size_t)(dot - qn)); + const char *scope = cbm_arena_strndup(ctx->arena, relative, (size_t)(dot - relative)); + if (!around || !scope || cbm_registry_lookup_type(ctx->proof_registry, around)) + return false; + if (!c_namespace_reached(ctx, scope, at)) + return true; + const char *forward_dot = strrchr(forward->name, '.'); + size_t forward_scope = forward_dot ? (size_t)(forward_dot - forward->name) : 0; + return (size_t)(dot - relative) < forward_scope; +} + +/* The class `class Fwd;` declares, wherever it is defined: the one definition in the project of + * that qualified name, as for a function declared by a prototype. Several definitions are several + * programs in one repository, and the one whose file includes this declaration was compiled against + * it. A class in an anonymous namespace has no linkage, a .c file holds no C++ class, and a name + * something else also has, or a class the facts did not read, says the search is not complete. */ +static const CBMRegisteredType *c_forward_class_definition(CLSPContext *ctx, + const CBMCDecl *forward) { + static const char none = 0; + if (!ctx->forward_defs) + ctx->forward_defs = cbm_ht_create(16); + const void *known = ctx->forward_defs ? cbm_ht_get(ctx->forward_defs, forward->name) : NULL; + if (known) + return known == &none ? NULL : known; + const char *dot = strrchr(forward->name, '.'); + const CBMRegisteredType *chosen = NULL, *compiled_against = NULL; + int definitions = 0, compiled_count = 0; + bool complete = true; + CBMTypeShortIter it; + cbm_registry_types_by_short_name_chain(ctx->proof_registry, dot ? dot + 1 : forward->name, &it); + int index; + while (complete && (index = cbm_type_short_iter_next(&it)) >= 0) { + const CBMRegisteredType *candidate = &it.reg->types[index]; + if (!candidate->project_source || !candidate->source_file_qn || + !c_type_has_relative_name(ctx, candidate, forward->name)) + continue; + if (c_type_is_self_alias(candidate)) + continue; + const CBMCFileFacts *facts = cbm_c_file_facts(ctx->c_decls, candidate->source_file_qn); + size_t file_len = strlen(candidate->source_file_qn); + static const char c_tail[] = ".c.__file__"; + bool c_source = + file_len >= sizeof(c_tail) - 1 && + !strcmp(candidate->source_file_qn + file_len - (sizeof(c_tail) - 1), c_tail); + if (c_source || (facts && facts->plain_header && !facts->cpp_syntax)) + continue; + complete = !candidate->alias_of && (candidate->source_flags & CBM_SOURCE_DECL_KNOWN) && + candidate->declaration_identity && candidate->graph_qualified_name; + if (!complete || (candidate->source_flags & CBM_SOURCE_DECL_STATIC)) + continue; + if (chosen && !strcmp(chosen->graph_qualified_name, candidate->graph_qualified_name)) + continue; + definitions++; + if (definitions <= 64 && + c_proof_file_reaches(ctx, candidate->source_file_qn, forward->file)) { + compiled_against = candidate; + compiled_count++; + } + chosen = candidate; + } + const CBMRegisteredType *result = !complete ? NULL + : definitions > 1 + ? (compiled_count == 1 ? compiled_against : NULL) + : chosen; + if (ctx->forward_defs) + cbm_ht_set(ctx->forward_defs, forward->name, result ? (void *)result : (void *)&none); + return result; +} + /* A written type name. Its declaration in this translation unit is the target, * which is a typedef's own node, not what the typedef aliases. */ static void c_emit_type_reference(CLSPContext *ctx, TSNode node) { @@ -9302,6 +9517,27 @@ static void c_emit_type_reference(CLSPContext *ctx, TSNode node) { /* 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; + /* `class Fwd;` in view says which class this is. A namesake found by its short name in another + * namespace is not it, and the class need not be defined anywhere this unit includes. */ + const CTUModule *forward_via = NULL; + const CBMCDecl *forward = + c_cxx_rules(ctx) ? c_forward_class_in_view(ctx, name, qualified, at, &forward_via) : NULL; + if (forward && type && c_forward_class_is_nearer(ctx, type, forward, at)) + type = NULL; + if (forward && !type) { + const CBMRegisteredType *defined = c_forward_class_definition(ctx, forward); + if (!defined || c_enclosing_class_declares_other(ctx, name, defined)) + return; + cbm_resolve_result_t declared = cbm_lsp_proof_type_lookup( + &ctx->field_types, name, node, "c-forward-declared-class", defined->qualified_name, + defined->declaration_identity, defined->graph_qualified_name, defined->source_flags, + "visible-forward-declaration;one-definition-of-that-qualified-name"); + if (forward_via) + declared = c_proof_import_join(ctx, declared, forward_via->via, CBM_PROOF_TYPE_BIND); + if (cbm_resolve_admit(&declared)) + c_emit_proven(ctx, &declared, name, CBM_RESOLVED_TYPE_REFERENCE, node); + 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; @@ -11494,6 +11730,10 @@ void c_lsp_process_file(CLSPContext *ctx, TSNode root) { cbm_ht_free(ctx->macro_defs); ctx->macro_defs = NULL; } + if (ctx->forward_defs) { + cbm_ht_free(ctx->forward_defs); + ctx->forward_defs = NULL; + } if (ctx->eval_failure && ctx->resolved_calls) ctx->resolved_calls->count = resolved_before; } diff --git a/internal/cbm/lsp/c_lsp.h b/internal/cbm/lsp/c_lsp.h index b7312759f..31401dcfc 100644 --- a/internal/cbm/lsp/c_lsp.h +++ b/internal/cbm/lsp/c_lsp.h @@ -50,6 +50,7 @@ typedef struct { bool macro_args_of_macro; /* Name to CMacroDefs: the definitions in this translation unit, each name scanned once. */ struct CBMHashTable *macro_defs; + struct CBMHashTable *forward_defs; /* per `class Fwd;` in view: its one definition */ bool macro_defs_ready; const cbm_resolve_import_snapshot_t *import_facts; bool require_import_proofs; diff --git a/internal/cbm/lsp/c_proof.c b/internal/cbm/lsp/c_proof.c index 9ce07bada..0ea027e00 100644 --- a/internal/cbm/lsp/c_proof.c +++ b/internal/cbm/lsp/c_proof.c @@ -30,6 +30,8 @@ typedef struct { bool c_language; /* `#ifdef __cplusplus extern "C" { #endif`: the brace the grammar cannot pair. */ bool file_extern_c; + /* What is declared here has no linkage, so no other file can define or name it. */ + bool anonymous_namespace; const char *module; CBMCFileFacts *facts; } csp_context_t; @@ -402,8 +404,33 @@ static const char *csp_base_type_name(csp_context_t *ctx, TSNode type) { } /* Enumerators sit in file scope whichever declaration wrote them, with no linkage. */ -static void csp_note_enumerators(csp_context_t *ctx, TSNode type, const char *owner) { - if (!ctx->facts || !csp_is(type, "enum_specifier") || strcmp(owner, ctx->module)) +/* `enum class` keeps its enumerators inside the enum. An unscoped enum puts them in what is around + * it. */ +static bool csp_enum_is_scoped(TSNode type) { + bool scoped = false; + TSTreeCursor head = ts_tree_cursor_new(type); + if (ts_tree_cursor_goto_first_child(&head)) + do { + const char *word = ts_node_type(ts_tree_cursor_current_node(&head)); + scoped |= !strcmp(word, "class") || !strcmp(word, "struct"); + } while (ts_tree_cursor_goto_next_sibling(&head)); + ts_tree_cursor_delete(&head); + return scoped; +} + +/* `typedef_name` names an enum with no tag of its own: `typedef enum { A } E;`. */ +static void csp_note_enumerators(csp_context_t *ctx, TSNode type, const char *owner, + const char *typedef_name) { + if (!ctx->facts || !csp_is(type, "enum_specifier")) + return; + bool at_file_scope = !strcmp(owner, ctx->module); + const char *tag = csp_text(ctx, csp_field(type, "name")); + if (!tag) + tag = typedef_name; + /* `Color::RED` needs C++. C has no way to write it. */ + bool qualifiable = tag && !ctx->c_language; + bool scoped = qualifiable && csp_enum_is_scoped(type); + if (!at_file_scope && !qualifiable) return; TSNode list = csp_field(type, "body"); uint32_t count = ts_node_is_null(list) ? 0 : ts_node_named_child_count(list); @@ -411,9 +438,21 @@ static void csp_note_enumerators(csp_context_t *ctx, TSNode type, const char *ow TSNode item = ts_node_named_child(list, i); const char *name = csp_is(item, "enumerator") ? csp_text(ctx, csp_field(item, "name")) : NULL; - CBMCDecl *fact = name ? csp_add_decl(ctx, CBM_C_DECL_VALUE, name, 0) : NULL; - if (fact) - fact->target = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module, name); + if (!name) + continue; + if (at_file_scope) { + CBMCDecl *fact = csp_add_decl(ctx, CBM_C_DECL_VALUE, name, 0); + if (fact) + fact->target = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module, name); + } + if (qualifiable) { + CBMCDecl *fact = csp_add_decl(ctx, CBM_C_DECL_ENUMERATOR, name, 0); + if (fact) { + fact->type_name = tag; + fact->target = scoped ? cbm_arena_sprintf(ctx->arena, "%s.%s.%s", owner, tag, name) + : cbm_arena_sprintf(ctx->arena, "%s.%s", owner, name); + } + } } } @@ -422,7 +461,7 @@ static void csp_note_enumerators(csp_context_t *ctx, TSNode type, const char *ow static void csp_note_values(csp_context_t *ctx, TSNode node, const char *owner) { if (!ctx->facts || strcmp(owner, ctx->module)) return; - csp_note_enumerators(ctx, csp_field(node, "type"), owner); + csp_note_enumerators(ctx, csp_field(node, "type"), owner, NULL); bool is_extern = csp_storage_is(ctx, node, "extern"); bool is_static = csp_storage_is(ctx, node, "static"); const char *type_name = csp_base_type_name(ctx, csp_field(node, "type")); @@ -1349,16 +1388,8 @@ static void csp_note_member(csp_context_t *ctx, TSNode node, const char *klass, csp_member_name(ctx, klass, csp_field(type, "name"), access); if (!csp_is(type, "enum_specifier")) return; - /* `enum class` keeps its enumerators inside the enum. An unscoped enum puts them in the - * class, and the graph names them there. */ - bool scoped = false; - TSTreeCursor head = ts_tree_cursor_new(type); - if (ts_tree_cursor_goto_first_child(&head)) - do { - const char *word = ts_node_type(ts_tree_cursor_current_node(&head)); - scoped |= !strcmp(word, "class") || !strcmp(word, "struct"); - } while (ts_tree_cursor_goto_next_sibling(&head)); - ts_tree_cursor_delete(&head); + /* An unscoped enum puts its enumerators in the class, and the graph names them there. */ + bool scoped = csp_enum_is_scoped(type); TSNode body = csp_field(type, "body"); uint32_t count = ts_node_is_null(body) ? 0 : ts_node_named_child_count(body); for (uint32_t i = 0; i < count; i++) { @@ -1368,6 +1399,11 @@ static void csp_note_member(csp_context_t *ctx, TSNode node, const char *klass, CBMCDecl *fact = csp_member_name(ctx, klass, csp_field(enumerator, "name"), access); if (fact && !scoped && klass_qn) fact->target = cbm_arena_sprintf(ctx->arena, "%s.%s", klass_qn, fact->name); + /* C has no class scope: an enumerator declared in a struct is a name of the file. */ + const char *name = fact && ctx->c_language ? fact->name : NULL; + CBMCDecl *value = name ? csp_add_decl(ctx, CBM_C_DECL_VALUE, name, 0) : NULL; + if (value) + value->target = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module, name); } } @@ -1382,12 +1418,16 @@ static TSNode csp_template_parameter_name(TSNode parameter) { csp_is(parameter, "optional_parameter_declaration") || csp_is(parameter, "variadic_parameter_declaration")) return null_node; - uint32_t count = ts_node_named_child_count(parameter); - for (uint32_t i = 0; i < count; i++) { - TSNode child = ts_node_named_child(parameter, i); - if (csp_is(child, "type_identifier")) - return child; + TSTreeCursor cursor = ts_tree_cursor_new(parameter); + bool more = ts_tree_cursor_goto_first_child(&cursor); + for (; more; more = ts_tree_cursor_goto_next_sibling(&cursor)) { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (csp_is(child, "type_identifier")) { + null_node = child; + break; + } } + ts_tree_cursor_delete(&cursor); return null_node; } @@ -1397,15 +1437,22 @@ static void csp_note_template_parameters(csp_context_t *ctx, TSNode klass, const if (!csp_is(up, "template_declaration")) continue; TSNode list = csp_field(up, "parameters"); - uint32_t count = ts_node_is_null(list) ? 0 : ts_node_named_child_count(list); - for (uint32_t i = 0; i < count; i++) { - TSNode parameter = csp_template_parameter_name(ts_node_named_child(list, i)); + if (ts_node_is_null(list)) + continue; + TSTreeCursor cursor = ts_tree_cursor_new(list); + bool more = ts_tree_cursor_goto_first_child(&cursor); + for (; more; more = ts_tree_cursor_goto_next_sibling(&cursor)) { + TSNode item = ts_tree_cursor_current_node(&cursor); + if (!ts_node_is_named(item)) + continue; + TSNode parameter = csp_template_parameter_name(item); const char *text = ts_node_is_null(parameter) ? NULL : csp_text(ctx, parameter); CBMCDecl *fact = text ? csp_add_decl(ctx, CBM_C_DECL_TEMPLATE_PARAMETER, text, 0) : NULL; if (fact) fact->type_name = name; } + ts_tree_cursor_delete(&cursor); } } @@ -1550,9 +1597,24 @@ static void csp_class(csp_context_t *ctx, TSNode node, const char *parent, ts_tree_cursor_delete(&cursor); csp_mark(ctx, node, qn, CBM_SOURCE_DECL_KNOWN | (complete ? CBM_SOURCE_CLASS_DOMAIN_COMPLETE : 0) | - (body_complete ? CBM_SOURCE_CLASS_BODY_COMPLETE : 0), + (body_complete ? CBM_SOURCE_CLASS_BODY_COMPLETE : 0) | + (ctx->anonymous_namespace ? CBM_SOURCE_DECL_STATIC : 0), -1); } + +/* `class Fwd;` where a namespace or the file holds it. In C++ this says which class Fwd is, by its + * qualified name: a class has linkage, and a program has one definition of it. A C tag has no + * linkage, so the same words there say nothing about a struct in another file. */ +static void csp_note_forward_class(csp_context_t *ctx, TSNode node, const char *owner) { + TSNode name_node = csp_field(node, "name"); + if (!ctx->facts || ctx->c_language || ctx->anonymous_namespace || + !ts_node_is_null(csp_field(node, "body")) || !csp_is(name_node, "type_identifier")) + return; + const char *name = csp_text(ctx, name_node); + const char *relative = name ? csp_relative(ctx, owner, name) : NULL; + if (relative) + (void)csp_add_decl(ctx, CBM_C_DECL_FORWARD_CLASS, relative, 0); +} /* An #ifdef that splits a function's braces leaves `bool f(int x)` and its statements loose in * 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. */ @@ -1717,12 +1779,14 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne return; } if (csp_is(node, "enum_specifier")) { - csp_note_enumerators(ctx, node, owner); + csp_note_enumerators(ctx, node, owner, NULL); return; } if (csp_is(node, "type_definition")) { TSNode type = csp_field(node, "type"), declarator = csp_field(node, "declarator"); - csp_note_enumerators(ctx, type, owner); + csp_note_enumerators(ctx, type, owner, + csp_is(declarator, "type_identifier") ? csp_text(ctx, declarator) + : NULL); 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); @@ -1785,6 +1849,7 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne } if (csp_is(node, "class_specifier") || csp_is(node, "struct_specifier") || csp_is(node, "union_specifier")) { + csp_note_forward_class(ctx, node, owner); csp_class(ctx, node, owner, NULL); return; } @@ -1794,7 +1859,10 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne /* An anonymous namespace adds no segment to what it declares, and those are seen around it. */ if (ts_node_is_null(csp_field(node, "name")) && !ts_node_is_null(body)) { + bool saved = ctx->anonymous_namespace; + ctx->anonymous_namespace = true; csp_walk(ctx, body, owner, depth + 1); + ctx->anonymous_namespace = saved; return; } if (name && !strstr(name, "::") && !ts_node_is_null(body)) { @@ -2019,7 +2087,10 @@ const CBMCDeclIndex *cbm_c_decl_index_build(CBMArena *arena, const CBMLSPDef *de *link = (CBMCDeclLink){.decl = decl, .next = index->names[bucket]}; index->names[bucket] = link; /* `ns.f` is also reachable by the name a call writes. */ - const char *leaf = decl->kind == CBM_C_DECL_PROTOTYPE ? strrchr(decl->name, '.') : NULL; + const char *leaf = + decl->kind == CBM_C_DECL_PROTOTYPE || decl->kind == CBM_C_DECL_FORWARD_CLASS + ? strrchr(decl->name, '.') + : NULL; if (leaf) { bucket = csp_hash(leaf + 1) & index->name_mask; link = cbm_arena_alloc(arena, sizeof(*link)); diff --git a/internal/cbm/lsp/c_proof.h b/internal/cbm/lsp/c_proof.h index 8222a98d0..69de02119 100644 --- a/internal/cbm/lsp/c_proof.h +++ b/internal/cbm/lsp/c_proof.h @@ -24,6 +24,9 @@ typedef enum { 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_UNNAMED_TYPEDEF, /* `typedef struct {...} Name;`: no tag is called Name */ + CBM_C_DECL_FORWARD_CLASS, /* `class Name;` in a namespace or at file scope: name is dotted + * from the module, and it is the C++ class of that name */ + CBM_C_DECL_ENUMERATOR, /* `enum E { A };`: name is A, type_name is E, target its QN */ } CBMCDeclKind; enum { diff --git a/internal/cbm/resolve/capabilities.c b/internal/cbm/resolve/capabilities.c index 5a43214a8..e7b392c8f 100644 --- a/internal/cbm/resolve/capabilities.c +++ b/internal/cbm/resolve/capabilities.c @@ -311,9 +311,10 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t * "with separate nodes, a template's dependent names, specialized class " "templates, ADL with a second visible function, callable-field " "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."; + "a macro in one preprocessor branch and a function in another, a macro " + "that an included header the index cannot read may redefine, a C tag " + "that is only forward-declared, and a forward-declared C++ class with " + "competing definitions abstain."; break; case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 9a55a7c18..95d7a708e 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -27635,7 +27635,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { {.case_id = "cpp.enum.red", .site = {.file = "use.cpp", .caller = "use.pick", - .needle = "Color::RED", + .needle = "RED", .occurrence_index = 0}, .relation = "USAGE", .target = {.file = "shapes.hpp", @@ -27643,7 +27643,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { .label = "Variable", .declaration_line = 1, .declaration_text = "enum Color { RED, GREEN };"}, - .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.", @@ -27652,7 +27652,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { {.case_id = "cpp.enum.add", .site = {.file = "use.cpp", .caller = "use.pick", - .needle = "gfx::Blend::ADD", + .needle = "ADD", .occurrence_index = 0}, .relation = "USAGE", .target = {.file = "shapes.hpp", @@ -27660,7 +27660,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { .label = "Variable", .declaration_line = 3, .declaration_text = "enum Blend { ADD, MULTIPLY };"}, - .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.", @@ -27683,7 +27683,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { {.case_id = "cpp.enum.fast", .site = {.file = "use.cpp", .caller = "use.pick", - .needle = "Mode::FAST", + .needle = "FAST", .occurrence_index = 0}, .relation = "USAGE", .target = {.file = "shapes.hpp", @@ -27691,7 +27691,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { .label = "Variable", .declaration_line = 9, .declaration_text = "enum class Mode { FAST, SLOW };"}, - .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.", @@ -27800,7 +27800,7 @@ TEST(pipeline_c_enum_inside_struct_reference_resolves_c1) { .label = "Variable", .declaration_line = 2, .declaration_text = " enum state { ST_IDLE, ST_BUSY } st;"}, - .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.", @@ -27815,7 +27815,7 @@ TEST(pipeline_c_enum_inside_struct_reference_resolves_c1) { .label = "Variable", .declaration_line = 3, .declaration_text = " enum { KIND_A, KIND_B } kind;"}, - .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.", diff --git a/tests/test_resolver_c.c b/tests/test_resolver_c.c index b78bf2111..781c9859e 100644 --- a/tests/test_resolver_c.c +++ b/tests/test_resolver_c.c @@ -5461,6 +5461,260 @@ TEST(resolver_cpp_tag_in_a_header_is_looked_up_from_its_own_module) { PASS(); } +/* `class Fwd;` says which class Fwd is, by its qualified name, wherever it is defined: a C++ class has + * linkage, and a program has one definition of it. This is what a prototype says of a function. It is + * not a unique short name: `n::OtherNs` with only `m::OtherNs` in the project stays unbound. A C tag has + * no linkage, so `struct CStyle;` says nothing about a struct in another file. */ +#define C_TU_FW_API \ + "namespace x { class Vetoed { public: int z; }; }\n" \ + "namespace n { namespace near_ns { struct Hidden { int h; }; } }\n" \ + "namespace n { struct Holder { struct Inner { int i; }; int member(Inner *v); }; }\n" \ + "namespace n {\n" \ + "class Fwd;\n" \ + "class Twice;\n" \ + "class TwiceInc;\n" \ + "class NoDef;\n" \ + "class InAnon;\n" \ + "class OtherNs;\n" \ + "class Vetoed;\n" \ + "void outside();\n" \ + "class Hidden;\n" \ + "class Inner;\n" \ + "template class Box;\n" \ + "}\n" \ + "class TopFwd;\n" \ + "class TopAnon;\n" \ + "class TopC;\n" \ + "struct CStyle;\n" +#define C_TU_FW_USE \ + "#include \"fw_api.hpp\"\n" \ + "#include \"fw_plain.h\"\n" \ + "int qualified(n::Fwd *a) { return a != nullptr; }\n" \ + "class OwnFwd;\n" \ + "int own_file(OwnFwd *z) { return z != nullptr; }\n" \ + "void n::outside() { Fwd *y = nullptr; (void)y; }\n" \ + "int rooted(::n::Fwd *b) { return b != nullptr; }\n" \ + "int top(TopFwd *c) { return c != nullptr; }\n" \ + "int tmpl(n::Box *d) { return d != nullptr; }\n" \ + "namespace n { int inside(Fwd *e) { return e != nullptr; } }\n" \ + "int compiled_against(n::TwiceInc *f) { return f != nullptr; }\n" \ + "namespace n { int redirected(Vetoed *g) { return g != nullptr; } }\n" \ + "namespace n { namespace near_ns { int nearer(Hidden *w) { return w != nullptr; } } }\n" \ + "namespace n { struct Derived : Holder { int inherited(Inner *x) { return x != nullptr; } }; }\n" \ + "int two_definitions(n::Twice *h) { return h != nullptr; }\n" \ + "int no_definition(n::NoDef *i) { return i != nullptr; }\n" \ + "int in_anonymous(n::InAnon *j) { return j != nullptr; }\n" \ + "int only_another_namespace(n::OtherNs *k) { return k != nullptr; }\n" \ + "int anonymous_top(TopAnon *l) { return l != nullptr; }\n" \ + "int defined_in_c(TopC *o) { return o != nullptr; }\n" \ + "int c_tag(struct CStyle *p) { return p != nullptr; }\n" \ + "int plain_header(PlainFwd *q) { return q != nullptr; }\n" \ + "int not_reached(Fwd *s) { return s != nullptr; }\n" \ + "int before_it(Later *t) { return t != nullptr; }\n" \ + "class Later;\n" \ + "using namespace n;\n" \ + "int after_using(Fwd *u) { return u != nullptr; }\n" +static const RQ_Source C_TU_FW[] = { + {.path = "fw_api.hpp", .text = C_TU_FW_API}, + {.path = "fw_plain.h", .text = "struct PlainFwd;\n"}, + {.path = "fw_defs.hpp", + .text = "namespace n { class Fwd { public: int z; }; }\n" + "namespace n { template class Box { public: T v; }; }\n" + "namespace n { class Vetoed { public: int z; }; }\n" + "namespace m { class OtherNs { public: int z; }; }\n" + "class TopFwd { public: int z; };\n" + "class Later { public: int z; };\n" + "namespace n { class Inner { public: int z; }; }\n" + "class OwnFwd { public: int z; };\n"}, + {.path = "fw_two_a.hpp", .text = "namespace n { class Twice { public: int z; }; }\n"}, + {.path = "fw_two_b.hpp", .text = "namespace n { class Twice { public: int y; }; }\n"}, + {.path = "fw_inc_a.cpp", + .text = "#include \"fw_api.hpp\"\nnamespace n { class TwiceInc { public: int z; }; }\n"}, + {.path = "fw_inc_b.cpp", .text = "namespace n { class TwiceInc { public: int y; }; }\n"}, + {.path = "fw_anon.cpp", + .text = "namespace { class TopAnon { public: int z; }; }\n" + "namespace n { namespace { class InAnon { public: int z; }; } }\n"}, + {.path = "fw_cdef.c", .text = "struct TopC { int c; };\nstruct CStyle { int c; };\n"}, + {.path = "fw_plain_def.cpp", .text = "struct PlainFwd { int c; };\n"}, + {.path = "fw_use.cpp", .text = C_TU_FW_USE}, +}; + +TEST(resolver_cpp_forward_declared_class_binds_by_qualified_name) { + const char *use = C_TU_FW_USE; + const RQ_Target fwd = {.file = "fw_defs.hpp", + .suffix = "fw_defs.n.Fwd", + .label = "Class", + .declaration_line = 1, + .declaration_text = "namespace n { class Fwd { public: int z; }; }"}; +#define FW_USE(id, caller, context, word, target_) \ + {.case_id = id, \ + .site = C_TU_WORD("fw_use.cpp", use, "fw_use." caller, context, word), \ + .relation = "USAGE", \ + .target = target_} +#define FW_NEVER(id, caller, context, word) \ + {.case_id = id, \ + .site = C_TU_WORD("fw_use.cpp", use, "fw_use." caller, context, word), \ + .relation = "USAGE", \ + .any_target = true} + const RQ_Expected expected[] = { + FW_USE("qualified", "qualified", "Fwd *a", "Fwd", fwd), + FW_USE("rooted", "rooted", "Fwd *b", "Fwd", fwd), + FW_USE("in-the-namespace", "n.inside", "Fwd *e", "Fwd", fwd), + FW_USE("by-a-using-directive", "after_using", "Fwd *u", "Fwd", fwd), + FW_USE("declared-in-this-file", "own_file", "OwnFwd *z", "OwnFwd", + ((RQ_Target){.file = "fw_defs.hpp", + .suffix = "fw_defs.OwnFwd", + .label = "Class", + .declaration_line = 8, + .declaration_text = "class OwnFwd { public: int z; };"})), + FW_USE("from-a-definition-written-outside-the-namespace", "n.outside", "Fwd *y", "Fwd", fwd), + FW_USE("at-file-scope", "top", "TopFwd *c", "TopFwd", + ((RQ_Target){.file = "fw_defs.hpp", + .suffix = "fw_defs.TopFwd", + .label = "Class", + .declaration_line = 5, + .declaration_text = "class TopFwd { public: int z; };"})), + FW_USE("a-template", "tmpl", "Box *d", "Box", + ((RQ_Target){.file = "fw_defs.hpp", + .suffix = "fw_defs.n.Box", + .label = "Class", + .declaration_line = 2, + .declaration_text = + "namespace n { template class Box { public: T v; }; }"})), + FW_USE("the-one-compiled-against-it", "compiled_against", "TwiceInc *f", "TwiceInc", + ((RQ_Target){.file = "fw_inc_a.cpp", + .suffix = "fw_inc_a.n.TwiceInc", + .label = "Class", + .declaration_line = 2, + .declaration_text = + "namespace n { class TwiceInc { public: int z; }; }"})), + FW_USE("a-namespace-nearer-than-the-declaration", "n.near_ns.nearer", "Hidden *w", "Hidden", + ((RQ_Target){.file = "fw_api.hpp", + .suffix = "fw_api.n.near_ns.Hidden", + .label = "Class", + .declaration_line = 2, + .declaration_text = + "namespace n { namespace near_ns { struct Hidden { int h; }; } }"})), + {.case_id = "a-nested-class-over-the-namespace-s", + .site = C_TU_WORD("fw_api.hpp", C_TU_FW_API, "fw_api", "Inner *v", "Inner"), + .relation = "USAGE", + .target = {.file = "fw_api.hpp", + .suffix = "fw_api.n.Holder.Inner", + .label = "Class", + .declaration_line = 3, + .declaration_text = "namespace n { struct Holder { struct Inner { int i; }; int " + "member(Inner *v); }; }"}}, + FW_USE("an-inherited-class-over-the-namespace-s", "n.Derived.inherited", "Inner *x", "Inner", + ((RQ_Target){.file = "fw_api.hpp", + .suffix = "fw_api.n.Holder.Inner", + .label = "Class", + .declaration_line = 3, + .declaration_text = "namespace n { struct Holder { struct Inner { int i; }; int " + "member(Inner *v); }; }"})), + FW_USE("the-declaration-over-a-namesake-in-view", "n.redirected", "Vetoed *g", "Vetoed", + ((RQ_Target){.file = "fw_defs.hpp", + .suffix = "fw_defs.n.Vetoed", + .label = "Class", + .declaration_line = 3, + .declaration_text = "namespace n { class Vetoed { public: int z; }; }"})), + }; + const RQ_Forbidden forbidden[] = { + FW_NEVER("two-definitions-neither-compiled-against-it", "two_definitions", "Twice *h", "Twice"), + FW_NEVER("no-definition", "no_definition", "NoDef *i", "NoDef"), + FW_NEVER("a-class-in-an-anonymous-namespace", "in_anonymous", "InAnon *j", "InAnon"), + FW_NEVER("the-short-name-in-another-namespace", "only_another_namespace", "OtherNs *k", + "OtherNs"), + FW_NEVER("an-anonymous-class-at-file-scope", "anonymous_top", "TopAnon *l", "TopAnon"), + FW_NEVER("a-struct-in-a-c-file", "defined_in_c", "TopC *o", "TopC"), + FW_NEVER("a-c-tag", "c_tag", "CStyle *p", "CStyle"), + FW_NEVER("declared-by-a-plain-header", "plain_header", "PlainFwd *q", "PlainFwd"), + FW_NEVER("a-namespace-not-reached", "not_reached", "Fwd *s", "Fwd"), + FW_NEVER("before-its-declaration", "before_it", "Later *t", "Later"), + }; +#undef FW_USE +#undef FW_NEVER + const CTUCase fixture = {.suite = "c-tu-forward-classes", + .sources = C_TU_FW, + .source_count = sizeof(C_TU_FW) / sizeof(C_TU_FW[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(); +} + +/* `Color::RED`: the written scope names an enum the unit sees, and that enum declares the name. */ +#define C_TU_EN_H \ + "enum Color { RED, GREEN };\n" \ + "namespace gfx {\n" \ + "enum Blend { ADD, MULTIPLY };\n" \ + "enum class Tone { WARM, COLD };\n" \ + "}\n" \ + "enum class Mode { FAST, SLOW };\n" \ + "typedef enum { T_ONE, T_TWO } TypedefEnum;\n" \ + "enum Decoy { BLUE };\n" \ + "enum class Other { ELSEWHERE };\n" +#define C_TU_EN_USE \ + "#include \"en.hpp\"\n" \ + "int unscoped() { return Color::RED; }\n" \ + "int in_namespace() { return gfx::Blend::ADD; }\n" \ + "int scoped() { return (int)Mode::FAST; }\n" \ + "int scoped_in_namespace() { return (int)gfx::Tone::COLD; }\n" \ + "int of_a_typedef() { return TypedefEnum::T_TWO; }\n" \ + "int another_enum_s() { return Color::BLUE; }\n" \ + "int another_enum_s_scoped() { return (int)Other::FAST; }\n" \ + "int enum_not_included() { return Hidden::HID; }\n" +static const RQ_Source C_TU_EN[] = {{.path = "en.hpp", .text = C_TU_EN_H}, + {.path = "en_hidden.hpp", .text = "enum Hidden { HID };\n"}, + {.path = "en_use.cpp", .text = C_TU_EN_USE}}; + +TEST(resolver_cpp_enumerator_is_read_from_the_enum_written) { + const char *use = C_TU_EN_USE; +#define EN_USE(id, caller, word, suffix_, line, written) \ + {.case_id = id, \ + .site = C_TU_WORD("en_use.cpp", use, "en_use." caller, word, word), \ + .relation = "USAGE", \ + .target = {.file = "en.hpp", \ + .suffix = suffix_, \ + .label = "Variable", \ + .declaration_line = line, \ + .declaration_text = written}} +#define EN_NEVER(id, caller, context, word) \ + {.case_id = id, \ + .site = C_TU_WORD("en_use.cpp", use, "en_use." caller, context, word), \ + .relation = "USAGE", \ + .any_target = true} + const RQ_Expected expected[] = { + EN_USE("unscoped", "unscoped", "RED", "en.RED", 1, "enum Color { RED, GREEN };"), + EN_USE("in-a-namespace", "in_namespace", "ADD", "en.gfx.ADD", 3, + "enum Blend { ADD, MULTIPLY };"), + EN_USE("scoped", "scoped", "FAST", "en.Mode.FAST", 6, "enum class Mode { FAST, SLOW };"), + EN_USE("scoped-in-a-namespace", "scoped_in_namespace", "COLD", "en.gfx.Tone.COLD", 4, + "enum class Tone { WARM, COLD };"), + EN_USE("of-a-typedef", "of_a_typedef", "T_TWO", "en.T_TWO", 7, + "typedef enum { T_ONE, T_TWO } TypedefEnum;"), + }; + const RQ_Forbidden forbidden[] = { + EN_NEVER("an-enumerator-of-another-enum", "another_enum_s", "BLUE", "BLUE"), + EN_NEVER("a-name-the-enum-does-not-declare", "another_enum_s_scoped", "Other::FAST", "FAST"), + EN_NEVER("an-enum-this-unit-does-not-see", "enum_not_included", "HID", "HID"), + }; +#undef EN_USE +#undef EN_NEVER + const CTUCase fixture = {.suite = "c-tu-qualified-enumerators", + .sources = C_TU_EN, + .source_count = sizeof(C_TU_EN) / sizeof(C_TU_EN[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(); +} + /* `enum class E : int;` in a block is a lone tag with no enumerators: reading them from the body it does * not have was a null dereference. A worker that crashes on one file has that file quarantined and the * index still reports success, so a repository lost the file silently until a second such file made the @@ -5507,6 +5761,212 @@ TEST(resolver_cpp_opaque_enum_in_a_block_does_not_crash_and_hides_the_outer_one) PASS(); } +/* `enum Kind { #define E(x) x, / #include "x.inc" };`: with both in the body, or two `#include`s, the + * parse breaks there and everything after it in the file lost its namespace: InstrProf.h, Type.h. + * `enum E pick(void)` and `enum E e = A;` have no body, so a directive after them stays where it is. */ +#define C_TU_ENUM_DIRECTIVES \ + "namespace ns {\n" \ + "enum Kind {\n" \ + "// the kinds; {see} below\n" \ + "/* and; {here} */\n" \ + "#define ENTRY(k) k,\n" \ + "#include \"kinds.inc\"\n" \ + "#undef ENTRY\n" \ + "};\n" \ + "enum Twin {\n" \ + "#include \"twin_a.inc\"\n" \ + "#include \"twin_b.inc\"\n" \ + "};\n" \ + "class Later { public: int z; };\n" \ + "int after_the_enum(Later *l) { return l->z; }\n" \ + "enum Kind pick(void) {\n" \ + "#define LOCAL_RESULT ((enum Kind)0)\n" \ + " return LOCAL_RESULT;\n" \ + "}\n" \ + "int helper(void) { return 1; }\n" \ + "#define CALLS_HELPER helper()\n" \ + "int macro_after_a_function(void) { return CALLS_HELPER; }\n" \ + "}\n" +/* A `#define` alone parses where it stands, so nothing is taken out of this body and the name in it + * is still read. */ +#define C_TU_ENUM_DEFINE_ALONE \ + "enum tcp_state {\n" \ + " STATE_OPEN = 0,\n" \ + " STATE_LOSS = 1\n" \ + "#define STATEF_LOSS (1 << STATE_LOSS)\n" \ + "};\n" +static const RQ_Source C_TU_ENUM_DIR[] = {{.path = "enumdir.hpp", .text = C_TU_ENUM_DIRECTIVES}, + {.path = "enumkeep.h", .text = C_TU_ENUM_DEFINE_ALONE}}; + +TEST(resolver_cpp_enum_holding_a_define_and_an_include_keeps_the_namespace) { + const char *source = C_TU_ENUM_DIRECTIVES; + const char *alone = C_TU_ENUM_DEFINE_ALONE; + const RQ_Expected expected[] = { + {.case_id = "the-class-after-it-keeps-the-namespace", + .site = C_TU_WORD("enumdir.hpp", source, "enumdir.ns.after_the_enum", "Later *l", "Later"), + .relation = "USAGE", + .target = {.file = "enumdir.hpp", + .suffix = "enumdir.ns.Later", + .label = "Class", + .declaration_line = 13, + .declaration_text = "class Later { public: int z; };"}}, + {.case_id = "and-a-member-of-it", + .site = C_TU_WORD("enumdir.hpp", source, "enumdir.ns.after_the_enum", "l->z", "z"), + .relation = "USAGE", + .target = {.file = "enumdir.hpp", + .suffix = "enumdir.ns.Later.z", + .label = "Field", + .declaration_line = 13, + .declaration_text = "class Later { public: int z; };"}}, + {.case_id = "a-define-alone-in-the-body-is-left-in-place", + .site = C_TU_WORD("enumkeep.h", alone, "enumkeep", "(1 << STATE_LOSS)", "STATE_LOSS"), + .relation = "USAGE", + .target = {.file = "enumkeep.h", + .suffix = "enumkeep.STATE_LOSS", + .label = "Variable", + .declaration_line = 3, + .declaration_text = " STATE_LOSS = 1"}}, + }; + const CTUCase fixture = {.suite = "c-tu-enum-directives", + .sources = C_TU_ENUM_DIR, + .source_count = 2, + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* A brace opened in one conditional block and closed in another. Short ones survive, so the members + * after it are what this needs: past a dozen, error recovery cost DeclSpec.h its whole namespace. How + * the second condition is spelled does not matter. With an `#else` there are two versions of the + * text, and that form keeps the namespace without any help: it must not be helped. */ +#define C_TU_SPLIT_MEMBERS \ + " void setA(bool V) { A = V; }\n bool getA() const { return A; }\n" \ + " void setB(bool V) { B = V; }\n bool getB() const { return B; }\n" \ + " void setC(bool V) { C = V; }\n bool getC() const { return C; }\n" \ + " void setD(bool V) { D = V; }\n bool getD() const { return D; }\n" \ + " void setE(bool V) { E = V; }\n bool getE() const { return E; }\n" \ + " void setF(bool V) { F = V; }\n bool getF() const { return F; }\n" \ + " void setG(bool V) { G = V; }\n bool getG() const { return G; }\n" \ + " void setH(bool V) { H = V; }\n bool getH() const { return H; }\n" +#define C_TU_SPLIT_TAIL \ + "};\n" \ + "class Later { public: int z; };\n" \ + "int after_the_block(Later *l) { return l->z; }\n" \ + "}\n" +#define C_TU_SPLIT_UNION \ + "namespace ns {\nclass Holder {\n#ifndef X\n union {\n#endif\n int a;\n#ifndef X\n };\n" \ + "#endif\n" C_TU_SPLIT_MEMBERS C_TU_SPLIT_TAIL +#define C_TU_SPLIT_SPELLED \ + "namespace ns {\nclass Holder {\n#ifndef X\n union {\n#endif\n int a;\n#if !defined(X)\n" \ + " };\n#endif\n" C_TU_SPLIT_MEMBERS C_TU_SPLIT_TAIL +#define C_TU_SPLIT_ELSE \ + "namespace ns {\nclass Holder {\n#ifdef X\n struct A {\n#else\n struct B {\n#endif\n" \ + " int a;\n#ifdef X\n };\n#else\n };\n#endif\n" C_TU_SPLIT_MEMBERS C_TU_SPLIT_TAIL +static const RQ_Source C_TU_SPLIT[] = {{.path = "split_union.hpp", .text = C_TU_SPLIT_UNION}, + {.path = "split_spelled.hpp", .text = C_TU_SPLIT_SPELLED}, + {.path = "split_else.hpp", .text = C_TU_SPLIT_ELSE}}; + +TEST(resolver_cpp_brace_split_across_conditionals_keeps_the_namespace) { + const char *with_union = C_TU_SPLIT_UNION; + const char *spelled = C_TU_SPLIT_SPELLED; + const char *with_else = C_TU_SPLIT_ELSE; +#define SPLIT_ROWS(id, stem, text, line) \ + {.case_id = id "-class", \ + .site = C_TU_WORD(stem ".hpp", text, stem ".ns.after_the_block", "Later *l", "Later"), \ + .relation = "USAGE", \ + .target = {.file = stem ".hpp", \ + .suffix = stem ".ns.Later", \ + .label = "Class", \ + .declaration_line = line, \ + .declaration_text = "class Later { public: int z; };"}}, \ + { \ + .case_id = id "-member", \ + .site = C_TU_WORD(stem ".hpp", text, stem ".ns.after_the_block", "l->z", "z"), \ + .relation = "USAGE", \ + .target = {.file = stem ".hpp", \ + .suffix = stem ".ns.Later.z", \ + .label = "Field", \ + .declaration_line = line, \ + .declaration_text = "class Later { public: int z; };"}, \ + } + const RQ_Expected expected[] = { + SPLIT_ROWS("split-union", "split_union", with_union, 27), + SPLIT_ROWS("split-spelled-differently", "split_spelled", spelled, 27), + SPLIT_ROWS("two-versions-left-alone", "split_else", with_else, 31), + }; +#undef SPLIT_ROWS + const CTUCase fixture = {.suite = "c-tu-split-block", + .sources = C_TU_SPLIT, + .source_count = 3, + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Whether or not the `#include` is in the tree, the preprocessor takes it and it is an edge of this + * file. The path ends at its quote, whatever follows, and neither a computed path nor another + * directive nor one in a comment is an include. */ +static int c_imports_of_exactly(const CBMFileResult *result, const char *path) { + int count = 0; + for (int i = 0; i < result->imports.count; i++) + count += result->imports.items[i].module_path && + !strcmp(result->imports.items[i].module_path, path); + return count; +} + +TEST(resolver_cpp_include_in_an_enum_body_stays_an_import) { + static const char source[] = C_TU_ENUM_DIRECTIVES; + CBMFileResult *result = + cbm_extract_file(source, (int)strlen(source), CBM_LANG_CPP, "p", "enumdir.hpp", 0, NULL, NULL); + ASSERT(result != NULL); + ASSERT_EQ(c_imports_of_exactly(result, "kinds.inc"), 1); + ASSERT_EQ(c_imports_of_exactly(result, "twin_a.inc"), 1); + ASSERT_EQ(c_imports_of_exactly(result, "twin_b.inc"), 1); + cbm_free_result(result); + /* A namespace is no place the grammar lacks: this one is a plain `#include`, inside. */ + static const char in_namespace[] = "namespace n {\n#include \"tables.inc\"\n}\n"; + CBMFileResult *inside = cbm_extract_file(in_namespace, (int)strlen(in_namespace), CBM_LANG_CPP, "p", + "tables.hpp", 0, NULL, NULL); + ASSERT(inside != NULL); + ASSERT_EQ(c_imports_of_exactly(inside, "tables.inc"), 1); + cbm_free_result(inside); + /* One directive to a body, so that it is this read and no other that finds it. */ + static const struct { + const char *source, *path, *site; + } shapes[] = { + {"enum S {\n#include \"trailing.h\" // why\n};\n", "trailing.h", "\"trailing.h\""}, + {"enum S {\n# include \n};\n", "sys/angle.h", ""}, + {"enum S {\n#include COMPUTED_PATH\n};\n", NULL, NULL}, + {"enum S {\n#pragma \"not_an_include.h\"\n};\n", NULL, NULL}, + {"enum S {\n#include_nothing \"nor_this.h\"\n};\n", NULL, NULL}, + {"struct S {\n#include \"fields.inc\"\n};\n", "fields.inc", "\"fields.inc\""}, + {"struct S {\n#pragma \"not_an_include.h\"\n};\n", NULL, NULL}, + {"enum S {\nA,\n// #include \"said.h\"\n/*\n#include \"block.h\"\n*/\nB\n};\n", NULL, NULL}, + }; + const CBMLanguage languages[] = {CBM_LANG_C, CBM_LANG_CPP}; + for (size_t l = 0; l < sizeof(languages) / sizeof(languages[0]); l++) { + for (size_t i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i++) { + const char *text = shapes[i].source; + CBMFileResult *shaped = + cbm_extract_file(text, (int)strlen(text), languages[l], "p", "shapes.h", 0, NULL, NULL); + ASSERT(shaped != NULL); + ASSERT_EQ(shaped->imports.count, shapes[i].path ? 1 : 0); + if (shapes[i].path) { + const CBMImport *import = &shaped->imports.items[0]; + ASSERT_STR_EQ(import->module_path, shapes[i].path); + ASSERT_EQ(import->site_end_byte - import->site_start_byte, strlen(shapes[i].site)); + ASSERT(!strncmp(text + import->site_start_byte, shapes[i].site, strlen(shapes[i].site))); + } + cbm_free_result(shaped); + } + } + 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) { @@ -6733,8 +7193,13 @@ SUITE(resolver_c) { 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_enum_holding_a_define_and_an_include_keeps_the_namespace); + RUN_TEST(resolver_cpp_brace_split_across_conditionals_keeps_the_namespace); + RUN_TEST(resolver_cpp_include_in_an_enum_body_stays_an_import); RUN_TEST(resolver_cpp_opaque_enum_in_a_block_does_not_crash_and_hides_the_outer_one); RUN_TEST(resolver_c_lone_tag_under_a_directive_is_no_occurrence); + RUN_TEST(resolver_cpp_forward_declared_class_binds_by_qualified_name); + RUN_TEST(resolver_cpp_enumerator_is_read_from_the_enum_written); 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);