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 <script> hosts it
drops the plain-CALLS fall-through only when the receiver-dropping
lookup produced the hit AND the receiver is bound to an ES import or a
CommonJS require of the file, to an AMD dependency, or is a builtin
global of the JS runtime (console, Math, JSON, Object, process, ...).
Every other receiver keeps its edge: object-literal methods and
functions assigned onto an object are stored at module level, so
`context.push()` reaching the module's `push` is usually right. A
whole-callee hit (`search.init()` -> <module>.search.init) keeps the
receiver in the match and is never refused. Service and route edges are
unaffected.

Measured with a fresh full index, sequential and parallel identical,
runs repeatable: vuejs/core same_module CALLS 1309 -> 1304, NodeBB
1721 -> 1680, no other strategy moves and no edge is added; every one
of the 49 removed call sites was read against the source and is a wrong
edge. helm (Go) is byte-identical.

The contributor's registry cases move to the guard as positive and
negative controls; pipeline tests cover JS, TS, TSX, ArkTS, Vue,
Svelte, Astro and HTML plus CommonJS and AMD, sequential and parallel,
and fail on main for every language.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-11-10 20:01:46 +01:00
parent 34c64bf8a1
commit 51c57c4e0c
6 changed files with 596 additions and 259 deletions
+7 -1
View File
@@ -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
+6 -1
View File
@@ -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
+17
View File
@@ -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
+183 -235
View File
@@ -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` -> <module>.Api.get), then the name after the first
* separator (`axios.get` -> <module>.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()` -> <scope>.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 <script> bodies are JS/TS (the weak-member
* guard's set without Python). Go must never join: its receiver methods carry
* flat QNs that only this lookup reaches. */
static bool same_module_receiver_guard_lang(CBMLanguage lang) {
return js_ts_family(lang) || lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
}
/* Builtin globals of the JS runtime (ECMAScript, browser, Node/CommonJS). Like
* PERL_BUILTINS a fact about the language, not about corpus fashion. MUST stay
* sorted ASCII-ascending for bsearch. */
static const char *const JS_BUILTIN_GLOBALS[] = {
"Array", "ArrayBuffer", "Atomics", "BigInt",
"Boolean", "Buffer", "DataView", "Date",
"Error", "Intl", "JSON", "Map",
"Math", "Number", "Object", "Promise",
"Proxy", "Reflect", "RegExp", "Set",
"String", "Symbol", "URL", "WeakMap",
"WeakSet", "WebAssembly", "console", "document",
"global", "globalThis", "history", "localStorage",
"location", "navigator", "performance", "process",
"require", "sessionStorage", "window",
};
/* Identifier bytes: ASCII letters, digits, `_`, `$`, and every byte of a
* multi-byte UTF-8 sequence (a non-ASCII identifier is still one name). */
enum { JS_FIRST_NON_ASCII_BYTE = 0x80 };
static bool js_identifier_byte(unsigned char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' ||
c == '$' || c >= JS_FIRST_NON_ASCII_BYTE;
}
/* Length of the identifier that roots `callee` when a member access (`.` or
* `?.`) follows it, else 0: `axios.get` -> 5, `a.b.c` -> 1, `_store().get` ->
* 0 (a call result is not a binding). The registry splits the callee at this
* same first separator. */
static size_t js_receiver_root_len(const char *callee) {
size_t n = 0;
while (js_identifier_byte((unsigned char)callee[n])) {
n++;
}
if (n == 0 || (callee[0] >= '0' && callee[0] <= '9')) {
return 0;
}
if (callee[n] == '.' || (callee[n] == '?' && callee[n + SKIP_ONE] == '.')) {
return n;
}
return 0;
}
static bool js_builtin_global(const char *root, size_t len) {
char key[CBM_SZ_32];
if (len >= sizeof(key)) {
return false; /* longer than every table entry */
}
memcpy(key, root, len);
key[len] = '\0';
return bsearch(key, JS_BUILTIN_GLOBALS,
sizeof(JS_BUILTIN_GLOBALS) / sizeof(JS_BUILTIN_GLOBALS[0]),
sizeof(JS_BUILTIN_GLOBALS[0]), python_builtin_member_cmp) != NULL;
}
/* Per-file scan: imports.count is a file-local number, never the corpus, so
* this stays O(file) per call (the Python receiver check does the same). A
* synthesized local (side-effect import, re-export, `require('x').y`) is the
* path's last segment, not a name the file binds: `require('node:stream')`
* must not make every `stream` parameter read as the stream module. */
static bool receiver_bound_by_import(const CBMImportArray *imports, const char *root, size_t len) {
if (!imports) {
return false;
}
for (int i = 0; i < imports->count; i++) {
const CBMImport *imp = &imports->items[i];
if (imp->local_name && !imp->local_name_synthesized &&
strncmp(imp->local_name, root, len) == 0 && imp->local_name[len] == '\0') {
return true;
}
}
return false;
}
/* True when the same_module hit kept the receiver: the whole-callee lookup
* matched `<scope>.search.init` for `search.init()`, so the receiver is part of
* what matched, not dropped. That lookup is never this guard's business. */
static bool same_module_hit_kept_receiver(const char *callee_name, const char *resolved_qn) {
size_t clen = strlen(callee_name);
size_t qlen = strlen(resolved_qn);
return qlen > clen && resolved_qn[qlen - clen - SKIP_ONE] == '.' &&
strcmp(resolved_qn + (qlen - clen), callee_name) == 0;
}
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) {
if (!same_module_receiver_guard_lang(lang) || !is_method) {
return false;
}
if (!strategy || strcmp(strategy, "same_module") != 0) {
return false;
}
if (!callee_name || !resolved_qn || same_module_hit_kept_receiver(callee_name, resolved_qn)) {
return false;
}
size_t len = js_receiver_root_len(callee_name);
if (len == 0) {
return false; /* no identifier receiver: `_store().get`, a bare call */
}
return receiver_is_amd_dependency || js_builtin_global(callee_name, len) ||
receiver_bound_by_import(file_imports, callee_name, len);
}
/* 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. */
@@ -1464,67 +1618,13 @@ static const char *same_module_candidate(const cbm_registry_t *r, const char *mo
return NULL;
}
static bool is_self_receiver(const char *prefix) {
if (!prefix || !prefix[0]) {
return true;
}
static const char *const self_receivers[] = {"self", "this", "cls", "@self", "$this",
"$self", "Me", "me", "Self", NULL};
for (int i = 0; self_receivers[i]; i++) {
if (strcmp(prefix, self_receivers[i]) == 0) {
return true;
}
}
return false;
}
static bool is_same_module_receiver(const char *prefix, const char *module_qn) {
if (is_self_receiver(prefix)) {
return true;
}
if (!module_qn || !module_qn[0]) {
return false;
}
char norm_prefix[CBM_SZ_256];
size_t plen = strlen(prefix);
if (plen >= sizeof(norm_prefix)) {
plen = sizeof(norm_prefix) - 1;
}
size_t j = 0;
for (size_t i = 0; i < plen && j + 1 < sizeof(norm_prefix); i++) {
if (prefix[i] == ':' && i + 1 < plen && prefix[i + 1] == ':') {
norm_prefix[j++] = '.';
i++;
} else {
norm_prefix[j++] = prefix[i];
}
}
norm_prefix[j] = '\0';
if (strcmp(norm_prefix, module_qn) == 0) {
return true;
}
size_t nlen = strlen(norm_prefix);
size_t mlen = strlen(module_qn);
if (mlen > nlen && module_qn[mlen - nlen - 1] == '.' &&
strcmp(module_qn + (mlen - nlen), norm_prefix) == 0) {
return true;
}
return false;
}
/* Strategy 2: Same-module match. Scoring elsewhere retains the raw file QN. */
static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char *callee_name,
const char *prefix, const char *suffix,
const char *module_qn, size_t scope_len,
CBMLanguage lang) {
const char *suffix, const char *module_qn,
size_t scope_len) {
const char *stored_key = same_module_candidate(r, module_qn, scope_len, callee_name);
if (!stored_key && suffix && suffix[0]) {
if (lang == CBM_LANG_GO || is_same_module_receiver(prefix, module_qn) ||
strchr(suffix, '.') != NULL || strstr(suffix, "::") != NULL) {
stored_key = same_module_candidate(r, module_qn, scope_len, suffix);
}
}
if (!stored_key && suffix && suffix[0])
stored_key = same_module_candidate(r, module_qn, scope_len, suffix);
if (stored_key)
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
return empty_result();
@@ -1753,143 +1853,10 @@ static void candidates_without_members(const cbm_registry_t *r, const qn_array_t
.lang = side ? lang : NULL};
}
static bool qn_ends_with_qualified(const char *candidate_qn, const char *callee_name) {
if (!candidate_qn || !callee_name) {
return false;
}
char norm_callee[CBM_SZ_256];
size_t clen = strlen(callee_name);
if (clen >= sizeof(norm_callee)) {
clen = sizeof(norm_callee) - 1;
}
size_t j = 0;
for (size_t i = 0; i < clen && j + 1 < sizeof(norm_callee); i++) {
if (callee_name[i] == ':' && i + 1 < clen && callee_name[i + 1] == ':') {
norm_callee[j++] = '.';
i++;
} else {
norm_callee[j++] = callee_name[i];
}
}
norm_callee[j] = '\0';
size_t nlen = strlen(norm_callee);
size_t qlen = strlen(candidate_qn);
if (qlen < nlen) {
return false;
}
if (strcmp(candidate_qn, norm_callee) == 0) {
return true;
}
if (qlen > nlen && candidate_qn[qlen - nlen - 1] == '.' &&
strcmp(candidate_qn + (qlen - nlen), norm_callee) == 0) {
return true;
}
return false;
}
static bool is_builtin_candidate(const char *qn) {
if (!qn) {
return false;
}
return strncmp(qn, "builtins.", 9) == 0 || strncmp(qn, "<python-builtins>.", 18) == 0 ||
strncmp(qn, "<kotlin-builtins>.", 18) == 0;
}
static bool is_candidate_method(const cbm_registry_t *r, const char *qn) {
const char *label = cbm_registry_label_of(r, qn);
return label && strcmp(label, "Method") == 0;
}
static bool is_imported_receiver(const char *prefix, const char **import_keys, int import_count) {
if (!prefix || !import_keys || import_count <= 0) {
return false;
}
for (int i = 0; i < import_count; i++) {
if (import_keys[i] && strcmp(import_keys[i], prefix) == 0) {
return true;
}
}
return false;
}
static bool is_known_type_or_namespace(const cbm_registry_t *r, const char *name) {
if (!r || !name || !name[0]) {
return false;
}
const char *exact_label = cbm_registry_label_of(r, name);
if (exact_label &&
(strcmp(exact_label, "Class") == 0 || strcmp(exact_label, "Module") == 0 ||
strcmp(exact_label, "Namespace") == 0 || strcmp(exact_label, "Struct") == 0 ||
strcmp(exact_label, "Interface") == 0 || strcmp(exact_label, "Enum") == 0 ||
strcmp(exact_label, "Package") == 0)) {
return true;
}
const qn_array_t *arr = cbm_ht_get(r->by_name, name);
if (!arr) {
return false;
}
for (int i = 0; i < arr->count; i++) {
const char *label = cbm_registry_label_of(r, arr->items[i]);
if (label && (strcmp(label, "Class") == 0 || strcmp(label, "Module") == 0 ||
strcmp(label, "Namespace") == 0 || strcmp(label, "Struct") == 0 ||
strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 ||
strcmp(label, "Package") == 0)) {
return true;
}
}
return false;
}
static bool should_reject_receiver(const cbm_registry_t *r, const char *callee_name,
const char *candidate_qn, const char **import_keys,
int import_count) {
if (!callee_name || !candidate_qn) {
return false;
}
const char *dot = strchr(callee_name, '.');
const char *colons = strstr(callee_name, "::");
const char *sep = dot;
if (colons && (!sep || colons < sep)) {
sep = colons;
}
if (!sep) {
return false;
}
char prefix[CBM_SZ_256];
size_t plen = (size_t)(sep - callee_name);
if (plen >= sizeof(prefix)) {
plen = sizeof(prefix) - 1;
}
memcpy(prefix, callee_name, plen);
prefix[plen] = '\0';
if (is_self_receiver(prefix)) {
return false;
}
if (is_builtin_candidate(candidate_qn)) {
return false;
}
if (qn_ends_with_qualified(candidate_qn, callee_name)) {
return false;
}
if (!is_candidate_method(r, candidate_qn)) {
return true;
}
if (is_imported_receiver(prefix, import_keys, import_count)) {
return true;
}
if (is_known_type_or_namespace(r, prefix)) {
return true;
}
return false;
}
/* Strategies 3.5, 3 and 4 over one same-name candidate set. */
static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const qn_array_t *arr,
const char *callee_name, const char *module_qn,
const char **import_keys, const char **import_vals,
int import_count) {
const char **import_vals, int import_count) {
/* Strategy 3.5: a qualified callee disambiguates among multiple same-name
* candidates by full qualified tail, before bare-name scoring collapses
* them onto a single winner. */
@@ -1905,9 +1872,6 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q
if (!receiver_chain_admits(callee_name, arr->items[0])) {
return empty_result();
}
if (should_reject_receiver(r, callee_name, arr->items[0], import_keys, import_count)) {
return empty_result();
}
double conf = CONF_UNIQUE_NAME;
if (import_vals && import_count > 0 &&
!is_import_reachable(arr->items[0], import_vals, import_count)) {
@@ -1917,27 +1881,19 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q
}
/* Strategy 4: multiple candidates */
cbm_resolution_t res;
if (import_vals && import_count > 0) {
res = resolve_multi_with_imports(r, arr, module_qn, import_vals, import_count);
} else {
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
qn_lang_flags(arr), arr->count, module_qn);
if (best) {
if (!receiver_chain_admits(callee_name, best)) {
return empty_result();
}
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
res = (cbm_resolution_t){best, "suffix_match", conf, arr->count};
} else {
res = empty_result();
return resolve_multi_with_imports(r, arr, module_qn, import_vals, import_count);
}
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
qn_lang_flags(arr), arr->count, module_qn);
if (best) {
if (!receiver_chain_admits(callee_name, best)) {
return empty_result();
}
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
}
if (res.qualified_name &&
should_reject_receiver(r, callee_name, res.qualified_name, import_keys, import_count)) {
return empty_result();
}
return res;
return empty_result();
}
/* Strategy 3+4: Name lookup + suffix match. skip_members: the callee cannot
@@ -1948,9 +1904,8 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q
* 345 -> 9 callers (2026-10-13 graph audit). Dropping only the Fields made
* libc `read()` the unique name of a C++ adapter's Method instead. */
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
const char *module_qn, const char **import_keys,
const char **import_vals, int import_count,
bool skip_members) {
const char *module_qn, const char **import_vals,
int import_count, bool skip_members) {
const char *lookup = simple_name(callee_name);
const qn_array_t *arr = cbm_ht_get(r->by_name, lookup);
if (!arr || arr->count == 0) {
@@ -1968,11 +1923,9 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
if (view.count == 0) {
return empty_result();
}
return resolve_name_candidates(r, &view, callee_name, module_qn, import_keys, import_vals,
import_count);
return resolve_name_candidates(r, &view, callee_name, module_qn, import_vals, import_count);
}
return resolve_name_candidates(r, arr, callee_name, module_qn, import_keys, import_vals,
import_count);
return resolve_name_candidates(r, arr, callee_name, module_qn, import_vals, import_count);
}
/* The strategy chain shared by both public resolve variants (no caching here —
@@ -1980,8 +1933,7 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const char *callee_name,
const char *module_qn, const char **import_map_keys,
const char **import_map_vals, int import_map_count,
size_t scope_len, bool skip_members,
CBMLanguage lang) {
size_t scope_len, bool skip_members) {
/* Split callee at the first path separator: "pkg.Func" → prefix="pkg",
* suffix="Func". Rust/C++ use "::" rather than ".", so honor whichever
* separator appears first ("lib::square" → prefix="lib", suffix="square").
@@ -2014,12 +1966,12 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch
resolve_import_map(r, prefix, suffix, import_map_keys, import_map_vals, import_map_count);
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 2: same module */
res = resolve_same_module(r, callee_name, prefix, suffix, module_qn, scope_len, lang);
res = resolve_same_module(r, callee_name, suffix, module_qn, scope_len);
}
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 3+4: name lookup */
res = resolve_name_lookup(r, callee_name, module_qn, import_map_keys, import_map_vals,
import_map_count, skip_members);
res = resolve_name_lookup(r, callee_name, module_qn, import_map_vals, import_map_count,
skip_members);
}
return res;
}
@@ -2070,8 +2022,7 @@ bool cbm_registry_name_guess_vetoed(const cbm_registry_t *r, CBMLanguage caller_
static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const char *callee_name,
const char *module_qn, const char **import_map_keys,
const char **import_map_vals, int import_map_count,
size_t scope_len, bool skip_members,
CBMLanguage lang) {
size_t scope_len, bool skip_members) {
if (!r || !callee_name) {
return empty_result();
}
@@ -2086,7 +2037,7 @@ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const c
cbm_resolution_t res =
registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals,
import_map_count, scope_len, skip_members, lang);
import_map_count, scope_len, skip_members);
/* Data relations (Table/View) are lineage-only registry members: common
* table names (users, orders, config) collide with code identifiers across
@@ -2128,8 +2079,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
const char *module_qn, const char **import_map_keys,
const char **import_map_vals, int import_map_count) {
return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals,
import_map_count, module_qn ? strlen(module_qn) : 0, false,
CBM_LANG_COUNT);
import_map_count, module_qn ? strlen(module_qn) : 0, false);
}
static size_t lang_scope_len(const char *module_qn, CBMLanguage lang) {
@@ -2142,7 +2092,7 @@ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char *
const char **import_map_vals, int import_map_count,
CBMLanguage lang) {
return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals,
import_map_count, lang_scope_len(module_qn, lang), false, lang);
import_map_count, lang_scope_len(module_qn, lang), false);
}
cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name,
@@ -2156,8 +2106,7 @@ cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const c
bool skip_members = lang == CBM_LANG_C && callee_name && !strchr(callee_name, '.') &&
!strstr(callee_name, "->");
return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals,
import_map_count, lang_scope_len(module_qn, lang), skip_members,
lang);
import_map_count, lang_scope_len(module_qn, lang), skip_members);
}
/* ── Class-qualified route handlers (PHP, #1146) ─────────────────── */
@@ -2325,8 +2274,7 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha
* it would poison one variant with the other's semantics. SQL files hold
* few distinct relation refs, so the chain walk stays cheap. */
return registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals,
import_map_count, module_qn ? strlen(module_qn) : 0, false,
CBM_LANG_COUNT);
import_map_count, module_qn ? strlen(module_qn) : 0, false);
}
/* ── Fuzzy Resolve ──────────────────────────────────────────────── */
+220
View File
@@ -9710,6 +9710,224 @@ static int count_nodes_named(cbm_store_t *s, const char *project, const char *na
return n;
}
/* #1128: a JS/TS-family member call whose receiver is bound to an import or is a
* builtin global must not bind the function this file happens to define under
* the member's name. Strategy 2 (same_module) found `axios.get()` -> <module>.get
* by dropping the receiver, a 0.90 edge the weak-member guard deliberately
* keeps. One file per language the guard covers (the four JS/TS languages and
* the four <script> hosts), plus CommonJS and AMD in JavaScript. NEGATIVE: an
* ES-imported receiver, a builtin global, a CommonJS require and an AMD
* dependency bind nothing. POSITIVE controls that keep their edge: an
* object-literal method (`context.push()`, the vuejs/core codegen shape), a self
* receiver (`this.get()`) and CommonJS / AMD `exports.get()`. The parallel twin
* pads past MIN_FILES_FOR_PARALLEL and pins CBM_WORKERS so pass_parallel.c's
* resolver is the one exercised. */
enum { SMR_LANGS = 8, SMR_PAD_FILES = 52 };
/* CALLS edges whose source and target qualified names end in `.caller` and
* `.callee`. */
static int smr_calls(cbm_store_t *s, const char *project, const char *caller, const char *callee) {
char src_tail[128];
char tgt_tail[128];
snprintf(src_tail, sizeof(src_tail), ".%s", caller);
snprintf(tgt_tail, sizeof(tgt_tail), ".%s", callee);
cbm_edge_t *edges = NULL;
int edge_count = 0;
if (cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count) != CBM_STORE_OK) {
return -1;
}
int hits = 0;
size_t cs = strlen(src_tail);
size_t ts = strlen(tgt_tail);
for (int i = 0; i < edge_count; i++) {
cbm_node_t src = {0};
cbm_node_t tgt = {0};
if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK &&
cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) {
size_t sl = strlen(src.qualified_name);
size_t tl = strlen(tgt.qualified_name);
if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, src_tail) == 0 &&
strcmp(tgt.qualified_name + tl - ts, tgt_tail) == 0) {
hits++;
}
}
cbm_node_free_fields(&src);
cbm_node_free_fields(&tgt);
}
if (edges) {
cbm_store_free_edges(edges, edge_count);
}
return hits;
}
static int check_same_module_bound_receiver(bool parallel) {
static const char *const body = "import axios from 'axios';\n"
"function get(key) { return key; }\n"
"function log(msg) { return msg; }\n"
"function push(item) { return item; }\n"
"function remoteCall(url) { return axios.get(url); }\n"
"function builtinCall(msg) { return console.log(msg); }\n"
"function literalCall(item) {\n"
" const context = { push };\n"
" return context.push(item);\n"
"}\n"
"function selfCall(key) { return this.get(key); }\n";
static const struct {
const char *path;
const char *stem;
const char *open;
const char *close;
} langs[SMR_LANGS] = {
{"src/smr_js.js", "smr_js", "", ""},
{"src/smr_ts.ts", "smr_ts", "", ""},
{"src/smr_tsx.tsx", "smr_tsx", "", ""},
{"src/smr_ets.ets", "smr_ets", "", ""},
{"src/smr_vue.vue", "smr_vue", "<template><div></div></template>\n<script>\n",
"</script>\n"},
{"src/smr_svelte.svelte", "smr_svelte", "<script>\n", "</script>\n<div></div>\n"},
{"src/smr_astro.astro", "smr_astro", "---\n", "---\n<div></div>\n"},
{"src/smr_html.html", "smr_html", "<html><body>\n<script type=\"module\">\n",
"</script>\n</body></html>\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);
+163 -22
View File
@@ -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"; /* <module>.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 <script> hosts are in; Go never
* (flat receiver-method QNs only this lookup reaches); Python, PHP are not. */
static const CBMLanguage in[] = {CBM_LANG_JAVASCRIPT, CBM_LANG_TYPESCRIPT, CBM_LANG_TSX,
CBM_LANG_ARKTS, CBM_LANG_HTML, CBM_LANG_VUE,
CBM_LANG_SVELTE, CBM_LANG_ASTRO};
for (size_t i = 0; i < sizeof(in) / sizeof(in[0]); i++) {
ASSERT_TRUE(cbm_suppress_same_module_bound_receiver(in[i], true, "console.get", hit,
"same_module", &imports, false));
}
static const CBMLanguage out[] = {CBM_LANG_GO, CBM_LANG_PYTHON, CBM_LANG_PHP};
for (size_t i = 0; i < sizeof(out) / sizeof(out[0]); i++) {
ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(out[i], true, "console.get", hit,
"same_module", &imports, true));
}
PASS();
}
/* #1128: these cases asked the name-only strategies (3/4) to refuse a member
* call whose receiver contradicts the candidate. The chain stays unchanged (the
* review of 2026-10-19), so they are answered at the call site instead: for the
* JS/TS family the weak-member guard (#592/#606) drops a member call bound by a
* name-only strategy, which every negative case below is, and the positive cases
* resolve as before and pass both guards. */
TEST(resolve_qualified_receiver_awareness) {
cbm_registry_t *r = cbm_registry_new();
const CBMLanguage js = CBM_LANG_JAVASCRIPT;
/* 1. Register a Method candidate under Class Service: proj.pkg.Service.get */
cbm_registry_add(r, "Service", "proj.pkg.Service", "Class");
@@ -329,21 +462,23 @@ TEST(resolve_qualified_receiver_awareness) {
/* 2. Register a Module axios */
cbm_registry_add(r, "axios", "proj.axios", "Module");
/* Test Case A: Unrelated receiver that is a known module/type -> should NOT resolve */
/* Test Case A: Unrelated receiver that is a known module/type -> name-only
* guess, dropped by the member guard */
/* axios.get called, axios is a known Module */
{
cbm_resolution_t res = cbm_registry_resolve(r, "axios.get", "proj.app", NULL, NULL, 0);
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, res.strategy));
}
/* Test Case B: Unrelated receiver that is an imported receiver -> should NOT resolve */
/* Test Case B: Unrelated receiver that is an imported receiver -> name-only
* guess, dropped by the member guard */
/* other_service.get called, other_service is an imported alias/key mapping to OtherService */
{
const char *keys[] = {"other_service"};
const char *vals[] = {"proj.pkg.OtherService"};
cbm_resolution_t res =
cbm_registry_resolve(r, "other_service.get", "proj.app", keys, vals, 1);
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, res.strategy));
}
/* Test Case B2: Related receiver that is an imported receiver -> should resolve */
@@ -353,6 +488,9 @@ TEST(resolve_qualified_receiver_awareness) {
const char *vals[] = {"proj.pkg.Service"};
cbm_resolution_t res = cbm_registry_resolve(r, "my_service.get", "proj.app", keys, vals, 1);
ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get");
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, res.strategy));
ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(
js, true, "my_service.get", res.qualified_name, res.strategy, NULL, false));
}
/* Test Case D: Fully qualified receiver matching the candidate's parent path -> should resolve
@@ -360,6 +498,8 @@ TEST(resolve_qualified_receiver_awareness) {
{
cbm_resolution_t res = cbm_registry_resolve(r, "Service.get", "proj.app", NULL, NULL, 0);
ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get");
ASSERT_FALSE(cbm_suppress_same_module_bound_receiver(
js, true, "Service.get", res.qualified_name, res.strategy, NULL, false));
}
/* Test Case E: Double colon C++/Perl style qualification (e.g. Service::get) -> should resolve
@@ -369,14 +509,14 @@ TEST(resolve_qualified_receiver_awareness) {
ASSERT_STR_EQ(res.qualified_name, "proj.pkg.Service.get");
}
/* Test Case F: If the candidate is a Function (not a Method), we always enforce qualified
* suffix match */
/* Test Case F: Function candidate behind an unrelated receiver -> name-only
* guess, dropped by the member guard */
/* Register unique Function helper.compute */
cbm_registry_add(r, "compute", "proj.pkg.helper.compute", "Function");
{
cbm_resolution_t res =
cbm_registry_resolve(r, "unrelated.compute", "proj.app", NULL, NULL, 0);
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, res.strategy));
}
cbm_registry_free(r);
@@ -2098,6 +2238,7 @@ SUITE(registry) {
/* Resolution */
RUN_TEST(resolve_same_module);
RUN_TEST(resolve_same_module_only_on_self_receiver);
RUN_TEST(suppress_same_module_bound_receiver_binding_facts);
RUN_TEST(resolve_qualified_disambiguates_same_name);
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
RUN_TEST(registry_indexes_by_passed_name_not_qn_tail);