Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'")
Expand Down
120 changes: 120 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 12 additions & 4 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 8 additions & 3 deletions src/pipeline/pass_definitions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 14 additions & 3 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/pipeline/pass_pkgmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
25 changes: 25 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading