Skip to content
Open
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 @@ -3551,6 +3551,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"));
// ObjectScript stores args under oref_method/method_args, not the
Expand Down
12 changes: 10 additions & 2 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -623,12 +623,20 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
* 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;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence);

/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
Expand Down
8 changes: 6 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2478,12 +2478,16 @@ 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;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence);

/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
Expand Down
9 changes: 9 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, 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);

/* #725: drop a suffix_match CALLS edge when the caller language and the
* target file's language disagree. unique_name (candidates == 1) is #1572
* and is left alone; same_module / import_map / lsp_* are kept. JS/TS/TSX
Expand Down
27 changes: 27 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,33 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}

bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence) {
if (!is_go || !is_method || !strategy || !strategy[0]) {
return false;
}
/* 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). Unlike the TS/JS list, field_type_hint is KEPT: 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, "suffix_match") == 0 || strcmp(strategy, "fuzzy") == 0) {
return true;
}
/* 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 strcmp(strategy, "unique_name") == 0 && confidence < CONF_UNIQUE_NAME;
}

static bool js_ts_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
Expand Down
39 changes: 36 additions & 3 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -4690,9 +4690,14 @@ TEST(extract_perl_method_call_flags_is_method) {
/* 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. */
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 @@ -4771,6 +4776,33 @@ TEST(extract_python_member_call_flags_is_method) {
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();
}

/* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above).
* A member call x.foo() with a non-this/super receiver is flagged is_method so
* the resolver can suppress a weak short-name match (`re.test()` must not bind a
Expand Down Expand Up @@ -6450,6 +6482,7 @@ SUITE(extraction) {
RUN_TEST(extract_perl_method_call_flags_is_method);
RUN_TEST(extract_flag_exempt_method_call_not_flagged_is_method);
RUN_TEST(extract_python_member_call_flags_is_method);
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
94 changes: 94 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -4688,6 +4688,99 @@ TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
PASS();
}

TEST(pipeline_go_receiver_suppresses_weak_method_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_recv_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}

/* go.mod makes project imports resolvable — real Go repos always have one,
* and import reachability (the unique_name penalty) depends on it. */
write_temp_file(tmp, "go.mod", "module example.com/myapp\n\ngo 1.22\n");
/* The lone project symbol named "Close" — a real method. */
write_temp_file(tmp, "storage/storage.go",
"package storage\n"
"\n"
"type Storage struct{ open bool }\n"
"\n"
"func NewStorage() *Storage { return &Storage{open: true} }\n"
"\n"
"func (s *Storage) Close() {\n"
"\ts.open = false\n"
"}\n"
"\n"
"func Boot() {\n"
"\ts := NewStorage()\n"
"\ts.Close()\n"
"}\n");
/* Cross-package control target: imported by hash.go, so the caller file
* has a non-empty import map (like any real Go file) and unreachable
* unique_name candidates get the import penalty. */
write_temp_file(tmp, "util/util.go",
"package util\n"
"\n"
"func Tag() string { return \"t\" }\n");
/* Stdlib receiver: `f.Close()` closes an *os.File, NOT the project method.
* The Go LSP cannot bind it to a project symbol → the registry would guess
* Close by short name (weak). This is the false edge to suppress —
* the exact shape that attached every file/rows/gzip Close in a real Go
* repo to one unrelated project method. */
write_temp_file(tmp, "hash/hash.go",
"package hash\n"
"\n"
"import (\n"
"\t\"os\"\n"
"\n"
"\t\"example.com/myapp/util\"\n"
")\n"
"\n"
"func FileLen(path string) int64 {\n"
"\tf, err := os.Open(path)\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\tdefer f.Close()\n"
"\tst, err := f.Stat()\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\treturn st.Size()\n"
"}\n"
"\n"
"func localHelper() int { return 1 }\n"
"\n"
"func CallsLocal() int { return localHelper() }\n"
"\n"
"func UsesUtil() string { return util.Tag() }\n");

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_recv.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);

/* (1) The false edge is suppressed (reproduce-first: RED before the fix). */
ASSERT_FALSE(cross_file_call_exists(s, project, "FileLen", "Close"));
/* (2) The same-package typed-receiver call survives (LSP / same_module —
* both outside the weak drop-list). */
ASSERT_TRUE(cross_file_call_exists(s, project, "Boot", "Close"));
/* (3) The bare local call survives (is_method stays false for bare calls). */
ASSERT_TRUE(cross_file_call_exists(s, project, "CallsLocal", "localHelper"));
/* (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"));

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;
Expand Down Expand Up @@ -12805,6 +12898,7 @@ SUITE(pipeline) {
#endif
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_go_receiver_suppresses_weak_method_edge);
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);
Expand Down
42 changes: 42 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,46 @@ TEST(dynamic_suppress_keeps_high_confidence_and_non_methods) {
PASS();
}

TEST(go_suppress_drops_weak_selector_matches) {
/* Go selector call with an untyped receiver, landed via a receiver-blind
* short-name strategy → drop (same failure class as #592/#606).
* suffix_match/fuzzy drop at any confidence; unique_name drops only when
* import-unreachability-penalized (CONF_UNIQUE_NAME 0.75 * 0.5 = 0.375 —
* the `io.Copy` -> project `Copy` stdlib-hijack shape). */
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "suffix_match", 0.9));
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));
PASS();
}

TEST(go_suppress_keeps_typed_and_import_aware_matches) {
/* Unpenalized unique_name = lone candidate inside the caller's import
* closure (or an import-free file, e.g. same-package) — a genuinely-typed
* 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));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "qualified_suffix", 0.9));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "callee_suffix", 0.5));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "service_pattern", 0.5));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "lsp_strategy_cross_file", 0.92));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "lsp_direct", 0.95));
/* A bare call (is_method=false) is a free-function call → never suppressed. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, false, "suffix_match", 0.11));
/* Non-Go languages are never affected by this gate. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(false, true, "suffix_match", 0.11));
/* No match (NULL/empty strategy) → nothing to suppress. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, NULL, 0.5));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "", 0.5));
PASS();
}

/* ── Suite ─────────────────────────────────────────────────────── */

/* Method call THROUGH an imported symbol that is itself an indexed node
Expand Down Expand Up @@ -947,4 +987,6 @@ SUITE(registry) {
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
RUN_TEST(go_suppress_drops_weak_selector_matches);
RUN_TEST(go_suppress_keeps_typed_and_import_aware_matches);
}
Loading