Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule to upgrade type alias annotations to keyword (UP040) #6289

Merged
merged 7 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/ruff/resources/test/fixtures/ruff/RUF017.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import typing
from typing import TypeAlias

# RUF017
x: typing.TypeAlias = int
x: TypeAlias = int


# RUF017 with generics (todo)
T = typing.TypeVar["T"]
x: typing.TypeAlias = list[T]
zanieb marked this conversation as resolved.
Show resolved Hide resolved


# 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 @@ -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(
Expand All @@ -1374,6 +1376,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
);
}
if checker.enabled(Rule::TypeAliasAnnotation) {
ruff::rules::type_alias_annotation(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 @@ -807,6 +807,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
(Ruff, "017") => (RuleGroup::Unspecified, rules::ruff::rules::TypeAliasAnnotation),
zanieb marked this conversation as resolved.
Show resolved Hide resolved
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),

Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod tests {
Path::new("RUF015.py")
)]
#[test_case(Rule::InvalidIndexType, Path::new("RUF016.py"))]
#[test_case(Rule::TypeAliasAnnotation, Path::new("RUF017.py"))]
#[cfg_attr(
feature = "unreachable-code",
test_case(Rule::UnreachableCode, Path::new("RUF014.py"))
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/ruff/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(crate) use mutable_class_default::*;
pub(crate) use mutable_dataclass_default::*;
pub(crate) use pairwise_over_zipped::*;
pub(crate) use static_key_dict_comprehension::*;
pub(crate) use type_alias_annotation::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
#[cfg(feature = "unreachable-code")]
pub(crate) use unreachable::*;
Expand All @@ -29,6 +30,7 @@ mod mutable_class_default;
mod mutable_dataclass_default;
mod pairwise_over_zipped;
mod static_key_dict_comprehension;
mod type_alias_annotation;
mod unnecessary_iterable_allocation_for_first_element;
#[cfg(feature = "unreachable-code")]
pub(crate) mod unreachable;
Expand Down
89 changes: 89 additions & 0 deletions crates/ruff/src/rules/ruff/rules/type_alias_annotation.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 TypeAliasAnnotation {
name: String,
}

impl Violation for TypeAliasAnnotation {
const AUTOFIX: AutofixKind = AutofixKind::Always;

#[derive_message_formats]
fn message(&self) -> String {
let TypeAliasAnnotation { name } = self;
format!("Type alias `{name}` uses `TypeAlias` annotation instead of the `type` keyword.")
zanieb marked this conversation as resolved.
Show resolved Hide resolved
}

fn autofix_title(&self) -> Option<String> {
Some("Use the `type` keyword instead".to_string())
zanieb marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// RUF017
pub(crate) fn type_alias_annotation(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
zanieb marked this conversation as resolved.
Show resolved Hide resolved
// as type params instead
let mut diagnostic = Diagnostic::new(TypeAliasAnnotation { 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,37 @@
---
source: crates/ruff/src/rules/ruff/mod.rs
---
RUF017.py:4:1: RUF017 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword.
|
2 | from typing import TypeAlias
3 |
4 | x: typing.TypeAlias = int
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF017
5 | x: TypeAlias = int
|
= help: Use the `type` keyword instead

ℹ Fix
1 1 | import typing
2 2 | from typing import TypeAlias
3 3 |
4 |-x: typing.TypeAlias = int
4 |+type x = int
5 5 | x: TypeAlias = int

RUF017.py:5:1: RUF017 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword.
|
4 | x: typing.TypeAlias = int
5 | x: TypeAlias = int
| ^^^^^^^^^^^^^^^^^^ RUF017
|
= help: Use the `type` keyword instead

ℹ Fix
2 2 | from typing import TypeAlias
3 3 |
4 4 | x: typing.TypeAlias = int
5 |-x: TypeAlias = int
5 |+type x = int


Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ if (
and [dddddddddddddd, eeeeeeeeee, fffffffffffffff]
):
pass

# Regression test for https://github.com/astral-sh/ruff/issues/6068
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or numpy and isinstance(ccccccccccc, dddddd)
):
pass

if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and numpy or isinstance(ccccccccccc, dddddd)
):
pass
```

## Output
Expand Down Expand Up @@ -136,6 +147,19 @@ if (
and [dddddddddddddd, eeeeeeeeee, fffffffffffffff]
):
pass

# Regression test for https://github.com/astral-sh/ruff/issues/6068
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
or numpy and isinstance(ccccccccccc, dddddd)
):
pass

if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and numpy
or isinstance(ccccccccccc, dddddd)
):
pass
```


Expand Down
3 changes: 2 additions & 1 deletion ruff.schema.json

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

Loading