mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
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:
co-authored by
Joseph-MingEn
parent
5d96377aa5
commit
95689b5c20
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user