fix(go): no field-type hint for Go; it is a name guess, par-only

The parallel resolver re-picks a multi-candidate registry match through
try_field_type_hint. That helper capitalizes the receiver VARIABLE name
and accepts any candidate whose QN contains it as a substring: `t.Run`
on a *testing.T became `T` and bound a project `TBench.Run`. The
sequential resolver has no such step. The Go selector guard treated
field_type_hint as receiver-aware and kept it, so the same weak pick it
drops in sequential mode survived in parallel mode under a new name.

On caddy + prometheus, parallel and sequential CALLS diverged by 1,684
(this branch) and 5,560 (with receiver-qualified method QNs, which put
type names into more QNs). At base the gap is 84. SCIP precision of Go
field_type_hint is 0.016 at base.

Go no longer takes the hint. The guard keeps field_type_hint on the
shared weak list, and the fixture of both twins now pins the
`t.Run` case. A typed field call (lrp_go_s8, `s.repo.Find()`) resolves
through the Go LSP and is unaffected.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-18 16:54:36 +02:00
1 parent 7528ec06fe
commit e554a2ab58
4 files changed
+51 -16

No files matched your search

+9 -1
View File
@@ -3298,7 +3298,15 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
}
_rc_t0 = extract_now_ns();
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
/* Not for Go (F-G9): the hint matches the capitalized receiver
* VARIABLE name as a substring of a candidate QN (`t.Run` -> any
* `...T...Run`), with no type evidence. The sequential resolver has no
* such step, so for Go it produced parallel-only edges (par/seq CALLS
* diverged), and the Go selector guard could not see them as the weak
* pick they are. SCIP precision of Go field_type_hint: 0.016. */
if (lang != CBM_LANG_GO) {
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
}
/* Cross-language veto — the same predicate as pass_calls.c, applied
* AFTER the field-type hint so the hint may still re-pick a compatible
* candidate first, and BEFORE the empty-resolution fallbacks below so a
+6 -8
View File
@@ -755,18 +755,16 @@ bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *s
* short-name strategy binds it to an arbitrary same-named project symbol
* (`f.Close()` on an os.File -> a project `Close`, suffix_match over 15
* candidates). Built on the shared drop-list so "weak" means one thing
* for every guard; Go narrows it in two places only. */
* for every guard; Go narrows it in one place only (unique_name below).
* field_type_hint stays weak: it is a receiver-NAME substring match, not
* a declared-type lookup (the parallel resolver no longer produces it for
* Go; a typed field call such as lrp_go_s8's `s.repo.Find()` resolves
* through the Go LSP). */
if (!is_go || !is_method || !weak_short_name_strategy(strategy)) {
return false;
}
/* field_type_hint is KEPT for Go: a Go struct field carries a declared
* type, so the parallel resolver's field-type hint is receiver-aware for
* Go (lrp_go_s8_field_type_hint), not a heuristic. */
if (strcmp(strategy, "field_type_hint") == 0) {
return false;
}
if (strcmp(strategy, "unique_name") != 0) {
return true; /* suffix_match, fuzzy */
return true; /* suffix_match, field_type_hint, fuzzy */
}
/* unique_name is dropped only when PENALIZED: resolve_name_lookup scales
* CONF_UNIQUE_NAME by DEFAULT_CONFIDENCE exactly when the lone candidate
+28
View File
@@ -8713,6 +8713,30 @@ static void write_go_receiver_fixture(const char *tmp, int pad_files) {
"func CallsLocal() int { return localHelper() }\n"
"\n"
"func UsesUtil() string { return util.Tag() }\n");
/* F-G9: two project methods named Run, one under a directory whose QN
* carries an uppercase T. `t.Run` on a *testing.T must bind neither. The
* parallel resolver field-type hint matched the capitalized receiver
* variable ("T") as a QN substring and produced a par-only edge. */
write_temp_file(tmp, "TBench/bench.go",
"package tbench\n"
"\n"
"type Bench struct{}\n"
"\n"
"func (b *Bench) Run(name string) bool { return name != \"\" }\n");
write_temp_file(tmp, "worker/worker.go",
"package worker\n"
"\n"
"type Worker struct{}\n"
"\n"
"func (w *Worker) Run(name string) bool { return false }\n");
write_temp_file(tmp, "hash/hash_test.go",
"package hash\n"
"\n"
"import \"testing\"\n"
"\n"
"func TestSubRun(t *testing.T) {\n"
"\tt.Run(\"a\", func(t *testing.T) {})\n"
"}\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
@@ -8750,6 +8774,8 @@ TEST(pipeline_go_receiver_suppresses_weak_method_edge) {
/* (4) The import-qualified cross-package call survives (import-aware
* strategies are outside the drop-list). */
ASSERT_TRUE(cross_file_call_exists(s, project, "UsesUtil", "Tag"));
/* (5) F-G9: the stdlib *testing.T Run binds no project Run. */
ASSERT_FALSE(cross_file_call_exists(s, project, "TestSubRun", "Run"));
cbm_store_close(s);
cbm_pipeline_free(p);
@@ -8793,6 +8819,8 @@ TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge) {
ASSERT_TRUE(cross_file_call_exists(s, project, "Boot", "Close"));
ASSERT_TRUE(cross_file_call_exists(s, project, "CallsLocal", "localHelper"));
ASSERT_TRUE(cross_file_call_exists(s, project, "UsesUtil", "Tag"));
/* F-G9: the parallel-only field-type hint must not bind `t.Run` either. */
ASSERT_FALSE(cross_file_call_exists(s, project, "TestSubRun", "Run"));
cbm_store_close(s);
cbm_pipeline_free(p);
+8 -7
View File
@@ -1673,11 +1673,12 @@ TEST(weak_call_guards_share_one_drop_list) {
strategies[i], member, binding);
}
ASSERT_EQ(member, binding);
/* The Go selector guard builds on the same list and narrows it in
* exactly one place at a penalized confidence (0.75 * 0.5): it keeps
* field_type_hint. Any other drift from the shared list fails here. */
/* The Go selector guard builds on the same list. At a penalized
* confidence (0.75 * 0.5) it agrees with it exactly; its one narrowing
* (an unpenalized unique_name) does not apply here. Any drift from
* the shared list fails here. */
bool go = cbm_go_suppress_weak_method_match(true, true, strategies[i], 0.375);
bool go_expected = member && strcmp(strategies[i], "field_type_hint") != 0;
bool go_expected = member;
if (go != go_expected) {
printf(" Go drop-list divergence on strategy \"%s\": go=%d expected=%d\n",
strategies[i], go, go_expected);
@@ -1697,6 +1698,9 @@ TEST(go_suppress_drops_weak_selector_matches) {
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "suffix_match", 0.11));
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "fuzzy", 0.9));
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "unique_name", 0.375));
/* field_type_hint is a receiver-NAME substring match, not type evidence
* (F-G9): weak for Go like the shared list. */
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "field_type_hint", 0.85));
PASS();
}
@@ -1706,9 +1710,6 @@ TEST(go_suppress_keeps_typed_and_import_aware_matches) {
* lone-candidate call never enters the field-type-hint upgrade, so it must
* survive (lrp_go_s8_field_type_hint). */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "unique_name", 0.75));
/* field_type_hint is receiver-aware for Go — struct fields carry declared
* types (lrp_go_s8_field_type_hint) — so it stays, unlike the TS/JS list. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "field_type_hint", 0.85));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "same_module", 0.9));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "import_map", 0.95));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "import_map_suffix", 0.9));