From cc00a0275efa37bbcf1a2f27da17b33f9b63add4 Mon Sep 17 00:00:00 2001 From: Ilya Brykau Date: Sun, 30 Aug 2026 15:21:53 +0200 Subject: [PATCH] fix(pipeline): a bare Go reference never binds a struct Field The READS/WRITES resolvers and the USAGE registry fallback hand bare reference text to the short-name registry, which contains Field nodes - so once Go struct fields exist (#1935), every Go local err := ... binds whichever struct field is named err, project-wide: 21308 USAGE and 5191 WRITES onto Go fields on the measured repo, top target a test struct's field T collecting 3013 edges. In Go that binding is impossible by construction: a field is only reachable through a selector expression (x.f), and selector references resolve on the LSP path - every Field-targeted reference edge in the census carried dot-less text. Add cbm_go_suppress_bare_field_ref() next to the #1928 predicate and consult it at the same four sites (both READS/WRITES resolvers, both USAGE registry fallbacks): drop the bind when the file is Go, the target label is Field, and the reference text has no '.'. Go-gated because C#/Java/C++/Python method bodies legitimately reference their own members bare (cp_reads_writes_cs_static_field pins that shape). Field-validated on the #1940 stack: USAGE onto Go fields 21308 -> 0, WRITES 5191 -> 0; the only remaining field-targeted edges are 2466 CALLS, which are #1906/#1907's selector-guard territory. Reproduce- first pipeline probes (sequential + parallel twins) were RED on the stack without this commit. Fixes #1942 Signed-off-by: Ilya Brykau --- src/pipeline/pass_parallel.c | 9 +++ src/pipeline/pass_usages.c | 9 +++ src/pipeline/pipeline.h | 8 +++ src/pipeline/registry.c | 18 ++++++ tests/test_pipeline.c | 104 +++++++++++++++++++++++++++++++++++ tests/test_registry.c | 21 +++++++ 6 files changed, 169 insertions(+) diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index a642fb142..13e395a04 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2660,6 +2660,11 @@ 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: a bare Go reference can never denote a struct field. */ + if (tgt && + cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->ref_name, tgt->label)) { + continue; + } if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow || cbm_pipeline_node_is_callable_target(tgt))) { continue; @@ -2745,6 +2750,10 @@ 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: a bare Go reference can never denote a struct field. */ + if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->var_name, tgt->label)) { + continue; + } const char *etype = rw->is_write ? "WRITES" : "READS"; cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}"); } diff --git a/src/pipeline/pass_usages.c b/src/pipeline/pass_usages.c index 755273b5f..690b45ad9 100644 --- a/src/pipeline/pass_usages.c +++ b/src/pipeline/pass_usages.c @@ -216,6 +216,11 @@ 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: a bare Go reference can never denote a struct field. */ + if (tgt && + cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->ref_name, tgt->label)) { + continue; + } if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow || cbm_pipeline_node_is_callable_target(tgt))) { continue; @@ -304,6 +309,10 @@ 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: a bare Go reference can never denote a struct field. */ + if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->var_name, tgt->label)) { + continue; + } const char *edge_type = rw->is_write ? "WRITES" : "READS"; cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}"); diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index c85e6ff32..d174bb6d6 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -296,6 +296,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, const char *ref_name, 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); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index da6e69b04..eefdb7596 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -546,6 +546,24 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target return true; } +bool cbm_go_suppress_bare_field_ref(bool is_go, const char *ref_name, const char *target_label) { + /* #1942: a bare (dot-less) Go reference can never denote a struct field — + * field access is always a selector expression (x.f), and selector + * references resolve through the LSP join, never through the bare-name + * registry fallback. Every Field-targeted reference edge in the field + * census carried dot-less text, so dropping the bind loses nothing real. + * Go-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 || !ref_name || !ref_name[0] || !target_label) { + return false; + } + if (strcmp(target_label, "Field") != 0) { + return false; + } + return strchr(ref_name, '.') == NULL; +} + /* ── Lifecycle ──────────────────────────────────────────────────── */ cbm_registry_t *cbm_registry_new(void) { diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index acc5d8879..8e27a5d5d 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -4832,6 +4832,108 @@ TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) { PASS(); } +static int count_nodes_named(cbm_store_t *s, const char *project, const char *name); + +/* Fixture for the #1942 bare-reference-vs-Field probes: a Go struct field + * named like the commonest local (`err`), and a function whose local of the + * same name must NOT bind it — in Go a field is only reachable through a + * selector expression, never a bare identifier. Needs Go Field extraction + * (#1935) to have anything to falsely bind. */ +static void write_go_bare_field_fixture(const char *tmp, int pad_files) { + write_temp_file(tmp, "go.mod", "module example.com/fxbare\n\ngo 1.22\n"); + write_temp_file(tmp, "state/state.go", + "package state\n" + "\n" + "type Tracker struct {\n" + "\terr error\n" + "\tn int\n" + "}\n"); + write_temp_file(tmp, "app/app.go", + "package app\n" + "\n" + "import \"errors\"\n" + "\n" + "func Run() error {\n" + "\terr := errors.New(\"x\")\n" + "\treturn err\n" + "}\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "pad/filler%d.go", i); + snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i); + write_temp_file(tmp, name, body); + } +} + +TEST(pipeline_go_bare_ref_never_binds_field) { + /* #1942: the READS/WRITES resolvers and the USAGE registry fallback hand + * bare reference text to the short-name registry, which contains Field + * nodes — so every Go local `err := …` bound whichever struct field was + * named err (21308 USAGE / 5191 WRITES onto Go fields on the measured + * repo, top target a test struct's field collecting 3013 edges). + * Sequential-path twin; the parallel twin follows. */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_bare_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_go_bare_field_fixture(tmp, 0); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_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 field must exist for the probe to mean anything (#1935's fix). */ + ASSERT_TRUE(count_nodes_named(s, project, "err") >= 1); + /* Reproduce-first: RED before the fix — the bare local binds the field. */ + ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES")); + ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS")); + ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE")); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_go_bare_ref_never_binds_field_parallel) { + /* Parallel twin: resolve_file_rw / resolve_file_usages are independent + * resolvers and must consult the same predicate (#1928's lesson). */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_barep_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_go_bare_field_fixture(tmp, 52); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_barep.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); + + ASSERT_TRUE(count_nodes_named(s, project, "err") >= 1); + ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES")); + ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS")); + ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE")); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + /* Count nodes with the given exact name in the project (e.g. a Route path). */ static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) { cbm_node_t *ns = NULL; @@ -12951,6 +13053,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel); + RUN_TEST(pipeline_go_bare_ref_never_binds_field); + RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel); RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges); RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges); RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier); diff --git a/tests/test_registry.c b/tests/test_registry.c index b4de91280..3e1d604af 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -839,6 +839,26 @@ TEST(cross_language_ref_drops_go_vs_c) { PASS(); } +TEST(go_bare_ref_never_binds_field) { + /* #1942: a bare (dot-less) Go reference can never denote a struct field — + * field access is always a selector expression. */ + ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, "err", "Field")); + ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, "config", "Field")); + /* A selector-shaped reference may bind a field. */ + ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "t.err", "Field")); + /* Bare references to non-fields are untouched. */ + ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", "Variable")); + ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", "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, "_count", "Field")); + /* Degenerate inputs → nothing to judge. */ + ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, NULL, "Field")); + ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "", "Field")); + ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", 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. @@ -974,6 +994,7 @@ SUITE(registry) { RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls); RUN_TEST(cross_language_suffix_match_drops_py_vs_js); RUN_TEST(cross_language_ref_drops_go_vs_c); + RUN_TEST(go_bare_ref_never_binds_field); RUN_TEST(dynamic_suppress_drops_weak_method_matches); RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods); }