mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
fix(pipeline): suppress weak Python member calls (distilled from #1324)
Python attribute calls were not classified as member calls, so weak short-name strategies (suffix_match / unique_name / field_type_hint) could bind `obj.method()` to an arbitrary same-named project symbol — `accelerator.print()` resolving to MockAccelerator.print. Flag a Python attribute call is_method when its receiver is unresolved, and extend the existing TS/JS weak-member guard to Python. Receivers that ARE known stay exempt: self/cls, super(), and any identifier bound by one of the file's imports (including the root of an attribute chain), so Python's canonical `module.function()` cross-file call still resolves. The guard helper is renamed cbm_tsjs_suppress_weak_method_match -> cbm_suppress_weak_member_match. Its drop-list is unchanged, and the per-language gate stays at the two call sites, which enumerate the same language set in pass_calls.c and pass_parallel.c so the sequential and parallel resolvers cannot diverge. ArkTS remains in that set. python/S6/inherited_method in the resolution probe now asserts calls == 0, recording the same trade TypeScript took when the receiver-aware guard landed: the edge it used to produce came from a unique_name registry fallback (measured on main: strategy=unique_name, cands=1, conf=0.7500) where a weak short-name guess happened to be right in a 2-file fixture. It flips back to >= 1 once py_lsp_cross resolves inheritance, matching the flip-back note already carried on the TypeScript S6 case. Fixes #1276 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> Co-authored-by: Enferlain <15861396+Enferlain@users.noreply.github.com>
This commit is contained in:
+6
-2
@@ -259,9 +259,13 @@ typedef struct {
|
||||
uint32_t site_start_byte; // exact AST occurrence span; end > start when present
|
||||
uint32_t site_end_byte; // exclusive byte offset in the source file
|
||||
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
|
||||
bool is_method; // method/member call with a non-self receiver. Perl:
|
||||
bool is_method; // method/member call with an UNRESOLVED receiver. Perl:
|
||||
// arrow/method call ($obj->m). TS/JS/TSX: member call
|
||||
// x.foo() whose receiver is not this/super. Default false.
|
||||
// x.foo() whose receiver is not this/super. Python:
|
||||
// x.foo() where x is not self/cls/super() and is not
|
||||
// rooted in an imported name. Read by the weak-member
|
||||
// guard and by the pxc synthetic-carrier dedup key in
|
||||
// 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.
|
||||
} CBMCall;
|
||||
|
||||
@@ -2740,6 +2740,66 @@ static char *resolve_objectscript_instance_call(CBMArena *a, TSNode node, const
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* True when a Python attribute-call receiver is EXEMPT from the weak-member
|
||||
* guard (#1276). Three exemptions, each because the receiver is in fact known:
|
||||
* - `self.x()` / `cls.x()` — a direct self/cls receiver keeps class-local
|
||||
* semantics; the enclosing class is the right namespace for a weak match.
|
||||
* - `super().x()` — same, via the base class.
|
||||
* - `helper.compute()` — an identifier bound by one of THIS file's
|
||||
* imports, including the root of an attribute chain (`pkg.sub.fn()`).
|
||||
* module.function() is Python's canonical cross-file call shape and the
|
||||
* import map resolves it; flagging it would kill the true edge.
|
||||
* Everything else — a parameter, a local, an attribute of self — has no
|
||||
* statically-known type here, so the call must not bind by short name alone.
|
||||
* Note `self.client.send()` is NOT exempt: the receiver is `self.client`, an
|
||||
* attribute of unknown type, not `self` itself. */
|
||||
static bool python_receiver_is_exempt(CBMExtractCtx *ctx, TSNode receiver) {
|
||||
if (ts_node_is_null(receiver)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* super().m() — the receiver is a call node whose function is `super`. */
|
||||
if (strcmp(ts_node_type(receiver), "call") == 0) {
|
||||
TSNode fn = ts_node_child_by_field_name(receiver, TS_FIELD("function"));
|
||||
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "identifier") == 0) {
|
||||
char *name = cbm_node_text(ctx->arena, fn, ctx->source);
|
||||
return name && strcmp(name, "super") == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Walk an attribute chain down to its root identifier: for `pkg.sub.fn()`
|
||||
* the receiver is `pkg.sub`, whose root is `pkg` — the name an import binds. */
|
||||
bool direct_identifier = strcmp(ts_node_type(receiver), "identifier") == 0;
|
||||
TSNode root = receiver;
|
||||
while (!ts_node_is_null(root) && strcmp(ts_node_type(root), "attribute") == 0) {
|
||||
root = ts_node_child_by_field_name(root, TS_FIELD("object"));
|
||||
}
|
||||
if (ts_node_is_null(root) || strcmp(ts_node_type(root), "identifier") != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char *name = cbm_node_text(ctx->arena, root, ctx->source);
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
/* self/cls only as a DIRECT receiver: `self.m()` is class-local, but
|
||||
* `self.client.m()` has receiver `self.client` of unknown type. */
|
||||
if (direct_identifier && (strcmp(name, "self") == 0 || strcmp(name, "cls") == 0)) {
|
||||
return true;
|
||||
}
|
||||
/* Import-bound root, incl. aliases (`import tools as toolkit` binds
|
||||
* local_name "toolkit"). Per-file scan: imports.count is a file-local
|
||||
* number (tens), never the corpus, so this stays O(file), not O(corpus). */
|
||||
for (int i = 0; i < ctx->result->imports.count; i++) {
|
||||
const char *local_name = ctx->result->imports.items[i].local_name;
|
||||
if (local_name && strcmp(local_name, name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_objectscript_language(CBMLanguage language) {
|
||||
return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE;
|
||||
}
|
||||
@@ -3357,6 +3417,20 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
|
||||
strcmp(ts_node_type(node), "method_call_expression") == 0) {
|
||||
call.is_method = true;
|
||||
}
|
||||
// Python receiver-aware guard (#1276; same intent as the Perl and
|
||||
// TS/JS flags). Flag an attribute call x.foo() whose receiver is not
|
||||
// self/cls/super() and is not rooted in an imported name, so the
|
||||
// call-resolution pass can suppress weak short-name matches for it
|
||||
// (`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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
|
||||
// as the Perl flag above). Flag a member call x.foo() whose receiver
|
||||
// is NOT `this`/`super`. When the TS-LSP cannot resolve the receiver
|
||||
|
||||
+24
-15
@@ -606,20 +606,29 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* TS/JS/TSX weak-method suppression (#592/#606). A member call x.foo() only
|
||||
* reaches the registry when the TS-LSP could not resolve the receiver type
|
||||
* (the LSP block above already returned for type-resolved calls, including
|
||||
* the "resolved but target out of gbuf" fall-through). Binding such a call
|
||||
* by a weak short-name strategy fabricates an edge (`re.test()` -> a project
|
||||
* `test`). Rather than drop it here — which would also skip the service
|
||||
* bypasses below and emit_classified_edge's route/HTTP/CONFIG branches —
|
||||
* defer to emit_classified_edge and suppress ONLY the plain-CALLS
|
||||
* fall-through, so every service edge stays main-identical. res.strategy may
|
||||
* be lsp_* here; the helper's explicit drop-list leaves lsp_* untouched. */
|
||||
bool is_tsjs = lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT ||
|
||||
lang == CBM_LANG_TSX || lang == CBM_LANG_ARKTS;
|
||||
bool tsjs_drop_plain_call =
|
||||
cbm_tsjs_suppress_weak_method_match(is_tsjs, call->is_method, res.strategy);
|
||||
/* Dynamic-language weak-member suppression (#592/#606/#1276). A member call
|
||||
* x.foo() only reaches the registry when the language's LSP could not
|
||||
* resolve the receiver type (the LSP block above already returned for
|
||||
* type-resolved calls, including the "resolved but target out of gbuf"
|
||||
* fall-through). Binding such a call by a weak short-name strategy
|
||||
* fabricates an edge (`re.test()` -> a project `test`,
|
||||
* `accelerator.print()` -> MockAccelerator.print). Rather than drop it here
|
||||
* — which would also skip the service bypasses below and
|
||||
* emit_classified_edge's route/HTTP/CONFIG branches — defer to
|
||||
* emit_classified_edge and suppress ONLY the plain-CALLS fall-through, so
|
||||
* every service edge stays main-identical. res.strategy may be lsp_* here;
|
||||
* the helper's explicit drop-list leaves lsp_* untouched.
|
||||
*
|
||||
* This language set MUST match the one in pass_parallel.c exactly — a
|
||||
* language gated on only one resolver produces an edge on the sequential
|
||||
* path and not the parallel one (or vice versa), breaking MT determinism.
|
||||
* ArkTS belongs to the JS/TS family here (#1842); dropping it would
|
||||
* reintroduce the #592/#606 false-edge class for .ets files. */
|
||||
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
|
||||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
|
||||
lang == CBM_LANG_ARKTS;
|
||||
bool drop_plain_call =
|
||||
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, 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
|
||||
@@ -652,7 +661,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
return 0;
|
||||
}
|
||||
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals,
|
||||
imp_count, tsjs_drop_plain_call);
|
||||
imp_count, drop_plain_call);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -2465,20 +2465,25 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
continue;
|
||||
}
|
||||
|
||||
/* TS/JS/TSX weak-method suppression (#592/#606). The receiver-aware guard
|
||||
* must NOT drop this call here: doing so would also skip the #523
|
||||
* callee-name service bypass below, emit_service_edge's route/gRPC/config
|
||||
* branches, and its unconditional detect_url_in_args (which classifies
|
||||
* verb-suffix HTTP clients like api.patch('/x')). Instead, defer to the
|
||||
* emit path and suppress ONLY the plain-CALLS fall-through
|
||||
* (emit_normal_calls_edge), so every service edge stays main-identical by
|
||||
* construction. res.strategy may carry an lsp_* value here (LSP-resolved
|
||||
* calls keep res through this point); the helper's EXPLICIT drop-list
|
||||
* leaves lsp_ts_method / lsp_cross untouched. See #606 direction. */
|
||||
bool is_tsjs = lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT ||
|
||||
lang == CBM_LANG_TSX || lang == CBM_LANG_ARKTS;
|
||||
bool tsjs_drop_plain_call =
|
||||
cbm_tsjs_suppress_weak_method_match(is_tsjs, call->is_method, res.strategy);
|
||||
/* Dynamic-language weak-member suppression (#592/#606/#1276). The
|
||||
* receiver-aware guard must NOT drop this call here: doing so would also
|
||||
* skip the #523 callee-name service bypass below, emit_service_edge's
|
||||
* route/gRPC/config branches, and its unconditional detect_url_in_args
|
||||
* (which classifies verb-suffix HTTP clients like api.patch('/x')).
|
||||
* Instead, defer to the emit path and suppress ONLY the plain-CALLS
|
||||
* fall-through (emit_normal_calls_edge), so every service edge stays
|
||||
* main-identical by construction. res.strategy may carry an lsp_* value
|
||||
* here (LSP-resolved calls keep res through this point); the helper's
|
||||
* EXPLICIT drop-list leaves lsp_ts_method / lsp_cross untouched. See
|
||||
* #606 direction.
|
||||
*
|
||||
* This language set MUST match the one in pass_calls.c exactly — see the
|
||||
* note there. ArkTS belongs to the JS/TS family (#1842). */
|
||||
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
|
||||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
|
||||
lang == CBM_LANG_ARKTS;
|
||||
bool drop_plain_call =
|
||||
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
|
||||
|
||||
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
|
||||
* service signal lives in the callee_name. The registry can mis-resolve
|
||||
@@ -2573,7 +2578,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
_rc_t0 = extract_now_ns();
|
||||
emit_service_edge(ws->local_edge_buf, source_node, target_node, call, &res, module_qn,
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count,
|
||||
tsjs_drop_plain_call);
|
||||
drop_plain_call);
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_emit, extract_now_ns() - _rc_t0,
|
||||
memory_order_relaxed);
|
||||
ws->calls_resolved++;
|
||||
|
||||
@@ -269,13 +269,16 @@ bool cbm_perl_is_builtin(const char *name);
|
||||
bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *callee_name,
|
||||
const char *strategy);
|
||||
|
||||
/* Decide whether a resolved TS/JS/TSX member-call edge is weak-strategy noise to
|
||||
* drop (#592/#606): true only for TS/JS, only for a member call with a
|
||||
* non-this/super receiver (is_method), and only when the match used a weak
|
||||
* short-name strategy (suffix_match / unique_name / field_type_hint / fuzzy).
|
||||
/* Decide whether a resolved member-call edge is weak-strategy noise to drop
|
||||
* (#592/#606/#1276): true only when the CALLER's per-language gate says the
|
||||
* guard applies (`enabled`), only for a member call with an unresolved receiver
|
||||
* (is_method), and only when the match used a weak short-name strategy
|
||||
* (suffix_match / unique_name / field_type_hint / fuzzy).
|
||||
* Explicit drop-list keeps every lsp_* / import / same-module / qualified match.
|
||||
* 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_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const char *strategy);
|
||||
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, 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
|
||||
|
||||
+21
-13
@@ -429,19 +429,27 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
|
||||
return true; /* weak short-name match (suffix_match / unique_name / …) → drop */
|
||||
}
|
||||
|
||||
/* TS/JS analogue of the Perl guard above (#592/#606 direction; precedent #477).
|
||||
* A member call `x.foo()` reaches the weak textual cascade ONLY when the TS-LSP
|
||||
* could not resolve the receiver type — type-resolved calls win via lsp_*
|
||||
* strategies before the registry runs. Binding such a call to a project symbol
|
||||
* by a weak short-name strategy fabricates a CALLS edge (`re.test()` ->
|
||||
* SalesforceRestClient.test, `date.toISOString()` -> any project toISOString).
|
||||
* Drop ONLY the weak strategies; keep import/same-module/qualified-tail matches
|
||||
* and every lsp_* strategy. Uses an EXPLICIT drop-list (not keep-list +
|
||||
* default-drop) because the parallel resolver runs lsp_* strategies through the
|
||||
* same guard variable — a default-drop would silently kill lsp_ts_method. Pure
|
||||
* + side-effect-free so the contract is unit-testable without a full pipeline. */
|
||||
bool cbm_tsjs_suppress_weak_method_match(bool is_tsjs, bool is_method, const char *strategy) {
|
||||
if (!is_tsjs || !is_method || !strategy || !strategy[0]) {
|
||||
/* Dynamic-language analogue of the Perl guard above (#592/#606/#1276
|
||||
* direction; precedent #477). A member call `x.foo()` reaches the weak textual
|
||||
* cascade ONLY when the language's LSP could not resolve the receiver type —
|
||||
* type-resolved calls win via lsp_* strategies before the registry runs.
|
||||
* Binding such a call to a project symbol by a weak short-name strategy
|
||||
* fabricates a CALLS edge (`re.test()` -> SalesforceRestClient.test,
|
||||
* `accelerator.print()` -> MockAccelerator.print). Drop ONLY the weak
|
||||
* strategies; keep import/same-module/qualified-tail matches and every lsp_*
|
||||
* strategy. Uses an EXPLICIT drop-list (not keep-list + default-drop) because
|
||||
* the parallel resolver runs lsp_* strategies through the same guard variable —
|
||||
* a default-drop would silently kill lsp_ts_method. Pure + side-effect-free so
|
||||
* the contract is unit-testable without a full pipeline.
|
||||
*
|
||||
* `enabled` is the CALLER's per-language gate, deliberately kept OUT of this
|
||||
* helper: the guard applies only to the language set each call site enumerates
|
||||
* (today Python plus the JS/TS family including ArkTS). Widening it is a
|
||||
* 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]) {
|
||||
return false;
|
||||
}
|
||||
/* Weak short-name strategies that actually reach the call-resolution guards:
|
||||
|
||||
@@ -4552,6 +4552,75 @@ TEST(extract_flag_exempt_method_call_not_flagged_is_method) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Python receiver-aware flag (#1276; same intent as the Perl and TS/JS flags).
|
||||
* Pins BOTH directions: an unknown receiver (a parameter, or an attribute of
|
||||
* self) IS flagged so the resolver can suppress a weak short-name match, while
|
||||
* self/cls/super() and import-bound receivers — Python's canonical cross-file
|
||||
* call shape — are NOT, so their true edges survive. */
|
||||
TEST(extract_python_member_call_flags_is_method) {
|
||||
CBMFileResult *r = extract("from pkg import helper\n"
|
||||
"import tools as toolkit\n"
|
||||
"\n"
|
||||
"class C(Base):\n"
|
||||
" def run(self, external):\n"
|
||||
" external.commit()\n"
|
||||
" self.client.send()\n"
|
||||
" self.helper()\n"
|
||||
" super ( ).render()\n"
|
||||
" helper.compute()\n"
|
||||
" toolkit.format()\n"
|
||||
" helper()\n",
|
||||
CBM_LANG_PYTHON, "t", "x.py");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
int external = 0;
|
||||
int nested = 0;
|
||||
int self_call = 0;
|
||||
int super_call = 0;
|
||||
int imported_module = 0;
|
||||
int imported_alias = 0;
|
||||
int bare = 0;
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const char *cn = r->calls.items[i].callee_name;
|
||||
if (strcmp(cn, "external.commit") == 0) {
|
||||
/* parameter receiver — unknown type */
|
||||
external++;
|
||||
ASSERT_TRUE(r->calls.items[i].is_method);
|
||||
} else if (strcmp(cn, "self.client.send") == 0) {
|
||||
/* receiver is `self.client`, an attribute of unknown type — NOT self */
|
||||
nested++;
|
||||
ASSERT_TRUE(r->calls.items[i].is_method);
|
||||
} else if (strcmp(cn, "self.helper") == 0) {
|
||||
self_call++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
} else if (strstr(cn, "render") != NULL) {
|
||||
super_call++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
} else if (strcmp(cn, "helper.compute") == 0) {
|
||||
imported_module++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
} else if (strcmp(cn, "toolkit.format") == 0) {
|
||||
/* aliased import: local_name is "toolkit" */
|
||||
imported_alias++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
} else if (strcmp(cn, "helper") == 0) {
|
||||
bare++;
|
||||
ASSERT_FALSE(r->calls.items[i].is_method);
|
||||
}
|
||||
}
|
||||
/* Each shape must appear exactly once, so a missed extraction cannot make
|
||||
* the loop above pass vacuously. */
|
||||
ASSERT_EQ(external, 1);
|
||||
ASSERT_EQ(nested, 1);
|
||||
ASSERT_EQ(self_call, 1);
|
||||
ASSERT_EQ(super_call, 1);
|
||||
ASSERT_EQ(imported_module, 1);
|
||||
ASSERT_EQ(imported_alias, 1);
|
||||
ASSERT_EQ(bare, 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
|
||||
@@ -6230,6 +6299,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_perl_builtin_call_is_function_not_method);
|
||||
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_ts_member_call_flags_is_method);
|
||||
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
|
||||
RUN_TEST(extract_js_member_call_flags_is_method);
|
||||
|
||||
@@ -952,11 +952,27 @@ TEST(lrp_python_s6_inherited_method) {
|
||||
{"child.py", "from .base import Base\n\n\nclass Child(Base):\n"
|
||||
" def extra(self):\n return 'extra'\n\n\n"
|
||||
"def run(c):\n return c.describe()\n"}};
|
||||
/* Uncertain: c.describe() on a Child — py_lsp_cross must see Child inherits Base
|
||||
* (requires INHERITS edge resolution). Given the Python extraction bug for
|
||||
* base_classes, this may be RED end-to-end even if py_lsp_cross is correct.
|
||||
* Assert the correct outcome; RED if extraction bug blocks resolution. */
|
||||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S6/inherited_method", 0));
|
||||
/* RED: c.describe() needs py_lsp_cross to walk `class Child(Base)`. Exactly
|
||||
* the TS/S6 situation above, and resolved the same way (#1276).
|
||||
* Until the receiver-aware guard landed, this scenario passed via a
|
||||
* unique_name registry fallback — MEASURED on main before the guard:
|
||||
* strategy=unique_name, cands=1, conf=0.7500. "describe" is the only symbol
|
||||
* of that name in this 2-file fixture, so a weak short-name guess happened
|
||||
* to be right. In a real repo the same guess binds an arbitrary same-named
|
||||
* method (the false edges #1276 targets: accelerator.print() ->
|
||||
* MockAccelerator.print), so the guard now suppresses weak member-call
|
||||
* matches whose receiver is unresolved — here `c`, a bare parameter.
|
||||
* c.describe() is the ONLY call in the fixture, so a correctly-suppressed
|
||||
* run yields exactly zero CALLS.
|
||||
* Tripwire: assert the store opened AND calls == 0 exactly, so an infra/DB
|
||||
* failure cannot pass vacuously; flip to ASSERT calls >= 1 once py_lsp_cross
|
||||
* resolves inheritance (lsp_py_*, a strategy the guard keeps). */
|
||||
LRP_Proj lp;
|
||||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int calls = cbm_store_count_edges_by_type(store, lp.project, "CALLS");
|
||||
lrp_cleanup(&lp, store);
|
||||
ASSERT_EQ(calls, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1776,7 +1792,8 @@ SUITE(lsp_resolution_probe) {
|
||||
RUN_TEST(lrp_rust_s8_field_call);
|
||||
RUN_TEST(lrp_rust_crossfile_macro_hidden_call_carrier);
|
||||
|
||||
/* ── Python (lsp_cross WIRED) — S1–S5,S7,S8 GREEN; S6 uncertain (extraction bug) ── */
|
||||
/* ── Python (lsp_cross WIRED) — S1–S5,S7,S8 GREEN; S6 suppressed by the
|
||||
* weak-member guard (#1276), asserted == 0 until py_lsp_cross inherits ── */
|
||||
RUN_TEST(lrp_python_s1_crossfile_call);
|
||||
RUN_TEST(lrp_python_s2_method_dispatch);
|
||||
RUN_TEST(lrp_python_s3_constructor);
|
||||
|
||||
@@ -4627,6 +4627,67 @@ TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Python counterpart to the TS/JS receiver guard (#1276), sequential path.
|
||||
* Pins BOTH directions. NEGATIVE: an attribute call on an unknown receiver
|
||||
* (a parameter) must not bind the lone same-named project method through
|
||||
* unique_name. POSITIVE: an import-bound receiver (helper.compute) and a bare
|
||||
* local call must still produce their CALLS edges.
|
||||
* Fewer than 50 files exercises pass_calls.c. */
|
||||
TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_python_recv_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "worker.py",
|
||||
"class Worker:\n"
|
||||
" def backward(self):\n"
|
||||
" return 1\n");
|
||||
write_temp_file(tmp, "pkg/__init__.py", "from .helper import compute\n");
|
||||
write_temp_file(tmp, "pkg/helper.py",
|
||||
"def compute(value):\n"
|
||||
" return value * 2\n");
|
||||
write_temp_file(tmp, "caller.py",
|
||||
"from pkg import helper\n"
|
||||
"\n"
|
||||
"def external_call(accelerator):\n"
|
||||
" return accelerator.backward()\n"
|
||||
"\n"
|
||||
"def imported_call():\n"
|
||||
" return helper.compute(42)\n"
|
||||
"\n"
|
||||
"def local_helper():\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
"def bare_call():\n"
|
||||
" return local_helper()\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/python_recv.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: `accelerator` is a parameter — Worker.backward must not bind. */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "external_call", "backward"));
|
||||
/* POSITIVE: import-bound receiver and bare local call both survive. */
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "imported_call", "compute"));
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "bare_call", "local_helper"));
|
||||
/* 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();
|
||||
}
|
||||
|
||||
/* Count nodes with the given exact name in the project (e.g. a Route path). */
|
||||
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) {
|
||||
cbm_node_t *ns = NULL;
|
||||
@@ -4775,6 +4836,103 @@ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
|
||||
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
|
||||
* arbitrary backward()/step() implementation. All are unresolved-receiver
|
||||
* calls and must remain absent, while the import-bound call and the bare local
|
||||
* call remain — the same both-directions pin as the sequential test above.
|
||||
* >= 50 files forces pass_parallel.c and try_field_type_hint(), so this is also
|
||||
* the guard against a sequential/parallel divergence. */
|
||||
TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_python_par_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "targets.py",
|
||||
"class MockAccelerator:\n"
|
||||
" def print(self, value):\n"
|
||||
" return value\n"
|
||||
"\n"
|
||||
"class OtherPrinter:\n"
|
||||
" def print(self, value):\n"
|
||||
" return value\n"
|
||||
"\n"
|
||||
"class FlashAttentionFunction:\n"
|
||||
" def backward(self, loss):\n"
|
||||
" return loss\n"
|
||||
"\n"
|
||||
"class OtherBackward:\n"
|
||||
" def backward(self, loss):\n"
|
||||
" return loss\n"
|
||||
"\n"
|
||||
"class EulerScheduler:\n"
|
||||
" def step(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
"class OtherScheduler:\n"
|
||||
" def step(self):\n"
|
||||
" return 1\n");
|
||||
write_temp_file(tmp, "pkg/__init__.py", "from .helper import compute\n");
|
||||
write_temp_file(tmp, "pkg/helper.py",
|
||||
"def compute(value):\n"
|
||||
" return value * 2\n");
|
||||
write_temp_file(tmp, "caller.py",
|
||||
"from pkg import helper\n"
|
||||
"\n"
|
||||
"def local_helper():\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
"def train(accelerator, trainer):\n"
|
||||
" accelerator.print('hello')\n"
|
||||
" accelerator.backward(1)\n"
|
||||
" trainer.lr_scheduler.step()\n"
|
||||
" helper.compute(42)\n"
|
||||
" return local_helper()\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/python_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: every receiver here is a parameter or an attribute of one. */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "train", "print"));
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "train", "backward"));
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "train", "step"));
|
||||
/* POSITIVE: import-bound and bare local calls survive the parallel path too. */
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "train", "compute"));
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "train", "local_helper"));
|
||||
|
||||
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
|
||||
@@ -12646,7 +12804,9 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_incremental_parallel_result_cache_alloc_failure_preserves_db_and_retries);
|
||||
#endif
|
||||
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
|
||||
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_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);
|
||||
|
||||
+24
-24
@@ -811,21 +811,21 @@ TEST(cross_language_suffix_match_drops_py_vs_js) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tsjs_suppress_drops_weak_method_matches) {
|
||||
/* #592/#606: a TS/JS member call whose receiver the LSP could not type, that
|
||||
TEST(dynamic_suppress_drops_weak_method_matches) {
|
||||
/* #592/#606/#1276: a member call whose receiver the LSP could not type, that
|
||||
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
|
||||
* The strategies that actually reach the guards are the registry's
|
||||
* suffix_match / unique_name and the parallel field_type_hint; "fuzzy" is
|
||||
* covered defensively (cbm_registry_fuzzy_resolve is not wired into the
|
||||
* resolvers today) so a future wiring cannot silently reintroduce it. */
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "suffix_match"));
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "unique_name"));
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "field_type_hint"));
|
||||
ASSERT_TRUE(cbm_tsjs_suppress_weak_method_match(true, true, "fuzzy"));
|
||||
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, "suffix_match"));
|
||||
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, "unique_name"));
|
||||
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, "field_type_hint"));
|
||||
ASSERT_TRUE(cbm_suppress_weak_member_match(true, true, "fuzzy"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tsjs_suppress_keeps_high_confidence_and_non_methods) {
|
||||
TEST(dynamic_suppress_keeps_high_confidence_and_non_methods) {
|
||||
/* Keep every receiver-/import-aware strategy. Because the PARALLEL resolver
|
||||
* runs lsp_* strategies through this same guard variable, an explicit
|
||||
* drop-list must never touch them — asserting the lsp_* keeps here is the
|
||||
@@ -833,23 +833,23 @@ TEST(tsjs_suppress_keeps_high_confidence_and_non_methods) {
|
||||
* enumerates the resolver's non-weak strategies: registry
|
||||
* {import_map, import_map_suffix, same_module, qualified_suffix}, parallel
|
||||
* {callee_suffix, service_pattern}, and lsp_*. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "same_module"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "import_map"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "import_map_suffix"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "qualified_suffix"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "callee_suffix"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "service_pattern"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "lsp_ts_method"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "lsp_cross"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, "lsp_ts_local"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "same_module"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "import_map"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "import_map_suffix"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "qualified_suffix"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "callee_suffix"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "service_pattern"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "lsp_ts_method"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "lsp_cross"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, "lsp_ts_local"));
|
||||
/* A bare call (is_method=false) is a free-function call → never suppressed. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, false, "unique_name"));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, false, "suffix_match"));
|
||||
/* Non-TS/JS languages are never affected. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(false, true, "suffix_match"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, false, "unique_name"));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, false, "suffix_match"));
|
||||
/* Languages outside the caller's guard set are never affected. */
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(false, true, "suffix_match"));
|
||||
/* No match (NULL/empty strategy) → nothing to suppress. */
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, NULL));
|
||||
ASSERT_FALSE(cbm_tsjs_suppress_weak_method_match(true, true, ""));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_weak_member_match(true, true, ""));
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -945,6 +945,6 @@ SUITE(registry) {
|
||||
RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches);
|
||||
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
|
||||
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
|
||||
RUN_TEST(tsjs_suppress_drops_weak_method_matches);
|
||||
RUN_TEST(tsjs_suppress_keeps_high_confidence_and_non_methods);
|
||||
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
|
||||
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user