diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 33923f3b05..eb7afc8317 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -4628,6 +4628,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { class_key: Idx, name: &Identifier, suggestion: &Option, + allow_class_body_forward_reference: bool, errors: &ErrorCollector, ) -> Type { let add_unknown_name_error = |errors: &ErrorCollector| { @@ -4642,11 +4643,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { builder.emit(); self.heap.mk_any_error() }; - // We're specifically looking for attributes that are inherited from the parent class + // Runtime class-body lookups can only see inherited fields. Postponed annotations and + // explicit forward references may also resolve fields declared later in this class. if let Some(cls) = &self.get_idx(class_key).as_ref().0 - && !self - .get_class_fields(cls) - .is_some_and(|f| f.contains(&name.id)) + && (allow_class_body_forward_reference + || !self + .get_class_fields(cls) + .is_some_and(|f| f.contains(&name.id))) { // If the attribute lookup fails here, we'll emit an `unknown-name` error, since this // is a deferred lookup that can't be calculated at the bindings step @@ -5787,7 +5790,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.binding_to_type_info(binding, errors).into_ty() } Binding::ClassBodyUnknownName(x) => { - self.binding_to_type_class_body_unknown_name(x.0, &x.1, &x.2, errors) + self.binding_to_type_class_body_unknown_name(x.0, &x.1, &x.2, x.3, errors) } Binding::Exhaustive(x) => self.binding_to_type_exhaustive(&x.narrow_entries), Binding::SuppressedException(x) => { diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index 91202e9fae..235078f4c0 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -2520,8 +2520,9 @@ pub enum Binding { Delete(Box), /// A name in the class body that wasn't found in the static scope /// It could either be an unbound name or a reference to an inherited attribute - /// We'll find out which when we solve the class - ClassBodyUnknownName(Box<(Idx, Identifier, Option)>), + /// We'll find out which when we solve the class. The boolean records whether a postponed or + /// quoted annotation may also resolve an attribute declared later in the same class body. + ClassBodyUnknownName(Box<(Idx, Identifier, Option, bool)>), /// A match statement or if/elif chain that may be type-exhaustive. /// Resolves to Never if ANY narrow entry narrows to Never, None otherwise. Exhaustive(Box), @@ -2827,12 +2828,13 @@ impl DisplayWith for Binding { } Self::Delete(x) => write!(f, "Delete({})", m.display(x)), Self::ClassBodyUnknownName(x) => { - let (class_key, name, suggestion) = x.as_ref(); + let (class_key, name, suggestion, allow_class_body_forward_reference) = x.as_ref(); write!( f, - "ClassBodyUnknownName({}, {}", + "ClassBodyUnknownName({}, {}, allow_class_body_forward_reference={}", m.display(ctx.idx_to_key(*class_key)), name, + allow_class_body_forward_reference, )?; if let Some(suggestion) = suggestion { write!(f, ", {suggestion}")?; diff --git a/pyrefly/lib/binding/expr.rs b/pyrefly/lib/binding/expr.rs index 62ac38c619..aa316be81b 100644 --- a/pyrefly/lib/binding/expr.rs +++ b/pyrefly/lib/binding/expr.rs @@ -286,7 +286,7 @@ impl<'a> BindingsBuilder<'a> { usage: &mut Usage, tparams_builder: &mut Option, ) -> Idx { - self.ensure_name_in_type(name, usage, tparams_builder, false) + self.ensure_name_in_type(name, usage, tparams_builder, false, false) } fn ensure_name_in_type( @@ -295,6 +295,7 @@ impl<'a> BindingsBuilder<'a> { usage: &mut Usage, tparams_builder: &mut Option, is_runtime_evaluated_annotation: bool, + allow_class_body_forward_reference: bool, ) -> Idx { self.ensure_name_impl( name, @@ -303,6 +304,7 @@ impl<'a> BindingsBuilder<'a> { .as_mut() .map(|tparams_builder| (tparams_builder, LegacyTParamId::Name(name.clone()))), is_runtime_evaluated_annotation, + allow_class_body_forward_reference, ) } @@ -320,6 +322,7 @@ impl<'a> BindingsBuilder<'a> { (tparams_builder, LegacyTParamId::Attr(value.clone(), attrs)) }), false, + false, ) } @@ -350,6 +353,7 @@ impl<'a> BindingsBuilder<'a> { usage: &mut Usage, tparams_lookup: Option<(&mut LegacyTParamCollector, LegacyTParamId)>, is_runtime_evaluated_annotation: bool, + allow_class_body_forward_reference: bool, ) -> Idx { let key = Key::BoundName(ShortIdentifier::new(name)); if name.is_empty() { @@ -446,7 +450,12 @@ impl<'a> BindingsBuilder<'a> { { self.insert_binding( key, - Binding::ClassBodyUnknownName(Box::new((cls, name.clone(), suggestion))), + Binding::ClassBodyUnknownName(Box::new(( + cls, + name.clone(), + suggestion, + allow_class_body_forward_reference, + ))), ) } else { // Record a type error and fall back to `Any`. @@ -1286,6 +1295,9 @@ impl<'a> BindingsBuilder<'a> { usage, tparams_builder, check_runtime_name && !in_string_literal, + in_string_literal + || self.scopes.has_future_annotations() + || self.sys_info.version().at_least(3, 14), ); } Expr::Subscript(ExprSubscript { value, .. }) diff --git a/pyrefly/lib/test/annotation.rs b/pyrefly/lib/test/annotation.rs index 7e6762f155..ee31bdc183 100644 --- a/pyrefly/lib/test/annotation.rs +++ b/pyrefly/lib/test/annotation.rs @@ -264,6 +264,34 @@ type Tree = Union[Leaf, Node] "#, ); +testcase!( + test_nested_class_forward_reference_in_enclosing_class_annotation, + TestEnv::new_with_version(PythonVersion::new(3, 13, 0)), + r#" +from __future__ import annotations +from typing import assert_type + +class Formatter: + a: _Section + class _Section: ... + b: _Section + +def check(formatter: Formatter) -> None: + assert_type(formatter.a, Formatter._Section) + assert_type(formatter.b, Formatter._Section) +"#, +); + +testcase!( + test_nested_class_runtime_reference_before_declaration_is_error, + TestEnv::new_with_version(PythonVersion::new(3, 13, 0)), + r#" +class Formatter: + a = _Section # E: Could not find name `_Section` + class _Section: ... +"#, +); + fn env_3_13_with_stub() -> TestEnv { let mut env = TestEnv::new_with_version(PythonVersion::new(3, 13, 0)); env.add_with_path("foo", "foo.pyi", "x: int | 'str'");