diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.py new file mode 100644 index 0000000000..c4ebf662a6 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.py @@ -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 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index fae75a22d1..5536a58a54 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -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, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index ce31b13908..08cbbc174f 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -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), diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 8b09cd22cb..12577c87de 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -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"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs index 3b7928f6e9..a3dbd706bf 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs @@ -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; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs new file mode 100644 index 0000000000..8349eae78f --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -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 { + 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 { + 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::>() + } + 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::>() + } + } + } +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.py.snap new file mode 100644 index 0000000000..4198822ad3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP043.py.snap @@ -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 | diff --git a/ruff.schema.json b/ruff.schema.json index ab7bad6544..2114f20e64 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3927,6 +3927,7 @@ "UP040", "UP041", "UP042", + "UP043", "W", "W1", "W19",