From 7909f3766e56c0992eaefdfd548328d3265208f7 Mon Sep 17 00:00:00 2001 From: Mark Feng Date: Wed, 17 Jun 2026 15:31:06 +0800 Subject: [PATCH] Report `super()` in a directly-defined NamedTuple method A zero-argument `super()` inside a method of a class that directly subclasses `NamedTuple` makes the class fail to define at runtime: the compiler creates a `__class__` cell for `super()`, but the NamedTuple machinery does not propagate `__classcell__` to `type.__new__`, raising `RuntimeError`. Subclasses of a concrete NamedTuple are built by ordinary class machinery and are unaffected. Record whether a NamedTuple is directly defined (vs inherited) in its metadata, and emit an `InvalidSuperCall` error for a no-argument `super()` resolved inside a directly-defined NamedTuple method. Closes #3763 --- pyrefly/lib/alt/class/class_metadata.rs | 11 ++- pyrefly/lib/alt/solve.rs | 16 ++++ pyrefly/lib/alt/types/class_metadata.rs | 2 + pyrefly/lib/test/named_tuple.rs | 97 +++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) diff --git a/pyrefly/lib/alt/class/class_metadata.rs b/pyrefly/lib/alt/class/class_metadata.rs index 61a7a71002..bc150eec1a 100644 --- a/pyrefly/lib/alt/class/class_metadata.rs +++ b/pyrefly/lib/alt/class/class_metadata.rs @@ -702,9 +702,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { Some(NamedTupleMetadata { elements: self.get_named_tuple_elements(cls, errors), has_dynamic_fields, + is_direct: true, }) } else { - metadata.named_tuple_metadata().cloned() + // A subclass of a concrete NamedTuple inherits its fields but is created by + // ordinary class machinery, so it does not directly define a NamedTuple. + metadata + .named_tuple_metadata() + .map(|nt| NamedTupleMetadata { + elements: nt.elements.clone(), + has_dynamic_fields: nt.has_dynamic_fields, + is_direct: false, + }) } }) } diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index bb519fd536..90ec6781e1 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -3006,6 +3006,22 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { match &self.get_idx(*self_binding).0 { Some(obj_cls) => { let obj_type = self.as_class_type_unchecked(obj_cls); + // A zero-arg `super()` in a method of a class that directly subclasses + // `NamedTuple` makes the class fail to define at runtime: the `__class__` + // cell the compiler creates for `super()` is not propagated by the + // NamedTuple machinery. See https://github.com/facebook/pyrefly/issues/3763. + if self + .get_metadata_for_class(obj_cls) + .named_tuple_metadata() + .is_some_and(|nt| nt.is_direct) + { + self.error( + errors, + range, + ErrorKind::InvalidSuperCall, + "`super` call with no arguments is not allowed in a `NamedTuple` method".to_owned(), + ); + } let lookup_cls = self.get_super_lookup_class(obj_cls, &obj_type).unwrap(); let obj = if method.id == dunder::NEW { // __new__ is special: it's the only static method in which the diff --git a/pyrefly/lib/alt/types/class_metadata.rs b/pyrefly/lib/alt/types/class_metadata.rs index 5d31646a0b..c63b8078df 100644 --- a/pyrefly/lib/alt/types/class_metadata.rs +++ b/pyrefly/lib/alt/types/class_metadata.rs @@ -581,6 +581,8 @@ pub struct NamedTupleMetadata { /// If true, the namedtuple fields were dynamically generated (e.g., using a /// generator or variable) and couldn't be statically resolved. pub has_dynamic_fields: bool, + /// Does this class directly extend `NamedTuple`? + pub is_direct: bool, } /// Defaults for `init_by_name` and `init_by_default`, per-field flags that control the name of diff --git a/pyrefly/lib/test/named_tuple.rs b/pyrefly/lib/test/named_tuple.rs index d628f9c606..7487772d3e 100644 --- a/pyrefly/lib/test/named_tuple.rs +++ b/pyrefly/lib/test/named_tuple.rs @@ -1062,3 +1062,100 @@ def f(X: int) -> None: N = namedtuple("N", [X]) # E: Expected a string literal "#, ); + +// https://github.com/facebook/pyrefly/issues/3763: a zero-arg `super()` in a method of a class +// that directly subclasses `NamedTuple` makes the class fail to define at runtime, because the +// `__class__` cell the compiler creates for `super()` is not propagated by the NamedTuple machinery. +testcase!( + test_named_tuple_super_call_disallowed, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int + def m(self) -> int: + super() # E: NamedTuple + return self.x +"#, +); + +testcase!( + test_named_tuple_super_attr_disallowed, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int + def m(self) -> str: + return super().__repr__() # E: NamedTuple +"#, +); + +testcase!( + test_named_tuple_classmethod_super_disallowed, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int + @classmethod + def c(cls) -> int: + super() # E: NamedTuple + return 0 +"#, +); + +// A subclass of a concrete NamedTuple is built by ordinary class machinery, so `super()` is fine. +testcase!( + test_named_tuple_subclass_super_ok, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int +class G(F): + def m(self) -> int: + super() + return self.x +"#, +); + +// The inherited (non-direct) NamedTuple metadata must not leak `super()` flagging to deeper +// subclasses either. +testcase!( + test_named_tuple_deep_subclass_super_ok, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int +class G(F): + pass +class H(G): + def m(self) -> int: + super() + return self.x +"#, +); + +testcase!( + test_named_tuple_no_super_ok, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int + def m(self) -> int: + return self.x +"#, +); + +// We only flag the zero-arg `super()` form, which is what the issue reports and the common case. +// An explicit `super(F, self)` also fails at runtime (the compiler still creates the `__class__` +// cell whenever `super` is referenced), but the lexical enclosing class is not threaded into the +// explicit-args binding, so we do not detect it yet. +testcase!( + bug = "explicit super(F, self) in a NamedTuple also fails at runtime but is not flagged", + test_named_tuple_explicit_super_args_not_flagged, + r#" +from typing import NamedTuple +class F(NamedTuple): + x: int + def m(self) -> str: + return super(F, self).__repr__() +"#, +);