mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
14 files changed
+895
-18
No files matched your search
+11
-3
@@ -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'")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user