diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.py new file mode 100644 index 0000000000..9ebd9ba16e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Callable + + +def demonstrate_bare_local_annotation(): + x: int + print(x) + +demonstrate_bare_local_annotation() + + +def make_closure_pair() -> tuple[Callable[[], int], Callable[[int], None]]: + x: int + + def get_value() -> int: + return x + + def set_value(new_value: int) -> None: + nonlocal x + x = new_value + + return get_value, set_value + +get_value, set_value = make_closure_pair() +set_value(123) +print(get_value()) diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 02cd5158a8..91f0de6a1f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -168,6 +168,7 @@ mod tests { #[test_case(Rule::UndefinedName, Path::new("F821_31.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))] #[test_case(Rule::UndefinedName, Path::new("F821_33.py"))] + #[test_case(Rule::UndefinedName, Path::new("F821_34.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] @@ -3667,7 +3668,7 @@ lambda: fu name: str print(name) ", - &[Rule::UndefinedName], + &[], ); flakes( r" diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap new file mode 100644 index 0000000000..d0b409f39e --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 78536071d7..3c15688916 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -476,9 +476,19 @@ impl<'a> SemanticModel<'a> { // The `name` in `print(name)` should be treated as unresolved, but the `name` in // `name: str` should be treated as used. // - // Stub files are an exception. In a stub file, it _is_ considered valid to - // resolve to a type annotation. - BindingKind::Annotation if !self.in_stub_file() => continue, + // There are two exceptions to this rule: + // 1. Stub files. In a stub file, it _is_ considered valid to resolve to a + // type annotation. + // 2. Bare annotations inside functions. Per PEP 526, these create local + // variables. + BindingKind::Annotation + if !self.in_stub_file() + && !self.scopes[self.bindings[binding_id].scope] + .kind + .is_function() => + { + continue; + } // If it's a deletion, don't treat it as resolved, since the name is now // unbound. For example, given: