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