[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.
This commit is contained in:
Carl Meyer 2025-04-18 06:46:21 -07:00 committed by GitHub
parent 44ad201262
commit 1918c61623
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 26 additions and 7 deletions

View File

@ -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: ...
```

View File

@ -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