Skip to content

Commit

Permalink
Allow bindings to be created and referenced within annotations (#7885)
Browse files Browse the repository at this point in the history
## Summary

Given:

```python
baz: Annotated[
    str,
    [qux for qux in foo],
]
```

We treat `baz` as `BindingKind::Annotation`, to ensure that references
to `baz` are marked as unbound. However, we were _also_ treating `qux`
as `BindingKind::Annotation`, which meant that the load in the
comprehension _also_ errored.

Closes #7879.
  • Loading branch information
charliermarsh authored Oct 10, 2023
1 parent ec7395b commit a3e8e77
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 6 deletions.
19 changes: 19 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F821_18.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Test bindings created within annotations."""

from typing import Annotated

foo = [1, 2, 3, 4, 5]


class Bar:
# OK: Allow list comprehensions in annotations (i.e., treat `qux` as a valid
# load in the scope of the annotation).
baz: Annotated[
str,
[qux for qux in foo],
]


# OK: Allow named expressions in annotations.
x: (y := 1)
print(y)
21 changes: 21 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F821_19.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Test bindings created within annotations under `__future__` annotations."""

from __future__ import annotations

from typing import Annotated

foo = [1, 2, 3, 4, 5]


class Bar:
# OK: Allow list comprehensions in annotations (i.e., treat `qux` as a valid
# load in the scope of the annotation).
baz: Annotated[
str,
[qux for qux in foo],
]


# Error: `y` is not defined.
x: (y := 1)
print(y)
15 changes: 9 additions & 6 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1168,13 +1168,14 @@ where
range: _,
}) = slice.as_ref()
{
if let Some(expr) = elts.first() {
let mut iter = elts.iter();
if let Some(expr) = iter.next() {
self.visit_expr(expr);
for expr in elts.iter().skip(1) {
self.visit_non_type_definition(expr);
}
self.visit_expr_context(ctx);
}
for expr in iter {
self.visit_non_type_definition(expr);
}
self.visit_expr_context(ctx);
} else {
debug!("Found non-Expr::Tuple argument to PEP 593 Annotation.");
}
Expand Down Expand Up @@ -1618,10 +1619,12 @@ impl<'a> Checker<'a> {
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
let parent = self.semantic.current_statement();

// Match the left-hand side of an annotated assignment, like `x` in `x: int`.
if matches!(
parent,
Stmt::AnnAssign(ast::StmtAnnAssign { value: None, .. })
) {
) && !self.semantic.in_annotation()
{
self.add_binding(
id,
expr.range(),
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 @@ -133,6 +133,8 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_15.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_16.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_19.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,12 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F821_19.py:21:7: F821 Undefined name `y`
|
19 | # Error: `y` is not defined.
20 | x: (y := 1)
21 | print(y)
| ^ F821
|


0 comments on commit a3e8e77

Please sign in to comment.