mirror of https://github.com/astral-sh/ruff
[`pyupgrade`] Implement `unnecessary-default-type-args` (`UP043`) (#12371)
## Summary Add new rule and implement for `unnecessary default type arguments` under the `UP` category (`UP043`). ```py // < py313 Generator[int, None, None] // >= py313 Generator[int] ``` I think that as Python 3.13 develops, there might be more default type arguments added besides `Generator` and `AsyncGenerator`. So, I made this more flexible to accommodate future changes. related issue: #12286 ## Test Plan snapshot included..!
This commit is contained in:
parent
1435b0f022
commit
1df51b1fbf
|
|
@ -0,0 +1,41 @@
|
|||
from typing import Generator, AsyncGenerator
|
||||
|
||||
|
||||
def func() -> Generator[int, None, None]:
|
||||
yield 42
|
||||
|
||||
|
||||
def func() -> Generator[int, None]:
|
||||
yield 42
|
||||
|
||||
|
||||
def func() -> Generator[int]:
|
||||
yield 42
|
||||
|
||||
|
||||
def func() -> Generator[int, int, int]:
|
||||
foo = yield 42
|
||||
return foo
|
||||
|
||||
|
||||
def func() -> Generator[int, int, None]:
|
||||
_ = yield 42
|
||||
return None
|
||||
|
||||
|
||||
def func() -> Generator[int, None, int]:
|
||||
yield 42
|
||||
return 42
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[int, None]:
|
||||
yield 42
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[int]:
|
||||
yield 42
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[int, int]:
|
||||
foo = yield 42
|
||||
return foo
|
||||
|
|
@ -110,6 +110,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
|||
ruff::rules::never_union(checker, expr);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) {
|
||||
if checker.settings.target_version >= PythonVersion::Py313 {
|
||||
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
|
||||
}
|
||||
}
|
||||
|
||||
if checker.any_enabled(&[
|
||||
Rule::SysVersionSlice3,
|
||||
Rule::SysVersion2,
|
||||
|
|
|
|||
|
|
@ -521,6 +521,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias),
|
||||
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
|
||||
(Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum),
|
||||
(Pyupgrade, "043") => (RuleGroup::Preview, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),
|
||||
|
||||
// pydocstyle
|
||||
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ mod tests {
|
|||
#[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))]
|
||||
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))]
|
||||
#[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))]
|
||||
#[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))]
|
||||
#[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))]
|
||||
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))]
|
||||
#[test_case(Rule::UnpackedListComprehension, Path::new("UP027.py"))]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub(crate) use unicode_kind_prefix::*;
|
|||
pub(crate) use unnecessary_builtin_import::*;
|
||||
pub(crate) use unnecessary_class_parentheses::*;
|
||||
pub(crate) use unnecessary_coding_comment::*;
|
||||
pub(crate) use unnecessary_default_type_args::*;
|
||||
pub(crate) use unnecessary_encode_utf8::*;
|
||||
pub(crate) use unnecessary_future_import::*;
|
||||
pub(crate) use unpacked_list_comprehension::*;
|
||||
|
|
@ -69,6 +70,7 @@ mod unicode_kind_prefix;
|
|||
mod unnecessary_builtin_import;
|
||||
mod unnecessary_class_parentheses;
|
||||
mod unnecessary_coding_comment;
|
||||
mod unnecessary_default_type_args;
|
||||
mod unnecessary_encode_utf8;
|
||||
mod unnecessary_future_import;
|
||||
mod unpacked_list_comprehension;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary default type arguments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python 3.13 introduced the ability for type parameters to specify default
|
||||
/// values. As such, the default type arguments for some types in the standard
|
||||
/// library (e.g., Generator, AsyncGenerator) are now optional.
|
||||
///
|
||||
/// Omitting type parameters that match the default values can make the code
|
||||
/// more concise and easier to read.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Generator, AsyncGenerator
|
||||
///
|
||||
///
|
||||
/// def sync_gen() -> Generator[int, None, None]:
|
||||
/// yield 42
|
||||
///
|
||||
///
|
||||
/// async def async_gen() -> AsyncGenerator[int, None]:
|
||||
/// yield 42
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Generator, AsyncGenerator
|
||||
///
|
||||
///
|
||||
/// def sync_gen() -> Generator[int]:
|
||||
/// yield 42
|
||||
///
|
||||
///
|
||||
/// async def async_gen() -> AsyncGenerator[int]:
|
||||
/// yield 42
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
///
|
||||
/// - [PEP 696 – Type Defaults for Type Parameters](https://peps.python.org/pep-0696/)
|
||||
/// - [typing.Generator](https://docs.python.org/3.13/library/typing.html#typing.Generator)
|
||||
/// - [typing.AsyncGenerator](https://docs.python.org/3.13/library/typing.html#typing.AsyncGenerator)
|
||||
#[violation]
|
||||
pub struct UnnecessaryDefaultTypeArgs;
|
||||
|
||||
impl AlwaysFixableViolation for UnnecessaryDefaultTypeArgs {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary default type arguments")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Remove default type arguments")
|
||||
}
|
||||
}
|
||||
|
||||
/// UP043
|
||||
pub(crate) fn unnecessary_default_type_args(checker: &mut Checker, expr: &Expr) {
|
||||
let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Expr::Tuple(ast::ExprTuple {
|
||||
elts,
|
||||
ctx: _,
|
||||
range: _,
|
||||
parenthesized: _,
|
||||
}) = slice.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The type annotation must be `Generator` or `AsyncGenerator`.
|
||||
let Some(type_annotation) = DefaultedTypeAnnotation::from_expr(value, checker.semantic())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let valid_elts = type_annotation.trim_unnecessary_defaults(elts);
|
||||
|
||||
// If we didn't trim any elements, then the default type arguments are necessary.
|
||||
if *elts == valid_elts {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDefaultTypeArgs, expr.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
checker
|
||||
.generator()
|
||||
.expr(&Expr::Subscript(ast::ExprSubscript {
|
||||
value: value.clone(),
|
||||
slice: Box::new(if let [elt] = valid_elts.as_slice() {
|
||||
elt.clone()
|
||||
} else {
|
||||
Expr::Tuple(ast::ExprTuple {
|
||||
elts: valid_elts,
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
parenthesized: true,
|
||||
})
|
||||
}),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
expr.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Trim trailing `None` literals from the given elements.
|
||||
///
|
||||
/// For example, given `[int, None, None]`, return `[int]`.
|
||||
fn trim_trailing_none(elts: &[Expr]) -> &[Expr] {
|
||||
match elts.iter().rposition(|elt| !elt.is_none_literal_expr()) {
|
||||
Some(trimmed_last_index) => elts[..=trimmed_last_index].as_ref(),
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Type annotations that include default type arguments as of Python 3.13.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DefaultedTypeAnnotation {
|
||||
/// `typing.Generator[YieldType, SendType = None, ReturnType = None]`
|
||||
Generator,
|
||||
/// `typing.AsyncGenerator[YieldType, SendType = None]`
|
||||
AsyncGenerator,
|
||||
}
|
||||
|
||||
impl DefaultedTypeAnnotation {
|
||||
/// Returns the [`DefaultedTypeAnnotation`], if the given expression is a type annotation that
|
||||
/// includes default type arguments.
|
||||
fn from_expr(expr: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> Option<Self> {
|
||||
let qualified_name = semantic.resolve_qualified_name(expr)?;
|
||||
if semantic.match_typing_qualified_name(&qualified_name, "Generator") {
|
||||
Some(Self::Generator)
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "AsyncGenerator") {
|
||||
Some(Self::AsyncGenerator)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim any unnecessary default type arguments from the given elements.
|
||||
fn trim_unnecessary_defaults(self, elts: &[Expr]) -> Vec<Expr> {
|
||||
match self {
|
||||
Self::Generator => {
|
||||
// Check only if the number of elements is 2 or 3 (e.g., `Generator[int, None]` or `Generator[int, None, None]`).
|
||||
// Otherwise, ignore (e.g., `Generator[]`, `Generator[int]`, `Generator[int, None, None, None]`)
|
||||
if elts.len() != 2 && elts.len() != 3 {
|
||||
return elts.to_vec();
|
||||
}
|
||||
|
||||
std::iter::once(elts[0].clone())
|
||||
.chain(trim_trailing_none(&elts[1..]).iter().cloned())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
Self::AsyncGenerator => {
|
||||
// Check only if the number of elements is 2 (e.g., `AsyncGenerator[int, None]`).
|
||||
// Otherwise, ignore (e.g., `AsyncGenerator[]`, `AsyncGenerator[int]`, `AsyncGenerator[int, None, None]`)
|
||||
if elts.len() != 2 {
|
||||
return elts.to_vec();
|
||||
}
|
||||
|
||||
std::iter::once(elts[0].clone())
|
||||
.chain(trim_trailing_none(&elts[1..]).iter().cloned())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP043.py:4:15: UP043 [*] Unnecessary default type arguments
|
||||
|
|
||||
4 | def func() -> Generator[int, None, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP043
|
||||
5 | yield 42
|
||||
|
|
||||
= help: Remove default type arguments
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | from typing import Generator, AsyncGenerator
|
||||
2 2 |
|
||||
3 3 |
|
||||
4 |-def func() -> Generator[int, None, None]:
|
||||
4 |+def func() -> Generator[int]:
|
||||
5 5 | yield 42
|
||||
6 6 |
|
||||
7 7 |
|
||||
|
||||
UP043.py:8:15: UP043 [*] Unnecessary default type arguments
|
||||
|
|
||||
8 | def func() -> Generator[int, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^ UP043
|
||||
9 | yield 42
|
||||
|
|
||||
= help: Remove default type arguments
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | yield 42
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 |-def func() -> Generator[int, None]:
|
||||
8 |+def func() -> Generator[int]:
|
||||
9 9 | yield 42
|
||||
10 10 |
|
||||
11 11 |
|
||||
|
||||
UP043.py:21:15: UP043 [*] Unnecessary default type arguments
|
||||
|
|
||||
21 | def func() -> Generator[int, int, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP043
|
||||
22 | _ = yield 42
|
||||
23 | return None
|
||||
|
|
||||
= help: Remove default type arguments
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | return foo
|
||||
19 19 |
|
||||
20 20 |
|
||||
21 |-def func() -> Generator[int, int, None]:
|
||||
21 |+def func() -> Generator[int, int]:
|
||||
22 22 | _ = yield 42
|
||||
23 23 | return None
|
||||
24 24 |
|
||||
|
||||
UP043.py:31:21: UP043 [*] Unnecessary default type arguments
|
||||
|
|
||||
31 | async def func() -> AsyncGenerator[int, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP043
|
||||
32 | yield 42
|
||||
|
|
||||
= help: Remove default type arguments
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 | return 42
|
||||
29 29 |
|
||||
30 30 |
|
||||
31 |-async def func() -> AsyncGenerator[int, None]:
|
||||
31 |+async def func() -> AsyncGenerator[int]:
|
||||
32 32 | yield 42
|
||||
33 33 |
|
||||
34 34 |
|
||||
|
|
@ -3927,6 +3927,7 @@
|
|||
"UP040",
|
||||
"UP041",
|
||||
"UP042",
|
||||
"UP043",
|
||||
"W",
|
||||
"W1",
|
||||
"W19",
|
||||
|
|
|
|||
Loading…
Reference in New Issue