fix(python): suppress bare calls shadowed by an enclosing parameter

A Python `foo()` whose callee identifier is bound as a parameter of an
enclosing scope cannot be the module-level `foo` -- the parameter shadows it
for the whole body -- so resolving the call to a project Function/Method by a
weak short-name strategy fabricates the edge by construction:

    def _run_with_heavy_slot(run):
        return run()          # bound an unrelated SatoriLive.run

The receiver-aware weak-member guard (#1276) cannot see this class at all: a
bare call has no receiver, so is_method is false and the guard never fires.
This is the bare-call counterpart of python_receiver_is_exempt.

Keyed on the SCOPE FACT, not on the callee's spelling. A list of
generic-looking names (get / run / execute) asserts that certain spellings are
usually noise, which is a claim about corpus fashion rather than about what the
resolver knew, and it ages invisibly: nothing fails when the distribution
shifts, the graph just quietly loses different edges. A parameter binding is
decidable from this file's AST outright.

Parameters only, deliberately. A parameter is in scope for the entire body
regardless of position and Python forbids `global` on one, so no flow analysis
is needed. Local assignments are flow- and binding-form-sensitive (`for`,
`with as`, `except as`, `:=`, unpacking, plus global/nonlocal overrides); a
partial body scan would suppress the wrong edges invisibly -- the same failure
mode that rules out the name list. Enclosing scopes are walked to the file root
so a closure over an outer parameter counts.

Wired at both pass_calls.c and pass_parallel.c with an identical language gate:
a guard on one resolver only diverges the sequential and parallel paths. The
weak-strategy drop-list is now a single shared predicate used by both the member
guard and this one, so they cannot disagree about what "weak" means; a unit test
pins that agreement.

Tests pin both directions. The pipeline positive control is a cross-file bare
call with NO import, so it resolves by a weak strategy this guard could have
killed -- asserting a same_module edge would prove nothing, since no guard
touches same_module for any input. Verified by breaking the guard in both
directions: under-suppressing fails the negatives and the extraction flag;
over-suppressing fails the positives, so they are not vacuous. Disabling only
the pass_parallel.c gate fails only the >=50-file test and nothing else.

805 passed / 0 failed across pipeline registry parallel extraction complexity
lsp_resolution_probe.

The defect class was identified by Joseph-MingEn in #1386, which proposed a
name-keyed shape; the diagnosis is theirs.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Joseph-MingEn <125283161+Joseph-MingEn@users.noreply.github.com>
This commit is contained in:
Martin Vogel
2026-08-29 17:59:58 +02:00
co-authored by Joseph-MingEn
parent 5d96377aa5
commit 95689b5c20
9 changed files with 436 additions and 12 deletions
+4
View File
@@ -268,6 +268,10 @@ typedef struct {
// pass_lsp_cross.c. Default false.
bool requires_lsp_resolution; // synthetic semantic candidate (for example an implicit
// C++ operator). Never fall back to textual resolution.
bool callee_is_locally_bound; // bare call foo() whose callee identifier is bound as a
// parameter of an enclosing function, so it cannot be the
// module-level foo. Python only today. Read by the
// weak-local-binding guard. Default false.
} CBMCall;
typedef struct {
+87
View File
@@ -2800,6 +2800,86 @@ static bool python_receiver_is_exempt(CBMExtractCtx *ctx, TSNode receiver) {
return false;
}
/* Name bound by one Python parameter node, or NULL when the shape binds none.
* Covers every binding form a `parameters` / `lambda_parameters` list produces:
* a bare `identifier`, the `name` field of default/typed parameters, and the
* identifier under a `*args` / `**kwargs` splat. A shape with no identifier (the
* bare `*` keyword separator) yields NULL and simply matches nothing. */
static const char *python_parameter_name(CBMExtractCtx *ctx, TSNode param) {
if (ts_node_is_null(param)) {
return NULL;
}
if (strcmp(ts_node_type(param), "identifier") == 0) {
return cbm_node_text(ctx->arena, param, ctx->source);
}
TSNode name = ts_node_child_by_field_name(param, TS_FIELD("name"));
if (!ts_node_is_null(name) && strcmp(ts_node_type(name), "identifier") == 0) {
return cbm_node_text(ctx->arena, name, ctx->source);
}
/* `*args` / `**kwargs`, and any typed shape without a `name` field: the
* bound identifier is the first named child. */
TSNode first = ts_node_named_child(param, 0);
if (!ts_node_is_null(first) && strcmp(ts_node_type(first), "identifier") == 0) {
return cbm_node_text(ctx->arena, first, ctx->source);
}
return NULL;
}
/* True when the callee of a BARE Python call `foo()` is bound as a parameter of
* an enclosing function or lambda — the bare-call counterpart of
* python_receiver_is_exempt above.
*
* A parameter binding shadows any module-level `foo` for the whole body, so
* resolving such a call to a project Function/Method by short name alone
* fabricates the edge BY CONSTRUCTION: `def _run_with_heavy_slot(run): run()`
* must not bind an unrelated `SatoriLive.run`. Unlike a receiver type this is
* decidable from the AST outright, with no flow analysis and no list of
* "generic-looking" callee names — Python forbids `global` on a parameter, and a
* parameter is in scope for the entire body regardless of position, so there is
* no ordering subtlety to get wrong.
*
* Enclosing scopes are walked to the file root so a closure over an outer
* parameter counts (`def outer(run): def inner(): return run()`).
*
* LOCAL ASSIGNMENTS are deliberately NOT covered. They are flow- and
* binding-form-sensitive (`for`, `with ... as`, `except ... as`, `:=`,
* unpacking, plus `global`/`nonlocal` overrides), so a partial body scan would
* suppress the wrong edges invisibly — the same failure mode that rules out a
* hardcoded name list. Parameters alone already cover the Callable-parameter
* shape that motivated this guard. Cost is O(enclosing depth x params) per bare
* call, never the corpus.
*
* Known and accepted: `def inner(): global run; return run()` nested in a
* function whose parameter is `run` is still flagged. Detecting it needs exactly
* the body scan this helper avoids, and it costs one edge in a shape that
* essentially does not occur. */
static bool python_callee_is_bound_parameter(CBMExtractCtx *ctx, TSNode call_node,
TSNode callee_ident) {
const char *callee_name = cbm_node_text(ctx->arena, callee_ident, ctx->source);
if (!callee_name || !callee_name[0]) {
return false;
}
for (TSNode scope = ts_node_parent(call_node); !ts_node_is_null(scope);
scope = ts_node_parent(scope)) {
const char *kind = ts_node_type(scope);
if (strcmp(kind, "function_definition") != 0 && strcmp(kind, "lambda") != 0) {
continue;
}
TSNode params = ts_node_child_by_field_name(scope, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
continue;
}
uint32_t count = ts_node_named_child_count(params);
for (uint32_t i = 0; i < count; i++) {
const char *pname = python_parameter_name(ctx, ts_node_named_child(params, i));
if (pname && strcmp(pname, callee_name) == 0) {
return true;
}
}
}
return false;
}
static bool is_objectscript_language(CBMLanguage language) {
return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
@@ -3424,11 +3504,18 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
// (`accelerator.print()` must not bind MockAccelerator.print).
// Imported receivers stay unflagged: module.function() is Python's
// canonical cross-file call and the import map resolves it.
// A BARE Python call foo() whose callee is bound as a parameter of an
// enclosing scope cannot be the module-level foo, so short-name
// resolution would fabricate the edge (`def f(run): run()` must not
// bind SatoriLive.run). Distinct from is_method: there is no receiver
// here, so the weak-member guard cannot see this class at all.
if (ctx->language == CBM_LANG_PYTHON && strcmp(ts_node_type(node), "call") == 0) {
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "attribute") == 0) {
TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object"));
call.is_method = !python_receiver_is_exempt(ctx, obj);
} else if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "identifier") == 0) {
call.callee_is_locally_bound = python_callee_is_bound_parameter(ctx, node, fn);
}
}
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
+9 -1
View File
@@ -627,8 +627,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
/* Bare-call local-binding suppression. A member call has a receiver the
* guard above can reason about; a bare `run()` has none, so that guard
* cannot see this class at all. Python-only today because the extraction
* flag is set only for Python — this gate MUST match pass_parallel.c's
* exactly, for the same divergence reason noted above. */
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
/* 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
@@ -2482,8 +2482,13 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
/* Bare-call local-binding suppression — see the note in pass_calls.c.
* This gate MUST stay identical to the one there. */
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
+12
View File
@@ -280,6 +280,18 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *strategy);
/* Bare-call counterpart of the guard above. True when a resolved BARE call edge
* binds a callee that is shadowed by an enclosing parameter, and the match came
* from a weak short-name strategy — so the edge is fabricated by construction
* (`def f(run): run()` must not bind an unrelated `SatoriLive.run`). Shares the
* member guard's drop-list, so lsp_* / import / same-module matches are kept.
* Deliberately keyed on the SCOPE FACT, not on the callee's spelling. The
* language set lives at the call sites (pass_calls.c / pass_parallel.c) and must
* be identical in both, or the sequential and parallel resolvers diverge.
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
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
+47 -10
View File
@@ -448,22 +448,59 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
* per-language decision made at the call sites in pass_calls.c and
* pass_parallel.c, which MUST stay in lockstep — a gate added to only one of
* them diverges the sequential and parallel resolvers. */
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *strategy) {
if (!enabled || !is_method || !strategy || !strategy[0]) {
/* The weak short-name strategies that actually reach the call-resolution
* guards: the registry's suffix_match / unique_name and the parallel
* field_type_hint. "fuzzy" is listed as defensive insurance only —
* cbm_registry_fuzzy_resolve is not wired into the sequential/parallel resolvers
* today, so it never reaches these helpers, but naming it keeps a future wiring
* from silently reintroducing the noise. Everything else — same_module /
* import_map / import_map_suffix / qualified_suffix / callee_suffix /
* service_pattern / lsp_* — is a receiver- or import-aware match and is KEPT.
*
* Shared by BOTH weak-call guards below so the drop-list exists exactly once: a
* list that drifted between the member guard and the local-binding guard would
* make the two disagree about what "weak" means. */
static bool weak_short_name_strategy(const char *strategy) {
if (!strategy || !strategy[0]) {
return false;
}
/* Weak short-name strategies that actually reach the call-resolution guards:
* the registry's suffix_match / unique_name and the parallel field_type_hint.
* "fuzzy" is listed as defensive insurance only — cbm_registry_fuzzy_resolve
* is not wired into the sequential/parallel resolvers today, so it never
* reaches this helper, but naming it keeps a future wiring from silently
* reintroducing the noise. Everything else — same_module / import_map /
* import_map_suffix / qualified_suffix / callee_suffix / service_pattern /
* lsp_* — is a receiver- or import-aware match and is KEPT. */
return strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "unique_name") == 0 ||
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *strategy) {
if (!enabled || !is_method) {
return false;
}
return weak_short_name_strategy(strategy);
}
/* Bare-call counterpart of the member guard above. A Python call `foo()` whose
* callee identifier is bound as a parameter of an enclosing scope cannot be the
* module-level `foo`: the parameter shadows it for the whole body. Binding such
* a call to a project Function/Method by a weak short-name strategy fabricates
* the edge by construction (`def _run_with_heavy_slot(run): run()` ->
* SatoriLive.run).
*
* This is deliberately NOT keyed on the callee's spelling. A list of
* "generic-looking" names (get / run / execute) asserts that certain spellings
* are usually noise, which is a claim about corpus fashion rather than about
* what the resolver knew — and it ages invisibly, because nothing fails when the
* distribution shifts, the graph just quietly loses different edges. A parameter
* binding is a fact about THIS file's scope, decidable outright.
*
* `enabled` is the caller's per-language gate, kept out of the helper for the
* same reason as the member guard: the two call sites in pass_calls.c and
* pass_parallel.c MUST enumerate the identical language set, or the sequential
* and parallel resolvers diverge. Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
const char *strategy) {
if (!enabled || !callee_is_locally_bound) {
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;
+80
View File
@@ -4621,6 +4621,85 @@ TEST(extract_python_member_call_flags_is_method) {
PASS();
}
/* Python bare-call local-binding flag (the bare-call counterpart of the
* receiver flag above). Pins BOTH directions: a callee shadowed by a parameter
* of an enclosing scope IS flagged so the resolver can suppress a weak
* short-name match, while an unshadowed callee — a genuine module-level
* function, an imported name, or a nested `def` — is NOT, so its true edge
* survives. Every parameter binding form the grammar produces is covered, since
* a form the extractor silently missed would leave that shape unguarded. */
TEST(extract_python_bare_call_flags_locally_bound_callee) {
CBMFileResult *r = extract("from pkg import helper\n"
"\n"
"def outer(run, *rest, timeout=5, label: str = 'x', **opts):\n"
" def inner():\n"
" return run()\n"
" rest()\n"
" timeout()\n"
" label()\n"
" opts()\n"
" module_level()\n"
" helper()\n"
" return inner()\n"
"\n"
"def typed(cb: Callable):\n"
" return cb()\n"
"\n"
"apply_it = lambda fn: fn()\n",
CBM_LANG_PYTHON, "t", "x.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* callee name -> (expected flag, seen count) */
struct {
const char *callee;
bool expect_bound;
int seen;
} cases[] = {
{"run", true, 0}, /* closure over an ENCLOSING function's parameter */
{"rest", true, 0}, /* *args -> list_splat_pattern */
{"timeout", true, 0}, /* default_parameter */
{"label", true, 0}, /* typed_default_parameter (keyword-only) */
{"opts", true, 0}, /* **kwargs -> dictionary_splat_pattern */
{"cb", true, 0}, /* typed_parameter, no default */
{"fn", true, 0}, /* lambda parameter */
{"module_level", false, 0}, /* unbound: the true cross-file edge */
{"helper", false, 0}, /* imported name, not a parameter */
{"inner", false, 0}, /* nested def: a real target, keep the edge */
};
const int case_count = (int)(sizeof(cases) / sizeof(cases[0]));
for (int i = 0; i < r->calls.count; i++) {
const char *cn = r->calls.items[i].callee_name;
if (!cn) {
continue;
}
for (int c = 0; c < case_count; c++) {
if (strcmp(cn, cases[c].callee) != 0) {
continue;
}
cases[c].seen++;
if (r->calls.items[i].callee_is_locally_bound != cases[c].expect_bound) {
printf(" bare-call flag mismatch for %s(): got %d, expected %d\n", cases[c].callee,
r->calls.items[i].callee_is_locally_bound ? 1 : 0,
cases[c].expect_bound ? 1 : 0);
}
ASSERT_EQ(r->calls.items[i].callee_is_locally_bound, cases[c].expect_bound);
}
}
/* Each shape must appear exactly once, so a missed extraction cannot let the
* loop above pass vacuously. */
for (int c = 0; c < case_count; c++) {
if (cases[c].seen != 1) {
printf(" bare call %s() extracted %d times, expected 1\n", cases[c].callee,
cases[c].seen);
}
ASSERT_EQ(cases[c].seen, 1);
}
cbm_free_result(r);
PASS();
}
/* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above).
* A member call x.foo() with a non-this/super receiver is flagged is_method so
* the resolver can suppress a weak short-name match (`re.test()` must not bind a
@@ -6300,6 +6379,7 @@ SUITE(extraction) {
RUN_TEST(extract_perl_method_call_flags_is_method);
RUN_TEST(extract_flag_exempt_method_call_not_flagged_is_method);
RUN_TEST(extract_python_member_call_flags_is_method);
RUN_TEST(extract_python_bare_call_flags_locally_bound_callee);
RUN_TEST(extract_ts_member_call_flags_is_method);
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
RUN_TEST(extract_js_member_call_flags_is_method);
+128
View File
@@ -4836,6 +4836,60 @@ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
PASS();
}
/* Python bare-call local-binding suppression, sequential path. The bare-call
* counterpart of the receiver guard above: `run` is a PARAMETER, so `run()`
* cannot be the module-level `run` and must not bind SatoriLive.run.
*
* The positive control is deliberately a CROSS-FILE bare call with no import,
* so it resolves by a weak short-name strategy — one this guard could have
* killed. Asserting a same-file (same_module) edge instead would prove nothing,
* because no guard in this codebase touches same_module for any input.
* Fewer than 50 files exercises pass_calls.c. */
TEST(pipeline_python_bare_local_binding_suppresses_weak_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_bare_seq_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "live.py",
"class SatoriLive:\n"
" def run(self):\n"
" return 1\n");
write_temp_file(tmp, "helpers.py",
"def compute_widget_total():\n"
" return 7\n");
write_temp_file(tmp, "gate.py",
"def _run_with_heavy_slot(run):\n"
" return run()\n"
"\n"
"def uses_free_function():\n"
" return compute_widget_total()\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/py_bare.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* NEGATIVE: the callee is shadowed by a parameter. */
ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "run"));
/* POSITIVE: an unshadowed cross-file bare call survives. */
ASSERT_TRUE(cross_file_call_exists(s, project, "uses_free_function", "compute_widget_total"));
/* Tripwire: a run that emitted no edges at all would satisfy the negative
* assertion vacuously. */
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* Parallel Python regression for #1276. The field-type heuristic capitalizes
* the receiver token and previously promoted accelerator.print() to
* MockAccelerator.print at 0.85; ordinary suffix matching also selected one
@@ -4933,6 +4987,78 @@ TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges) {
PASS();
}
/* Parallel counterpart. >= 50 files forces pass_parallel.c, which is wired with
* the same gate: a guard wired on only one resolver produces an edge on the
* sequential path and not the parallel one, breaking MT determinism. #1386
* wired both and tested only the sequential path, and the `parallel` suite is
* exactly what catches that. Same both-directions pin as the sequential test. */
TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_bare_par_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "live.py",
"class SatoriLive:\n"
" def run(self):\n"
" return 1\n"
"\n"
"class BatchJob:\n"
" def execute(self):\n"
" return 2\n");
write_temp_file(tmp, "helpers.py",
"def compute_widget_total():\n"
" return 7\n");
write_temp_file(tmp, "gate.py",
"def _run_with_heavy_slot(run, execute):\n"
" run()\n"
" return execute()\n"
"\n"
"def uses_free_function():\n"
" return compute_widget_total()\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "filler%d.py", i);
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/py_bare_par.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* NEGATIVE: both callees are shadowed by parameters. */
ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "run"));
ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "execute"));
/* POSITIVE: the unshadowed cross-file bare call survives the parallel path. */
ASSERT_TRUE(cross_file_call_exists(s, project, "uses_free_function", "compute_widget_total"));
/* Tripwire against a vacuous pass. */
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
/* Reproduce-first: pass_parallel's fused cross-LSP eligibility currently counts
* only parser-backed calls/call references. A Python binary operator has no
* parser CBMCall; its __add__ semantic record and carrier are created together
@@ -12807,6 +12933,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge);
RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier);
RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier);
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
+63
View File
@@ -853,6 +853,66 @@ TEST(dynamic_suppress_keeps_high_confidence_and_non_methods) {
PASS();
}
TEST(local_binding_suppress_drops_weak_shadowed_bare_calls) {
/* A bare `run()` whose callee is a parameter of an enclosing scope cannot be
* the module-level `run`, so a weak short-name match fabricates the edge. */
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "suffix_match"));
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "unique_name"));
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "field_type_hint"));
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "fuzzy"));
PASS();
}
TEST(local_binding_suppress_keeps_unshadowed_and_strong_strategies) {
/* THE RECALL PIN. A bare call to a genuine module-level function is NOT
* locally bound, so it is never suppressed — whatever the callee is spelled.
* This is the assertion a name-keyed guard (get/run/execute) would fail: it
* would drop these purely because of how the callee reads. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "suffix_match"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "unique_name"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "field_type_hint"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "fuzzy"));
/* Every receiver-/import-aware strategy is kept even when shadowed. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "same_module"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "import_map"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "import_map_suffix"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "qualified_suffix"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "callee_suffix"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "service_pattern"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "lsp_cross"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "lsp_py_method"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "lsp_direct"));
/* Languages outside the caller's gate are never affected. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(false, true, "suffix_match"));
/* No match (NULL/empty strategy) → nothing to suppress. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, NULL));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, ""));
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
* someone re-inlines one of the lists and edits only that copy, the two
* guards start disagreeing and this test catches it at the contract level
* rather than in a corpus months later. */
static const char *const strategies[] = {
"suffix_match", "unique_name", "field_type_hint", "fuzzy", "same_module",
"import_map", "import_map_suffix", "qualified_suffix", "callee_suffix",
"service_pattern", "lsp_cross", "lsp_ts_method", "lsp_py_method", "lsp_direct",
"", NULL};
for (int i = 0; strategies[i] != NULL; i++) {
bool member = cbm_suppress_weak_member_match(true, true, strategies[i]);
bool binding = cbm_suppress_weak_local_binding_call(true, true, strategies[i]);
if (member != binding) {
printf(" drop-list divergence on strategy \"%s\": member=%d binding=%d\n",
strategies[i], member, binding);
}
ASSERT_EQ(member, binding);
}
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
/* Method call THROUGH an imported symbol that is itself an indexed node
@@ -947,4 +1007,7 @@ SUITE(registry) {
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
RUN_TEST(local_binding_suppress_drops_weak_shadowed_bare_calls);
RUN_TEST(local_binding_suppress_keeps_unshadowed_and_strong_strategies);
RUN_TEST(weak_call_guards_share_one_drop_list);
}