mirror of https://github.com/astral-sh/ruff
[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:
parent
44ad201262
commit
1918c61623
|
|
@ -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: ...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue