Skip to content
15 changes: 10 additions & 5 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,12 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
* project-qualified form even when fallback prefixed the project. */
res.qualified_name = target_node->qualified_name;
res.confidence = lsp->confidence;
res.strategy = lsp->strategy;
res.strategy = lsp->strategy ? lsp->strategy : "lsp_override";
res.candidate_count = 1;
if (cbm_suppress_cross_language_calls_edge(lang, target_node->file_path,
res.strategy)) {
return SKIP_ONE;
}
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
imp_vals, imp_count, false);
return SKIP_ONE;
Expand Down Expand Up @@ -676,10 +680,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
if (!target_node || source_node->id == target_node->id) {
return 0;
}
/* #725: suffix_match is language-agnostic and will attach a Python
* Store.commit() call to a JS function named commit (or a Bash main
* to a Python main). Drop that weak cross-language edge. */
if (cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) {
/* #725/#1572: suffix_match and unique_name are language-agnostic and
* will attach a Python Store.commit() call to a JS function named
* commit, or a Python `with patch(...)` to a unique TSX `patch`. Drop
* those weak cross-language edges. */
if (cbm_suppress_cross_language_calls_edge(lang, target_node->file_path, res.strategy)) {
return 0;
}
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals,
Expand Down
7 changes: 4 additions & 3 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3023,9 +3023,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
atomic_fetch_add_explicit(&rc->time_ns_rc_target, extract_now_ns() - _rc_t0,
memory_order_relaxed);
if (target_node && source_node->id != target_node->id &&
cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) {
/* #725: same guard as pass_calls.c — do not emit a suffix_match
* CALLS edge across a language boundary. */
cbm_suppress_cross_language_calls_edge(lang, target_node->file_path, res.strategy)) {
/* #725/#1572: same guard as pass_calls.c — do not emit a
* suffix_match or unique_name CALLS edge across a language
* boundary (incl. lsp_direct homonym #1572). */
continue;
}
if (!target_node || source_node->id == target_node->id) {
Expand Down
18 changes: 13 additions & 5 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,22 @@ 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);

/* #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
* are one family so a .ts helper calling a .tsx function is not dropped.
* Pure; unit-tested in test_registry.c. */
/* #725/#1572: drop a suffix_match or unique_name CALLS edge when the caller
* language and the target file's language disagree. suffix_match is the
* import-distance winner among many same-named symbols (#725);
* unique_name is the candidates==1 case of the same class (#1572).
* same_module / import_map / lsp_* are kept. JS/TS/TSX are one family so a
* .ts helper calling a .tsx function is not dropped. Pure; unit-tested in
* test_registry.c. */
bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy);

/* CALLS emission guard: suffix_match / unique_name (#725/#1572) plus any other
* strategy that binds a cross-language unique_name homonym (import_map, lsp_*,
* …). */
bool cbm_suppress_cross_language_calls_edge(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy);

/* #1928: USAGE/WRITES/READS analog of the CALLS guard above. Reference edges
* resolved by the short-name registry carry no import-closure evidence, so a
* cross-language binding is a bare-name collision for EVERY strategy — drop
Expand Down
67 changes: 47 additions & 20 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,8 @@ bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_b

static bool js_ts_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
lang == CBM_LANG_ARKTS || lang == CBM_LANG_VUE || lang == CBM_LANG_SVELTE ||
lang == CBM_LANG_ASTRO;
}

/* C and C++ are one family for cross-language checks: .h maps to CBM_LANG_CPP
Expand All @@ -707,6 +708,10 @@ static bool c_cpp_family(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP;
}

static bool jvm_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN || lang == CBM_LANG_GROOVY;
}

static const char *path_basename(const char *path) {
if (!path || !path[0]) {
return path;
Expand All @@ -721,30 +726,17 @@ static const char *path_basename(const char *path) {
return slash ? slash + 1 : path;
}

/* Build and configuration languages have no cross-language call semantics: a
* Makefile's `$(eval ...)` or a CMake `function(...)` names nothing in a C
* file, so a bare-name bind into another language is always a collision
* (2026-09-16 probe: kernel Makefile targets bound to `sk_psock.eval`). */
static bool build_config_language(CBMLanguage lang) {
return lang == CBM_LANG_MAKEFILE || lang == CBM_LANG_CMAKE || lang == CBM_LANG_YAML ||
lang == CBM_LANG_TOML || lang == CBM_LANG_JSON || lang == CBM_LANG_INI ||
lang == CBM_LANG_DOCKERFILE;
}

bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy) {
/* Two same-named symbols in different languages: suffix_match picks one
* winner by import-distance and attaches every bare-name call to it
* (#725, Bash/Python main, JS/Python commit). unique_name is the
* candidates==1 case (#1572) and is not this guard — except for a build
* or configuration caller, where even a unique match into another
* language is a collision by construction. */
if (!strategy) {
return false;
}
bool config_caller = build_config_language(caller_lang);
if (strcmp(strategy, "suffix_match") != 0 &&
!(config_caller && strcmp(strategy, "unique_name") == 0)) {
* candidates==1 case of the same class (#1572, Python
* `from unittest.mock import patch` binding to a unique TSX `patch`).
* Build/config callers have no cross-language call semantics, so their
* unique_name collisions are covered by the same guard. */
if (!strategy ||
(strcmp(strategy, "suffix_match") != 0 && strcmp(strategy, "unique_name") != 0)) {
return false;
}
if (caller_lang == CBM_LANG_COUNT || !target_file_path || !target_file_path[0]) {
Expand All @@ -760,6 +752,41 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
if (js_ts_family(caller_lang) && js_ts_family(target_lang)) {
return false;
}
if (c_cpp_family(caller_lang) && c_cpp_family(target_lang)) {
return false;
}
if (jvm_family(caller_lang) && jvm_family(target_lang)) {
return false;
}
return true;
}

static bool lsp_strategy_is_external_builtin(const char *strategy) {
if (!strategy) {
return false;
}
return strcmp(strategy, "lsp_builtin") == 0 || strcmp(strategy, "lsp_builtin_method") == 0 ||
strcmp(strategy, "lsp_builtin_constructor") == 0;
}

bool cbm_suppress_cross_language_calls_edge(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy) {
if (cbm_suppress_cross_language_suffix_match(caller_lang, target_file_path, strategy)) {
return true;
}
/* #1572 / #725 pipeline: once the target is a cross-language unique_name
* homonym, every weak resolver (import_map, lsp_*, suffix_match, …) is the
* same failure mode as registry unique_name. Keep same_module (true local
* callee) and stdlib lsp_builtin* (usually off-graph). */
if (!cbm_suppress_cross_language_suffix_match(caller_lang, target_file_path, "unique_name")) {
return false;
}
if (strategy && strcmp(strategy, "same_module") == 0) {
return false;
}
if (lsp_strategy_is_external_builtin(strategy)) {
return false;
}
return true;
}

Expand Down
79 changes: 78 additions & 1 deletion tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -8072,7 +8072,7 @@ TEST(pipeline_python_cross_module_call) {
/* #725: two same-named symbols across languages must not share CALLS edges.
* Python Store.commit is the real callee of save(); the JS Editor.commit
* function is a distinct binding and must have no inbound CALLS from Python.
* unique_name (candidates==1) is #1572 and is not this claim. */
* unique_name (candidates==1) is the #1572 pipeline test. */
TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725) {
const char *files[] = {"store.py", "app.py", "web/src/pages/Editor.js"};
const char *contents[] = {"class Store:\n"
Expand Down Expand Up @@ -8153,6 +8153,82 @@ TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725) {
PASS();
}

/* #1572: unique_name (candidates==1) must not bind a Python mock.patch
* call to the only project symbol named `patch` when that symbol is TSX.
* The issue fixture is two files: frontend/Panel.tsx and backend/test_thing.py. */
TEST(pipeline_cross_language_unique_name_does_not_share_calls_issue1572) {
const char *files[] = {"frontend/Panel.tsx", "backend/test_thing.py"};
const char *contents[] = {
"function patch(x: string) {\n"
" return x;\n"
"}\n",

"from unittest.mock import patch\n"
"\n"
"def test_one():\n"
" with patch(\"os.path.exists\"):\n"
" pass\n"
"\n"
"def test_two():\n"
" with patch(\"os.path.join\"):\n"
" pass\n"};

if (setup_lang_repo(files, contents, 2) != 0)
FAIL("tmpdir");
char db[512];
snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir);

cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);

cbm_store_t *s = cbm_store_open_path(db);
ASSERT_NOT_NULL(s);
const char *proj = cbm_pipeline_project_name(p);

cbm_node_t *patches = NULL;
int npatch = 0;
cbm_store_find_nodes_by_name(s, proj, "patch", &patches, &npatch);
ASSERT_GT(npatch, 0);

int64_t tsx_id = 0;
for (int i = 0; i < npatch; i++) {
if (patches[i].file_path && strstr(patches[i].file_path, "Panel.tsx"))
tsx_id = patches[i].id;
}
ASSERT_TRUE(tsx_id != 0);

cbm_edge_t *into_tsx = NULL;
int ntsx = 0;
cbm_store_find_edges_by_target_type(s, tsx_id, "CALLS", &into_tsx, &ntsx);
ASSERT_EQ(ntsx, 0);

const char *callers[] = {"test_one", "test_two"};
for (int c = 0; c < 2; c++) {
cbm_node_t *fns = NULL;
int nfn = 0;
cbm_store_find_nodes_by_name(s, proj, callers[c], &fns, &nfn);
ASSERT_GT(nfn, 0);
cbm_edge_t *from_fn = NULL;
int nfrom = 0;
cbm_store_find_edges_by_source_type(s, fns[0].id, "CALLS", &from_fn, &nfrom);
for (int i = 0; i < nfrom; i++) {
ASSERT_TRUE(from_fn[i].target_id != tsx_id);
}
if (from_fn)
cbm_store_free_edges(from_fn, nfrom);
cbm_store_free_nodes(fns, nfn);
}

if (into_tsx)
cbm_store_free_edges(into_tsx, ntsx);
cbm_store_free_nodes(patches, npatch);
cbm_store_close(s);
cbm_pipeline_free(p);
teardown_lang_repo();
PASS();
}

TEST(pipeline_go_type_classification) {
/* Port of TestGoTypeClassification */
const char *files[] = {"types.go"};
Expand Down Expand Up @@ -15160,6 +15236,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_swift_cross_package_import);
RUN_TEST(pipeline_python_cross_module_call);
RUN_TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725);
RUN_TEST(pipeline_cross_language_unique_name_does_not_share_calls_issue1572);
RUN_TEST(pipeline_go_type_classification);
RUN_TEST(pipeline_go_grouped_types);
RUN_TEST(pipeline_kotlin_project);
Expand Down
66 changes: 60 additions & 6 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -831,9 +831,10 @@ TEST(cross_language_config_caller_drops_unique_name_too) {
cbm_suppress_cross_language_suffix_match(CBM_LANG_CMAKE, "src/main.c", "unique_name"));
ASSERT_TRUE(
cbm_suppress_cross_language_suffix_match(CBM_LANG_YAML, "app/models.py", "unique_name"));
/* A code caller's unique_name into another language is still #1572. */
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(
CBM_LANG_PYTHON, "web/src/pages/Editor.js", "unique_name"));
/* A code caller's unique_name into another language is #1572. */
ASSERT_TRUE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON,
"web/src/pages/Editor.js",
"unique_name"));
/* Same-language config targets are untouched. */
ASSERT_FALSE(
cbm_suppress_cross_language_suffix_match(CBM_LANG_MAKEFILE, "lib/Makefile", "unique_name"));
Expand Down Expand Up @@ -912,7 +913,7 @@ TEST(registry_tie_break_is_independent_of_registration_order) {

TEST(cross_language_suffix_match_drops_py_vs_js) {
/* #725: two same-named symbols in different languages. suffix_match is the
* strategy that collapses them; unique_name is #1572 and must stay. */
* strategy that collapses them. unique_name is covered by the #1572 test. */
ASSERT_TRUE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "web/src/pages/Editor.js",
"suffix_match"));
ASSERT_TRUE(
Expand All @@ -921,8 +922,6 @@ TEST(cross_language_suffix_match_drops_py_vs_js) {
cbm_suppress_cross_language_suffix_match(CBM_LANG_BASH, "cli/main.py", "suffix_match"));
ASSERT_FALSE(
cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "store.py", "suffix_match"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(
CBM_LANG_PYTHON, "web/src/pages/Editor.js", "unique_name"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(
CBM_LANG_PYTHON, "web/src/pages/Editor.js", "same_module"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON,
Expand Down Expand Up @@ -987,6 +986,59 @@ TEST(go_bare_ref_never_binds_field) {
PASS();
}

TEST(cross_language_unique_name_drops_py_vs_tsx) {
/* #1572: unique_name is the candidates==1 case of the same class as
* suffix_match. Python `from unittest.mock import patch` must not bind
* to a unique TSX `function patch`. */
ASSERT_TRUE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"unique_name"));
ASSERT_TRUE(cbm_suppress_cross_language_suffix_match(CBM_LANG_TSX, "backend/test_thing.py",
"unique_name"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "backend/test_thing.py",
"unique_name"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"same_module"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"import_map"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"lsp_direct"));
/* JS/TS/TSX are one family — a .ts caller of a .tsx unique_name stays. */
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_TYPESCRIPT, "frontend/Panel.tsx",
"unique_name"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_JAVASCRIPT, "frontend/Panel.tsx",
"unique_name"));
/* C/C++, JVM, and Vue→TS families stay exempt for unique_name. */
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_C, "bpf/probe.h", "unique_name"));
ASSERT_FALSE(
cbm_suppress_cross_language_suffix_match(CBM_LANG_GROOVY, "buildSrc/Foo.java", "unique_name"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_VUE, "services/Queue.ts",
"unique_name"));
PASS();
}

TEST(cross_language_calls_edge_treats_lsp_direct_homonym_as_unique_name) {
/* #1572 pipeline: py_lsp_cross can emit lsp_direct to a cross-language homonym. */
ASSERT_TRUE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"lsp_direct"));
ASSERT_FALSE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "backend/test_thing.py",
"lsp_direct"));
ASSERT_TRUE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"unique_name"));
ASSERT_TRUE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"lsp_callable_alias"));
ASSERT_TRUE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"lsp_override"));
ASSERT_TRUE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"import_map"));
ASSERT_TRUE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"import_map_suffix"));
ASSERT_FALSE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "frontend/Panel.tsx",
"lsp_builtin"));
ASSERT_FALSE(cbm_suppress_cross_language_calls_edge(CBM_LANG_PYTHON, "backend/test_thing.py",
"import_map"));
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.
Expand Down Expand Up @@ -1244,6 +1296,8 @@ SUITE(registry) {
RUN_TEST(registry_tie_break_is_independent_of_registration_order);
RUN_TEST(cross_language_ref_drops_go_vs_c);
RUN_TEST(go_bare_ref_never_binds_field);
RUN_TEST(cross_language_unique_name_drops_py_vs_tsx);
RUN_TEST(cross_language_calls_edge_treats_lsp_direct_homonym_as_unique_name);
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
RUN_TEST(python_builtin_member_table_matches_builtin_type_methods);
Expand Down
Loading