mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
Merge pull request #1647 from rudi193-cmd/fix/725-cross-language-suffix-match
fix(registry): drop suffix_match CALLS across language boundaries
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -2539,6 +2539,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"};
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user