Lazily evaluate all PEP 695 type alias values (#8033)

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

In https://github.com/astral-sh/ruff/pull/7968, I introduced a
regression whereby we started to treat imports used _only_ in type
annotation bounds (with `__future__` annotations) as unused.

The root of the issue is that I started using `visit_annotation` for
these bounds. So we'd queue up the bound in the list of deferred type
parameters, then when visiting, we'd further queue it up in the list of
deferred type annotations... Which we'd then never visit, since deferred
type annotations are visited _before_ deferred type parameters.

Anyway, the better solution here is to use a dedicated flag for these,
since they have slightly different behavior than type annotations.

I've also fixed what I _think_ is a bug whereby we previously failed to
resolve `Callable` in:

```python
type RecordCallback[R: Record] = Callable[[R], None]

from collections.abc import Callable
```

IIUC, the values in type aliases should be evaluated lazily, like type
parameters.

Closes https://github.com/astral-sh/ruff/issues/8017.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2023-10-17 21:50:26 -04:00 committed by GitHub
parent 94b4bb0f57
commit a62c735f9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 16 deletions

View file

@ -0,0 +1,17 @@
"""Test that type parameters are considered used."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
from .foo import Record as Record1
from .bar import Record as Record2
type RecordCallback[R: Record1] = Callable[[R], None]
def process_record[R: Record2](record: R) -> None:
...

View file

@ -0,0 +1,5 @@
"""Test lazy evaluation of type alias values."""
type RecordCallback[R: Record] = Callable[[R], None]
from collections.abc import Callable

View file

@ -1,4 +1,4 @@
use ruff_python_ast::{Expr, TypeParam}; use ruff_python_ast::Expr;
use ruff_python_semantic::{ScopeId, Snapshot}; use ruff_python_semantic::{ScopeId, Snapshot};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
@ -10,7 +10,7 @@ pub(crate) struct Deferred<'a> {
pub(crate) scopes: Vec<ScopeId>, pub(crate) scopes: Vec<ScopeId>,
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>, pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>,
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>, pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
pub(crate) type_param_definitions: Vec<(&'a TypeParam, Snapshot)>, pub(crate) type_param_definitions: Vec<(&'a Expr, Snapshot)>,
pub(crate) functions: Vec<Snapshot>, pub(crate) functions: Vec<Snapshot>,
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>, pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
pub(crate) for_loops: Vec<Snapshot>, pub(crate) for_loops: Vec<Snapshot>,

View file

@ -582,9 +582,9 @@ where
if let Some(type_params) = type_params { if let Some(type_params) = type_params {
self.visit_type_params(type_params); self.visit_type_params(type_params);
} }
// The value in a `type` alias has annotation semantics, in that it's never self.deferred
// evaluated at runtime. .type_param_definitions
self.visit_annotation(value); .push((value, self.semantic.snapshot()));
self.semantic.pop_scope(); self.semantic.pop_scope();
self.visit_expr(name); self.visit_expr(name);
} }
@ -1389,9 +1389,14 @@ where
} }
} }
// Step 2: Traversal // Step 2: Traversal
self.deferred if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
.type_param_definitions bound: Some(bound), ..
.push((type_param, self.semantic.snapshot())); }) = type_param
{
self.deferred
.type_param_definitions
.push((bound, self.semantic.snapshot()));
}
} }
} }
@ -1766,12 +1771,9 @@ impl<'a> Checker<'a> {
for (type_param, snapshot) in type_params { for (type_param, snapshot) in type_params {
self.semantic.restore(snapshot); self.semantic.restore(snapshot);
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar { self.semantic.flags |=
bound: Some(bound), .. SemanticModelFlags::TYPE_PARAM_DEFINITION | SemanticModelFlags::TYPE_DEFINITION;
}) = type_param self.visit_expr(type_param);
{
self.visit_annotation(bound);
}
} }
} }
self.semantic.restore(snapshot); self.semantic.restore(snapshot);

View file

@ -50,6 +50,7 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_16.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_16.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_17.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_17.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_18.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_18.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))] #[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))] #[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
@ -135,6 +136,7 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_17.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_17.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_18.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_18.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_19.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_19.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_20.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View file

@ -0,0 +1,14 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F821_20.py:3:24: F821 Undefined name `Record`
|
1 | """Test lazy evaluation of type alias values."""
2 |
3 | type RecordCallback[R: Record] = Callable[[R], None]
| ^^^^^^ F821
4 |
5 | from collections.abc import Callable
|

View file

@ -1600,6 +1600,16 @@ bitflags! {
/// ``` /// ```
const FUTURE_ANNOTATIONS = 1 << 14; const FUTURE_ANNOTATIONS = 1 << 14;
/// The model is in a type parameter definition.
///
/// For example, the model could be visiting `Record` in:
/// ```python
/// from typing import TypeVar
///
/// Record = TypeVar("Record")
///
const TYPE_PARAM_DEFINITION = 1 << 15;
/// The context is in any type annotation. /// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits(); const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits();
@ -1610,11 +1620,12 @@ bitflags! {
/// The context is in any deferred type definition. /// The context is in any deferred type definition.
const DEFERRED_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits() const DEFERRED_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()
| Self::COMPLEX_STRING_TYPE_DEFINITION.bits() | Self::COMPLEX_STRING_TYPE_DEFINITION.bits()
| Self::FUTURE_TYPE_DEFINITION.bits(); | Self::FUTURE_TYPE_DEFINITION.bits()
| Self::TYPE_PARAM_DEFINITION.bits();
/// The context is in a typing-only context. /// The context is in a typing-only context.
const TYPING_CONTEXT = Self::TYPE_CHECKING_BLOCK.bits() | Self::TYPING_ONLY_ANNOTATION.bits() | const TYPING_CONTEXT = Self::TYPE_CHECKING_BLOCK.bits() | Self::TYPING_ONLY_ANNOTATION.bits() |
Self::STRING_TYPE_DEFINITION.bits(); Self::STRING_TYPE_DEFINITION.bits() | Self::TYPE_PARAM_DEFINITION.bits();
} }
} }