Merge pull request #2295 from MopicMP/fix/java-record-accessor-calls

fix(java): resolve record accessor calls, and never bind a call to data
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-30 22:26:38 +01:00
commit edb4a6e735
14 files changed
+895 -18

No files matched your search

+11 -3
View File
@@ -298,9 +298,10 @@ typedef struct {
* are bound to the owning result's source identity before cross-file use. */
uint32_t test_name_start_byte, test_name_end_byte;
uint32_t test_body_start_byte, test_body_end_byte;
/* Documents only: facts a document definition adds to its node's
* properties, as a NULL-terminated key, value, key, value, ... list (an
* ADR's id, status, date, deciders, title). Keys are fixed literals; the
/* Facts a definition adds to its node's properties, as a NULL-terminated
* key, value, key, value, ... list: a document's (an ADR's id, status,
* date, deciders, title), or CBM_PROP_GENERATED on a member the compiler
* writes and the source never spells. Keys are fixed literals; the
* values are escaped where the properties are written. Tail field:
* zero-init stays valid. */
const char **extra_props;
@@ -321,6 +322,13 @@ typedef struct {
const char *alias_target;
} CBMDefinition;
/* extra_props key of a generated member, one the compiler writes and the
* source never spells: a Java record's implicit accessor carries
* `generated: record`. The node keeps the property, and the pipeline registers
* such a definition by qualified name only, so no short-name guess can land on
* it (pipeline.h cbm_def_props_generated). */
#define CBM_PROP_GENERATED "generated"
/* Argument captured from a call expression */
typedef struct {
const char *expr; // raw expression text ("payload.info", "MY_URL", "'hello'")
+120
View File
@@ -230,6 +230,7 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
const CBMLangSpec *spec);
static TSNode find_class_body(TSNode class_node, CBMLanguage lang);
static void extract_enum_members(CBMExtractCtx *ctx, TSNode node, const char *class_qn);
static void extract_record_accessors(CBMExtractCtx *ctx, TSNode node, const char *class_qn);
static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec);
// --- Helpers ---
@@ -8317,6 +8318,10 @@ static void emit_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s
extract_enum_members(ctx, node, class_qn);
}
if (ctx->language == CBM_LANG_JAVA && strcmp(kind, "record_declaration") == 0) {
extract_record_accessors(ctx, node, class_qn);
}
// Extract methods inside the class
int methods_from = ctx->result->defs.count;
extract_class_methods(ctx, node, class_qn, spec);
@@ -9642,6 +9647,121 @@ static void extract_enum_members(CBMExtractCtx *ctx, TSNode node, const char *cl
}
}
/* Whether the record body declares this accessor itself (JLS §8.10.3 lets a
* record override any of them), so the implicit one is not emitted twice. */
static bool record_declares_accessor(CBMExtractCtx *ctx, TSNode node, const char *comp_name) {
TSNode body = find_class_body(node, ctx->language);
if (ts_node_is_null(body)) {
return false;
}
uint32_t mc = ts_node_named_child_count(body);
for (uint32_t mi = 0; mi < mc; mi++) {
TSNode member = ts_node_named_child(body, mi);
if (strcmp(ts_node_type(member), "method_declaration") != 0) {
continue;
}
TSNode mname = ts_node_child_by_field_name(member, TS_FIELD("name"));
if (ts_node_is_null(mname)) {
continue;
}
char *name = cbm_node_text(ctx->arena, mname, ctx->source);
if (!name || strcmp(name, comp_name) != 0) {
continue;
}
/* Same name, no parameters — that is the accessor. */
TSNode params = ts_node_child_by_field_name(member, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return true;
}
/* Comments are named extras, not parameters. */
bool has_parameters = false;
uint32_t pc = ts_node_named_child_count(params);
for (uint32_t pi = 0; pi < pc; pi++) {
if (!ts_node_is_extra(ts_node_named_child(params, pi))) {
has_parameters = true;
break;
}
}
if (!has_parameters) {
return true;
}
}
return false;
}
/* Java records: the compiler writes one accessor per component, so `p.x()`
* calls a method that appears nowhere in the source. Without a definition for
* it the call has nothing to resolve to and falls through to the project-wide
* name registry, which binds it to the first `x` in the tree — typically an
* unrelated class's private field. Emitting the accessors makes the record's
* own API visible, exactly as the language defines it (JLS §8.10.3). */
static void extract_record_accessors(CBMExtractCtx *ctx, TSNode node, const char *class_qn) {
CBMArena *a = ctx->arena;
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return;
}
uint32_t pc = ts_node_named_child_count(params);
for (uint32_t pi = 0; pi < pc; pi++) {
TSNode comp = ts_node_named_child(params, pi);
bool spread = strcmp(ts_node_type(comp), "spread_parameter") == 0;
if (!spread && strcmp(ts_node_type(comp), "formal_parameter") != 0) {
continue;
}
TSNode cname = ts_node_child_by_field_name(comp, TS_FIELD("name"));
TSNode ctype = ts_node_child_by_field_name(comp, TS_FIELD("type"));
if (spread) {
/* Java varargs have an unnamed type child and a nested declarator. */
TSNode decl = cbm_find_child_by_kind(comp, "variable_declarator");
if (ts_node_is_null(decl)) {
continue;
}
cname = ts_node_child_by_field_name(decl, TS_FIELD("name"));
uint32_t nc = ts_node_named_child_count(comp);
for (uint32_t ci = 0; ci < nc; ci++) {
TSNode child = ts_node_named_child(comp, ci);
if (!ts_node_is_extra(child) && strcmp(ts_node_type(child), "modifiers") != 0) {
ctype = child;
break;
}
}
}
if (ts_node_is_null(cname)) {
continue;
}
char *comp_name = cbm_node_text(a, cname, ctx->source);
if (!comp_name || !comp_name[0] || record_declares_accessor(ctx, node, comp_name)) {
continue;
}
CBMDefinition adef;
memset(&adef, 0, sizeof(adef));
adef.name = comp_name;
adef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, comp_name);
adef.label = "Method";
adef.file_path = ctx->rel_path;
adef.start_line = ts_node_start_point(comp).row + TS_LINE_OFFSET;
adef.end_line = ts_node_end_point(comp).row + TS_LINE_OFFSET;
adef.lines = 1;
adef.parent_class = class_qn;
adef.is_exported = true;
if (!ts_node_is_null(ctype)) {
const char *type = cbm_node_text(a, ctype, ctx->source);
adef.return_type = spread && type ? cbm_arena_sprintf(a, "%s[]", type) : type;
}
/* The compiler writes this member and the source never spells it: its
* node says so, and the pipeline registers it by qualified name only,
* so no short-name guess can land on it. */
const char **kv = (const char **)cbm_arena_alloc(a, CBM_SZ_3 * sizeof(char *));
if (kv) {
kv[0] = CBM_PROP_GENERATED;
kv[SKIP_ONE] = "record";
kv[PAIR_LEN] = NULL;
}
adef.extra_props = kv;
cbm_defs_push(&ctx->result->defs, a, adef);
}
}
/* Resolve the identifier node from a destructure pattern child.
* pair_pattern → value field; shorthand/identifier → itself; others → first named child. */
static TSNode destructure_ident(TSNode pat_child) {
+12 -4
View File
@@ -1281,10 +1281,18 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
continue;
}
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
/* A generated member (a Java record's implicit accessor) is registered
* by qualified name only: it joins through its owner's Class.method
* tail below, never on its leaf name alone -- `Objects.nonNull(x)` is
* no call of some record's nonNull(). Nor does it make a same-named
* callable ambiguous: the leaf fallback sees the graph as if it were
* not there. */
if (!cbm_node_props_generated(cand->properties_json)) {
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
}
}
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
continue;
+11
View File
@@ -997,6 +997,11 @@ static bool calls_emit_lsp_target(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
if (!target_node || source_node->id == target_node->id) {
return false;
}
/* LSP textual fallbacks can also name data, as with static imports. The
* call is handled: no edge, and no registry fallback after it. */
if (cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
return true;
}
cbm_resolution_t res = {0};
/* Keep the canonical graph QN, including project prefixes added by fallback. */
res.qualified_name = target_node->qualified_name;
@@ -1333,6 +1338,12 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
return calls_emit_route_call(ctx, call, source_node, module_qn, imp_keys, imp_vals,
imp_count, route_mount, lang, evidence);
}
/* A Java call never targets data — see cbm_java_suppress_call_to_data_member.
* The language gate lives here, as with the guards above, and MUST match
* pass_parallel.c exactly or the two resolvers diverge. */
if (cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
return 0;
}
evidence->candidate_qn = target_node->qualified_name;
evidence->calls_emitted |=
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
+8 -3
View File
@@ -509,10 +509,15 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
* (helpers.c): callables + type-like containers (INHERITS/IMPLEMENTS/method/
* field resolution), Variable/Field (READS/WRITES resolution), and Table/View
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
* through the same predicate, so the three registries cannot diverge. */
* through the same predicate, so the three registries cannot diverge.
* A generated member is registered by qualified name only, at all three. */
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
+14 -3
View File
@@ -1811,10 +1811,15 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
return 0;
}
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
* (helpers.c) — see pass_definitions.c for the per-label rationale, and
* for the generated member registered by qualified name only. */
if (cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
(*reg_entries)++;
}
char marked[CBM_SZ_4K];
@@ -3618,6 +3623,12 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
imp_keys, imp_vals, imp_count, route_mount, lang);
continue;
}
if (target_node && source_node->id != target_node->id &&
cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
/* Same guard as pass_calls.c — a Java call never targets a
* Variable or a Field. */
continue;
}
if (!target_node || source_node->id == target_node->id) {
/* HTTP/ASYNC calls to an EXTERNAL client library (`requests.get(url)`)
* resolve to an unindexed QN (target_node == NULL), but their edge
+9
View File
@@ -2604,6 +2604,15 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
if (import_candidate_is_python_local(ctx->gbuf, cand)) {
continue;
}
/* Nor is a generated member (a Java record's implicit
* accessor, an instance method no import can name). It is
* registered by qualified name only, and a package segment
* that merely spells it (`javax.crypto.spec` -> spec())
* must not bind the import -- or every use of the
* imported name -- to it. */
if (cbm_node_props_generated(cand->properties_json)) {
continue;
}
if (source_file_qn && cand->qualified_name &&
strcmp(cand->qualified_name, source_file_qn) == 0) {
continue; /* self */
+25
View File
@@ -278,6 +278,24 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
const char *qualified_name, const char *label,
CBMLanguage lang, size_t module_prefix_len);
/* Register a symbol for exact (qualified-name) lookups only: it is never
* offered to the short-name strategies (unique_name, suffix_match,
* field_type_hint, fuzzy). For a generated member (cbm_def_props_generated),
* which the source never spells, so only a type-aware resolver that knows its
* owner may bind it. The three graph registration sites route such a symbol
* here instead of cbm_registry_add_lang_in_module. */
void cbm_registry_add_qn_only(cbm_registry_t *r, const char *qualified_name, const char *label);
/* True when a definition's extra_props (key, value, ..., NULL) carry the
* generated-member key (cbm.h CBM_PROP_GENERATED), e.g. a Java record's
* implicit accessor. Pure; unit-tested in test_registry.c. */
bool cbm_def_props_generated(const char *const *extra_props);
/* The stored-node twin of cbm_def_props_generated, for the incremental
* registry seed: true when a node's properties JSON carries that key as a
* string property. Pure; unit-tested in test_registry.c. */
bool cbm_node_props_generated(const char *properties_json);
/* True when code in `caller` can call a symbol defined in `target` by bare
* name: same language, same interop family (JS/TS + script hosts, C/C++/CUDA/
* ObjC, JVM, .NET, ...; the table lives in registry.c), or Swift with the C
@@ -512,6 +530,13 @@ bool cbm_c_member_rule_file(CBMLanguage lang, const char *rel_path);
* a member); NULL when none or several do. */
const char *cbm_registry_unique_field_qn(const cbm_registry_t *r, const char *member_name);
/* A Java call expression never names data: `p.x()` is a method invocation
* whatever `x` spells elsewhere in the project. Drops a CALLS bind whose
* target is a Variable or Field. Java only — Kotlin properties and C function
* pointers are callable names that are not methods. Pure; unit-tested in
* test_registry.c. */
bool cbm_java_suppress_call_to_data_member(bool is_java, const char *target_label);
/* Get the label of a qualified name, or NULL if not found. */
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
+49 -5
View File
@@ -590,6 +590,39 @@ static char *delta_owner_props(const char *project, const char *name, const char
return out;
}
/* `props` (a JSON object) with a generated member's mark (cbm.h
* CBM_PROP_GENERATED) added, or NULL when `generated` is NULL/empty
* (CBM_MEM_CLASS_OTHER; the caller frees). */
static char *delta_generated_props(const char *props, const char *generated) {
if (!generated || !generated[0]) {
return NULL;
}
size_t pl = props ? strlen(props) : 0;
bool has_members = pl > PAIR_LEN && props[0] == '{' && props[pl - SKIP_ONE] == '}';
size_t cap = pl + PAIR_LEN * strlen(generated) + CBM_SZ_32;
char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, cap);
if (!out) {
return NULL;
}
size_t k = 0;
if (has_members) {
memcpy(out, props, pl - SKIP_ONE);
k = pl - SKIP_ONE;
out[k++] = ',';
} else {
out[k++] = '{';
}
k += (size_t)snprintf(out + k, cap - k, "\"%s\":\"", CBM_PROP_GENERATED);
for (const char *p = generated; *p && k + CBM_SZ_3 < cap; p++) {
if (*p == '"' || *p == '\\') {
out[k++] = '\\';
}
out[k++] = *p;
}
snprintf(out + k, cap - k, "\"}");
return out;
}
int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf) {
sqlite3 *db = cbm_store_get_db(store);
if (!db) {
@@ -634,7 +667,12 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
* goes by -- a Go method's QN has no receiver, a class-body variable's QN
* no class. A document naming `Order.Reset` binds such a node through
* its owner (doc_links_pdf.c), so a re-resolved document needs it too.
* Only where owner + name differs from the QN (delta_owner_props). */
* Only where owner + name differs from the QN (delta_owner_props).
*
* And a generated member's mark (a Java record's implicit accessor, a
* Method node): the registry seed reads it to register the member by
* qualified name only, as the full build does (cbm_node_props_generated);
* without it the proxy came back into the short-name index. */
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(
db,
@@ -646,7 +684,10 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
" start_line, end_line,"
" CASE WHEN label IN ('Method','Variable')"
" AND instr(properties, '\"parent_class\"') > 0"
" THEN json_extract(properties, '$.parent_class') END"
" THEN json_extract(properties, '$.parent_class') END,"
" CASE WHEN label = 'Method'"
" AND instr(properties, '\"" CBM_PROP_GENERATED "\"') > 0"
" THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END"
" FROM nodes"
" WHERE project = ?1 AND label NOT IN"
" ('Macro','Comment','Section','Branch','Commit','Tag')"
@@ -670,13 +711,16 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
int start_line = sqlite3_column_int(stmt, 6);
int end_line = sqlite3_column_int(stmt, 7);
const char *parent = (const char *)sqlite3_column_text(stmt, 8);
const char *generated = (const char *)sqlite3_column_text(stmt, 9);
char *owner_props =
parent ? delta_owner_props(project, name, qn, parent, client_props) : NULL;
const char *props = owner_props ? owner_props : (client_props ? client_props : "{}");
char *generated_props = delta_generated_props(props, generated);
/* Pin the gbuf id to the database id: proxies ARE their rows. */
cbm_gbuf_set_next_id(gbuf, id);
int64_t got =
cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line, end_line,
owner_props ? owner_props : (client_props ? client_props : "{}"));
int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line,
end_line, generated_props ? generated_props : props);
cbm_free(CBM_MEM_CLASS_OTHER, generated_props);
cbm_free(CBM_MEM_CLASS_OTHER, owner_props);
if (got != id) {
/* A QN collision inside the preseed set would silently split
+6
View File
@@ -1353,6 +1353,12 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
if (!incr_label_is_registry_symbol(node->label)) {
return;
}
/* A generated member stays out of the short-name index here as in the
* full index (pass_definitions.c); its node carries the mark. */
if (cbm_node_props_generated(node->properties_json)) {
cbm_registry_add_qn_only(seed->registry, node->qualified_name, node->label);
return;
}
CBMLanguage lang = cbm_registry_synthetic_path_language(node->file_path);
if (lang == CBM_LANG_COUNT && seed->lang_by_path && node->file_path) {
void *v = cbm_ht_get(seed->lang_by_path, node->file_path);
+50
View File
@@ -1370,6 +1370,30 @@ bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access) {
return is_c && is_member_access;
}
bool cbm_java_suppress_call_to_data_member(bool is_java, const char *target_label) {
/* A Java call expression never names data. `p.x()` is a method invocation
* whatever `x` spells elsewhere in the tree, so a CALLS edge into a
* Variable or a Field is a spelling collision and not a call.
*
* The bind is reached when the type-aware resolver finds no method to
* return: a record's accessors are implicit, so `record Point(double x,
* double y, double z)` declares no `x()` for java_lookup_method to find,
* and every `p.x()` falls through to the short-name registry — which
* matches the first `x` anywhere in the project, typically an unrelated
* class's private field. Measured on a 600-file Java tree (2026-09-22):
* 278 of 2444 CALLS edges landed on fields that way, and the callers of
* one four-field class absorbed every record read in the project.
*
* Java-gated, like the Go guard above, because the veto is only sound
* where no callable name can be anything but a method: Kotlin properties
* hold function values (`val f: () -> Unit; f()`) and C has function
* pointers, so both legitimately call through data. */
if (!is_java || !target_label) {
return false;
}
return strcmp(target_label, "Variable") == 0 || strcmp(target_label, "Field") == 0;
}
/* ── Lifecycle ──────────────────────────────────────────────────── */
cbm_registry_t *cbm_registry_new(void) {
@@ -1629,6 +1653,32 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
}
}
/* A generated member (a Java record's implicit accessor) is a definition the
* source never spells: no call names it except through its owner's type. The
* exact map holds it for the LSP targets and the import/same-module lookups,
* as it holds a Python function's locals; the by-name index does not, so no
* unique_name / suffix_match / field_type_hint / fuzzy guess can land on it. */
void cbm_registry_add_qn_only(cbm_registry_t *r, const char *qualified_name, const char *label) {
if (!r || !qualified_name || !label) {
return;
}
(void)registry_add_exact(r, qualified_name, label);
}
bool cbm_def_props_generated(const char *const *extra_props) {
for (const char *const *kv = extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
if (strcmp(kv[0], CBM_PROP_GENERATED) == 0) {
return true;
}
}
return false;
}
bool cbm_node_props_generated(const char *properties_json) {
/* The key as the definition passes write it: "generated":"<value>". */
return properties_json && strstr(properties_json, "\"" CBM_PROP_GENERATED "\":\"") != NULL;
}
/* ── Lookup ──────────────────────────────────────────────────────── */
bool cbm_registry_exists(const cbm_registry_t *r, const char *qn) {
+229
View File
@@ -35,6 +35,40 @@ static int crc_edge_count(cbm_store_t *store, const char *project, const char *t
return matches;
}
/* Edges of `type` out of `source_name` whose target's qualified name ends with
* `target_qn_suffix`. Two methods in one project can share a short name, and
* then only the qualified name tells them apart. */
static int crc_edge_count_to_qn(cbm_store_t *store, const char *project, const char *type,
const char *source_name, const char *target_qn_suffix) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
if (cbm_store_find_edges_by_type(store, project, type, &edges, &edge_count) != CBM_STORE_OK) {
return -1;
}
int matches = 0;
size_t suffix_len = strlen(target_qn_suffix);
for (int i = 0; i < edge_count; i++) {
cbm_node_t source = {0};
cbm_node_t target = {0};
bool source_ok =
cbm_store_find_node_by_id(store, edges[i].source_id, &source) == CBM_STORE_OK;
bool target_ok =
cbm_store_find_node_by_id(store, edges[i].target_id, &target) == CBM_STORE_OK;
if (source_ok && target_ok && source.name && target.qualified_name &&
strcmp(source.name, source_name) == 0) {
size_t qn_len = strlen(target.qualified_name);
if (qn_len >= suffix_len &&
strcmp(target.qualified_name + qn_len - suffix_len, target_qn_suffix) == 0) {
matches++;
}
}
cbm_node_free_fields(&source);
cbm_node_free_fields(&target);
}
cbm_store_free_edges(edges, edge_count);
return matches;
}
static int crc_global_handler_edge_count(cbm_store_t *store, const char *project,
const char *caller) {
int references =
@@ -423,6 +457,195 @@ TEST(call_reference_go_ambiguous_promoted_method_stays_usage) {
PASS();
}
TEST(call_java_record_accessor_binds_the_record) {
/* A record's accessors are implicit, so `record Point(double x, ...)`
* declares no `x()` in its source. Extraction emits them, and `other.x()`
* binds to the record's own accessor.
*
* Two things must not happen instead. The call must not reach the
* project-wide short-name registry and land on the first `x` in the tree —
* here an unrelated class's private field. And the guard that refuses such
* data binds must not swallow a real method that merely shares the
* spelling: the builder's `x(int)`, declared beside its own `x` field,
* keeps its edge. */
static const RFile files[] = {
{"Point.java", "package com.example;\n"
"public record Point(double x, double y, double z) {\n"
" public double first(Point other) {\n"
" return Maths.abs(other.x()) + new Builder().x(5).made();\n"
" }\n"
"}\n"},
{"Spring.java", "package com.example;\n"
"public final class Spring {\n"
" private double x;\n"
" public void put(double v) { this.x = v; }\n"
"}\n"},
{"Builder.java", "package com.example;\n"
"public final class Builder {\n"
" private int x;\n"
" public Builder x(int v) { this.x = v; return this; }\n"
" public int made() { return x; }\n"
"}\n"},
{"Maths.java", "package com.example;\n"
"public final class Maths {\n"
" public static double abs(double v) { return v < 0 ? -v : v; }\n"
"}\n"}};
RProj project;
cbm_store_t *store = rh_index_files(&project, files, 4);
ASSERT_NOT_NULL(store);
int fabricated_field = crc_edge_count(store, project.project, "CALLS", "first", "Field", "x");
int fabricated_var = crc_edge_count(store, project.project, "CALLS", "first", "Variable", "x");
int genuine = crc_edge_count(store, project.project, "CALLS", "first", "Method", "abs");
int accessor = crc_edge_count_to_qn(store, project.project, "CALLS", "first", ".Point.x");
int builder = crc_edge_count_to_qn(store, project.project, "CALLS", "first", ".Builder.x");
rh_cleanup(&project, store);
ASSERT_EQ(fabricated_field, 0);
ASSERT_EQ(fabricated_var, 0);
ASSERT_EQ(genuine, 1);
ASSERT_EQ(accessor, 1);
ASSERT_EQ(builder, 1);
PASS();
}
/* Static imports can name both an inherited method and a field. Thread's
* activeCount() is external to this project, whereas Owner.activeCount is
* local data; an unresolved import must not turn the field into a callee. */
static int crc_java_static_import_data_call_count(bool parallel) {
RFile files[52] = {{"example/Owner.java", "package example;\n"
"public class Owner extends Thread {\n"
" public static int activeCount;\n"
"}\n"},
{"example/Caller.java", "package example;\n"
"import static example.Owner.activeCount;\n"
"class Caller {\n"
" int caller() { return activeCount(); }\n"
"}\n"}};
char names[50][64];
char sources[50][64];
int count = parallel ? 52 : 2;
for (int i = 2; i < count; i++) {
snprintf(names[i - 2], sizeof(names[i - 2]), "example/Pad%d.java", i);
snprintf(sources[i - 2], sizeof(sources[i - 2]), "package example; class Pad%d {}\n", i);
files[i].name = names[i - 2];
files[i].content = sources[i - 2];
}
RProj project;
cbm_store_t *store = rh_index_files(&project, files, count);
if (!store) {
return -1;
}
int fields = crc_edge_count(store, project.project, "CALLS", "caller", "Field", "activeCount");
int vars = crc_edge_count(store, project.project, "CALLS", "caller", "Variable", "activeCount");
rh_cleanup(&project, store);
return fields < 0 || vars < 0 ? -1 : fields + vars;
}
TEST(call_java_static_import_does_not_call_field_sequential) {
ASSERT_EQ(crc_java_static_import_data_call_count(false), 0);
PASS();
}
TEST(call_java_static_import_does_not_call_field_parallel) {
ASSERT_EQ(crc_java_static_import_data_call_count(true), 0);
PASS();
}
TEST(call_java_record_varargs_accessor_has_array_return) {
static const char *sources[] = {
"record Values(String... xs) {}\n", "record Values(String[]... xs) {}\n",
"record Values(int... xs) {}\n", "record Values(@Deprecated String... xs) {}\n",
"record Values(String... xs) { public String[] xs() { return xs; } }\n"};
static const char *return_types[] = {"String[]", "String[][]", "int[]", "String[]", "String[]"};
for (int fixture = 0; fixture < 5; fixture++) {
CBMFileResult *result =
cbm_extract_file(sources[fixture], (int)strlen(sources[fixture]), CBM_LANG_JAVA,
"repro", "Values.java", 0, NULL, NULL);
ASSERT_NOT_NULL(result);
int accessors = 0;
bool array_return = false;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *def = &result->defs.items[i];
if (strcmp(def->name, "xs") == 0 && strcmp(def->label, "Method") == 0) {
accessors++;
array_return =
def->return_type && strcmp(def->return_type, return_types[fixture]) == 0;
}
}
cbm_free_result(result);
ASSERT_EQ(accessors, 1);
ASSERT_TRUE(array_return);
}
PASS();
}
/* Comment extras in an otherwise empty parameter list must not create a
* second, implicit accessor at the record component's source location. */
TEST(call_java_record_commented_accessor_has_only_explicit_span) {
static const char *sources[] = {"record Value(int x) {\n"
" public int x(/* explanation */) {\n"
" return x;\n"
" }\n"
"}\n",
"record Value(int x) {\n"
" public int x(// explanation\n"
" ) {\n"
" return x;\n"
" }\n"
"}\n"};
static const int end_lines[] = {4, 5};
for (int fixture = 0; fixture < 2; fixture++) {
CBMFileResult *result =
cbm_extract_file(sources[fixture], (int)strlen(sources[fixture]), CBM_LANG_JAVA,
"repro", "Value.java", 0, NULL, NULL);
ASSERT_NOT_NULL(result);
int accessors = 0;
int explicit_spans = 0;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *def = &result->defs.items[i];
if (strcmp(def->name, "x") == 0 && strcmp(def->label, "Method") == 0) {
accessors++;
if (def->start_line == 2 && def->end_line == end_lines[fixture]) {
explicit_spans++;
}
}
}
cbm_free_result(result);
ASSERT_EQ(accessors, 1);
ASSERT_EQ(explicit_spans, 1);
}
PASS();
}
TEST(call_java_record_commented_overload_keeps_implicit_accessor) {
static const char source[] =
"record Value(int x) {\n"
" public int x(/* explanation */ int scale) { return x * scale; }\n"
"}\n";
CBMFileResult *result = cbm_extract_file(source, (int)strlen(source), CBM_LANG_JAVA, "repro",
"Value.java", 0, NULL, NULL);
ASSERT_NOT_NULL(result);
int methods = 0;
int implicit_spans = 0;
int overload_spans = 0;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *def = &result->defs.items[i];
if (strcmp(def->name, "x") == 0 && strcmp(def->label, "Method") == 0) {
methods++;
if (def->start_line == 1 && def->end_line == 1) {
implicit_spans++;
}
if (def->start_line == 2 && def->end_line == 2) {
overload_spans++;
}
}
}
cbm_free_result(result);
ASSERT_EQ(methods, 2);
ASSERT_EQ(implicit_spans, 1);
ASSERT_EQ(overload_spans, 1);
PASS();
}
SUITE(call_reference_contract) {
RUN_TEST(call_reference_typescript_direct_argument_is_exact);
RUN_TEST(call_reference_kotlin_alias_argument_is_exact);
@@ -439,4 +662,10 @@ SUITE(call_reference_contract) {
RUN_TEST(call_reference_python_later_decorated_method_rebinding_stays_usage);
RUN_TEST(call_reference_go_bound_method_argument_is_exact);
RUN_TEST(call_reference_go_ambiguous_promoted_method_stays_usage);
RUN_TEST(call_java_record_accessor_binds_the_record);
RUN_TEST(call_java_record_varargs_accessor_has_array_return);
RUN_TEST(call_java_record_commented_accessor_has_only_explicit_span);
RUN_TEST(call_java_record_commented_overload_keeps_implicit_accessor);
RUN_TEST(call_java_static_import_does_not_call_field_sequential);
RUN_TEST(call_java_static_import_does_not_call_field_parallel);
}
+282
View File
@@ -18688,6 +18688,285 @@ TEST(pipeline_python_nested_def_local_after_incremental_issue1548) {
PASS();
}
/* A Java record's implicit accessors are GENERATED members (#2295): Method
* nodes marked `generated: record`, registered by qualified name only, as a
* Python function's locals are. The type-aware resolver binds `p.y()` on a
* Point to Point.y; no short-name guess may land on a member the source never
* spells. `far` calls y() on a type outside the project, so the project-wide
* name registry is its only route to Point.y. `data` calls x(), whose other
* spelling is Spring's private field: neither the accessor (a name guess) nor
* the field (a Java call never names data) is its target. The record's
* hand-written z() and norm() are source members: no `generated` mark.
* `org.external.y.Vector` is not in the project, and its package segment `y`
* spells the accessor: the import's symbol-name fallback must not bind it
* there (no IMPORTS edge), or `new Vector()` in `make` would follow the import
* onto the accessor (elasticsearch: `import javax.crypto.spec.PBEKeySpec`
* bound a record's `spec()`). `stat` calls, and `refs` passes, the JDK's
* Objects.nonNull: no project callable is its target, and Seen's accessor --
* the only project callable called nonNull -- must not be taken for it on its
* leaf name alone (elasticsearch: `Objects.nonNull(x)` and
* `filter(Objects::nonNull)` bound a test record's nonNull()). */
static const char jgen_point_java[] =
"package com.example;\n"
"public record Point(double x, double y, double z) {\n"
" public double z() { return z; }\n"
" public double norm() { return Math.sqrt(x * x + y * y + z * z); }\n"
"}\n";
static const char jgen_seen_java[] = "package com.example;\n"
"record Seen(boolean nonNull) {}\n";
static const char jgen_spring_java[] = "package com.example;\n"
"public final class Spring {\n"
" private double x;\n"
" public void put(double v) { this.x = v; }\n"
"}\n";
static const char jgen_caller_java[] =
"package com.example;\n"
"import java.util.List;\n"
"import java.util.Objects;\n"
"import org.external.y.Vector;\n"
"public final class Caller {\n"
" public double near(Point p) { return p.y(); }\n"
" public double far(Vector v) { return v.y(); }\n"
" public double data(Vector v) { return v.x(); }\n"
" public Object make() { return new Vector(); }\n"
" public boolean stat(Object o) { return Objects.nonNull(o); }\n"
" public long refs(List<Object> xs) {\n"
" return xs.stream().filter(Objects::nonNull).count();\n"
" }\n"
"}\n";
/* The id of the one `label` node called `name` in `file_path`; 0 when there is
* none or more than one. `generated` (optional) receives whether its
* properties carry the `generated: record` mark. */
static int64_t jgen_node(cbm_store_t *s, const char *project, const char *name, const char *label,
const char *file_path, bool *generated) {
cbm_node_t *nodes = NULL;
int count = 0;
int64_t id = 0;
int hits = 0;
bool marked = false;
if (cbm_store_find_nodes_by_name(s, project, name, &nodes, &count) != CBM_STORE_OK) {
return 0;
}
for (int i = 0; i < count; i++) {
if (nodes[i].label && strcmp(nodes[i].label, label) == 0 && nodes[i].file_path &&
strcmp(nodes[i].file_path, file_path) == 0) {
hits++;
id = nodes[i].id;
marked = nodes[i].properties_json &&
strstr(nodes[i].properties_json, "\"generated\":\"record\"") != NULL;
}
}
cbm_store_free_nodes(nodes, count);
if (generated) {
*generated = marked;
}
return hits == 1 ? id : 0;
}
/* CALLS edges out of `source_id` into `target_id`, or (target_id 0) into any
* node called `target_name`. */
static int jgen_calls(cbm_store_t *s, int64_t source_id, int64_t target_id,
const char *target_name) {
cbm_edge_t *edges = NULL;
int ec = 0;
int n = 0;
if (cbm_store_find_edges_by_source_type(s, source_id, "CALLS", &edges, &ec) != CBM_STORE_OK) {
return -1;
}
for (int i = 0; i < ec; i++) {
if (target_id) {
n += edges[i].target_id == target_id;
continue;
}
cbm_node_t t = {0};
if (cbm_store_find_node_by_id(s, edges[i].target_id, &t) == CBM_STORE_OK && t.name &&
strcmp(t.name, target_name) == 0) {
n++;
}
cbm_node_free_fields(&t);
}
if (edges) {
cbm_store_free_edges(edges, ec);
}
return n;
}
/* Edges of `type` into `target_id`. */
static int jgen_edges_into(cbm_store_t *s, int64_t target_id, const char *type) {
cbm_edge_t *edges = NULL;
int ec = 0;
if (cbm_store_find_edges_by_target_type(s, target_id, type, &edges, &ec) != CBM_STORE_OK) {
return -1;
}
if (edges) {
cbm_store_free_edges(edges, ec);
}
return ec;
}
/* One fact of jgen_check: reported, counted, and the check goes on, so a run
* shows every fact that does not hold. */
#define JGEN_EXPECT(cond) \
do { \
if (!(cond)) { \
printf(" FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
failed++; \
} \
} while (0)
/* Every fact the fixture pins, on one indexed store; the number that fail. */
static int jgen_check(cbm_store_t *s, const char *project) {
int failed = 0;
bool gen_x = false;
bool gen_y = false;
bool gen_z = true;
bool gen_norm = true;
bool gen_nn = false;
int64_t acc_nn = jgen_node(s, project, "nonNull", "Method", "Seen.java", &gen_nn);
int64_t acc_x = jgen_node(s, project, "x", "Method", "Point.java", &gen_x);
int64_t acc_y = jgen_node(s, project, "y", "Method", "Point.java", &gen_y);
int64_t own_z = jgen_node(s, project, "z", "Method", "Point.java", &gen_z);
int64_t norm = jgen_node(s, project, "norm", "Method", "Point.java", &gen_norm);
int64_t near_m = jgen_node(s, project, "near", "Method", "Caller.java", NULL);
int64_t far_m = jgen_node(s, project, "far", "Method", "Caller.java", NULL);
int64_t data = jgen_node(s, project, "data", "Method", "Caller.java", NULL);
int64_t make = jgen_node(s, project, "make", "Method", "Caller.java", NULL);
int64_t stat = jgen_node(s, project, "stat", "Method", "Caller.java", NULL);
int64_t refs = jgen_node(s, project, "refs", "Method", "Caller.java", NULL);
JGEN_EXPECT(acc_x > 0 && acc_y > 0 && own_z > 0 && norm > 0 && acc_nn > 0);
JGEN_EXPECT(near_m > 0 && far_m > 0 && data > 0 && make > 0 && stat > 0 && refs > 0);
/* The compiler's members are marked; the hand-written ones are not. */
JGEN_EXPECT(gen_x);
JGEN_EXPECT(gen_y);
JGEN_EXPECT(gen_nn);
JGEN_EXPECT(!gen_z);
JGEN_EXPECT(!gen_norm);
/* Bound by qualified name through the receiver's type ... */
JGEN_EXPECT(jgen_calls(s, near_m, acc_y, NULL) == 1);
/* ... and never by a short-name guess. */
JGEN_EXPECT(jgen_calls(s, far_m, acc_y, NULL) == 0);
JGEN_EXPECT(jgen_calls(s, far_m, 0, "y") == 0);
/* Neither the accessor nor Spring's field `x`. */
JGEN_EXPECT(jgen_calls(s, data, 0, "x") == 0);
/* No import names a generated member, so nothing follows one onto it. */
JGEN_EXPECT(jgen_edges_into(s, acc_y, "IMPORTS") == 0);
JGEN_EXPECT(jgen_calls(s, make, 0, "y") == 0);
/* The JDK's Objects.nonNull is not Seen's accessor, called or referenced. */
JGEN_EXPECT(jgen_calls(s, stat, acc_nn, NULL) == 0);
JGEN_EXPECT(jgen_edges_into(s, acc_nn, "CALL_REFERENCE") == 0);
JGEN_EXPECT(jgen_edges_into(s, acc_nn, "USAGE") == 0);
return failed;
}
#undef JGEN_EXPECT
static int jgen_write_fixture(const char *tmp) {
write_temp_file(tmp, "Point.java", jgen_point_java);
write_temp_file(tmp, "Seen.java", jgen_seen_java);
write_temp_file(tmp, "Spring.java", jgen_spring_java);
write_temp_file(tmp, "Caller.java", jgen_caller_java);
return 0;
}
TEST(pipeline_java_record_accessors_generated_qn_only) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
jgen_write_fixture(tmp);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/java_gen.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = jgen_check(s, cbm_pipeline_project_name(p));
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
PASS();
}
/* The same fixture through pass_parallel.c: >= 50 files and CBM_WORKERS=4. */
TEST(pipeline_java_record_accessors_generated_qn_only_parallel) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_par_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
jgen_write_fixture(tmp);
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "Filler%d.java", i);
snprintf(body, sizeof(body),
"package com.example;\nclass Filler%d { int f() { return %d; } }\n", i, i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/java_gen_par.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
int run = p ? cbm_pipeline_run(p) : -1;
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
ASSERT_NOT_NULL(p);
ASSERT_EQ(run, 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = jgen_check(s, cbm_pipeline_project_name(p));
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
PASS();
}
/* An incremental re-index seeds its registry from the stored graph: the
* untouched Point.java's accessors come back from their nodes, and must stay
* out of the short-name index exactly as in the full index while the touched
* Caller.java is re-resolved. */
TEST(pipeline_java_record_accessors_generated_qn_only_after_incremental) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_incr_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
jgen_write_fixture(tmp);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/java_gen_incr.db", tmp);
cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p1);
ASSERT_EQ(cbm_pipeline_run(p1), 0);
cbm_pipeline_free(p1);
char caller_path[512];
snprintf(caller_path, sizeof(caller_path), "%s/Caller.java", tmp);
ASSERT_EQ(th_append_file(caller_path, "\n// touched: re-resolve Caller.java incrementally\n"),
0);
cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p2);
ASSERT_EQ(cbm_pipeline_run(p2), 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = jgen_check(s, cbm_pipeline_project_name(p2));
cbm_store_close(s);
cbm_pipeline_free(p2);
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
PASS();
}
/* As pyn_edge, and the edge's properties also contain `fragment`. */
static bool pyn_edge_with(cbm_store_t *s, const char *project, const char *src_rel,
const char *tgt_rel, const char *type, const char *fragment) {
@@ -29120,6 +29399,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_import_never_binds_a_local_def_issue1548);
RUN_TEST(pipeline_python_nested_defs_parallel_issue1548);
RUN_TEST(pipeline_python_nested_def_local_after_incremental_issue1548);
RUN_TEST(pipeline_java_record_accessors_generated_qn_only);
RUN_TEST(pipeline_java_record_accessors_generated_qn_only_parallel);
RUN_TEST(pipeline_java_record_accessors_generated_qn_only_after_incremental);
RUN_TEST(pipeline_python_local_def_shadows_module_def_issue1548);
RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548);
RUN_TEST(pipeline_python_super_follows_c3_mro);
+69
View File
@@ -1434,6 +1434,72 @@ TEST(c_member_rule_covers_c_files_and_every_dot_h) {
PASS();
}
TEST(java_call_never_binds_data_member) {
/* A Java call expression never names data, so a CALLS bind that landed on
* a Variable or a Field is a spelling collision. The shape that produces
* it: a record's accessors are implicit, `p.x()` finds no declared `x()`,
* and the short-name registry hands back the first `x` in the tree — an
* unrelated class's private field. */
ASSERT_TRUE(cbm_java_suppress_call_to_data_member(true, "Field"));
ASSERT_TRUE(cbm_java_suppress_call_to_data_member(true, "Variable"));
/* Everything callable is kept, including the constructor's Class target. */
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, "Method"));
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, "Function"));
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, "Class"));
/* Other languages have callable data: a Kotlin property or a C function
* pointer is a name that is not a method and is called all the same. */
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(false, "Field"));
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(false, "Variable"));
/* Degenerate input → nothing to judge. */
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, NULL));
PASS();
}
TEST(generated_member_registers_by_qn_only) {
/* A generated member (a Java record's implicit accessor) is found by its
* qualified name, never by a short-name guess. The same member registered
* the ordinary way is exactly what the guess binds. */
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add_qn_only(r, "proj.geo.Point.Point.y", "Method");
ASSERT_TRUE(cbm_registry_exists(r, "proj.geo.Point.Point.y"));
ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.geo.Point.Point.y"), "Method");
const char **qns = NULL;
int n = -1;
ASSERT_EQ(cbm_registry_find_by_name(r, "y", &qns, &n), 0);
ASSERT_EQ(n, 0);
cbm_resolution_t res = cbm_registry_resolve(r, "v.y", "proj.app.Caller", NULL, NULL, 0);
ASSERT_NULL(res.qualified_name);
cbm_registry_add(r, "x", "proj.geo.Point.Point.x", "Method");
res = cbm_registry_resolve(r, "v.x", "proj.app.Caller", NULL, NULL, 0);
ASSERT_STR_EQ(res.qualified_name, "proj.geo.Point.Point.x");
/* Degenerate input registers nothing. */
cbm_registry_add_qn_only(r, NULL, "Method");
cbm_registry_add_qn_only(r, "proj.geo.Point.Point.z", NULL);
cbm_registry_add_qn_only(NULL, "proj.geo.Point.Point.z", "Method");
ASSERT_EQ(cbm_registry_size(r), 2);
cbm_registry_free(r);
PASS();
}
TEST(generated_member_mark_on_defs_and_nodes) {
static const char *const record[] = {"generated", "record", NULL};
static const char *const later[] = {"source", "openapi", "generated", "lombok", NULL};
static const char *const doc[] = {"id", "ADR-7", "status", "accepted", NULL};
static const char *const keyless[] = {"generated", NULL};
ASSERT_TRUE(cbm_def_props_generated(record));
ASSERT_TRUE(cbm_def_props_generated(later));
ASSERT_FALSE(cbm_def_props_generated(doc));
ASSERT_FALSE(cbm_def_props_generated(keyless)); /* a key without a value is no fact */
ASSERT_FALSE(cbm_def_props_generated(NULL));
ASSERT_TRUE(
cbm_node_props_generated("{\"parent_class\":\"p.Point\",\"generated\":\"record\"}"));
ASSERT_FALSE(cbm_node_props_generated("{\"parent_class\":\"p.Point\"}"));
/* The word inside a value is not the key. */
ASSERT_FALSE(cbm_node_props_generated("{\"docstring\":\"code generated by protoc\"}"));
ASSERT_FALSE(cbm_node_props_generated(NULL));
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.
@@ -2597,6 +2663,9 @@ SUITE(registry) {
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(java_call_never_binds_data_member);
RUN_TEST(generated_member_registers_by_qn_only);
RUN_TEST(generated_member_mark_on_defs_and_nodes);
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);