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
13 changes: 6 additions & 7 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3789,10 +3789,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct
* field; the member half of a selector may. */
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote
* a struct field; the member half of a selector may. */
if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
Expand Down Expand Up @@ -3918,9 +3917,9 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct field;
* a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote a
* struct field; a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) {
continue;
}
const char *etype = rw->is_write ? "WRITES" : "READS";
Expand Down
13 changes: 6 additions & 7 deletions src/pipeline/pass_usages.c
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct
* field; the member half of a selector may. */
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote
* a struct field; the member half of a selector may. */
if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
Expand Down Expand Up @@ -416,9 +415,9 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct field;
* a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote a
* struct field; a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) {
continue;
}

Expand Down
15 changes: 8 additions & 7 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,14 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
* unit-tested in test_registry.c. */
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);

/* #1942: a bare (dot-less) Go reference can never denote a struct field —
* field access is always a selector expression, and selector references
* resolve on the LSP path. Drops a READS/WRITES/USAGE bind whose target is a
* Field when the reference text carries no '.'. Go only: other OO languages
* legitimately reference their own members bare inside method bodies. Pure;
* unit-tested in test_registry.c. */
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label);
/* #1942/#2666: a bare Go or C reference can never denote a struct field —
* field access is always a selector expression (or, in C, a designator), and
* the member half of one is recorded as is_member_access. Drops a
* READS/WRITES/USAGE bind whose target is a Field when the reference was not
* the member half of a selector. Go and C only: other OO languages (and C++,
* which every `.h` is parsed as) legitimately reference their own members
* bare inside method bodies. Pure; unit-tested in test_registry.c. */
bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access, const char *target_label);

/* What to do with a call whose registry resolution landed on a Field node.
* Pure; unit-tested in test_registry.c. */
Expand Down
19 changes: 15 additions & 4 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -1276,18 +1276,29 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
return true;
}

bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label) {
bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access,
const char *target_label) {
/* #1942/#1962: a bare Go identifier can never denote a struct field —
* field access is always a selector expression (x.f). The extractor
* strips the receiver before the resolver runs (resolve_lhs_write_name
* records the trailing field name; is_reference_node records the inner
* field_identifier), so the reference TEXT is always dot-less and cannot
* carry the distinction — the recorded is_member_access shape can. Only a
* reference that was never the member half of a selector is refused a
* Field bind. Go-gated: a C#/Java/C++/Python method body legitimately
* Field bind.
*
* #2666: C has the same rule. A struct member is named only as the member
* half of `a.b`, `a->b` or a designator `.b`, all recorded as
* field_identifier; a bare C identifier (a macro argument such as
* RUN_SELECTED_SUITE(mcp), a stray name in an unexpanded macro body) is
* never one. The call path already drops a bare C call onto a Field
* (cbm_call_onto_field_policy).
*
* Go- and C-gated: a C#/Java/C++/Python method body legitimately
* references its own members bare (cp_reads_writes_cs_static_field pins
* that shape as required), so a global veto would break those languages. */
if (!is_go || is_member_access || !target_label) {
* that shape as required), so a global veto would break those languages.
* A `.h` is parsed as C++ and keeps that behavior. */
if ((lang != CBM_LANG_GO && lang != CBM_LANG_C) || is_member_access || !target_label) {
return false;
}
return strcmp(target_label, "Field") == 0;
Expand Down
89 changes: 89 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -11516,6 +11516,93 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) {
PASS();
}

/* #2666: tests/test_main.c runs every suite through a macro,
* RUN_SELECTED_SUITE(mcp); the argument is a bare C identifier that names no
* declaration, and the registry fallback bound it to whichever struct member
* was called `mcp`. toggle() writes a bare name the extractor cannot see
* declared (a macro-made global, say) that a struct member also carries.
* state.c holds the genuine member uses that must keep their edges.
* pad_files pushes the run past MIN_FILES_FOR_PARALLEL for the parallel twin. */
static void write_c_bare_field_fixture(const char *tmp, int pad_files) {
write_temp_file(tmp, "include/suite.h",
"typedef struct suite_state {\n"
" char mcp[64];\n"
" int count;\n"
" int enabled;\n"
"} suite_state_t;\n");
write_temp_file(tmp, "tests/main.c",
"#include \"suite.h\"\n"
"#define RUN_SELECTED_SUITE(name) run_suite(#name)\n"
"int run_suite(const char *name);\n"
"int main(void) {\n"
" RUN_SELECTED_SUITE(mcp);\n"
" RUN_SELECTED_SUITE(count);\n"
" return 0;\n"
"}\n"
"void toggle(void) {\n"
" enabled = 1;\n"
"}\n");
write_temp_file(tmp, "src/state.c",
"#include \"suite.h\"\n"
"int state_count(const suite_state_t *s) { return s->count; }\n"
"void state_reset(suite_state_t *s) { s->enabled = 0; }\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "pad/filler%d.c", i);
snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i);
write_temp_file(tmp, name, body);
}
}

static int run_c_bare_field_case(const char *tmpl, int pad_files) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "%s", tmpl);
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_c_bare_field_fixture(tmp, pad_files);

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/c_bare.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);
const char *project = cbm_pipeline_project_name(p);

cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);

/* The fields must exist for the probes to mean anything. */
ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "mcp", "Field"), 1);
ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "count", "Field"), 1);
ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "enabled", "Field"), 1);
/* Reproduce-first: RED before the fix — the macro arguments bind the
* same-named struct members, and the bare write binds `enabled`. */
ASSERT_FALSE(cross_file_edge_exists(s, project, "main", "mcp", "USAGE"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "main", "count", "USAGE"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "toggle", "enabled", "WRITES"));
/* The member half of `s->count` / `s->enabled = 0` still binds the field. */
ASSERT_TRUE(cross_file_edge_exists(s, project, "state_count", "count", "USAGE"));
ASSERT_TRUE(cross_file_edge_exists(s, project, "state_reset", "enabled", "WRITES"));

cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}

TEST(pipeline_c_bare_ref_never_binds_field) {
/* Sequential path: resolve_usage_edges / resolve_rw_edges. */
return run_c_bare_field_case("/tmp/cbm_c_bare_XXXXXX", 0);
}

TEST(pipeline_c_bare_ref_never_binds_field_parallel) {
/* Parallel twin: resolve_file_usages / resolve_file_rw must consult the
* same predicate (#1928's lesson). */
return run_c_bare_field_case("/tmp/cbm_c_barep_XXXXXX", 52);
}

/* A C caller of a header function that shares its name with a struct member
* elsewhere (kernel: include/linux/mm.h put_page beside the arm64
* kvm_pgtable_mm_ops.put_page member). pad_files pushes the run past
Expand Down Expand Up @@ -28686,6 +28773,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
RUN_TEST(pipeline_c_bare_ref_never_binds_field);
RUN_TEST(pipeline_c_bare_ref_never_binds_field_parallel);
RUN_TEST(pipeline_c_call_reaches_header_function_beside_field);
RUN_TEST(pipeline_c_call_reaches_header_function_beside_field_parallel);
RUN_TEST(pipeline_c_member_access_binds_field_by_object_type);
Expand Down
32 changes: 26 additions & 6 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -1245,17 +1245,36 @@ TEST(go_bare_ref_never_binds_field) {
* trailing name; is_reference_node records the inner field_identifier),
* so the selector-vs-bare distinction arrives as the recorded
* is_member_access signal, never as a dot in the reference text. */
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, false, "Field"));
ASSERT_TRUE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Field"));
/* The member half of a selector may bind a field. */
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, true, "Field"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, true, "Field"));
/* Bare references to non-fields are untouched. */
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Variable"));
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Function"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Variable"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Function"));
/* Other languages reference their own members bare inside methods —
* never suppressed (cp_reads_writes_cs_static_field pins the C# shape). */
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(false, false, "Field"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_CSHARP, false, "Field"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_JAVA, false, "Field"));
/* Degenerate input → nothing to judge. */
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, NULL));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, NULL));
PASS();
}

TEST(c_bare_ref_never_binds_field) {
/* #2666: a C struct member is named only as the member half of `a.b`,
* `a->b` or a designator `.b` (field_identifier → is_member_access). A
* bare C identifier — a macro argument like RUN_SELECTED_SUITE(mcp) — is
* never one, so it must not bind a same-named Field. */
ASSERT_TRUE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Field"));
/* The member half still binds (through the C owner rows upstream). */
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, true, "Field"));
/* A bare C name keeps binding globals, functions and types. */
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Variable"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Function"));
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Type"));
/* C++ (and every `.h`, parsed as C++): a method body names its own
* members bare, so the bind is kept. */
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_CPP, false, "Field"));
PASS();
}

Expand Down Expand Up @@ -2571,6 +2590,7 @@ SUITE(registry) {
RUN_TEST(registry_tie_prefers_csharp_implementation_over_ref_stub);
RUN_TEST(cross_language_ref_drops_go_vs_c);
RUN_TEST(go_bare_ref_never_binds_field);
RUN_TEST(c_bare_ref_never_binds_field);
RUN_TEST(call_onto_field_follows_language_and_shape);
RUN_TEST(c_bare_call_does_not_weigh_field_namesakes);
RUN_TEST(c_arrow_member_call_is_c_family_and_arrow_only);
Expand Down
Loading