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
6 changes: 6 additions & 0 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,12 @@ 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)) {
return 0;
}
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals,
imp_count, tsjs_drop_plain_call);
return SKIP_ONE;
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2514,6 +2514,12 @@ 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. */
continue;
}
if (!target_node || source_node->id == target_node->id) {
/* HTTP/ASYNC calls to an EXTERNAL client library (`requests.get(url)`)
* resolve to an unindexed QN (target_node == NULL), but their edge
Expand Down
8 changes: 8 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ 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. */
bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy);

/* Get the label of a qualified name, or NULL if not found. */
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);

Expand Down
43 changes: 43 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,49 @@ bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const cha
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}

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

static const char *path_basename(const char *path) {
if (!path || !path[0]) {
return path;
}
const char *slash = strrchr(path, '/');
#ifdef _WIN32
const char *bslash = strrchr(path, '\\');
if (bslash && (!slash || bslash > slash)) {
slash = bslash;
}
#endif
return slash ? slash + 1 : 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) {
return false;
}
if (caller_lang == CBM_LANG_COUNT || !target_file_path || !target_file_path[0]) {
return false;
}
CBMLanguage target_lang = cbm_language_for_filename(path_basename(target_file_path));
if (target_lang == CBM_LANG_COUNT) {
return false;
}
if (caller_lang == target_lang) {
return false;
}
if (js_ts_family(caller_lang) && js_ts_family(target_lang)) {
return false;
}
return true;
}

/* ── Lifecycle ──────────────────────────────────────────────────── */

cbm_registry_t *cbm_registry_new(void) {
Expand Down
86 changes: 86 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -5893,6 +5893,91 @@ TEST(pipeline_python_cross_module_call) {
PASS();
}

/* #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. */
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"
" def commit(self):\n"
" return True\n",

"from store import Store\n"
"\n"
"def save():\n"
" return Store().commit()\n",

"export function commit() {\n"
" return 1;\n"
"}\n"};

if (setup_lang_repo(files, contents, 3) != 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 *commits = NULL;
int ncommit = 0;
cbm_store_find_nodes_by_name(s, proj, "commit", &commits, &ncommit);
ASSERT_GTE(ncommit, 2);

int64_t js_id = 0;
int64_t py_id = 0;
for (int i = 0; i < ncommit; i++) {
if (commits[i].file_path && strstr(commits[i].file_path, "Editor.js"))
js_id = commits[i].id;
if (commits[i].file_path && strstr(commits[i].file_path, "store.py"))
py_id = commits[i].id;
}
ASSERT_TRUE(js_id != 0);
ASSERT_TRUE(py_id != 0);

cbm_node_t *saves = NULL;
int nsave = 0;
cbm_store_find_nodes_by_name(s, proj, "save", &saves, &nsave);
ASSERT_GT(nsave, 0);

cbm_edge_t *from_save = NULL;
int nfrom = 0;
cbm_store_find_edges_by_source_type(s, saves[0].id, "CALLS", &from_save, &nfrom);
bool save_calls_py = false;
bool save_calls_js = false;
for (int i = 0; i < nfrom; i++) {
if (from_save[i].target_id == py_id)
save_calls_py = true;
if (from_save[i].target_id == js_id)
save_calls_js = true;
}
ASSERT_TRUE(save_calls_py);
ASSERT_FALSE(save_calls_js);

cbm_edge_t *into_js = NULL;
int njs = 0;
cbm_store_find_edges_by_target_type(s, js_id, "CALLS", &into_js, &njs);
ASSERT_EQ(njs, 0);

if (from_save)
cbm_store_free_edges(from_save, nfrom);
if (into_js)
cbm_store_free_edges(into_js, njs);
cbm_store_free_nodes(commits, ncommit);
cbm_store_free_nodes(saves, nsave);
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 @@ -11668,6 +11753,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_go_cross_package_call);
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_go_type_classification);
RUN_TEST(pipeline_go_grouped_types);
RUN_TEST(pipeline_kotlin_project);
Expand Down
29 changes: 29 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,34 @@ TEST(perl_suppress_keeps_high_confidence_and_genuine_calls) {
PASS();
}

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. */
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",
"suffix_match"));
ASSERT_TRUE(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, "web/src/pages/Editor.js",
"import_map"));
/* JS/TS/TSX are one family. */
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_JAVASCRIPT, "lib/util.ts",
"suffix_match"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_TYPESCRIPT, "ui/Panel.tsx",
"suffix_match"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_PYTHON, NULL, "suffix_match"));
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(CBM_LANG_COUNT, "store.py",
"suffix_match"));
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.
Expand Down Expand Up @@ -893,6 +921,7 @@ SUITE(registry) {
RUN_TEST(perl_builtin_set_rejects_project_subs);
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(tsjs_suppress_drops_weak_method_matches);
RUN_TEST(tsjs_suppress_keeps_high_confidence_and_non_methods);
}
Loading