Skip to content

Commit

Permalink
Lazily evaluate all PEP 695 type alias values (#8033)
Browse files Browse the repository at this point in the history
<!--
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 #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 #8017.

## Test Plan

`cargo test`
  • Loading branch information
charliermarsh committed Oct 18, 2023
1 parent 94b4bb0 commit a62c735
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 16 deletions.
17 changes: 17 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F401_19.py
Original file line number Diff line number Diff line change
@@ -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:
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Test lazy evaluation of type alias values."""

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

from collections.abc import Callable
4 changes: 2 additions & 2 deletions crates/ruff_linter/src/checkers/ast/deferred.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ruff_python_ast::{Expr, TypeParam};
use ruff_python_ast::Expr;
use ruff_python_semantic::{ScopeId, Snapshot};
use ruff_text_size::TextRange;

Expand All @@ -10,7 +10,7 @@ pub(crate) struct Deferred<'a> {
pub(crate) scopes: Vec<ScopeId>,
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, 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) lambdas: Vec<(&'a Expr, Snapshot)>,
pub(crate) for_loops: Vec<Snapshot>,
Expand Down
26 changes: 14 additions & 12 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,9 +582,9 @@ where
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
// The value in a `type` alias has annotation semantics, in that it's never
// evaluated at runtime.
self.visit_annotation(value);
self.deferred
.type_param_definitions
.push((value, self.semantic.snapshot()));
self.semantic.pop_scope();
self.visit_expr(name);
}
Expand Down Expand Up @@ -1389,9 +1389,14 @@ where
}
}
// Step 2: Traversal
self.deferred
.type_param_definitions
.push((type_param, self.semantic.snapshot()));
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
bound: Some(bound), ..
}) = type_param
{
self.deferred
.type_param_definitions
.push((bound, self.semantic.snapshot()));
}
}
}

Expand Down Expand Up @@ -1766,12 +1771,9 @@ impl<'a> Checker<'a> {
for (type_param, snapshot) in type_params {
self.semantic.restore(snapshot);

if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
bound: Some(bound), ..
}) = type_param
{
self.visit_annotation(bound);
}
self.semantic.flags |=
SemanticModelFlags::TYPE_PARAM_DEFINITION | SemanticModelFlags::TYPE_DEFINITION;
self.visit_expr(type_param);
}
}
self.semantic.restore(snapshot);
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod tests {
#[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_18.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
Expand Down Expand Up @@ -135,6 +136,7 @@ mod tests {
#[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_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_1.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

Original file line number Diff line number Diff line change
@@ -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
|


15 changes: 13 additions & 2 deletions crates/ruff_python_semantic/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,16 @@ bitflags! {
/// ```
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.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits();

Expand All @@ -1610,11 +1620,12 @@ bitflags! {
/// The context is in any deferred type definition.
const DEFERRED_TYPE_DEFINITION = Self::SIMPLE_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.
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();
}
}

Expand Down

0 comments on commit a62c735

Please sign in to comment.