mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-15 13:56:34 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 ──────────────────────────────────────────────── */
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user