mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(cpp): forward-declared classes, scoped enum values and three parse repairs
A forward-declared C++ class in view binds to the one definition of that qualified name, by the one-definition rule; with several definitions, to the one whose file includes the declaration. C tags have no linkage and stay abstained. A forward in view outranks a short-name match in another namespace, which removes false edges such as lldb's Stream to llvm::yaml::Stream. Color::RED, gfx::Blend::ADD and typedef'd enum values bind. Repairs: an #include in an enum body beside a #define no longer breaks the rest of the file; a brace opened in one #ifndef block and closed in another, as DeclSpec.h does, no longer costs the file its namespace; #include inside a namespace is read. Whole llvm-project: CALLS 647,135 to 654,085, USAGE 970,146 to 1,015,783, rc 0, 603 s, no quarantine. Changed relations sampled from source: no wrong edge. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
5af6035371
commit
2d7e422e7c
11 files changed
+1050
-67
No files matched your search
+89
-3
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 `<path>` 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")
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+263
-23
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+99
-28
@@ -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));
|
||||
|
||||
@@ -24,6 +24,9 @@ typedef enum {
|
||||
CBM_C_DECL_TEMPLATE_PARAMETER, /* `template <class Value> 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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 T> 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<int> *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 T> 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<int> *d", "Box",
|
||||
((RQ_Target){.file = "fw_defs.hpp",
|
||||
.suffix = "fw_defs.n.Box",
|
||||
.label = "Class",
|
||||
.declaration_line = 2,
|
||||
.declaration_text =
|
||||
"namespace n { template <class T> 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 <sys/angle.h>\n};\n", "sys/angle.h", "<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);
|
||||
|
||||
Reference in new issue
Block a user