From 1918c616237d792c242e28aeb66d9e6836f3dfa0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 06:46:21 -0700 Subject: [PATCH] [red-knot] class bases are not affected by __future__.annotations (#17456) ## Summary We were over-conflating the conditions for deferred name resolution. `from __future__ import annotations` defers annotations, but not class bases. In stub files, class bases are also deferred. Modeling this correctly also reduces likelihood of cycles in Python files using `from __future__ import annotations` (since deferred resolution is inherently cycle-prone). The same cycles are still possible in `.pyi` files, but much less likely, since typically there isn't anything in a `pyi` file that would cause an early return from a scope, or otherwise cause visibility constraints to persist to end of scope. Usually there is only code at module global scope and class scope, which can't have `return` statements, and `raise` or `assert` statements in a stub file would be very strange. (Technically according to the spec we'd be within our rights to just forbid a whole bunch of syntax outright in a stub file, but I kinda like minimizing unnecessary differences between the handling of Python files and stub files.) ## Test Plan Added mdtests. --- .../resources/mdtest/annotations/deferred.md | 21 +++++++++++++++++++ .../src/types/infer.rs | 12 +++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md index 7ee6e84060..8db8d90409 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md @@ -156,3 +156,24 @@ def _(): def f(self) -> C: return self ``` + +## Base class references + +### Not deferred by __future__.annotations + +```py +from __future__ import annotations + +class A(B): # error: [unresolved-reference] + pass + +class B: + pass +``` + +### Deferred in stub files + +```pyi +class A(B): ... +class B: ... +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 29a92c058d..c2d5467c4c 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -585,8 +585,8 @@ impl<'db> TypeInferenceBuilder<'db> { /// Are we currently inferring types in file with deferred types? /// This is true for stub files and files with `__future__.annotations` - fn are_all_types_deferred(&self) -> bool { - self.index.has_future_annotations() || self.file().is_stub(self.db().upcast()) + fn defer_annotations(&self) -> bool { + self.index.has_future_annotations() || self.in_stub() } /// Are we currently inferring deferred types? @@ -1467,7 +1467,7 @@ impl<'db> TypeInferenceBuilder<'db> { // If there are type params, parameters and returns are evaluated in that scope, that is, in // `infer_function_type_params`, rather than here. if type_params.is_none() { - if self.are_all_types_deferred() { + if self.defer_annotations() { self.types.deferred.insert(definition); } else { self.infer_optional_annotation_expression( @@ -1791,9 +1791,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Only defer the references that are actually string literals, instead of // deferring the entire class definition if a string literal occurs anywhere in the // base class list. - if self.are_all_types_deferred() - || class_node.bases().iter().any(contains_string_literal) - { + if self.in_stub() || class_node.bases().iter().any(contains_string_literal) { self.types.deferred.insert(definition); } else { for base in class_node.bases() { @@ -2919,7 +2917,7 @@ impl<'db> TypeInferenceBuilder<'db> { let mut declared_ty = self.infer_annotation_expression( annotation, - DeferredExpressionState::from(self.are_all_types_deferred()), + DeferredExpressionState::from(self.defer_annotations()), ); if target