Skip to content
Open
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
24 changes: 24 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -4780,6 +4780,20 @@ static TSNode find_class_member_body(TSNode class_node, CBMLanguage lang) {
return ts_node_is_null(declarations) ? body : declarations;
}

/* Go structs keep their field_declaration nodes one level below the body that
* find_class_body() returns: type_spec's `type` child is a struct_type whose
* only named child is a field_declaration_list. Interfaces need no such step --
* interface_type holds its method specs directly, which is why interface members
* extracted correctly while every struct field was silently skipped. Normalize
* here, the same way the Java enum_body_declarations step above does. */
static TSNode go_normalize_struct_body(TSNode body) {
if (ts_node_is_null(body) || strcmp(ts_node_type(body), "struct_type") != 0) {
return body;
}
TSNode list = cbm_find_child_by_kind(body, "field_declaration_list");
return ts_node_is_null(list) ? body : list;
}

// Dart: resolve method name from method_signature/function_signature.
static TSNode resolve_dart_method_name(TSNode child, const char *ck) {
if (strcmp(ck, "method_signature") == 0) {
Expand Down Expand Up @@ -6656,6 +6670,9 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
}

TSNode body = find_class_member_body(class_node, ctx->language);
if (ctx->language == CBM_LANG_GO) {
body = go_normalize_struct_body(body);
}
if (ts_node_is_null(body)) {
return;
}
Expand Down Expand Up @@ -6945,6 +6962,13 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
continue;
}

/* Go: `_` is the blank identifier, used for explicit struct padding in
* generated code. It is not a referenceable field, and emitting it gives
* every `_` in the repository a same-named node to collide with. */
if (ctx->language == CBM_LANG_GO && strcmp(name, "_") == 0) {
continue;
}

const char *field_qn = cbm_arena_sprintf(a, "%s.%s", class_qn, name);

CBMDefinition def;
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2654,6 +2654,12 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
continue;
}
tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
/* #1928: the registry fallback is a bare-name guess — never let it
* bind a reference across a language boundary. Mirrors the
* sequential twin (pass_usages.c). */
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
Expand Down Expand Up @@ -2713,7 +2719,7 @@ static void resolve_file_throws(resolve_ctx_t *rc, resolve_worker_state_t *ws,
/* Resolve reads/writes for one file. */
static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count) {
const char **imp_vals, int imp_count, CBMLanguage lang) {
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
if (!rw->var_name) {
Expand All @@ -2733,6 +2739,12 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
if (!tgt || src->id == tgt->id) {
continue;
}
/* #1928: every resolution here is a bare-name registry guess — never
* let it bind a read/write across a language boundary. Mirrors the
* sequential twin (pass_usages.c). */
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
const char *etype = rw->is_write ? "WRITES" : "READS";
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}");
}
Expand Down Expand Up @@ -3179,7 +3191,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {

/* ── READS / WRITES ────────────────────────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_rw(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count);
resolve_file_rw(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang);
atomic_fetch_add_explicit(&rc->time_ns_rw, extract_now_ns() - _ph_t0, memory_order_relaxed);

/* ── INHERITS + DECORATES + IMPLEMENTS ──────────────────── */
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline/pass_usages.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
continue;
}
tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
/* #1928: the registry fallback is a bare-name guess — never let it
* bind a reference across a language boundary (the LSP-backed
* semantic branch above is not affected). */
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
Expand Down Expand Up @@ -270,7 +276,7 @@ static int resolve_throw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
/* Resolve READS/WRITES edges for one file's extracted read/write accesses. */
static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, const char *rel,
const char *module_qn, const char **imp_keys, const char **imp_vals,
int imp_count) {
int imp_count, CBMLanguage lang) {
int resolved = 0;
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
Expand All @@ -293,6 +299,11 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
if (!tgt || src->id == tgt->id) {
continue;
}
/* #1928: every resolution here is a bare-name registry guess — never
* let it bind a read/write across a language boundary. */
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}

const char *edge_type = rw->is_write ? "WRITES" : "READS";
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}");
Expand Down Expand Up @@ -359,7 +370,8 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil
imp_count, files[i].language);
throw_resolved +=
resolve_throw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count,
files[i].language);

free(module_qn);
free_import_map(imp_keys, imp_vals, imp_count);
Expand Down
8 changes: 8 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy);

/* #1928: USAGE/WRITES/READS analog of the CALLS guard above. Reference edges
* resolved by the short-name registry carry no import-closure evidence, so a
* cross-language binding is a bare-name collision for EVERY strategy — drop
* it whenever the caller's language and the target file's language disagree
* (JS/TS family members and the C/C++ header family excepted). Pure;
* unit-tested in test_registry.c. */
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);

/* 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
38 changes: 38 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,13 @@ static bool js_ts_family(CBMLanguage lang) {
lang == CBM_LANG_ARKTS;
}

/* C and C++ are one family for cross-language checks: .h maps to CBM_LANG_CPP
* in the extension table, so a .c file referencing a symbol declared in its
* own header would otherwise read as a language boundary. */
static bool c_cpp_family(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP;
}

static const char *path_basename(const char *path) {
if (!path || !path[0]) {
return path;
Expand Down Expand Up @@ -508,6 +515,37 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
return true;
}

bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path) {
/* #1928: USAGE / WRITES / READS analog of the CALLS guard above. A
* variable or field reference resolved by the short-name registry must
* not cross a language boundary: unlike CALLS, a reference edge carries
* no import-closure evidence at all — a Go test's local `event` and an
* eBPF C probe's automatic `event` share nothing but the spelling, so
* EVERY registry strategy is a bare-name guess here and none is exempt.
* LSP-backed semantic references resolve before the registry fallback
* and never reach this predicate, which is where a genuine cross-language
* binding (a future cgo resolver) would live. The JS/TS family keeps its
* exemption (.js/.ts/.d.ts pairs legitimately share symbols), and C/C++
* count as one family (.h maps to CBM_LANG_CPP). */
if (caller_lang == CBM_LANG_COUNT || !target_file_path || !target_file_path[0]) {
return false;
}
CBMLanguage target_lang = cbm_language_for_filename(path_basename(target_file_path));
if (target_lang == CBM_LANG_COUNT) {
return false;
}
if (caller_lang == target_lang) {
return false;
}
if (js_ts_family(caller_lang) && js_ts_family(target_lang)) {
return false;
}
if (c_cpp_family(caller_lang) && c_cpp_family(target_lang)) {
return false;
}
return true;
}

/* ── Lifecycle ──────────────────────────────────────────────────── */

cbm_registry_t *cbm_registry_new(void) {
Expand Down
54 changes: 54 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -3015,6 +3015,59 @@ TEST(go_imports) {
PASS();
}

/* #1935: Go struct fields were never extracted — find_class_body() returns the
* struct_type node, whose only named child is a field_declaration_list, so the
* member loop matched nothing and every field was silently skipped (0 Field
* nodes for ~5055 declarations on the measured repo). Interfaces hold their
* method specs directly and always worked. The blank identifier `_` is struct
* padding, not a referenceable field, and must stay out (241 collision edges
* on two generated structs otherwise). */
TEST(extract_go_struct_fields_have_nodes) {
CBMFileResult *r = extract("package fxf\n\n"
"type Config struct {\n"
"\tName string\n"
"\tTimeout int\n"
"\tNested *Config\n"
"\t_ [8]byte\n"
"}\n\n"
"type Reader interface {\n"
"\tRead(p []byte) (int, error)\n"
"\tClose() error\n"
"}\n",
CBM_LANG_GO, "t", "cfg.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* RED before the descend fix: count is 0. The blank identifier must not
* bring it to 4. */
ASSERT_EQ(count_defs_with_label(r, "Field"), 3);
ASSERT_TRUE(has_def(r, "Field", "Name"));
ASSERT_TRUE(has_def(r, "Field", "Timeout"));
ASSERT_TRUE(has_def(r, "Field", "Nested"));
ASSERT_FALSE(has_def(r, "Field", "_"));
/* Each field carries its declared type in return_type. */
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (!d->label || strcmp(d->label, "Field") != 0) {
continue;
}
ASSERT_NOT_NULL(d->return_type);
if (strcmp(d->name, "Name") == 0) {
ASSERT_TRUE(strcmp(d->return_type, "string") == 0);
}
if (strcmp(d->name, "Timeout") == 0) {
ASSERT_TRUE(strcmp(d->return_type, "int") == 0);
}
if (strcmp(d->name, "Nested") == 0) {
ASSERT_TRUE(strcmp(d->return_type, "*Config") == 0);
}
}
/* Interface members keep extracting exactly as before. */
ASSERT_TRUE(has_def(r, "Method", "Read"));
ASSERT_TRUE(has_def(r, "Method", "Close"));
cbm_free_result(r);
PASS();
}

TEST(java_imports) {
CBMFileResult *r = extract(
"import java.util.List;\nimport java.util.ArrayList;\nimport static java.lang.Math.PI;\n"
Expand Down Expand Up @@ -6699,6 +6752,7 @@ SUITE(extraction) {
RUN_TEST(python_imports);
RUN_TEST(js_imports);
RUN_TEST(go_imports);
RUN_TEST(extract_go_struct_fields_have_nodes);
RUN_TEST(java_imports);
RUN_TEST(rust_imports);
RUN_TEST(c_imports);
Expand Down
Loading
Loading