diff --git a/README.md b/README.md index 485035dd61..7ba20e9bf5 100644 --- a/README.md +++ b/README.md @@ -857,6 +857,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI. | UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 | | UP035 | import-replacements | Import from `{module}` instead: {names} | 🛠 | | UP036 | outdated-version-block | Version block is outdated for minimum Python version | 🛠 | +| UP037 | quoted-annotation | Remove quotes from type annotation | 🛠 | ### flake8-2020 (YTT) diff --git a/resources/test/fixtures/pyupgrade/UP037.py b/resources/test/fixtures/pyupgrade/UP037.py new file mode 100644 index 0000000000..825df3323c --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP037.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import ( + Annotated, + Callable, + List, + Literal, + NamedTuple, + Tuple, + TypeVar, + TypedDict, + cast, +) + +from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg + + +def foo(var: "MyClass") -> "MyClass": + x: "MyClass" + + +def foo(*, inplace: "bool"): + pass + + +def foo(*args: "str", **kwargs: "int"): + pass + + +x: Tuple["MyClass"] + +x: Callable[["MyClass"], None] + + +class Foo(NamedTuple): + x: "MyClass" + + +class D(TypedDict): + E: TypedDict("E", foo="int", total=False) + + +class D(TypedDict): + E: TypedDict("E", {"foo": "int"}) + + +x: Annotated["str", "metadata"] + +x: Arg("str", "name") + +x: DefaultArg("str", "name") + +x: NamedArg("str", "name") + +x: DefaultNamedArg("str", "name") + +x: DefaultNamedArg("str", name="name") + +x: VarArg("str") + +x: List[List[List["MyClass"]]] + +x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) + +x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) + +x: NamedTuple(typename="X", fields=[("foo", "int")]) + +x = TypeVar("x", "str", "int") + +x = cast("str", x) + +X = List["MyClass"] + +X: MyCallable("X") + + +# OK +class D(TypedDict): + E: TypedDict("E") + + +x: Annotated[()] + +x: DefaultNamedArg(name="name", quox="str") + +x: DefaultNamedArg(name="name") + +x: NamedTuple("X", [("foo",), ("bar",)]) + +x: NamedTuple("X", ["foo", "bar"]) + +x: NamedTuple() + +x: Literal["foo", "bar"] + +x = cast(x, "str") + + +def foo(x, *args, **kwargs): + ... + + +def foo(*, inplace): + ... + + +x: Annotated[1:2] = ... diff --git a/ruff.schema.json b/ruff.schema.json index b667e0594f..e18e7b2722 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1950,6 +1950,7 @@ "UP034", "UP035", "UP036", + "UP037", "W", "W2", "W29", diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index 933e133f43..314535395a 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -3349,20 +3349,37 @@ where Some(Callable::MypyExtension) => { self.visit_expr(func); - // Ex) DefaultNamedArg(bool | None, name="some_prop_name") - let mut arguments = args.iter().chain(keywords.iter().map(|keyword| { - let KeywordData { value, .. } = &keyword.node; - value - })); - if let Some(expr) = arguments.next() { + if let Some(arg) = args.first() { + // Ex) DefaultNamedArg(bool | None, name="some_prop_name") self.in_type_definition = true; - self.visit_expr(expr); - self.in_type_definition = prev_in_type_definition; - } - for expr in arguments { - self.in_type_definition = false; - self.visit_expr(expr); + self.visit_expr(arg); self.in_type_definition = prev_in_type_definition; + + for arg in args.iter().skip(1) { + self.in_type_definition = false; + self.visit_expr(arg); + self.in_type_definition = prev_in_type_definition; + } + for keyword in keywords { + let KeywordData { value, .. } = &keyword.node; + self.in_type_definition = false; + self.visit_expr(value); + self.in_type_definition = prev_in_type_definition; + } + } else { + // Ex) DefaultNamedArg(type="bool", name="some_prop_name") + for keyword in keywords { + let KeywordData { value, arg, .. } = &keyword.node; + if arg.as_ref().map_or(false, |arg| arg == "type") { + self.in_type_definition = true; + self.visit_expr(value); + self.in_type_definition = prev_in_type_definition; + } else { + self.in_type_definition = false; + self.visit_expr(value); + self.in_type_definition = prev_in_type_definition; + } + } } } None => { @@ -4366,6 +4383,11 @@ impl<'a> Checker<'a> { self.deferred_string_type_definitions.pop() { if let Ok(mut expr) = parser::parse_expression(expression, "") { + if self.annotations_future_enabled { + if self.settings.rules.enabled(&Rule::QuotedAnnotation) { + pyupgrade::rules::quoted_annotation(self, expression, range); + } + } relocate_expr(&mut expr, range); allocator.push(expr); stacks.push((in_annotation, context)); diff --git a/src/registry.rs b/src/registry.rs index 5c19d40b63..1403e4cc9d 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -260,6 +260,7 @@ ruff_macros::define_rule_mapping!( UP034 => rules::pyupgrade::rules::ExtraneousParentheses, UP035 => rules::pyupgrade::rules::ImportReplacements, UP036 => rules::pyupgrade::rules::OutdatedVersionBlock, + UP037 => rules::pyupgrade::rules::QuotedAnnotation, // pydocstyle D100 => rules::pydocstyle::rules::PublicModule, D101 => rules::pydocstyle::rules::PublicClass, diff --git a/src/rules/pyupgrade/mod.rs b/src/rules/pyupgrade/mod.rs index 86ccbcf2f0..334226d182 100644 --- a/src/rules/pyupgrade/mod.rs +++ b/src/rules/pyupgrade/mod.rs @@ -64,6 +64,7 @@ mod tests { #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_2.py"); "UP036_2")] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_3.py"); "UP036_3")] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_4.py"); "UP036_4")] + #[test_case(Rule::QuotedAnnotation, Path::new("UP037.py"); "UP037")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/src/rules/pyupgrade/rules/mod.rs b/src/rules/pyupgrade/rules/mod.rs index 8875beaa33..9bb6adeed5 100644 --- a/src/rules/pyupgrade/rules/mod.rs +++ b/src/rules/pyupgrade/rules/mod.rs @@ -19,6 +19,7 @@ pub(crate) use open_alias::{open_alias, OpenAlias}; pub(crate) use os_error_alias::{os_error_alias, OSErrorAlias}; pub(crate) use outdated_version_block::{outdated_version_block, OutdatedVersionBlock}; pub(crate) use printf_string_formatting::{printf_string_formatting, PrintfStringFormatting}; +pub(crate) use quoted_annotation::{quoted_annotation, QuotedAnnotation}; pub(crate) use redundant_open_modes::{redundant_open_modes, RedundantOpenModes}; pub(crate) use replace_stdout_stderr::{replace_stdout_stderr, ReplaceStdoutStderr}; pub(crate) use replace_universal_newlines::{replace_universal_newlines, ReplaceUniversalNewlines}; @@ -59,6 +60,7 @@ mod open_alias; mod os_error_alias; mod outdated_version_block; mod printf_string_formatting; +mod quoted_annotation; mod redundant_open_modes; mod replace_stdout_stderr; mod replace_universal_newlines; diff --git a/src/rules/pyupgrade/rules/quoted_annotation.rs b/src/rules/pyupgrade/rules/quoted_annotation.rs new file mode 100644 index 0000000000..a4ae7486d6 --- /dev/null +++ b/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -0,0 +1,35 @@ +use ruff_macros::derive_message_formats; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::define_violation; +use crate::fix::Fix; +use crate::registry::{Diagnostic, Rule}; +use crate::violation::AlwaysAutofixableViolation; + +define_violation!( + pub struct QuotedAnnotation; +); +impl AlwaysAutofixableViolation for QuotedAnnotation { + #[derive_message_formats] + fn message(&self) -> String { + format!("Remove quotes from type annotation") + } + + fn autofix_title(&self) -> String { + "Remove quotes".to_string() + } +} + +/// UP037 +pub fn quoted_annotation(checker: &mut Checker, annotation: &str, range: Range) { + let mut diagnostic = Diagnostic::new(QuotedAnnotation, range); + if checker.patch(&Rule::QuotedAnnotation) { + diagnostic.amend(Fix::replacement( + annotation.to_string(), + range.location, + range.end_location, + )); + } + checker.diagnostics.push(diagnostic); +} diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037_UP037.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037_UP037.py.snap new file mode 100644 index 0000000000..9e6ea7d087 --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037_UP037.py.snap @@ -0,0 +1,599 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + QuotedAnnotation: ~ + location: + row: 18 + column: 13 + end_location: + row: 18 + column: 22 + fix: + content: + - MyClass + location: + row: 18 + column: 13 + end_location: + row: 18 + column: 22 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 18 + column: 27 + end_location: + row: 18 + column: 36 + fix: + content: + - MyClass + location: + row: 18 + column: 27 + end_location: + row: 18 + column: 36 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 19 + column: 7 + end_location: + row: 19 + column: 16 + fix: + content: + - MyClass + location: + row: 19 + column: 7 + end_location: + row: 19 + column: 16 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 22 + column: 20 + end_location: + row: 22 + column: 26 + fix: + content: + - bool + location: + row: 22 + column: 20 + end_location: + row: 22 + column: 26 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 26 + column: 15 + end_location: + row: 26 + column: 20 + fix: + content: + - str + location: + row: 26 + column: 15 + end_location: + row: 26 + column: 20 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 26 + column: 32 + end_location: + row: 26 + column: 37 + fix: + content: + - int + location: + row: 26 + column: 32 + end_location: + row: 26 + column: 37 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 30 + column: 9 + end_location: + row: 30 + column: 18 + fix: + content: + - MyClass + location: + row: 30 + column: 9 + end_location: + row: 30 + column: 18 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 32 + column: 13 + end_location: + row: 32 + column: 22 + fix: + content: + - MyClass + location: + row: 32 + column: 13 + end_location: + row: 32 + column: 22 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 36 + column: 7 + end_location: + row: 36 + column: 16 + fix: + content: + - MyClass + location: + row: 36 + column: 7 + end_location: + row: 36 + column: 16 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 40 + column: 26 + end_location: + row: 40 + column: 31 + fix: + content: + - int + location: + row: 40 + column: 26 + end_location: + row: 40 + column: 31 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 44 + column: 30 + end_location: + row: 44 + column: 35 + fix: + content: + - int + location: + row: 44 + column: 30 + end_location: + row: 44 + column: 35 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 47 + column: 13 + end_location: + row: 47 + column: 18 + fix: + content: + - str + location: + row: 47 + column: 13 + end_location: + row: 47 + column: 18 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 49 + column: 7 + end_location: + row: 49 + column: 12 + fix: + content: + - str + location: + row: 49 + column: 7 + end_location: + row: 49 + column: 12 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 51 + column: 14 + end_location: + row: 51 + column: 19 + fix: + content: + - str + location: + row: 51 + column: 14 + end_location: + row: 51 + column: 19 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 53 + column: 12 + end_location: + row: 53 + column: 17 + fix: + content: + - str + location: + row: 53 + column: 12 + end_location: + row: 53 + column: 17 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 55 + column: 19 + end_location: + row: 55 + column: 24 + fix: + content: + - str + location: + row: 55 + column: 19 + end_location: + row: 55 + column: 24 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 57 + column: 19 + end_location: + row: 57 + column: 24 + fix: + content: + - str + location: + row: 57 + column: 19 + end_location: + row: 57 + column: 24 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 59 + column: 10 + end_location: + row: 59 + column: 15 + fix: + content: + - str + location: + row: 59 + column: 10 + end_location: + row: 59 + column: 15 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 61 + column: 18 + end_location: + row: 61 + column: 27 + fix: + content: + - MyClass + location: + row: 61 + column: 18 + end_location: + row: 61 + column: 27 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 63 + column: 28 + end_location: + row: 63 + column: 33 + fix: + content: + - int + location: + row: 63 + column: 28 + end_location: + row: 63 + column: 33 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 63 + column: 44 + end_location: + row: 63 + column: 49 + fix: + content: + - str + location: + row: 63 + column: 44 + end_location: + row: 63 + column: 49 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 65 + column: 28 + end_location: + row: 65 + column: 33 + fix: + content: + - foo + location: + row: 65 + column: 28 + end_location: + row: 65 + column: 33 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 65 + column: 35 + end_location: + row: 65 + column: 40 + fix: + content: + - int + location: + row: 65 + column: 35 + end_location: + row: 65 + column: 40 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 65 + column: 44 + end_location: + row: 65 + column: 49 + fix: + content: + - bar + location: + row: 65 + column: 44 + end_location: + row: 65 + column: 49 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 65 + column: 51 + end_location: + row: 65 + column: 56 + fix: + content: + - str + location: + row: 65 + column: 51 + end_location: + row: 65 + column: 56 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 67 + column: 23 + end_location: + row: 67 + column: 26 + fix: + content: + - X + location: + row: 67 + column: 23 + end_location: + row: 67 + column: 26 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 67 + column: 37 + end_location: + row: 67 + column: 42 + fix: + content: + - foo + location: + row: 67 + column: 37 + end_location: + row: 67 + column: 42 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 67 + column: 44 + end_location: + row: 67 + column: 49 + fix: + content: + - int + location: + row: 67 + column: 44 + end_location: + row: 67 + column: 49 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 69 + column: 17 + end_location: + row: 69 + column: 22 + fix: + content: + - str + location: + row: 69 + column: 17 + end_location: + row: 69 + column: 22 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 69 + column: 24 + end_location: + row: 69 + column: 29 + fix: + content: + - int + location: + row: 69 + column: 24 + end_location: + row: 69 + column: 29 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 71 + column: 9 + end_location: + row: 71 + column: 14 + fix: + content: + - str + location: + row: 71 + column: 9 + end_location: + row: 71 + column: 14 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 73 + column: 9 + end_location: + row: 73 + column: 18 + fix: + content: + - MyClass + location: + row: 73 + column: 9 + end_location: + row: 73 + column: 18 + parent: ~ +- kind: + QuotedAnnotation: ~ + location: + row: 75 + column: 14 + end_location: + row: 75 + column: 17 + fix: + content: + - X + location: + row: 75 + column: 14 + end_location: + row: 75 + column: 17 + parent: ~ +