@@ -2740,6 +2740,66 @@ static char *resolve_objectscript_instance_call(CBMArena *a, TSNode node, const
27402740 return NULL ;
27412741}
27422742
2743+ /* True when a Python attribute-call receiver is EXEMPT from the weak-member
2744+ * guard (#1276). Three exemptions, each because the receiver is in fact known:
2745+ * - `self.x()` / `cls.x()` — a direct self/cls receiver keeps class-local
2746+ * semantics; the enclosing class is the right namespace for a weak match.
2747+ * - `super().x()` — same, via the base class.
2748+ * - `helper.compute()` — an identifier bound by one of THIS file's
2749+ * imports, including the root of an attribute chain (`pkg.sub.fn()`).
2750+ * module.function() is Python's canonical cross-file call shape and the
2751+ * import map resolves it; flagging it would kill the true edge.
2752+ * Everything else — a parameter, a local, an attribute of self — has no
2753+ * statically-known type here, so the call must not bind by short name alone.
2754+ * Note `self.client.send()` is NOT exempt: the receiver is `self.client`, an
2755+ * attribute of unknown type, not `self` itself. */
2756+ static bool python_receiver_is_exempt (CBMExtractCtx * ctx , TSNode receiver ) {
2757+ if (ts_node_is_null (receiver )) {
2758+ return false;
2759+ }
2760+
2761+ /* super().m() — the receiver is a call node whose function is `super`. */
2762+ if (strcmp (ts_node_type (receiver ), "call" ) == 0 ) {
2763+ TSNode fn = ts_node_child_by_field_name (receiver , TS_FIELD ("function" ));
2764+ if (!ts_node_is_null (fn ) && strcmp (ts_node_type (fn ), "identifier" ) == 0 ) {
2765+ char * name = cbm_node_text (ctx -> arena , fn , ctx -> source );
2766+ return name && strcmp (name , "super" ) == 0 ;
2767+ }
2768+ return false;
2769+ }
2770+
2771+ /* Walk an attribute chain down to its root identifier: for `pkg.sub.fn()`
2772+ * the receiver is `pkg.sub`, whose root is `pkg` — the name an import binds. */
2773+ bool direct_identifier = strcmp (ts_node_type (receiver ), "identifier" ) == 0 ;
2774+ TSNode root = receiver ;
2775+ while (!ts_node_is_null (root ) && strcmp (ts_node_type (root ), "attribute" ) == 0 ) {
2776+ root = ts_node_child_by_field_name (root , TS_FIELD ("object" ));
2777+ }
2778+ if (ts_node_is_null (root ) || strcmp (ts_node_type (root ), "identifier" ) != 0 ) {
2779+ return false;
2780+ }
2781+
2782+ char * name = cbm_node_text (ctx -> arena , root , ctx -> source );
2783+ if (!name ) {
2784+ return false;
2785+ }
2786+ /* self/cls only as a DIRECT receiver: `self.m()` is class-local, but
2787+ * `self.client.m()` has receiver `self.client` of unknown type. */
2788+ if (direct_identifier && (strcmp (name , "self" ) == 0 || strcmp (name , "cls" ) == 0 )) {
2789+ return true;
2790+ }
2791+ /* Import-bound root, incl. aliases (`import tools as toolkit` binds
2792+ * local_name "toolkit"). Per-file scan: imports.count is a file-local
2793+ * number (tens), never the corpus, so this stays O(file), not O(corpus). */
2794+ for (int i = 0 ; i < ctx -> result -> imports .count ; i ++ ) {
2795+ const char * local_name = ctx -> result -> imports .items [i ].local_name ;
2796+ if (local_name && strcmp (local_name , name ) == 0 ) {
2797+ return true;
2798+ }
2799+ }
2800+ return false;
2801+ }
2802+
27432803static bool is_objectscript_language (CBMLanguage language ) {
27442804 return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE ;
27452805}
@@ -3357,6 +3417,20 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
33573417 strcmp (ts_node_type (node ), "method_call_expression" ) == 0 ) {
33583418 call .is_method = true;
33593419 }
3420+ // Python receiver-aware guard (#1276; same intent as the Perl and
3421+ // TS/JS flags). Flag an attribute call x.foo() whose receiver is not
3422+ // self/cls/super() and is not rooted in an imported name, so the
3423+ // call-resolution pass can suppress weak short-name matches for it
3424+ // (`accelerator.print()` must not bind MockAccelerator.print).
3425+ // Imported receivers stay unflagged: module.function() is Python's
3426+ // canonical cross-file call and the import map resolves it.
3427+ if (ctx -> language == CBM_LANG_PYTHON && strcmp (ts_node_type (node ), "call" ) == 0 ) {
3428+ TSNode fn = ts_node_child_by_field_name (node , TS_FIELD ("function" ));
3429+ if (!ts_node_is_null (fn ) && strcmp (ts_node_type (fn ), "attribute" ) == 0 ) {
3430+ TSNode obj = ts_node_child_by_field_name (fn , TS_FIELD ("object" ));
3431+ call .is_method = !python_receiver_is_exempt (ctx , obj );
3432+ }
3433+ }
33603434 // TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
33613435 // as the Perl flag above). Flag a member call x.foo() whose receiver
33623436 // is NOT `this`/`super`. When the TS-LSP cannot resolve the receiver
0 commit comments