Merge pull request #1912 from DeusData/feat/python-bare-local-binding

fix(python): suppress bare calls shadowed by an enclosing parameter
This commit is contained in:
Martin Vogel
2026-09-03 09:39:46 +02:00
committed by GitHub
11 changed files with 785 additions and 48 deletions
+4
View File
@@ -269,6 +269,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 {
+42
View File
@@ -2973,6 +2973,41 @@ 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. */
/* 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.
*
* The answer is CARRIED BY THE WALK rather than recomputed here. The unified
* walk binds a def's or lambda's parameters when it opens that scope and
* unwinds them when it closes it, so this is an O(1) map lookup. Deciding it
* by ascending the tree instead -- with ts_node_parent() or with a copied walk
* cursor -- costs O(depth) per call, and since every level of f(f(f(...))) is
* itself a bare call, that is quadratic across the file: the 30,000-deep
* fixture in tests/test_stack_overflow.c turned it into a hang rather than a
* slowdown. An O(1) lookup needs no hop cap, so unlike a capped walk this can
* no longer fail open on deep-but-ordinary code.
*
* It still answers false if the walk's own tracking hit an allocation failure,
* which costs a suppression and never a true edge. */
static bool python_callee_is_bound_parameter(CBMExtractCtx *ctx, WalkState *state,
TSNode callee_ident) {
const char *callee_name = cbm_node_text(ctx->arena, callee_ident, ctx->source);
return cbm_walk_python_param_is_bound(state, callee_name);
}
static bool is_objectscript_language(CBMLanguage language) {
return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
@@ -3597,11 +3632,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, state, fn);
}
}
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
+195
View File
@@ -119,11 +119,196 @@ static uint32_t add_lexical_scope(WalkState *state, TSNode node, CBMLexicalScope
return scope->id;
}
/* ── #1912: Python parameter bindings held live by the walk ───────────────────
*
* A bare Python call `run()` cannot honestly resolve to a project function by
* short name when `run` is bound as a parameter of an enclosing def or lambda:
* the parameter shadows any module-level `run` for the whole body, so the match
* is fabricated by construction.
*
* Deciding that per call by ASCENDING the tree is the trap this replaces. Both
* ts_node_parent() (O(depth) per hop) and a walk-cursor ascent (O(1) per hop,
* O(depth) per call) are per-call costs, and every level of f(f(f(...))) is
* itself a bare call, so the file-wide cost is quadratic or worse -- exactly
* what CBMWalkScope's comment records for the state it replaced. Scanning the
* frame stack has the same shape.
*
* So the walk carries the answer instead: a name -> active-count map, pushed
* when a Python function or lambda scope opens and unwound when it closes.
* Lookup is O(1), which is what removes the need for a hop cap.
*
* A COUNT, not a flag: `def outer(run): def inner(run):` binds one name twice,
* and leaving the inner scope must not unbind the outer one.
*
* Every failure path answers "not bound", which can only ever cost a
* suppression -- never a true edge. */
static uint32_t py_param_hash(const char *s) {
uint32_t h = 2166136261u; /* FNV-1a */
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
h = (h ^ *p) * 16777619u;
}
return h ? h : 1u; /* 0 marks an empty slot */
}
static CBMParamSlot *py_param_slot(WalkState *state, const char *name, uint32_t hash) {
int mask = state->py_param_slot_capacity - 1;
int i = (int)(hash & (uint32_t)mask);
for (;;) {
CBMParamSlot *slot = &state->py_param_slots[i];
if (slot->hash == 0) {
return slot; /* free slot -- caller decides whether to claim it */
}
if (slot->hash == hash && strcmp(slot->name, name) == 0) {
return slot;
}
i = (i + 1) & mask;
}
}
static bool py_param_grow(WalkState *state) {
int new_capacity = state->py_param_slot_capacity * PAIR_LEN;
if (new_capacity <= state->py_param_slot_capacity) {
return false;
}
CBMParamSlot *grown =
(CBMParamSlot *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memset(grown, 0, (size_t)new_capacity * sizeof(*grown));
CBMParamSlot *old = state->py_param_slots;
int old_capacity = state->py_param_slot_capacity;
state->py_param_slots = grown;
state->py_param_slot_capacity = new_capacity;
for (int i = 0; i < old_capacity; i++) {
if (old[i].hash != 0) {
*py_param_slot(state, old[i].name, old[i].hash) = old[i];
}
}
return true;
}
static bool py_param_stack_reserve(WalkState *state) {
if (state->py_param_stack_count < state->py_param_stack_capacity) {
return true;
}
int new_capacity = state->py_param_stack_capacity * PAIR_LEN;
if (new_capacity <= state->py_param_stack_capacity) {
return false;
}
const char **grown =
(const char **)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memcpy(grown, state->py_param_stack, (size_t)state->py_param_stack_count * sizeof(*grown));
state->py_param_stack = grown;
state->py_param_stack_capacity = new_capacity;
return true;
}
/* Bind one parameter name for the lifetime of the frame currently on top. */
static void py_param_bind(WalkState *state, const char *name) {
if (state->py_param_tracking_failed || !name || !name[0]) {
return;
}
/* Grow before inserting: the probe below must always find a free slot, and
* a table above ~70% load degrades toward linear probing. */
if ((state->py_param_slot_used + 1) * 10 >= state->py_param_slot_capacity * 7) {
if (!py_param_grow(state)) {
state->py_param_tracking_failed = true;
return;
}
}
if (!py_param_stack_reserve(state)) {
state->py_param_tracking_failed = true;
return;
}
uint32_t hash = py_param_hash(name);
CBMParamSlot *slot = py_param_slot(state, name, hash);
if (slot->hash == 0) {
slot->hash = hash;
slot->name = name;
slot->count = 0;
state->py_param_slot_used++;
}
slot->count++;
state->py_param_stack[state->py_param_stack_count++] = name;
}
/* Unwind to a frame's entry height, undoing exactly what that frame bound. */
static void py_param_unwind_to(WalkState *state, int base) {
if (state->py_param_tracking_failed) {
return;
}
while (state->py_param_stack_count > base) {
const char *name = state->py_param_stack[--state->py_param_stack_count];
CBMParamSlot *slot = py_param_slot(state, name, py_param_hash(name));
if (slot->hash != 0 && slot->count > 0) {
slot->count--;
}
}
}
bool cbm_walk_python_param_is_bound(const WalkState *state, const char *name) {
if (!state || !name || !name[0] || state->py_param_tracking_failed ||
state->py_param_slot_used == 0) {
return false;
}
const CBMParamSlot *slot = py_param_slot((WalkState *)state, name, py_param_hash(name));
return slot->hash != 0 && slot->count > 0;
}
/* The identifier a Python parameter node binds. Handles the bare, typed,
* defaulted, keyword-only, *args and **kwargs shapes. */
static const char *py_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;
}
/* Bind this node's parameters into the frame just pushed for it. Called after
* every push in push_boundary_scopes, so the top frame is this node's own and
* pop_expired_scopes unwinds them at exactly the right depth. */
static void py_bind_scope_parameters(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
if (ctx->language != CBM_LANG_PYTHON || state->scope_top == 0) {
return;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "function_definition") != 0 && strcmp(kind, "lambda") != 0) {
return;
}
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return;
}
uint32_t count = ts_node_named_child_count(params);
for (uint32_t i = 0; i < count; i++) {
py_param_bind(state, py_parameter_name(ctx, ts_node_named_child(params, i)));
}
}
static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
if (!ensure_scope_capacity(state)) {
return false;
}
CBMWalkScope *f = &state->scopes[state->scope_top];
f->prev_py_param_stack_count = state->py_param_stack_count;
f->kind = kind;
f->depth = depth;
f->qn = qn;
@@ -260,6 +445,7 @@ static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
state->inside_import = f->prev_inside_import;
state->loop_depth = f->prev_loop_depth;
state->branch_depth = f->prev_branch_depth;
py_param_unwind_to(state, f->prev_py_param_stack_count);
}
}
@@ -2354,6 +2540,7 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
}
push_lexical_boundary(node, state, depth);
py_bind_scope_parameters(ctx, node, state);
push_call_scope(state, depth, invocation);
if (is_actual_import_boundary(ctx, node, spec) && !is_export_of_declaration(node)) {
push_scope(state, SCOPE_IMPORT, depth, NULL);
@@ -2387,6 +2574,14 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
state.scope_capacity = MAX_SCOPES;
state.lexical_scopes = state.inline_lexical_scopes;
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
state.py_param_slots = state.inline_py_param_slots;
state.py_param_slot_capacity = INLINE_PY_PARAM_SLOTS;
state.py_param_slot_used = 0;
memset(state.inline_py_param_slots, 0, sizeof(state.inline_py_param_slots));
state.py_param_stack = state.inline_py_param_stack;
state.py_param_stack_capacity = INLINE_PY_PARAM_STACK;
state.py_param_stack_count = 0;
state.py_param_tracking_failed = false;
state.usage_start_index = ctx->result->usages.count;
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
/* Base walk-state tuple (previously established by the first
+46
View File
@@ -18,6 +18,8 @@
#define INLINE_LEXICAL_SCOPES 64
#define INLINE_LEXICAL_BINDINGS 64
#define INLINE_PYTHON_DIRECTIVES 16
#define INLINE_PY_PARAM_SLOTS 64
#define INLINE_PY_PARAM_STACK 64
// ObjectScript type map: variable name → class name (for instance_method_call
// resolution). Stack-allocated, per-method scope. Overflow is silent (no crash).
@@ -51,6 +53,15 @@ typedef struct {
bool raw_call_emitted;
} CBMInvocationDescriptor;
/* One name bound as a function/lambda parameter by a scope currently OPEN on
* the walk stack. A count, not a flag: `def outer(run): def inner(run):` binds
* the same name twice and the inner pop must not unbind the outer. */
typedef struct {
const char *name;
uint32_t hash;
int count;
} CBMParamSlot;
typedef struct {
const char *qn;
uint32_t depth;
@@ -73,6 +84,9 @@ typedef struct {
bool prev_inside_import;
int prev_loop_depth;
int prev_branch_depth;
/* #1912: py_param_stack height on entry. Pop unwinds back to it, so a
* frame unbinds exactly the parameters it bound and nothing else. */
int prev_py_param_stack_count;
} CBMWalkScope;
typedef enum {
@@ -158,11 +172,43 @@ typedef struct {
CBMPythonDirective inline_python_directives[INLINE_PYTHON_DIRECTIVES];
int python_directive_capacity;
int python_directive_count;
/* #1912 -- Python bare-call shadowing. A name bound as a parameter by ANY
* enclosing function or lambda shadows every project function, so a bare
* `run()` under `def outer(run)` cannot honestly resolve by short name.
*
* Maintained as a live name->count map pushed and popped BY THE WALK, not
* recomputed per call. Walking ancestors (either ts_node_parent or a
* cursor) and scanning the frame stack are both O(depth) per call, and
* since every level of f(f(f(...))) is itself a bare call that is
* quadratic across the file -- the same trap CBMWalkScope records above.
* Lookup here is O(1), so no hop cap and no fail-open cap are needed.
*
* The other binding table (CBMLexicalBinding) cannot serve this: it is
* qsort-ed in cbm_finalize_lexical_usages AFTER the walk and its
* active_end stays 0 until then, so its binary search is invalid from
* handle_calls, which runs mid-walk and before handle_usages. */
CBMParamSlot *py_param_slots;
CBMParamSlot inline_py_param_slots[INLINE_PY_PARAM_SLOTS];
int py_param_slot_capacity;
int py_param_slot_used;
const char **py_param_stack;
const char *inline_py_param_stack[INLINE_PY_PARAM_STACK];
int py_param_stack_capacity;
int py_param_stack_count;
/* Allocation failure: stop tracking and answer "not bound" forever after,
* which can only cost a suppression, never a true edge. */
bool py_param_tracking_failed;
CBMLanguage language;
os_type_map_t os_type_map; // ObjectScript variable → type mapping
} WalkState;
/* #1912: is `name` bound as a parameter by a Python function or lambda scope
* currently open on the walk stack? O(1). Answers false on any failure, so a
* caller can only ever lose a suppression, never a true edge. */
bool cbm_walk_python_param_is_bound(const WalkState *state, const char *name);
// Per-node handler prototypes. Each is called once per node during the
// unified cursor walk, replacing the old recursive walk_* functions.
CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
+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
@@ -2492,8 +2492,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;
+222 -36
View File
@@ -1277,19 +1277,18 @@ TEST(form_procedure) {
/* --- Oracle PL/SQL --- */
TEST(plsql_package_and_call) {
const char *src =
"CREATE OR REPLACE PACKAGE BODY emp_pkg AS\n"
" FUNCTION hire(p_name VARCHAR2) RETURN NUMBER IS\n"
" v_sal NUMBER;\n"
" BEGIN\n"
" v_sal := util_pkg.calc_salary(p_name);\n"
" IF v_sal > 0 THEN\n"
" RETURN v_sal;\n"
" END IF;\n"
" RAISE no_data_found;\n"
" END;\n"
"END emp_pkg;\n"
"/\n";
const char *src = "CREATE OR REPLACE PACKAGE BODY emp_pkg AS\n"
" FUNCTION hire(p_name VARCHAR2) RETURN NUMBER IS\n"
" v_sal NUMBER;\n"
" BEGIN\n"
" v_sal := util_pkg.calc_salary(p_name);\n"
" IF v_sal > 0 THEN\n"
" RETURN v_sal;\n"
" END IF;\n"
" RAISE no_data_found;\n"
" END;\n"
"END emp_pkg;\n"
"/\n";
CBMFileResult *r = extract(src, CBM_LANG_PLSQL, "t", "emp_pkg.pkb");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
@@ -2272,9 +2271,10 @@ TEST(dbt_source_and_two_arg_ref) {
/* Both dbt builtins name the relation in their LAST string argument:
* source('group','table') -> table, and the two-argument
* ref('package','model') form -> model. */
CBMFileResult *r = extract("SELECT * FROM {{ source('raw', 'customers') }}\n"
"UNION ALL SELECT * FROM {{ ref('analytics', 'legacy_customers') }}\n",
CBM_LANG_SQL, "t", "models/stg_customers.sql");
CBMFileResult *r =
extract("SELECT * FROM {{ source('raw', 'customers') }}\n"
"UNION ALL SELECT * FROM {{ ref('analytics', 'legacy_customers') }}\n",
CBM_LANG_SQL, "t", "models/stg_customers.sql");
ASSERT_NOT_NULL(r);
ASSERT(has_def(r, "Model", "stg_customers"));
ASSERT(has_usage(r, "customers"));
@@ -3346,8 +3346,8 @@ TEST(vue_embedded_structure_negative_controls_issue1410) {
}
TEST(vue_embedded_structure_host_controls_issue1410) {
CBMFileResult *plain = extract("function plainTs(): void { target(); }\n", CBM_LANG_TYPESCRIPT,
"t", "plain.ts");
CBMFileResult *plain =
extract("function plainTs(): void { target(); }\n", CBM_LANG_TYPESCRIPT, "t", "plain.ts");
ASSERT_NOT_NULL(plain);
ASSERT_FALSE(plain->has_error);
ASSERT_EQ(count_defs_named(plain, "Function", "plainTs"), 1);
@@ -3805,11 +3805,11 @@ TEST(extract_java_method_annotations_issue382) {
/* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */
TEST(arkts_component_struct) {
CBMFileResult *r = extract(
"@Entry\n@Component\nstruct Index {\n @State message: string = 'Hello'\n\n"
" build() {\n Column() {\n Text(this.message).fontSize(20)\n }\n"
" .width('100%')\n }\n}\n",
CBM_LANG_ARKTS, "t", "Index.ets");
CBMFileResult *r =
extract("@Entry\n@Component\nstruct Index {\n @State message: string = 'Hello'\n\n"
" build() {\n Column() {\n Text(this.message).fontSize(20)\n }\n"
" .width('100%')\n }\n}\n",
CBM_LANG_ARKTS, "t", "Index.ets");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Struct", "Index"));
@@ -3840,12 +3840,12 @@ TEST(arkts_exported_struct_decorators) {
}
TEST(arkts_member_decorators) {
CBMFileResult *r = extract(
"@Component\nstruct S {\n @State a: number = 0\n @Prop b: string\n"
" @Link c: boolean\n @Provide('k') d: string = ''\n @Consume('k') e: string\n"
" @StorageLink('s') f: number = 1\n @State @Watch('onW') g: boolean = false\n\n"
" build() {\n }\n}\n",
CBM_LANG_ARKTS, "t", "S.ets");
CBMFileResult *r =
extract("@Component\nstruct S {\n @State a: number = 0\n @Prop b: string\n"
" @Link c: boolean\n @Provide('k') d: string = ''\n @Consume('k') e: string\n"
" @StorageLink('s') f: number = 1\n @State @Watch('onW') g: boolean = false\n\n"
" build() {\n }\n}\n",
CBM_LANG_ARKTS, "t", "S.ets");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(decorators_contain(find_def_by_name(r, "a"), "State"));
@@ -3885,11 +3885,11 @@ TEST(arkts_no_phantom_builtin_defs) {
}
TEST(arkts_builder_extend_styles) {
CBMFileResult *r = extract(
"@Builder\nfunction card(t: string) {\n Column() {\n Text(t)\n }\n}\n\n"
"@Extend(Text)\nfunction fancy(size: number) {\n .fontSize(size)\n}\n\n"
"@Styles\nfunction pressed() {\n .backgroundColor('#eee')\n}\n",
CBM_LANG_ARKTS, "t", "b.ets");
CBMFileResult *r =
extract("@Builder\nfunction card(t: string) {\n Column() {\n Text(t)\n }\n}\n\n"
"@Extend(Text)\nfunction fancy(size: number) {\n .fontSize(size)\n}\n\n"
"@Styles\nfunction pressed() {\n .backgroundColor('#eee')\n}\n",
CBM_LANG_ARKTS, "t", "b.ets");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Function", "card"));
@@ -5006,6 +5006,190 @@ 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();
}
/* The bare-call flag is DEPTH-INDEPENDENT, and the binding is UNWOUND when its
* scope closes.
*
* Both properties come from the same design decision. The answer is carried by
* the unified walk -- parameters are bound when a def or lambda scope opens and
* unwound when it closes -- rather than recomputed per call by ascending the
* tree. An ascending walk is O(depth) per call, and every level of f(f(f(...)))
* is itself a bare call, so it is quadratic in a file's nesting depth; that hung
* stack_overflow_b's 30,000-deep fixture rather than merely slowing it. An
* earlier cut capped the ascent at 64 ancestors and FAILED OPEN past it, which
* silently stopped suppressing on deep-but-ordinary code.
*
* Pinned deterministically rather than by wall clock -- a timing assertion would
* be a lottery, not a gate. The depth case fails if a cap is reintroduced; the
* unwind case fails if a frame's bindings outlive its scope. */
TEST(extract_python_bare_call_flag_is_depth_independent) {
/* Shallow: return_statement / block / function_definition — 3 ancestors. */
CBMFileResult *shallow = extract("def shallow(handler):\n"
" return handler()\n",
CBM_LANG_PYTHON, "t", "s.py");
ASSERT_NOT_NULL(shallow);
ASSERT_FALSE(shallow->has_error);
int shallow_seen = 0;
for (int i = 0; i < shallow->calls.count; i++) {
const char *cn = shallow->calls.items[i].callee_name;
if (cn && strcmp(cn, "handler") == 0) {
shallow_seen++;
ASSERT_TRUE(shallow->calls.items[i].callee_is_locally_bound);
}
}
ASSERT_EQ(shallow_seen, 1);
cbm_free_result(shallow);
/* Deep: 200 parenthesized_expression ancestors separate the SAME call from
* its enclosing def. The parameter still shadows it, so it stays flagged --
* depth changes nothing. This is the case a 64-ancestor cap got wrong. */
const int PARENS = 200;
size_t sz = (size_t)PARENS * 2 + 128;
char *src = malloc(sz);
ASSERT_NOT_NULL(src);
char *w = src;
w += snprintf(w, sz, "def deep(handler):\n return ");
memset(w, '(', (size_t)PARENS);
w += PARENS;
w += snprintf(w, sz - (size_t)(w - src), "handler()");
memset(w, ')', (size_t)PARENS);
w += PARENS;
snprintf(w, sz - (size_t)(w - src), "\n");
CBMFileResult *deep = extract(src, CBM_LANG_PYTHON, "t", "d.py");
ASSERT_NOT_NULL(deep);
ASSERT_FALSE(deep->has_error);
int deep_seen = 0;
for (int i = 0; i < deep->calls.count; i++) {
const char *cn = deep->calls.items[i].callee_name;
if (cn && strcmp(cn, "handler") == 0) {
deep_seen++;
ASSERT_TRUE(deep->calls.items[i].callee_is_locally_bound);
}
}
ASSERT_EQ(deep_seen, 1);
cbm_free_result(deep);
free(src);
/* Unwind: `handler` is a parameter of shadowed() and a module-level function
* of the same name. The call INSIDE shadowed() is flagged; the call in
* sibling(), after that scope closed, must NOT be — it really does resolve
* to the module-level def. A binding that outlived its frame would flag it
* and destroy a true edge, which is the one direction this guard must never
* fail in. Nested same-name defs also pin the count: leaving the inner scope
* must not unbind the outer one. */
CBMFileResult *unwound = extract("def handler():\n"
" return 1\n"
"\n"
"def shadowed(handler):\n"
" def inner(handler):\n"
" return handler()\n"
" return inner(handler) or handler()\n"
"\n"
"def sibling():\n"
" return handler()\n",
CBM_LANG_PYTHON, "t", "u.py");
ASSERT_NOT_NULL(unwound);
ASSERT_FALSE(unwound->has_error);
int flagged = 0;
int unflagged = 0;
for (int i = 0; i < unwound->calls.count; i++) {
const char *cn = unwound->calls.items[i].callee_name;
if (!cn || strcmp(cn, "handler") != 0) {
continue;
}
if (unwound->calls.items[i].callee_is_locally_bound) {
flagged++;
} else {
unflagged++;
}
}
/* Two shadowed calls (inner body, and shadowed()'s own tail) and exactly one
* unshadowed call in sibling(). */
ASSERT_EQ(flagged, 2);
ASSERT_EQ(unflagged, 1);
cbm_free_result(unwound);
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
@@ -5414,8 +5598,8 @@ TEST(extract_c_test_dir_marks_is_test_issue1294) {
* not be (#1294). */
TEST(extract_python_method_test_dir_marks_is_test_issue1294) {
const char *src = "class Foo:\n"
" def helper(self):\n"
" pass\n";
" def helper(self):\n"
" pass\n";
/* Python's LSP layer injects synthetic builtin stub Methods (str.upper,
* dict.get, ...) into defs.items alongside real ones (py_builtins.c), so
@@ -6675,6 +6859,8 @@ 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_python_bare_call_flag_is_depth_independent);
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
@@ -5102,6 +5102,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
@@ -5199,6 +5253,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
@@ -13141,6 +13267,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
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_arg_url_rejects_non_http_slash_arguments);
+74
View File
@@ -902,6 +902,77 @@ 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
@@ -998,4 +1069,7 @@ SUITE(registry) {
RUN_TEST(go_bare_ref_never_binds_field);
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);
}