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 0000000000000..69854a8af75f4 --- /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 55e34d28cdb1d..bfa5cda197557 100644 --- a/crates/ruff/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff/src/checkers/ast/analyze/statement.rs @@ -1346,12 +1346,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( @@ -1374,6 +1376,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 64e795cde81a3..8a246d42427a2 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -438,6 +438,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 7eb0434f31ef6..b37f23c7b5462 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 0ec3c25e4b7db..7ffeb2b1165ff 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 16ca52a57b49b..0a924c145120d 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 0000000000000..64d00f6a80cf9 --- /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 0000000000000..870ad3bf5d625 --- /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 0000000000000..c46a4b8879b8f --- /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 96ff8fe0a8fd5..15a0e5a35a60f 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2666,6 +2666,8 @@ "UP037", "UP038", "UP039", + "UP04", + "UP040", "W", "W1", "W19", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index c18d7a86c91b5..a60c992de6102 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -82,6 +82,7 @@ "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",