Skip to content

Commit e5194ff

Browse files
fix
1 parent 3dbb6ee commit e5194ff

4 files changed

Lines changed: 88 additions & 0 deletions

File tree

pyrefly/lib/alt/class/class_field.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3039,6 +3039,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
30393039
let mut parent_attr_found = false;
30403040
let mut parent_has_any = false;
30413041
let is_typed_dict_field = self.is_typed_dict_field(metadata.as_ref(), field_name);
3042+
let is_named_tuple_element = metadata
3043+
.named_tuple_metadata()
3044+
.is_some_and(|named_tuple| named_tuple.elements.contains(field_name));
30423045

30433046
let bases_to_check: Box<dyn Iterator<Item = &ClassType>> = if bases.is_empty() {
30443047
// If the class doesn't have any base type, we should just use `object` as base to ensure
@@ -3063,6 +3066,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
30633066
format!("Cannot override named tuple element `{field_name}`"),
30643067
);
30653068
}
3069+
if is_named_tuple_element {
3070+
continue;
3071+
}
30663072
let Some(want_field) = self.get_class_member(parent_cls, field_name) else {
30673073
continue;
30683074
};

pyrefly/lib/alt/expr.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ use crate::alt::nn_module_specials::is_nn_module_dict;
7272
use crate::alt::solve::TypeFormContext;
7373
use crate::alt::unwrap::Hint;
7474
use crate::alt::unwrap::HintRef;
75+
use crate::binding::binding::Binding;
7576
use crate::binding::binding::Key;
7677
use crate::binding::binding::KeyYield;
7778
use crate::binding::binding::KeyYieldFrom;
@@ -221,6 +222,18 @@ impl Display for ConditionRedundantReason {
221222
static MAX_TUPLE_LENGTH: usize = 256;
222223

223224
impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
225+
fn synthesized_functional_class_type(&self, call: &ExprCall) -> Option<Type> {
226+
let anon_key = match call.arguments.args.first()? {
227+
Expr::StringLiteral(name) => Key::Anon(name.range()),
228+
_ => Key::Anon(call.range),
229+
};
230+
let idx = self
231+
.bindings()
232+
.key_to_idx_hashed_opt(Hashed::new(&anon_key))?;
233+
matches!(self.bindings().get(idx), Binding::ClassDef(..))
234+
.then(|| self.get_hashed(Hashed::new(&anon_key)).ty().clone())
235+
}
236+
224237
/// Infer a type for an expression, with an optional type hint that influences the inferred type.
225238
/// The inferred type is also checked against the hint.
226239
pub fn expr(
@@ -578,6 +591,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
578591
Expr::YieldFrom(x) => self.get(&KeyYieldFrom(x.range)).return_ty.clone(),
579592
Expr::Compare(x) => self.compare_infer(x, errors),
580593
Expr::Call(x) => {
594+
if let Some(ty) = self.synthesized_functional_class_type(x) {
595+
return ty;
596+
}
581597
let callee_ty = self.expr_infer(&x.func, errors);
582598
if let Some(d) = self.call_to_dict(&callee_ty, &x.arguments) {
583599
self.dict_infer(&d, hint, x.range, errors)

pyrefly/lib/binding/expr.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use pyrefly_graph::index::Idx;
99
use pyrefly_python::ast::Ast;
1010
use pyrefly_python::module_path::ModuleStyle;
11+
use pyrefly_python::nesting_context::NestingContext;
1112
use pyrefly_python::short_identifier::ShortIdentifier;
1213
use pyrefly_util::visit::VisitMut;
1314
use ruff_python_ast::Arguments;
@@ -30,6 +31,7 @@ use ruff_python_ast::ExprYieldFrom;
3031
use ruff_python_ast::Identifier;
3132
use ruff_python_ast::Operator;
3233
use ruff_python_ast::StringLiteral;
34+
use ruff_python_ast::name::Name;
3335
use ruff_text_size::Ranged;
3436
use ruff_text_size::TextRange;
3537
use starlark_map::Hashed;
@@ -549,6 +551,49 @@ impl<'a> BindingsBuilder<'a> {
549551
}
550552
}
551553

554+
fn bind_inline_functional_named_tuple(&mut self, call: &mut ExprCall) {
555+
let Some(kind) = self.as_special_export(&call.func) else {
556+
return;
557+
};
558+
let Some(Expr::StringLiteral(name)) = call.arguments.args.first() else {
559+
return;
560+
};
561+
let class_name = Identifier::new(Name::new(name.value.to_str()), name.range());
562+
let parent = NestingContext::toplevel();
563+
let class_idx = match kind {
564+
SpecialExport::CollectionsNamedTuple => {
565+
let Some((_arg_name, members)) = call.arguments.args.split_first_mut() else {
566+
return;
567+
};
568+
self.synthesize_collections_named_tuple_def(
569+
class_name,
570+
&parent,
571+
&mut call.func,
572+
members,
573+
&mut call.arguments.keywords,
574+
false,
575+
)
576+
}
577+
SpecialExport::TypingNamedTuple => {
578+
let Some((_arg_name, members)) = call.arguments.args.split_first_mut() else {
579+
return;
580+
};
581+
self.synthesize_typing_named_tuple_def(
582+
class_name,
583+
&parent,
584+
&mut call.func,
585+
members,
586+
false,
587+
)
588+
}
589+
_ => return,
590+
};
591+
self.insert_binding(
592+
Key::Anon(call.range),
593+
Binding::ClassDef(class_idx, Box::new([])),
594+
);
595+
}
596+
552597
fn record_yield(&mut self, mut x: ExprYield) {
553598
let mut yield_link = self.declare_current_idx(Key::YieldLink(x.range));
554599
let idx = self.idx_for_promise(KeyYield(x.range));
@@ -661,6 +706,14 @@ impl<'a> BindingsBuilder<'a> {
661706
self.finish_bool_op_fork();
662707
}
663708
}
709+
Expr::Call(call)
710+
if matches!(
711+
self.as_special_export(&call.func),
712+
Some(SpecialExport::CollectionsNamedTuple | SpecialExport::TypingNamedTuple)
713+
) && matches!(call.arguments.args.first(), Some(Expr::StringLiteral(_))) =>
714+
{
715+
self.bind_inline_functional_named_tuple(call);
716+
}
664717
Expr::Call(ExprCall {
665718
node_index: _,
666719
range: _,

pyrefly/lib/test/named_tuple.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ Point3(1) # E: Missing argument `y` in function `Point3.__new__`
8080
"#,
8181
);
8282

83+
// Regression test for https://github.com/facebook/pyrefly/issues/2811
84+
testcase!(
85+
test_inline_collections_namedtuple_constructor,
86+
r#"
87+
import collections
88+
from typing import Any, assert_type
89+
90+
device = collections.namedtuple("FakeDevice", ["type", "index"])("lazy-caster", 0)
91+
assert_type(device.type, Any)
92+
assert_type(device.index, Any)
93+
"#,
94+
);
95+
8396
// Regression test for https://github.com/facebook/pyrefly/issues/2734
8497
// Starred expressions in field lists can't be fully resolved statically,
8598
// so the namedtuple has dynamic fields. String literals alongside the

0 commit comments

Comments
 (0)