Merge pull request #2345 from DeusData/fix/issue-2127

fix(pipeline): never bind an external Python import to a same-named project symbol (#2127)
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-01 22:38:18 +02:00
commit bedbc0ac00
8 files changed
+456 -6

No files matched your search

+11 -3
View File
@@ -645,7 +645,8 @@ static const cbm_gbuf_node_t *calls_find_source(cbm_pipeline_ctx_t *ctx, const c
/* Resolve one call and emit the appropriate edge. Returns 1 if resolved, 0 if not. */
static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count, CBMLanguage lang) {
const char **imp_vals, int imp_count, CBMLanguage lang,
const CBMImportArray *imports) {
const CBMResolvedCallArray *lsp_calls = &result->resolved_calls;
const cbm_gbuf_node_t *source_node = calls_find_source(ctx, rel, call->enclosing_func_qn);
if (!source_node) {
@@ -847,7 +848,14 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
call->receiver_is_self_attribute, call->callee_name,
res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
call->callee_is_locally_bound, res.strategy) ||
/* Import-binding suppression (#2127): an external `from m import f`
* binds `f` for the whole module, so a weak short-name match onto a
* project `X.f` outside m is fabricated. Python-only (the import
* 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));
/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
@@ -1039,7 +1047,7 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
}
total_calls++;
if (resolve_single_call(ctx, call, result, rel, module_qn, imp_keys, imp_vals,
imp_count, files[i].language)) {
imp_count, files[i].language, &result->imports)) {
resolved++;
} else {
unresolved++;
+8 -1
View File
@@ -3052,7 +3052,14 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
call->receiver_is_self_attribute,
call->callee_name, res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
call->callee_is_locally_bound, res.strategy) ||
/* Import-binding suppression (#2127) — see pass_calls.c; this gate
* MUST stay identical to the one there. */
(lang == CBM_LANG_PYTHON &&
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));
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
+217 -2
View File
@@ -1543,6 +1543,213 @@ static bool import_targetable_label(const char *label) {
return false;
}
/* #2127: a Python import path spells the module chain that owns the imported
* name (`from unittest.mock import patch` -> unittest, mock). Strategy 3 matches
* the leaf name alone, so an EXTERNAL import (stdlib / third party: no project
* module, Strategy 1 misses) bound to any same-named project definition -- a
* REST view's `patch` handler became the target of every mock.patch call at
* import_map confidence. For such an external import, accept a symbol hit only
* when the module segments preceding `name` in the import path occur, in
* order, among the hit's enclosing QN segments -- a src/ layout or sys.path
* root the module resolver missed (`from template_tests.utils import setup` ->
* proj.tests.template_tests.utils.setup) still qualifies. Relative prefixes
* (leading dots) carry no segment and are skipped. Imports of project modules
* are never judged by it (they may re-export from anywhere). Python only. */
enum { PY_IMPORT_MAX_SEGS = 64 };
bool cbm_python_import_path_matches_qn(const char *module_path, const char *name,
const char *hit_qn) {
if (!module_path || !name || !hit_qn) {
return true;
}
char path[1024];
snprintf(path, sizeof(path), "%s", module_path);
char *as = strstr(path, " as ");
if (as) {
*as = '\0';
}
const char *p = path;
while (*p == '.') {
p++;
}
/* Segments of the import path; name_at = index of the LAST `name`. */
const char *segs[PY_IMPORT_MAX_SEGS];
size_t lens[PY_IMPORT_MAX_SEGS];
int nseg = 0;
int name_at = -1;
size_t name_len = strlen(name);
while (*p && nseg < PY_IMPORT_MAX_SEGS) {
const char *dot = strchr(p, '.');
size_t len = dot ? (size_t)(dot - p) : strlen(p);
if (len == name_len && strncmp(p, name, len) == 0) {
name_at = nseg;
}
segs[nseg] = p;
lens[nseg] = len;
nseg++;
if (!dot) {
break;
}
p = dot + SKIP_ONE;
}
if (name_at <= 0) {
return true; /* no module chain before the name: nothing to check */
}
/* Ordered-subsequence match against the hit's enclosing segments (every
* segment before the hit's own leaf). */
const char *leaf = strrchr(hit_qn, '.');
const char *q = hit_qn;
int want = 0;
while (want < name_at && leaf && q < leaf) {
const char *dot = strchr(q, '.');
size_t len = (size_t)(dot - q);
if (len == lens[want] && strncmp(q, segs[want], len) == 0) {
want++;
}
q = dot + SKIP_ONE;
}
return want == name_at;
}
/* #2127, call side: an imported name binds the file's identifier to that
* import for the whole module, exactly like a parameter binds it for a body.
* When the import could not be materialized (external module) the resolver
* falls through to project-wide short-name strategies and binds `patch(...)`
* (or `mock.patch(...)`) to whatever project definition shares the leaf. True
* when the callee's root identifier is bound by this file's Python imports and
* EVERY such binding's module chain (`unittest.mock` for `from unittest.mock
* import patch`; the callee's own dotted path for a root `import a.b`) is
* absent from `resolved_qn`'s enclosing segments AND the binding is external
* (no IMPORTS edge materialized it: the module is not in the project). An
* internal binding is never judged here -- a project module can re-export a
* name from anywhere -- and a chain consistent with the target (src/ layout)
* keeps the edge. `gbuf` NULL means "no IMPORTS edges" (all external). */
static bool python_import_local_materialized(const cbm_gbuf_t *gbuf, const char *project_name,
const char *rel_path, const char *local_name) {
if (!gbuf || !rel_path) {
return false;
}
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL;
safe_free(file_qn);
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
if (!file_node || cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges,
&edge_count) != 0) {
return false;
}
char needle[CBM_SZ_256];
snprintf(needle, sizeof(needle), "\"local_name\":\"%s\"", local_name);
for (int i = 0; i < edge_count; i++) {
if (edges[i]->properties_json && strstr(edges[i]->properties_json, needle)) {
return true;
}
}
return false;
}
bool cbm_python_import_binding_contradicts(const CBMImportArray *imports, const char *callee_name,
const char *resolved_qn, const cbm_gbuf_t *gbuf,
const char *project_name, const char *rel_path) {
if (!imports || imports->count <= 0 || !callee_name || !callee_name[0] || !resolved_qn ||
!resolved_qn[0]) {
return false;
}
const char *root_end = strchr(callee_name, '.');
size_t root_len = root_end ? (size_t)(root_end - callee_name) : strlen(callee_name);
bool bound = false;
for (int i = 0; i < imports->count; i++) {
const CBMImport *imp = &imports->items[i];
if (!imp->local_name || !imp->module_path || strlen(imp->local_name) != root_len ||
strncmp(imp->local_name, callee_name, root_len) != 0) {
continue;
}
char full[1024];
snprintf(full, sizeof(full), "%s", imp->module_path);
char *as = strstr(full, " as ");
if (as) {
*as = '\0';
}
const char *path_leaf_seg = strrchr(full, '.');
bool from_import = path_leaf_seg && strcmp(path_leaf_seg + SKIP_ONE, imp->local_name) == 0;
if (!as && !from_import && strncmp(full, callee_name, root_len) == 0 &&
(full[root_len] == '.' || full[root_len] == '\0')) {
/* `import a.b` binds the ROOT package `a`: the callee already
* spells its own full dotted path (`a.b.f()`). */
snprintf(full, sizeof(full), "%s", callee_name);
}
/* Otherwise (`from m import x [as y]`) only the import's own module
* chain is evidence: members reached THROUGH x (`x.objects.create`)
* may live on any type, so they are not compared. */
const char *leaf = strrchr(full, '.');
leaf = leaf ? leaf + SKIP_ONE : full;
if (cbm_python_import_path_matches_qn(full, leaf, resolved_qn) ||
python_import_local_materialized(gbuf, project_name, rel_path, imp->local_name)) {
return false; /* consistent with the target, or a project import */
}
bound = true;
}
return bound;
}
/* #2127: whether a Python import's module lives in this project. Relative
* imports always do; otherwise the module part (the path minus the imported
* name) must resolve to a graph node. Strategy 1 has already failed on the
* full path when this is asked. */
static bool python_import_module_in_project(const cbm_pipeline_ctx_t *ctx, const char *source_rel,
const char *module_path) {
if (!module_path || module_path[0] == '.') {
return true;
}
char part[1024];
snprintf(part, sizeof(part), "%s", module_path);
char *as = strstr(part, " as ");
if (as) {
*as = '\0';
}
char *dot = strrchr(part, '.');
if (!dot) {
return false; /* `import x`: Strategy 1 already missed x itself */
}
*dot = '\0';
char *qn = cbm_pipeline_resolve_module(ctx, source_rel, part);
bool found = qn && cbm_gbuf_find_by_qn(ctx->gbuf, qn) != NULL;
safe_free(qn);
return found;
}
/* `import a.b` / `import a` (as opposed to `from a import b`): every segment
* names a module. The extractor binds the ROOT (`a`) for a dotted plain
* import and the leaf for a from-import; a dot-less path is always a plain
* import. An aliased form is ambiguous and reads as a from-import. */
static bool python_is_plain_module_import(const CBMImport *imp) {
const char *p = imp->module_path;
if (!p || p[0] == '.' || strstr(p, " as ")) {
return false;
}
const char *first_dot = strchr(p, '.');
if (!first_dot) {
return true;
}
const char *leaf = strrchr(p, '.') + SKIP_ONE;
size_t root_len = (size_t)(first_dot - p);
return imp->local_name && strlen(imp->local_name) == root_len &&
strncmp(imp->local_name, p, root_len) == 0 && strcmp(leaf, imp->local_name) != 0;
}
/* #2127: Strategy 3 hit filter for an EXTERNAL Python import. The hit must
* spell the import's module chain (a src/ layout the module resolver missed
* still does), and whatever names a module -- an enclosing path segment, or
* any segment of a plain `import x` -- can only be a Module/File node (`import
* copy` must not fall back to a project method called `copy`). */
static bool python_external_hit_rejected(const CBMImport *imp, const char *name,
bool enclosing_segment, const cbm_gbuf_node_t *hit) {
if (!cbm_python_import_path_matches_qn(imp->module_path, name, hit->qualified_name)) {
return true;
}
bool names_module = enclosing_segment || python_is_plain_module_import(imp);
return names_module && strcmp(hit->label, "Module") != 0 && strcmp(hit->label, "File") != 0;
}
/* #1934: whether the name-guess import fallbacks — Strategy 1b (sibling file,
* whose label filter admits symbols) and Strategy 3 (symbol name) — may run
* for imports from this language. A Go import path names a package — never a
@@ -1882,8 +2089,8 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
src_base = pb + SKIP_ONE;
}
}
const bool symbol_fallback_allowed =
cbm_import_symbol_fallback_allowed(cbm_language_for_filename(src_base));
const CBMLanguage src_lang = cbm_language_for_filename(src_base);
const bool symbol_fallback_allowed = cbm_import_symbol_fallback_allowed(src_lang);
/* Strategy 1b: sibling-file resolution for build/markup grammars whose
* import string is a sibling filename or directory (SCSS partials, Just/
@@ -2033,6 +2240,11 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
*dot = '\0';
end = dot;
}
/* #2127: a Python import whose module is not in the project (stdlib /
* third party) may only fall back to a hit that spells its chain. */
const bool py_external =
symbol_fallback_allowed && src_lang == CBM_LANG_PYTHON &&
!python_import_module_in_project(ctx, source_rel, imp->module_path);
for (int ci = 0; symbol_fallback_allowed && ci < ncands; ci++) {
const cbm_gbuf_node_t **hits = NULL;
int n = 0;
@@ -2054,6 +2266,9 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
strcmp(cand->qualified_name, source_file_qn) == 0) {
continue; /* self */
}
if (py_external && python_external_hit_rejected(imp, cands[ci], ci > 0, cand)) {
continue; /* #2127: not the external module's symbol */
}
if (!best || (cand->qualified_name && best->qualified_name &&
strcmp(cand->qualified_name, best->qualified_name) < 0)) {
best = cand;
+10
View File
@@ -334,6 +334,16 @@ bool cbm_weak_member_unique_name_exempt(bool is_python, bool receiver_is_self_at
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
const char *strategy);
/* Import-binding counterpart (#2127). True when the callee's root identifier
* is bound by an import of this file whose module chain contradicts the
* resolved target (`from unittest.mock import patch; patch()` must not bind a
* project `PkgConfigView.patch`), and the match came from a weak short-name
* strategy. Same drop-list as the guards above; the language set lives at the
* call sites and must be identical in pass_calls.c and pass_parallel.c.
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_import_bound_call(bool enabled, bool import_binding_contradicts,
const char *strategy);
/* #725: drop a suffix_match CALLS edge when the caller language and the
* target file's language disagree. unique_name (candidates == 1) is #1572
* and is left alone; same_module / import_map / lsp_* are kept. JS/TS/TSX
+17
View File
@@ -300,6 +300,23 @@ static inline int cbm_pipeline_check_cancel(const cbm_pipeline_ctx_t *ctx) {
* ei_go_import_never_binds_symbol. */
bool cbm_import_symbol_fallback_allowed(CBMLanguage lang);
/* #2127: true when the module segments preceding `name` in a Python import
* path (`unittest.mock.patch` -> unittest, mock) occur, in order, among the
* enclosing segments of `hit_qn`. Leading relative dots and ` as alias` are
* ignored; a path with no module chain before `name` always matches. Gates
* the import resolver's symbol-name fallback for Python. */
bool cbm_python_import_path_matches_qn(const char *module_path, const char *name,
const char *hit_qn);
/* #2127: true when the callee's root identifier is bound by the file's Python
* imports and every such binding is EXTERNAL (no IMPORTS edge from `rel_path`
* in `gbuf` carries its local name; NULL gbuf = none) with a module chain that
* contradicts `resolved_qn`. Feeds cbm_suppress_weak_import_bound_call at both
* resolver call sites. */
bool cbm_python_import_binding_contradicts(const CBMImportArray *imports, const char *callee_name,
const char *resolved_qn, const cbm_gbuf_t *gbuf,
const char *project_name, const char *rel_path);
/* Check if a file path is worth tracking for git history analysis. */
bool cbm_is_trackable_file(const char *path);
+16
View File
@@ -695,6 +695,22 @@ bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_b
return weak_short_name_strategy(strategy);
}
/* Import-binding counterpart of the parameter guard above (#2127). An import
* binds the identifier for the whole module; when that import is external it
* never materializes, and the chain falls through to project-wide short-name
* strategies (`from unittest.mock import patch; patch()` -> a REST view's
* `PkgConfigView.patch`). The caller decides whether the binding contradicts
* the target (cbm_python_import_binding_contradicts: a SCOPE FACT about this
* file, not a spelling list); only weak strategies are dropped, so import_map /
* same_module / lsp_* edges are untouched. Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_import_bound_call(bool enabled, bool import_binding_contradicts,
const char *strategy) {
if (!enabled || !import_binding_contradicts) {
return false;
}
return weak_short_name_strategy(strategy);
}
static bool js_ts_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
+108
View File
@@ -645,6 +645,113 @@ TEST(ei_go_import_never_binds_symbol) {
PASS();
}
/* #2127 helper: inbound edges of `edge_type` onto the (single) node named
* `name` with label `label`; -1 when that node is missing. */
static int ei_inbound_edges_on(cbm_store_t *store, const char *project, const char *name,
const char *label, const char *edge_type) {
cbm_node_t *nodes = NULL;
int count = 0;
if (cbm_store_find_nodes_by_name(store, project, name, &nodes, &count) != CBM_STORE_OK) {
return -1;
}
int64_t id = 0;
for (int i = 0; i < count; i++) {
if (nodes[i].label && strcmp(nodes[i].label, label) == 0) {
id = nodes[i].id;
}
}
cbm_store_free_nodes(nodes, count);
if (id == 0) {
return -1;
}
cbm_edge_t *edges = NULL;
int n = 0;
if (cbm_store_find_edges_by_target_type(store, id, edge_type, &edges, &n) != CBM_STORE_OK) {
return -1;
}
cbm_store_free_edges(edges, n);
return n;
}
/* #2127: `from unittest.mock import patch` names an EXTERNAL module. Strategy
* 1 cannot resolve it, and Strategy 3's symbol-name fallback bound the import
* to the only project definition named `patch` — an unrelated REST view's
* HTTP handler — so every patch(...) call became an import_map CALLS edge at
* confidence 0.95 (and, once the import edge is gone, a unique_name edge; the
* member form `mock.patch(...)` a suffix_match edge). A Python import path
* names its module chain, so a symbol hit whose QN does not contain the chain
* of an EXTERNAL import is not the imported thing. The true project import
* (`from app.util import helper`) must keep both its IMPORTS and its CALLS
* edge. `pad` > MIN_FILES_FOR_PARALLEL(50) runs the
* same fixture through the parallel pipeline, so both drivers are covered. */
static int ei_py_external_import_case(int pad) {
enum { EI_2127_BASE = 5, EI_2127_MAX = EI_2127_BASE + 64 };
static char names[EI_2127_MAX][32];
EILangFile f[EI_2127_MAX];
int n = 0;
f[n++] = (EILangFile){"app/views.py", "class PkgConfigView:\n"
" def get(self, request):\n return 1\n\n"
" def patch(self, request):\n return 2\n\n"
" def copy(self):\n return 3\n"};
f[n++] = (EILangFile){"app/util.py", "def helper():\n return 1\n"};
/* Recall pin: a PROJECT module re-exporting a name defined elsewhere
* (`app.base` re-exports `app.errors.BoomError`) is an internal import;
* its weak resolution is never judged by the #2127 guard. */
f[n++] = (EILangFile){"app/errors.py", "class BoomError(Exception):\n pass\n"};
f[n++] = (EILangFile){"app/base.py", "from app.errors import BoomError\n"};
f[n++] = (EILangFile){"tests/test_views.py", "import copy\n"
"from unittest import mock\n"
"from unittest.mock import patch\n"
"from app.base import BoomError\n"
"from app.util import helper\n\n\n"
"def test_something():\n"
" mock.patch(\"app.views.other\")\n"
" copy.copy(helper)\n"
" if helper() > 1:\n"
" raise BoomError()\n"
" with patch(\"app.views.thing\"):\n"
" return helper()\n"};
for (int i = 0; i < pad && n < EI_2127_MAX; i++) {
snprintf(names[n], sizeof(names[n]), "pad/mod_%02d.py", i);
f[n] = (EILangFile){names[n], "def filler():\n return 0\n"};
n++;
}
EILangProj lp;
cbm_store_t *store = ei_index_files(&lp, f, n);
int bad_imports =
store ? ei_inbound_edges_on(store, lp.project, "patch", "Method", "IMPORTS") : -1;
int bad_calls = store ? ei_inbound_edges_on(store, lp.project, "patch", "Method", "CALLS") : -1;
/* `import copy` (a plain module import of stdlib `copy`) is no project
* method: neither the import nor `copy.copy(...)` may bind it. */
bad_imports += store ? ei_inbound_edges_on(store, lp.project, "copy", "Method", "IMPORTS") : 0;
bad_calls += store ? ei_inbound_edges_on(store, lp.project, "copy", "Method", "CALLS") : 0;
int good_imports =
store ? ei_inbound_edges_on(store, lp.project, "helper", "Function", "IMPORTS") : -1;
int good_calls =
store ? ei_inbound_edges_on(store, lp.project, "helper", "Function", "CALLS") : -1;
int reexport_calls =
store ? ei_inbound_edges_on(store, lp.project, "BoomError", "Class", "CALLS") : -1;
int ok = bad_imports == 0 && bad_calls == 0 && good_imports >= 1 && good_calls >= 1 &&
reexport_calls >= 1;
if (!ok) {
fprintf(stderr,
" [#2127 pad=%d] PkgConfigView.patch+copy IMPORTS=%d CALLS=%d (want 0/0); "
"helper IMPORTS=%d CALLS=%d (want >=1/>=1); BoomError CALLS=%d (want >=1)\n",
pad, bad_imports, bad_calls, good_imports, good_calls, reexport_calls);
}
ei_cleanup(&lp, store);
return ok;
}
TEST(ei_py_external_import_never_binds_project_symbol) {
/* Both legs run before asserting so a failure diagnoses both drivers. */
int sequential_ok = ei_py_external_import_case(0);
int parallel_ok = ei_py_external_import_case(60);
ASSERT_TRUE(sequential_ok);
ASSERT_TRUE(parallel_ok);
PASS();
}
/* C++: header include should resolve to the header file node, not the same-stem
* source node. Also exercises angle-bracket include resolution. */
TEST(ei_cpp_header_include_targets_header_file) {
@@ -1171,6 +1278,7 @@ SUITE(edge_imports) {
RUN_TEST(ei_go_blank_import);
RUN_TEST(ei_go_two_consumers_same_package);
RUN_TEST(ei_go_import_never_binds_symbol);
RUN_TEST(ei_py_external_import_never_binds_project_symbol);
RUN_TEST(ei_cpp_header_include_targets_header_file);
/* ── RED REPRODUCTIONS — Rust (expected to FAIL until pipeline fixed) ── */
+69
View File
@@ -6,6 +6,7 @@
*/
#include "test_framework.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h" /* cbm_python_import_binding_contradicts (#2127) */
#include <stdlib.h>
#include <string.h>
@@ -1163,6 +1164,72 @@ TEST(local_binding_suppress_keeps_unshadowed_and_strong_strategies) {
PASS();
}
/* #2127: the import-binding guard drops only weak strategies, and only when the
* caller established that the file's import contradicts the target. */
TEST(import_binding_suppress_drops_only_weak_contradicted_calls) {
ASSERT_TRUE(cbm_suppress_weak_import_bound_call(true, true, "unique_name"));
ASSERT_TRUE(cbm_suppress_weak_import_bound_call(true, true, "suffix_match"));
ASSERT_TRUE(cbm_suppress_weak_import_bound_call(true, true, "fuzzy"));
ASSERT_FALSE(cbm_suppress_weak_import_bound_call(true, false, "unique_name"));
ASSERT_FALSE(cbm_suppress_weak_import_bound_call(false, true, "unique_name"));
ASSERT_FALSE(cbm_suppress_weak_import_bound_call(true, true, "import_map"));
ASSERT_FALSE(cbm_suppress_weak_import_bound_call(true, true, "same_module"));
ASSERT_FALSE(cbm_suppress_weak_import_bound_call(true, true, "lsp_py_method"));
ASSERT_FALSE(cbm_suppress_weak_import_bound_call(true, true, NULL));
PASS();
}
/* #2127: when does a Python import binding contradict a resolved target? */
TEST(python_import_binding_contradicts_only_foreign_chains) {
CBMImport items[] = {
{.local_name = "patch", .module_path = "unittest.mock.patch"},
{.local_name = "f", .module_path = "pkg.f"},
{.local_name = "app", .module_path = "app.util"},
{.local_name = "h", .module_path = "app.util.helper"},
{.local_name = "rel", .module_path = ".views.rel"},
{.local_name = "mock", .module_path = "unittest.mock"},
{.local_name = "Author", .module_path = ".models.Author"},
{.local_name = "copy", .module_path = "copy.copy"},
};
CBMImportArray imps = {.items = items, .count = 8, .cap = 8};
/* `from copy import copy` is a from-import (leaf == local), not a root
* package import: its chain `copy` must appear in the target. */
ASSERT_TRUE(cbm_python_import_binding_contradicts(
&imps, "copy", "proj.forms.utils.ErrorList.copy", NULL, NULL, NULL));
/* Member call through an external module import: foreign → dropped. */
ASSERT_TRUE(cbm_python_import_binding_contradicts(
&imps, "mock.patch", "proj.views.generic.RedirectView.patch", NULL, NULL, NULL));
/* Member reached THROUGH an imported class may live on any type: only the
* import's own chain (`models`) is compared, so the manager call stays. */
ASSERT_FALSE(cbm_python_import_binding_contradicts(
&imps, "Author.objects.create", "proj.db.models.query.QuerySet.create", NULL, NULL, NULL));
/* The report: external mock.patch must not be a project REST handler. */
ASSERT_TRUE(cbm_python_import_binding_contradicts(
&imps, "patch", "proj.app.views.PkgConfigView.patch", NULL, NULL, NULL));
/* Re-export / src layout: the chain `pkg` is present → keep. */
ASSERT_FALSE(
cbm_python_import_binding_contradicts(&imps, "f", "proj.src.pkg.core.f", NULL, NULL, NULL));
/* `import app.util` binds the root package; the callee spells its path. */
ASSERT_FALSE(cbm_python_import_binding_contradicts(&imps, "app.util.helper",
"proj.app.util.helper", NULL, NULL, NULL));
/* Aliased from-import: consistent target kept, foreign target dropped. */
ASSERT_FALSE(cbm_python_import_binding_contradicts(&imps, "h", "proj.app.util.helper", NULL,
NULL, NULL));
ASSERT_TRUE(
cbm_python_import_binding_contradicts(&imps, "h", "proj.other.helper", NULL, NULL, NULL));
/* Relative import: leading dots carry no segment; `views` must appear. */
ASSERT_FALSE(cbm_python_import_binding_contradicts(&imps, "rel", "proj.app.views.rel", NULL,
NULL, NULL));
ASSERT_TRUE(cbm_python_import_binding_contradicts(&imps, "rel", "proj.app.models.rel", NULL,
NULL, NULL));
/* Not import-bound → never a contradiction (the recall pin). */
ASSERT_FALSE(
cbm_python_import_binding_contradicts(&imps, "helper", "proj.x.helper", NULL, NULL, NULL));
ASSERT_FALSE(
cbm_python_import_binding_contradicts(NULL, "patch", "proj.x.patch", NULL, NULL, NULL));
PASS();
}
TEST(weak_call_guards_share_one_drop_list) {
/* The member guard and the local-binding guard must agree on what "weak"
* means. They share a single static predicate for exactly this reason; if
@@ -1302,5 +1369,7 @@ SUITE(registry) {
RUN_TEST(weak_member_unique_name_exempt_is_python_self_rooted_unique_and_specific_only);
RUN_TEST(local_binding_suppress_drops_weak_shadowed_bare_calls);
RUN_TEST(local_binding_suppress_keeps_unshadowed_and_strong_strategies);
RUN_TEST(import_binding_suppress_drops_only_weak_contradicted_calls);
RUN_TEST(python_import_binding_contradicts_only_foreign_chains);
RUN_TEST(weak_call_guards_share_one_drop_list);
}