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
18 changes: 18 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -4493,6 +4493,24 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
}
}
}
// Go receiver-aware guard (same direction as the TS/JS flag above).
// Flag a selector call x.foo(). The Go AST cannot separate a method
// call on a value from a package-qualified call — but every selector
// call the Go LSP or the import/qualified registry strategies CAN
// place never reaches the weak short-name guards, so the flag only
// bites on unresolvable receivers (`f.Close()` on an os.File,
// `sha256.New()` behind an unindexed import), where a project-wide
// short-name match fabricates an edge to an unrelated project
// symbol sharing the name. Bare calls (helper()) keep
// is_method=false and resolve same-module/import paths as before.
if (ctx->language == CBM_LANG_GO &&
strcmp(ts_node_type(node), "call_expression") == 0) {
TSNode gofn = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (!ts_node_is_null(gofn) &&
strcmp(ts_node_type(gofn), "selector_expression") == 0) {
call.is_method = true;
}
}

TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
/* tree-sitter-elixir attaches NO field name to a call's arguments
Expand Down
10 changes: 9 additions & 1 deletion src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,13 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
* language gated on only one resolver produces an edge on the sequential
* path and not the parallel one (or vice versa), breaking MT determinism.
* ArkTS belongs to the JS/TS family here (#1842); dropping it would
* reintroduce the #592/#606 false-edge class for .ets files. */
* reintroduce the #592/#606 false-edge class for .ets files.
*
* Go (#1906) rides the same deferred-drop plumbing through its OWN
* predicate: its drop-list differs (field_type_hint is receiver-aware for
* Go, and unique_name drops only when import-unreachability-penalized), so
* it composes via cbm_go_suppress_weak_method_match instead of widening
* the shared gate. Same lockstep rule: mirror pass_parallel.c. */
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS ||
Expand Down Expand Up @@ -1144,6 +1150,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence) ||
/* Import-binding suppression (#2127): an external `from m import f`
* binds `f` for the whole module, so a weak short-name match onto a
* project `X.f` outside m is fabricated. Python-only (the import
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 @@ -3348,7 +3348,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
Expand Down Expand Up @@ -3385,7 +3393,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* #606 direction.
*
* This language set MUST match the one in pass_calls.c exactly — see the
* note there. ArkTS belongs to the JS/TS family (#1842). */
* note there. ArkTS belongs to the JS/TS family (#1842). Go (#1906)
* composes via its own predicate (different drop-list — see
* cbm_go_suppress_weak_method_match), mirrored in pass_calls.c. */
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS ||
Expand All @@ -3403,6 +3413,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
call->callee_name, res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence) ||
/* Import-binding suppression (#2127) — see pass_calls.c; this gate
* MUST stay identical to the one there. */
(lang == CBM_LANG_PYTHON &&
Expand Down
9 changes: 9 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,15 @@ bool cbm_weak_member_unique_name_exempt(bool is_python, bool receiver_is_self_at
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
const char *strategy);

/* Go analog of the TS/JS guard, same failure class: a selector call whose
* receiver the Go LSP could not type must not be bound by a receiver-blind
* short-name strategy. Drops suffix_match / fuzzy always, and unique_name only
* when its confidence is import-unreachability-penalized (the stdlib/vendor
* hijack shape). field_type_hint is deliberately NOT dropped for Go — struct
* fields carry declared types, so the hint is receiver-aware there. */
bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence);

/* Import-binding counterpart (#2127). True when the callee's root identifier
* is bound by an import of this file whose module chain contradicts the
* resolved target (`from unittest.mock import patch; patch()` must not bind a
Expand Down
29 changes: 29 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,35 @@ bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_b
return weak_short_name_strategy(strategy);
}

bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence) {
/* Go analog of the TS/JS guard above, same failure class: a selector call
* whose receiver the Go LSP could not type reaches the registry and a bare
* 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 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;
}
if (strcmp(strategy, "unique_name") != 0) {
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
* is not reachable through the caller's imports — the stdlib/vendor
* hijack shape (`io.Copy` -> a project `Copy`). An unpenalized
* unique_name target sits inside the caller's import closure (or the
* file has no imports, e.g. a same-package call) and must be kept —
* dropping it kills genuinely-typed lone-candidate calls that never
* enter the field-type-hint upgrade (candidate_count == 1). */
return confidence < CONF_UNIQUE_NAME;
}

/* Import-binding counterpart of the parameter guard above (#2127). An import
* binds the identifier for the whole module; when that import is external it
* never materializes, and the chain falls through to project-wide short-name
Expand Down
43 changes: 38 additions & 5 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -7353,12 +7353,17 @@ TEST(extract_rust_generic_impl_call_scope_matches_def) {
PASS();
}

/* Languages OUTSIDE the is_method flag set (only Perl and TS/JS/TSX set it) must
* be unaffected: a Go method call never sets is_method. */
/* Languages OUTSIDE the is_method flag set (Perl, Python, TS/JS/TSX and Go set
* it) must be unaffected: a Rust method call never sets is_method. */
TEST(extract_flag_exempt_method_call_not_flagged_is_method) {
CBMFileResult *r = extract("package m\n"
"func run(o Obj) { o.Commit(); helper() }\n",
CBM_LANG_GO, "t", "x.go");
/* Rust is flag-exempt: only Perl, Python, TS/JS and Go set is_method.
* Guards the blast radius of the receiver-aware flags for every other
* language. */
CBMFileResult *r = extract("fn run(o: Obj) {\n"
" o.commit();\n"
" helper();\n"
"}\n",
CBM_LANG_RUST, "t", "x.rs");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
for (int i = 0; i < r->calls.count; i++) {
Expand Down Expand Up @@ -7516,6 +7521,33 @@ TEST(extract_python_bare_call_flags_locally_bound_callee) {
PASS();
}

TEST(extract_go_selector_call_flags_is_method) {
/* Go selector calls are flagged so the weak-match guard can fire when the
* Go LSP cannot type the receiver; bare calls stay unflagged. */
CBMFileResult *r = extract("package m\n"
"func run(o Obj) { o.Commit(); helper() }\n",
CBM_LANG_GO, "t", "x.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
bool saw_selector = false;
bool saw_bare = false;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *c = &r->calls.items[i];
if (c->callee_name && strstr(c->callee_name, "Commit") != NULL) {
ASSERT_TRUE(c->is_method);
saw_selector = true;
}
if (c->callee_name && strcmp(c->callee_name, "helper") == 0) {
ASSERT_FALSE(c->is_method);
saw_bare = true;
}
}
ASSERT_TRUE(saw_selector);
ASSERT_TRUE(saw_bare);
cbm_free_result(r);
PASS();
}

/* The bare-call flag is DEPTH-INDEPENDENT, and the binding is UNWOUND when its
* scope closes.
*
Expand Down Expand Up @@ -12728,6 +12760,7 @@ SUITE(extraction) {
RUN_TEST(extract_python_member_call_flags_is_method);
RUN_TEST(extract_python_bare_call_flags_locally_bound_callee);
RUN_TEST(extract_python_bare_call_flag_is_depth_independent);
RUN_TEST(extract_go_selector_call_flags_is_method);
RUN_TEST(extract_ts_member_call_flags_is_method);
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
RUN_TEST(extract_js_member_call_flags_is_method);
Expand Down
Loading
Loading