Skip to content

Commit

Permalink
Add rule to upgrade type alias annotations to keyword (UP040) (#6289)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zanieb authored Aug 3, 2023
1 parent c75e8a8 commit 718e394
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 7 deletions.
16 changes: 16 additions & 0 deletions crates/ruff/resources/test/fixtures/pyupgrade/UP040.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 11 additions & 6 deletions crates/ruff/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3422,7 +3422,7 @@ mod tests {
}

#[test]
fn type_alias_annotations() {
fn use_pep695_type_aliass() {
flakes(
r#"
from typing_extensions import TypeAlias
Expand Down
26 changes: 26 additions & 0 deletions crates/ruff/src/rules/pyupgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/pyupgrade/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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;
89 changes: 89 additions & 0 deletions crates/ruff/src/rules/pyupgrade/rules/use_pep695_type_alias.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pyupgrade/mod.rs
---

Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions scripts/check_docs_formatted.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 718e394

Please sign in to comment.