From 519a65007fe2ff714103fb023aacb49a71551a7f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 22 May 2024 15:44:31 -0400 Subject: [PATCH] Mark quotes as unnecessary for non-evaluated annotations (#11485) ## Summary Similar to #11414, this PR extends `UP037` to flag quoted annotations that are located in positions that won't be evaluated at runtime. For example, the quotes on `Tuple` are unnecessary in: ```python from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Tuple def foo(): x: "Tuple[int, int]" = (0, 0) foo() ``` --- crates/ruff/tests/integration_test.rs | 2 +- .../pyupgrade/{UP037.py => UP037_0.py} | 0 .../test/fixtures/pyupgrade/UP037_1.py | 14 +++++ crates/ruff_linter/src/checkers/ast/mod.rs | 2 +- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 3 +- .../pyupgrade/rules/quoted_annotation.rs | 19 ++++++ ..._rules__pyupgrade__tests__UP037_0.py.snap} | 60 +++++++++---------- ...__rules__pyupgrade__tests__UP037_1.py.snap | 22 +++++++ 8 files changed, 88 insertions(+), 34 deletions(-) rename crates/ruff_linter/resources/test/fixtures/pyupgrade/{UP037.py => UP037_0.py} (100%) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_1.py rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP037.py.snap => ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap} (86%) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 8892dcfcb7540..52f1b09df1368 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -1414,7 +1414,7 @@ fn check_input_from_argfile() -> Result<()> { fs::write(&file_a_path, b"import os")?; fs::write(&file_b_path, b"print('hello, world!')")?; - // Create a the input file for argfile to expand + // Create the input file for argfile to expand let input_file_path = tempdir.path().join("file_paths.txt"); fs::write( &input_file_path, diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037.py rename to crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_0.py diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_1.py new file mode 100644 index 0000000000000..ab1a7f46818d0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_1.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Tuple + + +def foo(): + # UP037 + x: "Tuple[int, int]" = (0, 0) + print(x) + + +# OK +X: "Tuple[int, int]" = (0, 0) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 391012b0f12c6..f93d25736b123 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2152,7 +2152,7 @@ impl<'a> Checker<'a> { self.semantic.restore(snapshot); - if self.semantic.in_annotation() && self.semantic.future_annotations_or_stub() { + if self.semantic.in_annotation() && self.semantic.in_typing_only_annotation() { if self.enabled(Rule::QuotedAnnotation) { pyupgrade::rules::quoted_annotation(self, value, range); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 427b448d765b1..554efe8f6bec0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -55,7 +55,8 @@ mod tests { #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_5.py"))] #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"))] #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"))] - #[test_case(Rule::QuotedAnnotation, Path::new("UP037.py"))] + #[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))] + #[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))] #[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))] #[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))] #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs index 2a94d946a3baf..1476077de9ad1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -10,11 +10,18 @@ use crate::checkers::ast::Checker; /// /// ## Why is this bad? /// In Python, type annotations can be quoted to avoid forward references. +/// /// However, if `from __future__ import annotations` is present, Python /// will always evaluate type annotations in a deferred manner, making /// the quotes unnecessary. /// +/// Similarly, if the annotation is located in a typing-only context and +/// won't be evaluated by Python at runtime, the quotes will also be +/// considered unnecessary. For example, Python does not evaluate type +/// annotations on assignments in function bodies. +/// /// ## Example +/// Given: /// ```python /// from __future__ import annotations /// @@ -32,6 +39,18 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// +/// Given: +/// ```python +/// def foo() -> None: +/// bar: "Bar" +/// ``` +/// +/// Use instead: +/// ```python +/// def foo() -> None: +/// bar: Bar +/// ``` +/// /// ## References /// - [PEP 563](https://peps.python.org/pep-0563/) /// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap similarity index 86% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037.py.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap index ba2b0900af42b..4b4eb9e71624c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- -UP037.py:18:14: UP037 [*] Remove quotes from type annotation +UP037_0.py:18:14: UP037 [*] Remove quotes from type annotation | 18 | def foo(var: "MyClass") -> "MyClass": | ^^^^^^^^^ UP037 @@ -19,7 +19,7 @@ UP037.py:18:14: UP037 [*] Remove quotes from type annotation 20 20 | 21 21 | -UP037.py:18:28: UP037 [*] Remove quotes from type annotation +UP037_0.py:18:28: UP037 [*] Remove quotes from type annotation | 18 | def foo(var: "MyClass") -> "MyClass": | ^^^^^^^^^ UP037 @@ -37,7 +37,7 @@ UP037.py:18:28: UP037 [*] Remove quotes from type annotation 20 20 | 21 21 | -UP037.py:19:8: UP037 [*] Remove quotes from type annotation +UP037_0.py:19:8: UP037 [*] Remove quotes from type annotation | 18 | def foo(var: "MyClass") -> "MyClass": 19 | x: "MyClass" @@ -55,7 +55,7 @@ UP037.py:19:8: UP037 [*] Remove quotes from type annotation 21 21 | 22 22 | def foo(*, inplace: "bool"): -UP037.py:22:21: UP037 [*] Remove quotes from type annotation +UP037_0.py:22:21: UP037 [*] Remove quotes from type annotation | 22 | def foo(*, inplace: "bool"): | ^^^^^^ UP037 @@ -73,7 +73,7 @@ UP037.py:22:21: UP037 [*] Remove quotes from type annotation 24 24 | 25 25 | -UP037.py:26:16: UP037 [*] Remove quotes from type annotation +UP037_0.py:26:16: UP037 [*] Remove quotes from type annotation | 26 | def foo(*args: "str", **kwargs: "int"): | ^^^^^ UP037 @@ -91,7 +91,7 @@ UP037.py:26:16: UP037 [*] Remove quotes from type annotation 28 28 | 29 29 | -UP037.py:26:33: UP037 [*] Remove quotes from type annotation +UP037_0.py:26:33: UP037 [*] Remove quotes from type annotation | 26 | def foo(*args: "str", **kwargs: "int"): | ^^^^^ UP037 @@ -109,7 +109,7 @@ UP037.py:26:33: UP037 [*] Remove quotes from type annotation 28 28 | 29 29 | -UP037.py:30:10: UP037 [*] Remove quotes from type annotation +UP037_0.py:30:10: UP037 [*] Remove quotes from type annotation | 30 | x: Tuple["MyClass"] | ^^^^^^^^^ UP037 @@ -128,7 +128,7 @@ UP037.py:30:10: UP037 [*] Remove quotes from type annotation 32 32 | x: Callable[["MyClass"], None] 33 33 | -UP037.py:32:14: UP037 [*] Remove quotes from type annotation +UP037_0.py:32:14: UP037 [*] Remove quotes from type annotation | 30 | x: Tuple["MyClass"] 31 | @@ -147,7 +147,7 @@ UP037.py:32:14: UP037 [*] Remove quotes from type annotation 34 34 | 35 35 | class Foo(NamedTuple): -UP037.py:36:8: UP037 [*] Remove quotes from type annotation +UP037_0.py:36:8: UP037 [*] Remove quotes from type annotation | 35 | class Foo(NamedTuple): 36 | x: "MyClass" @@ -165,7 +165,7 @@ UP037.py:36:8: UP037 [*] Remove quotes from type annotation 38 38 | 39 39 | class D(TypedDict): -UP037.py:40:27: UP037 [*] Remove quotes from type annotation +UP037_0.py:40:27: UP037 [*] Remove quotes from type annotation | 39 | class D(TypedDict): 40 | E: TypedDict("E", foo="int", total=False) @@ -183,7 +183,7 @@ UP037.py:40:27: UP037 [*] Remove quotes from type annotation 42 42 | 43 43 | class D(TypedDict): -UP037.py:44:31: UP037 [*] Remove quotes from type annotation +UP037_0.py:44:31: UP037 [*] Remove quotes from type annotation | 43 | class D(TypedDict): 44 | E: TypedDict("E", {"foo": "int"}) @@ -201,7 +201,7 @@ UP037.py:44:31: UP037 [*] Remove quotes from type annotation 46 46 | 47 47 | x: Annotated["str", "metadata"] -UP037.py:47:14: UP037 [*] Remove quotes from type annotation +UP037_0.py:47:14: UP037 [*] Remove quotes from type annotation | 47 | x: Annotated["str", "metadata"] | ^^^^^ UP037 @@ -220,7 +220,7 @@ UP037.py:47:14: UP037 [*] Remove quotes from type annotation 49 49 | x: Arg("str", "name") 50 50 | -UP037.py:49:8: UP037 [*] Remove quotes from type annotation +UP037_0.py:49:8: UP037 [*] Remove quotes from type annotation | 47 | x: Annotated["str", "metadata"] 48 | @@ -241,7 +241,7 @@ UP037.py:49:8: UP037 [*] Remove quotes from type annotation 51 51 | x: DefaultArg("str", "name") 52 52 | -UP037.py:51:15: UP037 [*] Remove quotes from type annotation +UP037_0.py:51:15: UP037 [*] Remove quotes from type annotation | 49 | x: Arg("str", "name") 50 | @@ -262,7 +262,7 @@ UP037.py:51:15: UP037 [*] Remove quotes from type annotation 53 53 | x: NamedArg("str", "name") 54 54 | -UP037.py:53:13: UP037 [*] Remove quotes from type annotation +UP037_0.py:53:13: UP037 [*] Remove quotes from type annotation | 51 | x: DefaultArg("str", "name") 52 | @@ -283,7 +283,7 @@ UP037.py:53:13: UP037 [*] Remove quotes from type annotation 55 55 | x: DefaultNamedArg("str", "name") 56 56 | -UP037.py:55:20: UP037 [*] Remove quotes from type annotation +UP037_0.py:55:20: UP037 [*] Remove quotes from type annotation | 53 | x: NamedArg("str", "name") 54 | @@ -304,7 +304,7 @@ UP037.py:55:20: UP037 [*] Remove quotes from type annotation 57 57 | x: DefaultNamedArg("str", name="name") 58 58 | -UP037.py:57:20: UP037 [*] Remove quotes from type annotation +UP037_0.py:57:20: UP037 [*] Remove quotes from type annotation | 55 | x: DefaultNamedArg("str", "name") 56 | @@ -325,7 +325,7 @@ UP037.py:57:20: UP037 [*] Remove quotes from type annotation 59 59 | x: VarArg("str") 60 60 | -UP037.py:59:11: UP037 [*] Remove quotes from type annotation +UP037_0.py:59:11: UP037 [*] Remove quotes from type annotation | 57 | x: DefaultNamedArg("str", name="name") 58 | @@ -346,7 +346,7 @@ UP037.py:59:11: UP037 [*] Remove quotes from type annotation 61 61 | x: List[List[List["MyClass"]]] 62 62 | -UP037.py:61:19: UP037 [*] Remove quotes from type annotation +UP037_0.py:61:19: UP037 [*] Remove quotes from type annotation | 59 | x: VarArg("str") 60 | @@ -367,7 +367,7 @@ UP037.py:61:19: UP037 [*] Remove quotes from type annotation 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | -UP037.py:63:29: UP037 [*] Remove quotes from type annotation +UP037_0.py:63:29: UP037 [*] Remove quotes from type annotation | 61 | x: List[List[List["MyClass"]]] 62 | @@ -388,7 +388,7 @@ UP037.py:63:29: UP037 [*] Remove quotes from type annotation 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | -UP037.py:63:45: UP037 [*] Remove quotes from type annotation +UP037_0.py:63:45: UP037 [*] Remove quotes from type annotation | 61 | x: List[List[List["MyClass"]]] 62 | @@ -409,7 +409,7 @@ UP037.py:63:45: UP037 [*] Remove quotes from type annotation 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | -UP037.py:65:29: UP037 [*] Remove quotes from type annotation +UP037_0.py:65:29: UP037 [*] Remove quotes from type annotation | 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 | @@ -430,7 +430,7 @@ UP037.py:65:29: UP037 [*] Remove quotes from type annotation 67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")]) 68 68 | -UP037.py:65:36: UP037 [*] Remove quotes from type annotation +UP037_0.py:65:36: UP037 [*] Remove quotes from type annotation | 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 | @@ -451,7 +451,7 @@ UP037.py:65:36: UP037 [*] Remove quotes from type annotation 67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")]) 68 68 | -UP037.py:65:45: UP037 [*] Remove quotes from type annotation +UP037_0.py:65:45: UP037 [*] Remove quotes from type annotation | 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 | @@ -472,7 +472,7 @@ UP037.py:65:45: UP037 [*] Remove quotes from type annotation 67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")]) 68 68 | -UP037.py:65:52: UP037 [*] Remove quotes from type annotation +UP037_0.py:65:52: UP037 [*] Remove quotes from type annotation | 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 | @@ -493,7 +493,7 @@ UP037.py:65:52: UP037 [*] Remove quotes from type annotation 67 67 | x: NamedTuple(typename="X", fields=[("foo", "int")]) 68 68 | -UP037.py:67:24: UP037 [*] Remove quotes from type annotation +UP037_0.py:67:24: UP037 [*] Remove quotes from type annotation | 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 | @@ -514,7 +514,7 @@ UP037.py:67:24: UP037 [*] Remove quotes from type annotation 69 69 | X: MyCallable("X") 70 70 | -UP037.py:67:38: UP037 [*] Remove quotes from type annotation +UP037_0.py:67:38: UP037 [*] Remove quotes from type annotation | 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 | @@ -535,7 +535,7 @@ UP037.py:67:38: UP037 [*] Remove quotes from type annotation 69 69 | X: MyCallable("X") 70 70 | -UP037.py:67:45: UP037 [*] Remove quotes from type annotation +UP037_0.py:67:45: UP037 [*] Remove quotes from type annotation | 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 | @@ -554,6 +554,4 @@ UP037.py:67:45: UP037 [*] Remove quotes from type annotation 67 |+x: NamedTuple(typename="X", fields=[("foo", int)]) 68 68 | 69 69 | X: MyCallable("X") -70 70 | - - +70 70 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap new file mode 100644 index 0000000000000..57379b20edbb2 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP037_1.py:9:8: UP037 [*] Remove quotes from type annotation + | + 7 | def foo(): + 8 | # UP037 + 9 | x: "Tuple[int, int]" = (0, 0) + | ^^^^^^^^^^^^^^^^^ UP037 +10 | print(x) + | + = help: Remove quotes + +ℹ Safe fix +6 6 | +7 7 | def foo(): +8 8 | # UP037 +9 |- x: "Tuple[int, int]" = (0, 0) + 9 |+ x: Tuple[int, int] = (0, 0) +10 10 | print(x) +11 11 | +12 12 |