Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pyrefly/lib/alt/class/class_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
})
}
Expand Down
16 changes: 16 additions & 0 deletions pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pyrefly/lib/alt/types/class_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions pyrefly/lib/test/named_tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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__()
"#,
);
Loading