diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 1eb3bddff..abf183f19 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -1126,7 +1126,13 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM * paths are module chains) — MUST match pass_parallel.c exactly. */ (lang == CBM_LANG_PYTHON && cbm_suppress_weak_import_bound_call(true, true, res.strategy) && cbm_python_import_binding_contradicts(imports, call->callee_name, res.qualified_name, - ctx->gbuf, ctx->project_name, rel)); + ctx->gbuf, ctx->project_name, rel)) || + /* Same-module bound-receiver suppression (#1128): `axios.get()` with + * axios imported must not bind the `get` this file happens to define. + * The language list is inside the helper - MUST match pass_parallel.c. */ + cbm_suppress_same_module_bound_receiver(lang, call->is_method, call->callee_name, + res.qualified_name, res.strategy, imports, + call->receiver_is_amd_dependency); /* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g. * `requests.get("/api/orders/{id}")`) resolve to a QN containing the library diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 2c0a9023a..96ef12ca0 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -3359,7 +3359,12 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB cbm_suppress_weak_import_bound_call(true, true, res.strategy) && cbm_python_import_binding_contradicts(&result->imports, call->callee_name, res.qualified_name, rc->main_gbuf, - rc->project_name, rel)); + rc->project_name, rel)) || + /* Same-module bound-receiver suppression (#1128) - see pass_calls.c; + * the language list is inside the helper, so the two cannot drift. */ + cbm_suppress_same_module_bound_receiver( + lang, call->is_method, call->callee_name, res.qualified_name, res.strategy, + &result->imports, call->receiver_is_amd_dependency); /* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the * service signal lives in the callee_name. The registry can mis-resolve diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index fbae8044f..8d1237323 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -907,6 +907,23 @@ bool cbm_python_import_binding_contradicts(const CBMImportArray *imports, const const char *resolved_qn, const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path); +/* #1128: true when a JS/TS-family member call resolved by `same_module` only + * through the lookup that drops its receiver (`resolved_qn` does not end in the + * whole callee), while that receiver is bound to an import of the file + * (`file_imports`: ES import or CommonJS require), to an AMD dependency + * (`receiver_is_amd_dependency`, the extractor's per-call fact), or is a + * builtin global of the JS runtime (`console`, `Math`, `JSON`, ...): + * `axios.get()` must not bind the `get` this file happens to define. Any other + * receiver keeps the edge (`context.push()` onto an object-literal method + * stored at module level). The language list is inside the helper, so + * pass_calls.c and pass_parallel.c cannot drift. Only the plain-CALLS + * fall-through is dropped. Pure; unit-tested in test_registry.c. */ +bool cbm_suppress_same_module_bound_receiver(CBMLanguage lang, bool is_method, + const char *callee_name, const char *resolved_qn, + const char *strategy, + const CBMImportArray *file_imports, + bool receiver_is_amd_dependency); + /* Base-class twin: true when a Python base spelling (`unittest.TestCase`, * `TestCase` under `from unittest import TestCase`, `ut.TestCase` under * `import unittest as ut`) is bound by an EXTERNAL import whose chain diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 9bbb21504..93fdd7b90 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -769,6 +769,160 @@ static bool js_ts_family(CBMLanguage lang) { lang == CBM_LANG_ARKTS; } +/* ── Same-module bound-receiver guard (#1128) ───────────────────────────── + * Strategy 2 (same_module) looks a callee up in its own module twice: first the + * whole callee (`Api.get` -> .Api.get), then the name after the first + * separator (`axios.get` -> .get). The second lookup drops the + * receiver, so a member call binds a module-level definition only because this + * file happens to define one with the member's name: `axios.get(url)` in a file + * with its own `function get()` became a 0.90 CALLS edge to that function. The + * weak-member guard above deliberately keeps same_module, so this class needs a + * guard of its own. + * + * Refusing every non-self receiver is wrong for this family: methods of an + * object literal (`const context = { push() {} }`) and functions assigned onto + * an object (`Translator.create = function create() {}`) are stored as plain + * module-level functions, so `context.push()` reaching the module's `push` + * through this lookup is usually right (2026-10-19 measurement on vuejs/core + * and NodeBB). So the binding is refused only when the caller's own source + * says the receiver is something else: + * - an import of this file: an ES `import` (default, namespace or named) or a + * CommonJS `const x = require(...)`, read from the file's import list; + * - an AMD dependency: a parameter of the factory passed to + * `define([...], function (dep) {...})` or `require([...], ...)`. AMD + * dependencies are not imports (they mint no IMPORTS edges), so the + * extractor's walk decides it per call (CBMCall.receiver_is_amd_dependency); + * - a builtin global of the JS runtime (`console.log`, `Math.max`, + * `JSON.parse`, `Object.keys`, `process.exit`, ...). + * Only the receiver-dropping lookup is refused. When the whole callee matched + * (`search.init()` -> .search.init: in NodeBB's chats.js that is the + * AMD dependency's own module) the receiver is part of what matched. A local + * name an import statement only synthesizes from its path (side-effect import, + * re-export, `require('x').y`) binds nothing and does not count. + * Self receivers (`this`/`super`) never reach it: the extractor leaves those + * calls unflagged (is_method false). CommonJS `exports.get()` and + * `module.exports.get()` name this module and are in neither list. + * + * It is a call-site suppressor, not a change to the strategy chain: the chain + * also resolves type references, field types, reads/writes, SQL lineage and + * the cross-LSP pass, for every language at once. Like the guards above it only + * marks the plain-CALLS fall-through for dropping, so service and route edges + * are unchanged. The language list lives here and both call sites read it + * through this helper, so pass_calls.c and pass_parallel.c cannot drift: the + * JS/TS family plus the hosts whose \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