From 477b96b6d7dec05bd8c314343b672c9d5c18f570 Mon Sep 17 00:00:00 2001 From: rudi193-cmd Date: Mon, 17 Aug 2026 19:36:36 -0600 Subject: [PATCH] fix(registry): drop unique_name CALLS across language boundaries #1647 dropped suffix_match only. unique_name (candidates==1) still bound Python `from unittest.mock import patch` to a unique TSX `patch`. Same language-family guard; JS/TS/TSX stay one family. Fixes #1572 Signed-off-by: rudi193-cmd Co-authored-by: Cursor --- src/pipeline/pass_calls.c | 7 ++-- src/pipeline/pass_parallel.c | 5 ++- src/pipeline/pipeline.h | 12 +++--- src/pipeline/registry.c | 10 +++-- tests/test_pipeline.c | 79 +++++++++++++++++++++++++++++++++++- tests/test_registry.c | 29 +++++++++++-- 6 files changed, 124 insertions(+), 18 deletions(-) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 189dd9f2d..47df0db56 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -635,9 +635,10 @@ 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. */ + /* #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_suffix_match(lang, target_node->file_path, res.strategy)) { return 0; } diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 0c91db09a..1b5ecb0e3 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2541,8 +2541,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB 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. */ + /* #725/#1572: same guard as pass_calls.c — do not emit a + * suffix_match or unique_name CALLS edge across a language + * boundary. */ continue; } if (!target_node || source_node->id == target_node->id) { diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index 5cde0c0f0..18d3d5d7a 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -264,11 +264,13 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c * Pure; unit-tested in test_registry.c. */ bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, 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); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index dbb16d2d7..64be4aaa4 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -476,10 +476,12 @@ static const char *path_basename(const char *path) { 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. */ - if (!strategy || strcmp(strategy, "suffix_match") != 0) { + * winner by import-distance (#725, Bash/Python main, JS/Python commit). + * unique_name is the candidates==1 case of the same class (#1572, + * Python `from unittest.mock import patch` binding to a unique TSX + * `patch`). */ + 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]) { diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 25878a8b3..9395d5585 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6034,7 +6034,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[] = { @@ -6116,6 +6116,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"}; @@ -12021,6 +12097,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); diff --git a/tests/test_registry.c b/tests/test_registry.c index 387bdc143..8611865e3 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -763,7 +763,7 @@ TEST(perl_suppress_keeps_high_confidence_and_genuine_calls) { 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(cbm_suppress_cross_language_suffix_match(CBM_LANG_JAVASCRIPT, "store.py", @@ -772,8 +772,6 @@ TEST(cross_language_suffix_match_drops_py_vs_js) { "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, "web/src/pages/Editor.js", @@ -789,6 +787,30 @@ TEST(cross_language_suffix_match_drops_py_vs_js) { 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")); + PASS(); +} + TEST(tsjs_suppress_drops_weak_method_matches) { /* #592/#606: a TS/JS member call whose receiver the LSP could not type, that * landed via a WEAK short-name strategy, is generic-resolver noise → drop. @@ -922,6 +944,7 @@ SUITE(registry) { RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches); RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls); RUN_TEST(cross_language_suffix_match_drops_py_vs_js); + RUN_TEST(cross_language_unique_name_drops_py_vs_tsx); RUN_TEST(tsjs_suppress_drops_weak_method_matches); RUN_TEST(tsjs_suppress_keeps_high_confidence_and_non_methods); }