diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 84958954cf..85c48435a8 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -676,9 +676,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 e85ad015f1..6ca7f6af2c 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -3024,8 +3024,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 e5a672cb0b..f76c54be13 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -320,11 +320,13 @@ 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); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index c99c72d8d5..ca432e4ec3 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -697,14 +697,21 @@ 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 || lang == CBM_LANG_HTML; } /* C and C++ are one family for cross-language checks: .h maps to CBM_LANG_CPP * in the extension table, so a .c file referencing a symbol declared in its - * own header would otherwise read as a language boundary. */ + * own header would otherwise read as a language boundary. CUDA and Objective-C + * use the same front end for header calls (#1702 census addendum). */ static bool c_cpp_family(CBMLanguage lang) { - return lang == CBM_LANG_C || lang == CBM_LANG_CPP; + return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || + lang == CBM_LANG_OBJC; +} + +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) { @@ -721,14 +728,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; +/* Target language from basename alone is wrong for extensions whose real + * language is decided by file content during discovery (.m → MATLAB in the + * table but often Objective-C, etc.). Never suppress on filename guess alone. */ +static bool target_lang_from_filename_ambiguous(const char *target_file_path) { + const char *base = path_basename(target_file_path); + const char *dot = strrchr(base, '.'); + if (!dot || dot == base) { + return false; + } + return strcmp(dot, ".m") == 0 || strcmp(dot, ".cls") == 0 || strcmp(dot, ".inc") == 0 || + strcmp(dot, ".cfc") == 0 || strcmp(dot, ".frm") == 0; } bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path, @@ -736,15 +746,12 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha /* 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]) { @@ -757,9 +764,18 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha if (caller_lang == target_lang) { return false; } + if (target_lang_from_filename_ambiguous(target_file_path)) { + return false; + } 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; } @@ -785,6 +801,9 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target if (caller_lang == target_lang) { return false; } + if (target_lang_from_filename_ambiguous(target_file_path)) { + return false; + } if (js_ts_family(caller_lang) && js_ts_family(target_lang)) { return false; } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 6ef16a89be..3aa197cfba 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8102,7 +8102,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" diff --git a/tests/test_registry.c b/tests/test_registry.c index 5f751a8993..033852af26 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -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")); @@ -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( @@ -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, @@ -987,6 +986,48 @@ 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")); + /* #1702 addendum: CUDA / ObjC / HTML inline script stay in-family. */ + ASSERT_FALSE( + cbm_suppress_cross_language_suffix_match(CBM_LANG_CUDA, "kernels/vec.h", "unique_name")); + ASSERT_FALSE( + cbm_suppress_cross_language_suffix_match(CBM_LANG_OBJC, "include/Foo.h", "unique_name")); + ASSERT_FALSE( + cbm_suppress_cross_language_suffix_match(CBM_LANG_HTML, "static/app.js", "unique_name")); + /* .m targets are MATLAB by extension table but often ObjC — do not suppress. */ + ASSERT_FALSE( + cbm_suppress_cross_language_suffix_match(CBM_LANG_OBJC, "models/Foo.m", "unique_name")); + 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. @@ -1244,6 +1285,7 @@ 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(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);