Skip to content

Commit 718e394

Browse files
authored
Add rule to upgrade type alias annotations to keyword (UP040) (#6289)
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
1 parent c75e8a8 commit 718e394

File tree

11 files changed

+214
-7
lines changed

11 files changed

+214
-7
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import typing
2+
from typing import TypeAlias
3+
4+
# UP040
5+
x: typing.TypeAlias = int
6+
x: TypeAlias = int
7+
8+
9+
# UP040 with generics (todo)
10+
T = typing.TypeVar["T"]
11+
x: typing.TypeAlias = list[T]
12+
13+
14+
# OK
15+
x: TypeAlias
16+
x: int = 1

crates/ruff/src/checkers/ast/analyze/statement.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,12 +1362,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
13621362
}
13631363
}
13641364
}
1365-
Stmt::AnnAssign(ast::StmtAnnAssign {
1366-
target,
1367-
value,
1368-
annotation,
1369-
..
1370-
}) => {
1365+
Stmt::AnnAssign(
1366+
assign_stmt @ ast::StmtAnnAssign {
1367+
target,
1368+
value,
1369+
annotation,
1370+
..
1371+
},
1372+
) => {
13711373
if let Some(value) = value {
13721374
if checker.enabled(Rule::LambdaAssignment) {
13731375
pycodestyle::rules::lambda_assignment(
@@ -1390,6 +1392,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
13901392
stmt,
13911393
);
13921394
}
1395+
if checker.enabled(Rule::NonPEP695TypeAlias) {
1396+
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
1397+
}
13931398
if checker.is_stub {
13941399
if let Some(value) = value {
13951400
if checker.enabled(Rule::AssignmentDefaultInStub) {

crates/ruff/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
439439
(Pyupgrade, "037") => (RuleGroup::Unspecified, rules::pyupgrade::rules::QuotedAnnotation),
440440
(Pyupgrade, "038") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP604Isinstance),
441441
(Pyupgrade, "039") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UnnecessaryClassParentheses),
442+
(Pyupgrade, "040") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP695TypeAlias),
442443

443444
// pydocstyle
444445
(Pydocstyle, "100") => (RuleGroup::Unspecified, rules::pydocstyle::rules::UndocumentedPublicModule),

crates/ruff/src/rules/pyflakes/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3422,7 +3422,7 @@ mod tests {
34223422
}
34233423

34243424
#[test]
3425-
fn type_alias_annotations() {
3425+
fn use_pep695_type_aliass() {
34263426
flakes(
34273427
r#"
34283428
from typing_extensions import TypeAlias

crates/ruff/src/rules/pyupgrade/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,32 @@ mod tests {
8888
Ok(())
8989
}
9090

91+
#[test]
92+
fn non_pep695_type_alias_not_applied_py311() -> Result<()> {
93+
let diagnostics = test_path(
94+
Path::new("pyupgrade/UP040.py"),
95+
&settings::Settings {
96+
target_version: PythonVersion::Py311,
97+
..settings::Settings::for_rule(Rule::NonPEP695TypeAlias)
98+
},
99+
)?;
100+
assert_messages!(diagnostics);
101+
Ok(())
102+
}
103+
104+
#[test]
105+
fn non_pep695_type_alias_py312() -> Result<()> {
106+
let diagnostics = test_path(
107+
Path::new("pyupgrade/UP040.py"),
108+
&settings::Settings {
109+
target_version: PythonVersion::Py312,
110+
..settings::Settings::for_rule(Rule::NonPEP695TypeAlias)
111+
},
112+
)?;
113+
assert_messages!(diagnostics);
114+
Ok(())
115+
}
116+
91117
#[test]
92118
fn future_annotations_keep_runtime_typing_p37() -> Result<()> {
93119
let diagnostics = test_path(

crates/ruff/src/rules/pyupgrade/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub(crate) use unpacked_list_comprehension::*;
3232
pub(crate) use use_pep585_annotation::*;
3333
pub(crate) use use_pep604_annotation::*;
3434
pub(crate) use use_pep604_isinstance::*;
35+
pub(crate) use use_pep695_type_alias::*;
3536
pub(crate) use useless_metaclass_type::*;
3637
pub(crate) use useless_object_inheritance::*;
3738
pub(crate) use yield_in_for_loop::*;
@@ -70,6 +71,7 @@ mod unpacked_list_comprehension;
7071
mod use_pep585_annotation;
7172
mod use_pep604_annotation;
7273
mod use_pep604_isinstance;
74+
mod use_pep695_type_alias;
7375
mod useless_metaclass_type;
7476
mod useless_object_inheritance;
7577
mod yield_in_for_loop;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use ruff_python_ast::{Expr, ExprName, Ranged, Stmt, StmtAnnAssign, StmtTypeAlias};
2+
3+
use crate::{registry::AsRule, settings::types::PythonVersion};
4+
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
5+
use ruff_macros::{derive_message_formats, violation};
6+
use ruff_text_size::TextRange;
7+
8+
use crate::checkers::ast::Checker;
9+
10+
/// ## What it does
11+
/// Checks for use of `TypeAlias` annotation for declaring type aliases.
12+
///
13+
/// ## Why is this bad?
14+
/// The `type` keyword was introduced in Python 3.12 by PEP-695 for defining type aliases.
15+
/// The type keyword is easier to read and provides cleaner support for generics.
16+
///
17+
/// ## Example
18+
/// ```python
19+
/// ListOfInt: TypeAlias = list[int]
20+
/// ```
21+
///
22+
/// Use instead:
23+
/// ```python
24+
/// type ListOfInt = list[int]
25+
/// ```
26+
#[violation]
27+
pub struct NonPEP695TypeAlias {
28+
name: String,
29+
}
30+
31+
impl Violation for NonPEP695TypeAlias {
32+
const AUTOFIX: AutofixKind = AutofixKind::Always;
33+
34+
#[derive_message_formats]
35+
fn message(&self) -> String {
36+
let NonPEP695TypeAlias { name } = self;
37+
format!("Type alias `{name}` uses `TypeAlias` annotation instead of the `type` keyword")
38+
}
39+
40+
fn autofix_title(&self) -> Option<String> {
41+
Some("Use the `type` keyword".to_string())
42+
}
43+
}
44+
45+
/// UP040
46+
pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign) {
47+
let StmtAnnAssign {
48+
target,
49+
annotation,
50+
value,
51+
..
52+
} = stmt;
53+
54+
// Syntax only available in 3.12+
55+
if checker.settings.target_version < PythonVersion::Py312 {
56+
return;
57+
}
58+
59+
if !checker
60+
.semantic()
61+
.match_typing_expr(annotation, "TypeAlias")
62+
{
63+
return;
64+
}
65+
66+
let Expr::Name(ExprName { id: name, .. }) = target.as_ref() else {
67+
return;
68+
};
69+
70+
let Some(value) = value else {
71+
return;
72+
};
73+
74+
// TODO(zanie): We should check for generic type variables used in the value and define them
75+
// as type params instead
76+
let mut diagnostic = Diagnostic::new(NonPEP695TypeAlias { name: name.clone() }, stmt.range());
77+
if checker.patch(diagnostic.kind.rule()) {
78+
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
79+
checker.generator().stmt(&Stmt::from(StmtTypeAlias {
80+
range: TextRange::default(),
81+
name: target.clone(),
82+
type_params: None,
83+
value: value.clone(),
84+
})),
85+
stmt.range(),
86+
)));
87+
}
88+
checker.diagnostics.push(diagnostic);
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
source: crates/ruff/src/rules/pyupgrade/mod.rs
3+
---
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
source: crates/ruff/src/rules/pyupgrade/mod.rs
3+
---
4+
UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
5+
|
6+
4 | # UP040
7+
5 | x: typing.TypeAlias = int
8+
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
9+
6 | x: TypeAlias = int
10+
|
11+
= help: Use the `type` keyword
12+
13+
Fix
14+
2 2 | from typing import TypeAlias
15+
3 3 |
16+
4 4 | # UP040
17+
5 |-x: typing.TypeAlias = int
18+
5 |+type x = int
19+
6 6 | x: TypeAlias = int
20+
7 7 |
21+
8 8 |
22+
23+
UP040.py:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
24+
|
25+
4 | # UP040
26+
5 | x: typing.TypeAlias = int
27+
6 | x: TypeAlias = int
28+
| ^^^^^^^^^^^^^^^^^^ UP040
29+
|
30+
= help: Use the `type` keyword
31+
32+
ℹ Fix
33+
3 3 |
34+
4 4 | # UP040
35+
5 5 | x: typing.TypeAlias = int
36+
6 |-x: TypeAlias = int
37+
6 |+type x = int
38+
7 7 |
39+
8 8 |
40+
9 9 | # UP040 with generics (todo)
41+
42+
UP040.py:11:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
43+
|
44+
9 | # UP040 with generics (todo)
45+
10 | T = typing.TypeVar["T"]
46+
11 | x: typing.TypeAlias = list[T]
47+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP040
48+
|
49+
= help: Use the `type` keyword
50+
51+
ℹ Fix
52+
8 8 |
53+
9 9 | # UP040 with generics (todo)
54+
10 10 | T = typing.TypeVar["T"]
55+
11 |-x: typing.TypeAlias = list[T]
56+
11 |+type x = list[T]
57+
12 12 |
58+
13 13 |
59+
14 14 | # OK
60+
61+

ruff.schema.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)