diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py index 86b8d9aebfc4b..32f3f6fad5112 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py @@ -94,3 +94,20 @@ # t-strings are not native literals str(t"hey") + +# UP018 - Extended detections +str("A" "B") +str("A" "B").lower() +str( + "A" + "B" +) +str(object="!") +complex(1j) +complex(real=1j) +complex() +complex(0j) +complex(real=0j) +(complex(0j)).real +complex(1j).real +complex(real=1j).real diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs index c1b6d0d5404fb..df90a5ac055f0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -16,6 +16,7 @@ enum LiteralType { Int, Float, Bool, + Complex, } impl FromStr for LiteralType { @@ -28,6 +29,7 @@ impl FromStr for LiteralType { "int" => Ok(LiteralType::Int), "float" => Ok(LiteralType::Float), "bool" => Ok(LiteralType::Bool), + "complex" => Ok(LiteralType::Complex), _ => Err(()), } } @@ -63,6 +65,15 @@ impl LiteralType { } .into(), LiteralType::Bool => ast::ExprBooleanLiteral::default().into(), + LiteralType::Complex => ast::ExprNumberLiteral { + value: ast::Number::Complex { + real: 0.0, + imag: 0.0, + }, + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::NONE, + } + .into(), } } } @@ -78,7 +89,7 @@ impl TryFrom> for LiteralType { match value { ast::Number::Int(_) => Ok(LiteralType::Int), ast::Number::Float(_) => Ok(LiteralType::Float), - ast::Number::Complex { .. } => Err(()), + ast::Number::Complex { .. } => Ok(LiteralType::Complex), } } LiteralExpressionRef::BooleanLiteral(_) => Ok(LiteralType::Bool), @@ -97,12 +108,13 @@ impl fmt::Display for LiteralType { LiteralType::Int => fmt.write_str("int"), LiteralType::Float => fmt.write_str("float"), LiteralType::Bool => fmt.write_str("bool"), + LiteralType::Complex => fmt.write_str("complex"), } } } /// ## What it does -/// Checks for unnecessary calls to `str`, `bytes`, `int`, `float`, and `bool`. +/// Checks for unnecessary calls to `str`, `bytes`, `int`, `float`, `bool`, and `complex`. /// /// ## Why is this bad? /// The mentioned constructors can be replaced with their respective literal @@ -127,6 +139,7 @@ impl fmt::Display for LiteralType { /// - [Python documentation: `int`](https://docs.python.org/3/library/functions.html#int) /// - [Python documentation: `float`](https://docs.python.org/3/library/functions.html#float) /// - [Python documentation: `bool`](https://docs.python.org/3/library/functions.html#bool) +/// - [Python documentation: `complex`](https://docs.python.org/3/library/functions.html#complex) #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.193")] pub(crate) struct NativeLiterals { @@ -148,10 +161,26 @@ impl AlwaysFixableViolation for NativeLiterals { LiteralType::Int => "Replace with integer literal".to_string(), LiteralType::Float => "Replace with float literal".to_string(), LiteralType::Bool => "Replace with boolean literal".to_string(), + LiteralType::Complex => "Replace with complex literal".to_string(), } } } +/// Returns `true` if the keyword argument is redundant for the given builtin. +fn is_redundant_keyword(builtin: &str, keyword: &ast::Keyword) -> bool { + let Some(arg) = keyword.arg.as_ref() else { + return false; + }; + match builtin { + "str" => arg == "object", + // Python 3.14 emits a `SyntaxWarning` for `complex(real=1j)`. While this + // does change the behavior, upgrading it to 1j is very much in the spirit of this rule + // and removing the `SyntaxWarning` is a nice side effect. + "complex" => arg == "real", + _ => false, + } +} + /// UP018 pub(crate) fn native_literals( checker: &Checker, @@ -171,17 +200,21 @@ pub(crate) fn native_literals( node_index: _, } = call; - if !keywords.is_empty() || args.len() > 1 { - return; - } - - let tokens = checker.tokens(); let semantic = checker.semantic(); let Some(builtin) = semantic.resolve_builtin_symbol(func) else { return; }; + let call_arg = match (args.as_ref(), keywords.as_ref()) { + ([], []) => None, + ([arg], []) => Some(arg), + ([], [keyword]) if is_redundant_keyword(builtin, keyword) => Some(&keyword.value), + _ => return, + }; + + let tokens = checker.tokens(); + let Ok(literal_type) = LiteralType::from_str(builtin) else { return; }; @@ -198,19 +231,20 @@ pub(crate) fn native_literals( } } - match args.first() { + match call_arg { None => { - // Do not suggest fix for attribute access on an int like `int().attribute` - // Ex) `int().denominator` is valid but `0.denominator` is not - if literal_type == LiteralType::Int && matches!(parent_expr, Some(Expr::Attribute(_))) { - return; - } - let mut diagnostic = checker.report_diagnostic(NativeLiterals { literal_type }, call.range()); let expr = literal_type.as_zero_value_expr(checker); - let content = checker.generator().expr(&expr); + let mut content = checker.generator().expr(&expr); + + // Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float + // Ex) `(0).denominator` is valid but `0.denominator` is not + if literal_type == LiteralType::Int && matches!(parent_expr, Some(Expr::Attribute(_))) { + content = format!("({content})"); + } + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( content, call.range(), @@ -218,10 +252,6 @@ pub(crate) fn native_literals( } Some(arg) => { let (has_unary_op, literal_expr) = if let Some(literal_expr) = arg.as_literal_expr() { - // Skip implicit concatenated strings. - if literal_expr.is_implicit_concatenated() { - return; - } (false, literal_expr) } else if let Expr::UnaryOp(ast::ExprUnaryOp { op: UnaryOp::UAdd | UnaryOp::USub, @@ -269,6 +299,14 @@ pub(crate) fn native_literals( // Expressions including newlines must be parenthesised to be valid syntax (_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"), + // Implicitly concatenated strings spanning multiple lines must be parenthesized + (_, LiteralType::Str | LiteralType::Bytes, _) + if literal_expr.is_implicit_concatenated() + && find_newline(arg_code).is_some() => + { + format!("({arg_code})") + } + // Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float // Ex) `(7).denominator` is valid but `7.denominator` is not // Note that floats do not have this problem diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap index 05c508ea6f12a..febc64ee3734b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap @@ -1,6 +1,68 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- +UP018 [*] Unnecessary `str` call (rewrite as a literal) + --> UP018.py:8:1 + | + 6 | str("foo", **k) + 7 | str("foo", encoding="UTF-8") + 8 | / str("foo" + 9 | | "bar") + | |__________^ +10 | str(b"foo") +11 | bytes("foo", encoding="UTF-8") + | +help: Replace with string literal +5 | str(**k) +6 | str("foo", **k) +7 | str("foo", encoding="UTF-8") + - str("foo" +8 + ("foo" +9 | "bar") +10 | str(b"foo") +11 | bytes("foo", encoding="UTF-8") + +UP018 [*] Unnecessary `bytes` call (rewrite as a literal) + --> UP018.py:15:1 + | +13 | bytes("foo", *a) +14 | bytes("foo", **a) +15 | / bytes(b"foo" +16 | | b"bar") + | |_____________^ +17 | bytes("foo") +18 | bytes(1) + | +help: Replace with bytes literal +12 | bytes(*a) +13 | bytes("foo", *a) +14 | bytes("foo", **a) + - bytes(b"foo" +15 + (b"foo" +16 | b"bar") +17 | bytes("foo") +18 | bytes(1) + +UP018 [*] Unnecessary `int` call (rewrite as a literal) + --> UP018.py:34:1 + | +32 | bool(b"") +33 | bool(1.0) +34 | int().denominator + | ^^^^^ +35 | +36 | # These become literals + | +help: Replace with integer literal +31 | bool("") +32 | bool(b"") +33 | bool(1.0) + - int().denominator +34 + (0).denominator +35 | +36 | # These become literals +37 | str() + UP018 [*] Unnecessary `str` call (rewrite as a literal) --> UP018.py:37:1 | @@ -667,3 +729,240 @@ help: Replace with boolean literal 93 | 94 | 95 | # t-strings are not native literals + +UP018 [*] Unnecessary `str` call (rewrite as a literal) + --> UP018.py:99:1 + | + 98 | # UP018 - Extended detections + 99 | str("A" "B") + | ^^^^^^^^^^^^ +100 | str("A" "B").lower() +101 | str( + | +help: Replace with string literal +96 | str(t"hey") +97 | +98 | # UP018 - Extended detections + - str("A" "B") +99 + "A" "B" +100 | str("A" "B").lower() +101 | str( +102 | "A" + +UP018 [*] Unnecessary `str` call (rewrite as a literal) + --> UP018.py:100:1 + | + 98 | # UP018 - Extended detections + 99 | str("A" "B") +100 | str("A" "B").lower() + | ^^^^^^^^^^^^ +101 | str( +102 | "A" + | +help: Replace with string literal +97 | +98 | # UP018 - Extended detections +99 | str("A" "B") + - str("A" "B").lower() +100 + "A" "B".lower() +101 | str( +102 | "A" +103 | "B" + +UP018 [*] Unnecessary `str` call (rewrite as a literal) + --> UP018.py:101:1 + | + 99 | str("A" "B") +100 | str("A" "B").lower() +101 | / str( +102 | | "A" +103 | | "B" +104 | | ) + | |_^ +105 | str(object="!") +106 | complex(1j) + | +help: Replace with string literal +98 | # UP018 - Extended detections +99 | str("A" "B") +100 | str("A" "B").lower() + - str( + - "A" + - "B" + - ) +101 + ("A" +102 + "B") +103 | str(object="!") +104 | complex(1j) +105 | complex(real=1j) + +UP018 [*] Unnecessary `str` call (rewrite as a literal) + --> UP018.py:105:1 + | +103 | "B" +104 | ) +105 | str(object="!") + | ^^^^^^^^^^^^^^^ +106 | complex(1j) +107 | complex(real=1j) + | +help: Replace with string literal +102 | "A" +103 | "B" +104 | ) + - str(object="!") +105 + "!" +106 | complex(1j) +107 | complex(real=1j) +108 | complex() + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:106:1 + | +104 | ) +105 | str(object="!") +106 | complex(1j) + | ^^^^^^^^^^^ +107 | complex(real=1j) +108 | complex() + | +help: Replace with complex literal +103 | "B" +104 | ) +105 | str(object="!") + - complex(1j) +106 + 1j +107 | complex(real=1j) +108 | complex() +109 | complex(0j) + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:107:1 + | +105 | str(object="!") +106 | complex(1j) +107 | complex(real=1j) + | ^^^^^^^^^^^^^^^^ +108 | complex() +109 | complex(0j) + | +help: Replace with complex literal +104 | ) +105 | str(object="!") +106 | complex(1j) + - complex(real=1j) +107 + 1j +108 | complex() +109 | complex(0j) +110 | complex(real=0j) + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:108:1 + | +106 | complex(1j) +107 | complex(real=1j) +108 | complex() + | ^^^^^^^^^ +109 | complex(0j) +110 | complex(real=0j) + | +help: Replace with complex literal +105 | str(object="!") +106 | complex(1j) +107 | complex(real=1j) + - complex() +108 + 0j +109 | complex(0j) +110 | complex(real=0j) +111 | (complex(0j)).real + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:109:1 + | +107 | complex(real=1j) +108 | complex() +109 | complex(0j) + | ^^^^^^^^^^^ +110 | complex(real=0j) +111 | (complex(0j)).real + | +help: Replace with complex literal +106 | complex(1j) +107 | complex(real=1j) +108 | complex() + - complex(0j) +109 + 0j +110 | complex(real=0j) +111 | (complex(0j)).real +112 | complex(1j).real + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:110:1 + | +108 | complex() +109 | complex(0j) +110 | complex(real=0j) + | ^^^^^^^^^^^^^^^^ +111 | (complex(0j)).real +112 | complex(1j).real + | +help: Replace with complex literal +107 | complex(real=1j) +108 | complex() +109 | complex(0j) + - complex(real=0j) +110 + 0j +111 | (complex(0j)).real +112 | complex(1j).real +113 | complex(real=1j).real + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:111:2 + | +109 | complex(0j) +110 | complex(real=0j) +111 | (complex(0j)).real + | ^^^^^^^^^^^ +112 | complex(1j).real +113 | complex(real=1j).real + | +help: Replace with complex literal +108 | complex() +109 | complex(0j) +110 | complex(real=0j) + - (complex(0j)).real +111 + (0j).real +112 | complex(1j).real +113 | complex(real=1j).real + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:112:1 + | +110 | complex(real=0j) +111 | (complex(0j)).real +112 | complex(1j).real + | ^^^^^^^^^^^ +113 | complex(real=1j).real + | +help: Replace with complex literal +109 | complex(0j) +110 | complex(real=0j) +111 | (complex(0j)).real + - complex(1j).real +112 + 1j.real +113 | complex(real=1j).real + +UP018 [*] Unnecessary `complex` call (rewrite as a literal) + --> UP018.py:113:1 + | +111 | (complex(0j)).real +112 | complex(1j).real +113 | complex(real=1j).real + | ^^^^^^^^^^^^^^^^ + | +help: Replace with complex literal +110 | complex(real=0j) +111 | (complex(0j)).real +112 | complex(1j).real + - complex(real=1j).real +113 + 1j.real