From 718e3945e37d7d7f74318097824c0091bdf734ac Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 3 Aug 2023 11:13:06 -0500 Subject: [PATCH] Add rule to upgrade type alias annotations to keyword (UP040) (#6289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds rule to convert type aliases defined with annotations i.e. `x: TypeAlias = int` to the new PEP-695 syntax e.g. `type x = int`. Does not support using new generic syntax for type variables, will be addressed in a follow-up. Added as part of pyupgrade — ~the code 100 as chosen to avoid collision with real pyupgrade codes~. Part of #4617 Builds on #5062 --- .../test/fixtures/pyupgrade/UP040.py | 16 ++++ .../src/checkers/ast/analyze/statement.rs | 17 ++-- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/pyflakes/mod.rs | 2 +- crates/ruff/src/rules/pyupgrade/mod.rs | 26 ++++++ crates/ruff/src/rules/pyupgrade/rules/mod.rs | 2 + .../pyupgrade/rules/use_pep695_type_alias.rs | 89 +++++++++++++++++++ ...n_pep695_type_alias_not_applied_py311.snap | 4 + ...e__tests__non_pep695_type_alias_py312.snap | 61 +++++++++++++ ruff.schema.json | 2 + scripts/check_docs_formatted.py | 1 + 11 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pyupgrade/UP040.py create mode 100644 crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs create mode 100644 crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_not_applied_py311.snap create mode 100644 crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_py312.snap diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP040.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP040.py new file mode 100644 index 0000000000..69854a8af7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP040.py @@ -0,0 +1,16 @@ +import typing +from typing import TypeAlias + +# UP040 +x: typing.TypeAlias = int +x: TypeAlias = int + + +# UP040 with generics (todo) +T = typing.TypeVar["T"] +x: typing.TypeAlias = list[T] + + +# OK +x: TypeAlias +x: int = 1 diff --git a/crates/ruff/src/checkers/ast/analyze/statement.rs b/crates/ruff/src/checkers/ast/analyze/statement.rs index 1a1910dc7a..dc88a36f6e 100644 --- a/crates/ruff/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff/src/checkers/ast/analyze/statement.rs @@ -1362,12 +1362,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - Stmt::AnnAssign(ast::StmtAnnAssign { - target, - value, - annotation, - .. - }) => { + Stmt::AnnAssign( + assign_stmt @ ast::StmtAnnAssign { + target, + value, + annotation, + .. + }, + ) => { if let Some(value) = value { if checker.enabled(Rule::LambdaAssignment) { pycodestyle::rules::lambda_assignment( @@ -1390,6 +1392,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { stmt, ); } + if checker.enabled(Rule::NonPEP695TypeAlias) { + pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); + } if checker.is_stub { if let Some(value) = value { if checker.enabled(Rule::AssignmentDefaultInStub) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 7874bb8bbb..f908c2f88f 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -439,6 +439,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "037") => (RuleGroup::Unspecified, rules::pyupgrade::rules::QuotedAnnotation), (Pyupgrade, "038") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP604Isinstance), (Pyupgrade, "039") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UnnecessaryClassParentheses), + (Pyupgrade, "040") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP695TypeAlias), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Unspecified, rules::pydocstyle::rules::UndocumentedPublicModule), diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 7eb0434f31..b37f23c7b5 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -3422,7 +3422,7 @@ mod tests { } #[test] - fn type_alias_annotations() { + fn use_pep695_type_aliass() { flakes( r#" from typing_extensions import TypeAlias diff --git a/crates/ruff/src/rules/pyupgrade/mod.rs b/crates/ruff/src/rules/pyupgrade/mod.rs index 0ec3c25e4b..7ffeb2b116 100644 --- a/crates/ruff/src/rules/pyupgrade/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/mod.rs @@ -88,6 +88,32 @@ mod tests { Ok(()) } + #[test] + fn non_pep695_type_alias_not_applied_py311() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/UP040.py"), + &settings::Settings { + target_version: PythonVersion::Py311, + ..settings::Settings::for_rule(Rule::NonPEP695TypeAlias) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn non_pep695_type_alias_py312() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/UP040.py"), + &settings::Settings { + target_version: PythonVersion::Py312, + ..settings::Settings::for_rule(Rule::NonPEP695TypeAlias) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test] fn future_annotations_keep_runtime_typing_p37() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff/src/rules/pyupgrade/rules/mod.rs b/crates/ruff/src/rules/pyupgrade/rules/mod.rs index 16ca52a57b..0a924c1451 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/mod.rs @@ -32,6 +32,7 @@ pub(crate) use unpacked_list_comprehension::*; pub(crate) use use_pep585_annotation::*; pub(crate) use use_pep604_annotation::*; pub(crate) use use_pep604_isinstance::*; +pub(crate) use use_pep695_type_alias::*; pub(crate) use useless_metaclass_type::*; pub(crate) use useless_object_inheritance::*; pub(crate) use yield_in_for_loop::*; @@ -70,6 +71,7 @@ mod unpacked_list_comprehension; mod use_pep585_annotation; mod use_pep604_annotation; mod use_pep604_isinstance; +mod use_pep695_type_alias; mod useless_metaclass_type; mod useless_object_inheritance; mod yield_in_for_loop; diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs new file mode 100644 index 0000000000..64d00f6a80 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs @@ -0,0 +1,89 @@ +use ruff_python_ast::{Expr, ExprName, Ranged, Stmt, StmtAnnAssign, StmtTypeAlias}; + +use crate::{registry::AsRule, settings::types::PythonVersion}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_text_size::TextRange; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for use of `TypeAlias` annotation for declaring type aliases. +/// +/// ## Why is this bad? +/// The `type` keyword was introduced in Python 3.12 by PEP-695 for defining type aliases. +/// The type keyword is easier to read and provides cleaner support for generics. +/// +/// ## Example +/// ```python +/// ListOfInt: TypeAlias = list[int] +/// ``` +/// +/// Use instead: +/// ```python +/// type ListOfInt = list[int] +/// ``` +#[violation] +pub struct NonPEP695TypeAlias { + name: String, +} + +impl Violation for NonPEP695TypeAlias { + const AUTOFIX: AutofixKind = AutofixKind::Always; + + #[derive_message_formats] + fn message(&self) -> String { + let NonPEP695TypeAlias { name } = self; + format!("Type alias `{name}` uses `TypeAlias` annotation instead of the `type` keyword") + } + + fn autofix_title(&self) -> Option { + Some("Use the `type` keyword".to_string()) + } +} + +/// UP040 +pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) { + let StmtAnnAssign { + target, + annotation, + value, + .. + } = stmt; + + // Syntax only available in 3.12+ + if checker.settings.target_version < PythonVersion::Py312 { + return; + } + + if !checker + .semantic() + .match_typing_expr(annotation, "TypeAlias") + { + return; + } + + let Expr::Name(ExprName { id: name, .. }) = target.as_ref() else { + return; + }; + + let Some(value) = value else { + return; + }; + + // TODO(zanie): We should check for generic type variables used in the value and define them + // as type params instead + let mut diagnostic = Diagnostic::new(NonPEP695TypeAlias { name: name.clone() }, stmt.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + checker.generator().stmt(&Stmt::from(StmtTypeAlias { + range: TextRange::default(), + name: target.clone(), + type_params: None, + value: value.clone(), + })), + stmt.range(), + ))); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_not_applied_py311.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_not_applied_py311.snap new file mode 100644 index 0000000000..870ad3bf5d --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_not_applied_py311.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_py312.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_py312.snap new file mode 100644 index 0000000000..c46a4b8879 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__non_pep695_type_alias_py312.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- +UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword + | +4 | # UP040 +5 | x: typing.TypeAlias = int + | ^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 +6 | x: TypeAlias = int + | + = help: Use the `type` keyword + +ℹ Fix +2 2 | from typing import TypeAlias +3 3 | +4 4 | # UP040 +5 |-x: typing.TypeAlias = int + 5 |+type x = int +6 6 | x: TypeAlias = int +7 7 | +8 8 | + +UP040.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword + | +4 | # UP040 +5 | x: typing.TypeAlias = int +6 | x: TypeAlias = int + | ^^^^^^^^^^^^^^^^^^ UP040 + | + = help: Use the `type` keyword + +ℹ Fix +3 3 | +4 4 | # UP040 +5 5 | x: typing.TypeAlias = int +6 |-x: TypeAlias = int + 6 |+type x = int +7 7 | +8 8 | +9 9 | # UP040 with generics (todo) + +UP040.py:11:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword + | + 9 | # UP040 with generics (todo) +10 | T = typing.TypeVar["T"] +11 | x: typing.TypeAlias = list[T] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040 + | + = help: Use the `type` keyword + +ℹ Fix +8 8 | +9 9 | # UP040 with generics (todo) +10 10 | T = typing.TypeVar["T"] +11 |-x: typing.TypeAlias = list[T] + 11 |+type x = list[T] +12 12 | +13 13 | +14 14 | # OK + + diff --git a/ruff.schema.json b/ruff.schema.json index 6ca8426ed0..fc25a95e01 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2668,6 +2668,8 @@ "UP037", "UP038", "UP039", + "UP04", + "UP040", "W", "W1", "W19", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index c18d7a86c9..a60c992de6 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -82,6 +82,7 @@ KNOWN_PARSE_ERRORS = [ "missing-newline-at-end-of-file", "mixed-spaces-and-tabs", "no-indented-block", + "non-pep695-type-alias", # requires Python 3.12 "tab-after-comma", "tab-after-keyword", "tab-after-operator",