Skip to content

Commit 192f296

Browse files
authored
Merge pull request DeusData#2006 from ilyabrykau-orca/fix/go-field-ref-member-signal
fix(pipeline): key the Go Field guard on the recorded selector shape, not reference text
2 parents 01bf5f2 + 349aecb commit 192f296

9 files changed

Lines changed: 86 additions & 41 deletions

File tree

‎internal/cbm/cbm.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ typedef struct {
292292
uint32_t site_start_byte; // exact reference-token span; end > start when present
293293
uint32_t site_end_byte; // exclusive byte offset in the source file
294294
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
295+
bool is_member_access; // token is the member half of a selector/attribute
296+
// (Go x.f — field_identifier). The extractor strips
297+
// the receiver, so this is the only surviving record
298+
// of selector shape (#1962). Default false.
295299
} CBMUsage;
296300

297301
typedef struct {
@@ -303,6 +307,9 @@ typedef struct {
303307
const char *var_name; // variable name
304308
const char *enclosing_func_qn; // QN of enclosing function
305309
bool is_write; // true = write, false = read
310+
bool is_member_access; // var_name is the field half of a selector/member LHS
311+
// (`t.err = x` → "err"); the receiver is stripped here,
312+
// so this is the only record of selector shape (#1962)
306313
} CBMReadWrite;
307314

308315
typedef struct {

‎internal/cbm/extract_semantic.c‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ static void walk_throws(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec
146146
// - field/member/selector access (`self.total = ...`, `obj.Field = ...` →
147147
// write the trailing field name `total`/`Field`)
148148
// Returns NULL if no simple write target can be determined.
149-
static char *resolve_lhs_write_name(CBMExtractCtx *ctx, TSNode left) {
149+
// Sets *is_member_out when the written name is the field half of a
150+
// member/selector LHS — the receiver is stripped from the returned name, so
151+
// this out-param is the only surviving record of the selector shape (#1962).
152+
static char *resolve_lhs_write_name(CBMExtractCtx *ctx, TSNode left, bool *is_member_out) {
150153
// Unwrap a single-element expression_list (Go).
151154
if (strcmp(ts_node_type(left), "expression_list") == 0) {
152155
if (ts_node_named_child_count(left) != 1) {
@@ -184,6 +187,9 @@ static char *resolve_lhs_write_name(CBMExtractCtx *ctx, TSNode left) {
184187
fld = ts_node_child_by_field_name(left, TS_FIELD("name"));
185188
}
186189
if (!ts_node_is_null(fld)) {
190+
if (is_member_out) {
191+
*is_member_out = true;
192+
}
187193
return cbm_node_text(ctx->arena, fld, ctx->source);
188194
}
189195
return NULL;
@@ -251,12 +257,14 @@ static void try_emit_assignment_write(CBMExtractCtx *ctx, TSNode node, const cha
251257
if (ts_node_is_null(left)) {
252258
return;
253259
}
254-
char *name = resolve_lhs_write_name(ctx, left);
260+
bool is_member = false;
261+
char *name = resolve_lhs_write_name(ctx, left, &is_member);
255262
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
256263
CBMReadWrite rw;
257264
rw.var_name = name;
258265
rw.is_write = true;
259266
rw.enclosing_func_qn = func_qn;
267+
rw.is_member_access = is_member;
260268
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
261269
}
262270
}
@@ -329,12 +337,14 @@ void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
329337
TSNode left = resolve_write_lhs_node(node);
330338

331339
if (!ts_node_is_null(left)) {
332-
char *name = resolve_lhs_write_name(ctx, left);
340+
bool is_member = false;
341+
char *name = resolve_lhs_write_name(ctx, left, &is_member);
333342
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
334343
CBMReadWrite rw;
335344
rw.var_name = name;
336345
rw.is_write = true;
337346
rw.enclosing_func_qn = state->enclosing_func_qn;
347+
rw.is_member_access = is_member;
338348
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
339349
}
340350
}

‎internal/cbm/extract_usages.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2673,6 +2673,10 @@ void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Wal
26732673
usage.ref_name = name;
26742674
usage.enclosing_func_qn = state->enclosing_func_qn;
26752675
usage.lexical_scope_id = usage_lexical_scope_id_for_node(ctx, state, node);
2676+
/* The member half of a selector is its own reference node
2677+
* (field_identifier); record that shape — the name alone cannot carry
2678+
* it, and the Go Field guard keys on it (#1962). */
2679+
usage.is_member_access = strcmp(ts_node_type(node), "field_identifier") == 0;
26762680
stamp_usage_site(ctx, &usage, node, name, state);
26772681
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
26782682
}

‎src/pipeline/pass_parallel.c‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2670,9 +2670,10 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
26702670
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
26712671
continue;
26722672
}
2673-
/* #1942: a bare Go reference can never denote a struct field. */
2674-
if (tgt &&
2675-
cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->ref_name, tgt->label)) {
2673+
/* #1942/#1962: a bare Go reference can never denote a struct
2674+
* field; the member half of a selector may. */
2675+
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
2676+
tgt->label)) {
26762677
continue;
26772678
}
26782679
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
@@ -2760,8 +2761,9 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
27602761
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
27612762
continue;
27622763
}
2763-
/* #1942: a bare Go reference can never denote a struct field. */
2764-
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->var_name, tgt->label)) {
2764+
/* #1942/#1962: a bare Go reference can never denote a struct field;
2765+
* a selector-LHS write (`t.err = x`) may bind it. */
2766+
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
27652767
continue;
27662768
}
27672769
const char *etype = rw->is_write ? "WRITES" : "READS";

‎src/pipeline/pass_usages.c‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,10 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
216216
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
217217
continue;
218218
}
219-
/* #1942: a bare Go reference can never denote a struct field. */
220-
if (tgt &&
221-
cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->ref_name, tgt->label)) {
219+
/* #1942/#1962: a bare Go reference can never denote a struct
220+
* field; the member half of a selector may. */
221+
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
222+
tgt->label)) {
222223
continue;
223224
}
224225
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
@@ -309,8 +310,9 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
309310
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
310311
continue;
311312
}
312-
/* #1942: a bare Go reference can never denote a struct field. */
313-
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->var_name, tgt->label)) {
313+
/* #1942/#1962: a bare Go reference can never denote a struct field;
314+
* a selector-LHS write (`t.err = x`) may bind it. */
315+
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
314316
continue;
315317
}
316318

‎src/pipeline/pipeline.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
302302
* Field when the reference text carries no '.'. Go only: other OO languages
303303
* legitimately reference their own members bare inside method bodies. Pure;
304304
* unit-tested in test_registry.c. */
305-
bool cbm_go_suppress_bare_field_ref(bool is_go, const char *ref_name, const char *target_label);
305+
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label);
306306

307307
/* Get the label of a qualified name, or NULL if not found. */
308308
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);

‎src/pipeline/registry.c‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -546,22 +546,21 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
546546
return true;
547547
}
548548

549-
bool cbm_go_suppress_bare_field_ref(bool is_go, const char *ref_name, const char *target_label) {
550-
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
551-
* field access is always a selector expression (x.f), and selector
552-
* references resolve through the LSP join, never through the bare-name
553-
* registry fallback. Every Field-targeted reference edge in the field
554-
* census carried dot-less text, so dropping the bind loses nothing real.
555-
* Go-gated: a C#/Java/C++/Python method body legitimately references its
556-
* own members bare (cp_reads_writes_cs_static_field pins that shape as
557-
* required), so a global veto would break those languages. */
558-
if (!is_go || !ref_name || !ref_name[0] || !target_label) {
549+
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label) {
550+
/* #1942/#1962: a bare Go identifier can never denote a struct field —
551+
* field access is always a selector expression (x.f). The extractor
552+
* strips the receiver before the resolver runs (resolve_lhs_write_name
553+
* records the trailing field name; is_reference_node records the inner
554+
* field_identifier), so the reference TEXT is always dot-less and cannot
555+
* carry the distinction — the recorded is_member_access shape can. Only a
556+
* reference that was never the member half of a selector is refused a
557+
* Field bind. Go-gated: a C#/Java/C++/Python method body legitimately
558+
* references its own members bare (cp_reads_writes_cs_static_field pins
559+
* that shape as required), so a global veto would break those languages. */
560+
if (!is_go || is_member_access || !target_label) {
559561
return false;
560562
}
561-
if (strcmp(target_label, "Field") != 0) {
562-
return false;
563-
}
564-
return strchr(ref_name, '.') == NULL;
563+
return strcmp(target_label, "Field") == 0;
565564
}
566565

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

‎tests/test_pipeline.c‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4857,6 +4857,17 @@ static void write_go_bare_field_fixture(const char *tmp, int pad_files) {
48574857
"\terr := errors.New(\"x\")\n"
48584858
"\treturn err\n"
48594859
"}\n");
4860+
/* #1962: genuine selector references from a sibling file of the same
4861+
* package. `t.err = nil` writes the field through a selector; `t.n` reads
4862+
* it. The extractor strips the receiver on both paths, so only the
4863+
* is_member_access signal can distinguish these from Run's bare local. */
4864+
write_temp_file(tmp, "state/reset.go",
4865+
"package state\n"
4866+
"\n"
4867+
"func (t *Tracker) Reset() int {\n"
4868+
"\tt.err = nil\n"
4869+
"\treturn t.n\n"
4870+
"}\n");
48604871
for (int i = 0; i < pad_files; i++) {
48614872
char name[64];
48624873
char body[128];
@@ -4896,6 +4907,11 @@ TEST(pipeline_go_bare_ref_never_binds_field) {
48964907
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES"));
48974908
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS"));
48984909
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE"));
4910+
/* #1962, reproduce-first: RED while the guard is a blanket veto — genuine
4911+
* selector references must reach the field (write via `t.err = nil`,
4912+
* value use via `t.n`). */
4913+
ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "err", "WRITES"));
4914+
ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "n", "USAGE"));
48994915

49004916
cbm_store_close(s);
49014917
cbm_pipeline_free(p);
@@ -4927,6 +4943,10 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) {
49274943
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES"));
49284944
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS"));
49294945
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE"));
4946+
/* #1962 parallel twin: resolve_file_rw / resolve_file_usages must honour
4947+
* the member-access signal exactly like the sequential resolvers. */
4948+
ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "err", "WRITES"));
4949+
ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "n", "USAGE"));
49304950

49314951
cbm_store_close(s);
49324952
cbm_pipeline_free(p);

‎tests/test_registry.c‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -840,22 +840,23 @@ TEST(cross_language_ref_drops_go_vs_c) {
840840
}
841841

842842
TEST(go_bare_ref_never_binds_field) {
843-
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
844-
* field access is always a selector expression. */
845-
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, "err", "Field"));
846-
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, "config", "Field"));
847-
/* A selector-shaped reference may bind a field. */
848-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "t.err", "Field"));
843+
/* #1942/#1962: a bare Go identifier can never denote a struct field —
844+
* field access is always a selector expression. The extractor strips the
845+
* receiver before the resolver runs (resolve_lhs_write_name writes the
846+
* trailing name; is_reference_node records the inner field_identifier),
847+
* so the selector-vs-bare distinction arrives as the recorded
848+
* is_member_access signal, never as a dot in the reference text. */
849+
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, false, "Field"));
850+
/* The member half of a selector may bind a field. */
851+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, true, "Field"));
849852
/* Bare references to non-fields are untouched. */
850-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", "Variable"));
851-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", "Function"));
853+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Variable"));
854+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Function"));
852855
/* Other languages reference their own members bare inside methods —
853856
* never suppressed (cp_reads_writes_cs_static_field pins the C# shape). */
854-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(false, "_count", "Field"));
855-
/* Degenerate inputs → nothing to judge. */
856-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, NULL, "Field"));
857-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "", "Field"));
858-
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", NULL));
857+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(false, false, "Field"));
858+
/* Degenerate input → nothing to judge. */
859+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, NULL));
859860
PASS();
860861
}
861862

0 commit comments

Comments
 (0)