From 51c57c4e0c7caa268e0e5fa38bf64a4ebf7d8a9c Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 10 Nov 2026 20:01:46 +0100 Subject: [PATCH] fix(pipeline): refuse same_module for import-bound or builtin JS/TS receivers Strategy 2 (same_module) looks a member callee up a second time without its receiver, so `axios.get(url)` in a file that defines its own `get` became a 0.90 CALLS edge to that local function. The weak-member guard deliberately keeps same_module, so this class was unguarded (#1128). Restore the registry strategy chain to main (review of 2026-10-19, item 1): the chain also resolves type references, field types, reads/writes, SQL lineage and the cross-LSP pass for every language, and the chain-level restriction broke the C typed-field and Python external-import pipeline tests. Instead add a call-site suppressor, wired identically in pass_calls.c and pass_parallel.c with the language list inside the helper: for the JS/TS family and the \n"}, + {"src/smr_svelte.svelte", "smr_svelte", "\n
\n"}, + {"src/smr_astro.astro", "smr_astro", "---\n", "---\n
\n"}, + {"src/smr_html.html", "smr_html", "\n\n\n"}, + }; + + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_smr_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + for (int i = 0; i < SMR_LANGS; i++) { + char content[1024]; + snprintf(content, sizeof(content), "%s%s%s", langs[i].open, body, langs[i].close); + write_temp_file(tmp, langs[i].path, content); + } + write_temp_file(tmp, "src/smr_cjs.js", + "const db = require('./database');\n" + "function getObject(key) { return key; }\n" + "function cjsCall(key) { return db.getObject(key); }\n" + "function exportsCall(key) { return exports.getObject(key); }\n"); + write_temp_file(tmp, "src/smr_amd.js", + "define('forum/smr', ['api', 'exports'], function (api, exports) {\n" + " function get(url) { return url; }\n" + " function amdCall(url) { return api.get(url); }\n" + " function amdExports(url) { return exports.get(url); }\n" + " return { amdCall, amdExports };\n" + "});\n"); + for (int i = 0; parallel && i < SMR_PAD_FILES; i++) { + char name[64]; + char pad[128]; + snprintf(name, sizeof(name), "src/smr_pad%d.ts", i); + snprintf(pad, sizeof(pad), "export function smrPad%d(): number {\n return %d;\n}\n", i, i); + write_temp_file(tmp, name, pad); + } + + const char *old_workers = getenv("CBM_WORKERS"); + char *saved_workers = old_workers ? strdup(old_workers) : NULL; + const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); + char *saved_single = old_single ? strdup(old_single) : NULL; + if ((old_workers && !saved_workers) || (old_single && !saved_single)) { + free(saved_workers); + free(saved_single); + th_rmtree(tmp); + FAIL("failed to save pipeline environment"); + } + int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0; + int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0; + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/smr.db", tmp); + cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0 + ? cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL) + : NULL; + int run_rc = p ? cbm_pipeline_run(p) : -1; + cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; + bool store_opened = s != NULL; + int callers[SMR_LANGS]; + int remote[SMR_LANGS]; + int builtin[SMR_LANGS]; + int literal[SMR_LANGS]; + int self_recv[SMR_LANGS]; + int cjs_callers = -1; + int cjs = -1; + int cjs_exports = -1; + int amd_callers = -1; + int amd = -1; + int amd_exports = -1; + for (int i = 0; i < SMR_LANGS; i++) { + callers[i] = remote[i] = builtin[i] = literal[i] = self_recv[i] = -1; + } + if (s) { + const char *project = cbm_pipeline_project_name(p); + for (int i = 0; i < SMR_LANGS; i++) { + char caller[64]; + char target[64]; + callers[i] = fixture_node_count(s, project, langs[i].path, "remoteCall", "Function"); + snprintf(target, sizeof(target), "%s.get", langs[i].stem); + snprintf(caller, sizeof(caller), "%s.remoteCall", langs[i].stem); + remote[i] = smr_calls(s, project, caller, target); + snprintf(caller, sizeof(caller), "%s.selfCall", langs[i].stem); + self_recv[i] = smr_calls(s, project, caller, target); + snprintf(target, sizeof(target), "%s.log", langs[i].stem); + snprintf(caller, sizeof(caller), "%s.builtinCall", langs[i].stem); + builtin[i] = smr_calls(s, project, caller, target); + snprintf(target, sizeof(target), "%s.push", langs[i].stem); + snprintf(caller, sizeof(caller), "%s.literalCall", langs[i].stem); + literal[i] = smr_calls(s, project, caller, target); + } + cjs_callers = fixture_node_count(s, project, "src/smr_cjs.js", "cjsCall", "Function"); + cjs = smr_calls(s, project, "smr_cjs.cjsCall", "smr_cjs.getObject"); + cjs_exports = smr_calls(s, project, "smr_cjs.exportsCall", "smr_cjs.getObject"); + amd_callers = fixture_node_count(s, project, "src/smr_amd.js", "amdCall", "Function"); + amd = smr_calls(s, project, "smr_amd.amdCall", "smr_amd.get"); + amd_exports = smr_calls(s, project, "smr_amd.amdExports", "smr_amd.get"); + cbm_store_close(s); + } + + cbm_pipeline_free(p); + int restore_workers_rc = + saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS"); + int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1) + : cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); + free(saved_workers); + free(saved_single); + th_rmtree(tmp); + + ASSERT_EQ(pin_workers_rc, 0); + ASSERT_EQ(pin_single_rc, 0); + ASSERT_EQ(restore_workers_rc, 0); + ASSERT_EQ(restore_single_rc, 0); + ASSERT_EQ(run_rc, 0); + ASSERT_TRUE(store_opened); + for (int i = 0; i < SMR_LANGS; i++) { + if (callers[i] != 1 || remote[i] != 0 || builtin[i] != 0 || literal[i] != 1 || + self_recv[i] != 1) { + printf(" %s: callers=%d remote=%d builtin=%d literal=%d self=%d\n", langs[i].path, + callers[i], remote[i], builtin[i], literal[i], self_recv[i]); + } + } + for (int i = 0; i < SMR_LANGS; i++) { + ASSERT_EQ(callers[i], 1); /* the negatives below are not vacuous */ + ASSERT_EQ(remote[i], 0); /* axios.get(): ES-imported receiver, no edge */ + ASSERT_EQ(builtin[i], 0); /* console.log(): builtin global, no edge */ + ASSERT_EQ(literal[i], 1); /* context.push(): object-literal method, kept */ + ASSERT_EQ(self_recv[i], 1); /* this.get(): kept */ + } + ASSERT_EQ(cjs_callers, 1); + ASSERT_EQ(cjs, 0); /* db.getObject(): CommonJS require receiver, no edge */ + ASSERT_EQ(cjs_exports, 1); /* exports.getObject(): this module, kept */ + ASSERT_EQ(amd_callers, 1); + ASSERT_EQ(amd, 0); /* api.get(): AMD dependency receiver, no edge */ + ASSERT_EQ(amd_exports, 1); /* exports.get(): the AMD `exports` pseudo-dependency, kept */ + PASS(); +} + +TEST(pipeline_tsjs_same_module_bound_receiver_not_bound) { + return check_same_module_bound_receiver(false); +} + +TEST(pipeline_tsjs_same_module_bound_receiver_not_bound_parallel) { + return check_same_module_bound_receiver(true); +} + /* Parallel-resolver regression for the TS/JS receiver guard (>= 50 files forces * pass_parallel.c's resolve_file_calls). The guard must not drop a weak member * match before the service classification runs — it suppresses ONLY the plain @@ -23491,6 +23709,8 @@ TEST(pipeline_calls_args_overlong_argument_stops_cleanly) { SUITE(pipeline) { RUN_TEST(pipeline_python_module_callers_sequential); RUN_TEST(pipeline_python_module_callers_parallel); + RUN_TEST(pipeline_tsjs_same_module_bound_receiver_not_bound); + RUN_TEST(pipeline_tsjs_same_module_bound_receiver_not_bound_parallel); RUN_TEST(pipeline_python_class_body_calls_sequential); RUN_TEST(pipeline_python_class_body_calls_parallel); RUN_TEST(pipeline_python_init_scope_sequential); diff --git a/tests/test_registry.c b/tests/test_registry.c index eb347ac58..66c544d26 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -286,41 +286,174 @@ TEST(resolve_same_module) { PASS(); } +/* #1128: these cases moved from the strategy chain to the call-site guard (the + * review of 2026-10-19). The chain stays unchanged - it also resolves type + * references, field types, reads/writes, SQL lineage and the cross-LSP pass for + * every language - so it still returns the raw same_module candidate, and + * cbm_suppress_same_module_bound_receiver decides whether a JS/TS member call + * keeps it: refused only when the receiver is bound to an import, to an AMD + * dependency, or is a builtin global. `is_method` is what the extractor sets + * (false only for a this/super receiver). */ TEST(resolve_same_module_only_on_self_receiver) { cbm_registry_t *r = cbm_registry_new(); cbm_registry_add(r, "get", "proj.pkg.service.get", "Function"); + const CBMLanguage js = CBM_LANG_JAVASCRIPT; + const char *mod = "proj.pkg.service"; + CBMImport axios_import = {.local_name = "axios", .module_path = "axios", .is_default = true}; + const CBMImportArray imports = {.items = &axios_import, .count = 1, .cap = 1}; - /* Dotted call with unrelated receiver (e.g. "axios.get") -> should NOT resolve */ - cbm_resolution_t res1 = cbm_registry_resolve(r, "axios.get", "proj.pkg.service", NULL, NULL, 0); - ASSERT_TRUE(res1.qualified_name == NULL || res1.qualified_name[0] == '\0'); + /* Dotted call with an imported receiver (`import axios from 'axios'`, + * "axios.get"): the chain still finds the local get by its member name, and + * the guard drops the binding. */ + cbm_resolution_t res1 = cbm_registry_resolve(r, "axios.get", mod, NULL, NULL, 0); + ASSERT_STR_EQ(res1.qualified_name, "proj.pkg.service.get"); + ASSERT_STR_EQ(res1.strategy, "same_module"); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(js, true, "axios.get", res1.qualified_name, + res1.strategy, &imports, false)); + /* The same receiver with no binding fact is an ordinary variable and keeps + * the edge: `context.push()` onto an object-literal method stored at + * module level is usually right. */ + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(js, true, "axios.get", res1.qualified_name, + res1.strategy, NULL, false)); - /* Dotted call with delegation pattern (e.g. "_get_store().get") -> should NOT resolve */ - cbm_resolution_t res2 = - cbm_registry_resolve(r, "_get_store().get", "proj.pkg.service", NULL, NULL, 0); - ASSERT_TRUE(res2.qualified_name == NULL || res2.qualified_name[0] == '\0'); + /* Dotted call with delegation pattern ("_get_store().get"): a call result + * carries no binding fact, so it is outside this guard and keeps the edge. */ + cbm_resolution_t res2 = cbm_registry_resolve(r, "_get_store().get", mod, NULL, NULL, 0); + ASSERT_STR_EQ(res2.strategy, "same_module"); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver( + js, true, "_get_store().get", res2.qualified_name, res2.strategy, &imports, false)); - /* Dotted call with self-receiver (e.g. "self.get") -> should resolve */ - cbm_resolution_t res3 = cbm_registry_resolve(r, "self.get", "proj.pkg.service", NULL, NULL, 0); + /* Dotted call with self-receiver ("this.get" in JS): unflagged by the + * extractor, so the guard keeps it. */ + cbm_resolution_t res3 = cbm_registry_resolve(r, "this.get", mod, NULL, NULL, 0); ASSERT_STR_EQ(res3.qualified_name, "proj.pkg.service.get"); ASSERT_STR_EQ(res3.strategy, "same_module"); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(js, false, "this.get", res3.qualified_name, + res3.strategy, &imports, false)); - /* Dotted call with exact module name prefix (e.g. "proj.pkg.service.get") -> should resolve */ - cbm_resolution_t res4 = - cbm_registry_resolve(r, "proj.pkg.service.get", "proj.pkg.service", NULL, NULL, 0); + /* Dotted call with exact module name prefix ("proj.pkg.service.get"): the + * whole callee names the target, kept. */ + cbm_resolution_t res4 = cbm_registry_resolve(r, "proj.pkg.service.get", mod, NULL, NULL, 0); ASSERT_STR_EQ(res4.qualified_name, "proj.pkg.service.get"); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver( + js, true, "proj.pkg.service.get", res4.qualified_name, res4.strategy, &imports, false)); - /* Dotted call with namespace/module last segment (e.g. "service.get") -> should resolve */ - cbm_resolution_t res5 = - cbm_registry_resolve(r, "service.get", "proj.pkg.service", NULL, NULL, 0); + /* Dotted call with namespace/module last segment ("service.get"): the + * receiver is bound to nothing, kept. */ + cbm_resolution_t res5 = cbm_registry_resolve(r, "service.get", mod, NULL, NULL, 0); ASSERT_STR_EQ(res5.qualified_name, "proj.pkg.service.get"); ASSERT_STR_EQ(res5.strategy, "same_module"); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver( + js, true, "service.get", res5.qualified_name, res5.strategy, &imports, false)); cbm_registry_free(r); PASS(); } +/* #1128: the guard's binding facts, its keep cases and its language gate. */ +TEST(suppress_same_module_bound_receiver_binding_facts) { + const CBMLanguage ts = CBM_LANG_TYPESCRIPT; + const char *hit = "proj.src.client.get"; /* .get: the receiver was dropped */ + CBMImport items[] = { + {.local_name = "axios", .module_path = "axios", .is_default = true}, + {.local_name = "fs", .module_path = "node:fs"}, /* namespace */ + {.local_name = "http", .module_path = "./client", .member = "client"}, /* named */ + {.local_name = "db", .module_path = "../database"}, /* require */ + {.local_name = "stream", .module_path = "node:stream", .local_name_synthesized = true}, + }; + const CBMImportArray imports = {.items = items, .count = 5, .cap = 5}; + + /* Refused: each binding fact on its own. */ + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "axios.get", hit, "same_module", + &imports, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "fs.get", hit, "same_module", + &imports, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "http.get", hit, "same_module", + &imports, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "db.get", hit, "same_module", + &imports, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "api.get", hit, "same_module", + NULL, true)); /* AMD dependency */ + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "console.get", hit, "same_module", + NULL, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "Math.get", hit, "same_module", + NULL, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "window.get", hit, "same_module", + NULL, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "axios?.get", hit, "same_module", + &imports, false)); + ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(ts, true, "axios.defaults.get", + "proj.src.client.defaults.get", + "same_module", &imports, false)); + + /* Kept: a synthesized local (`require('node:stream').Readable` binds no + * `stream`), an unbound receiver, CommonJS `exports`/`module`, a near-miss + * of a builtin or import name, no identifier receiver. */ + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "stream.get", hit, "same_module", + &imports, false)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "context.get", hit, + "same_module", &imports, false)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "exports.get", hit, + "same_module", &imports, false)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "module.exports.get", + "proj.src.client.exports.get", + "same_module", &imports, false)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "consoles.get", hit, + "same_module", &imports, false)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "axio.get", hit, "same_module", + &imports, false)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "_store().get", hit, + "same_module", &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "get", hit, "same_module", + &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "9x.get", hit, "same_module", + &imports, false)); + + /* Kept: the whole-callee lookup matched (the receiver is part of the hit), + * another strategy, an unflagged call, malformed input. */ + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver( + ts, true, "axios.get", "proj.src.client.axios.get", "same_module", &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "axios.get", hit, "unique_name", + &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "axios.get", hit, "import_map", + &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "axios.get", hit, + "lsp_ts_method", &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, false, "axios.get", hit, "same_module", + &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, NULL, hit, "same_module", + &imports, true)); + ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(ts, true, "axios.get", NULL, "same_module", + &imports, true)); + ASSERT_FALSE( + cbm_suppress_same_module_bound_receiver(ts, true, "axios.get", hit, NULL, &imports, true)); + + /* Language gate: the JS/TS family and the