From c504001b329949524e684c8b8278b13930748a7e Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Wed, 7 May 2025 04:34:08 -0300 Subject: [PATCH] [`pyupgrade`] Add spaces between tokens as necessary to avoid syntax errors in `UP018` autofix (#17648) Co-authored-by: Micha Reiser --- .../test/fixtures/pyupgrade/UP018.py | 6 ++ .../rules/pyupgrade/rules/native_literals.rs | 22 +++++- ...er__rules__pyupgrade__tests__UP018.py.snap | 79 +++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py index 8f2dd70d97..a5b7e1d894 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py @@ -84,3 +84,9 @@ str( '''Lorem ipsum''' # Comment ).foo + +# https://github.com/astral-sh/ruff/issues/17606 +bool(True)and None +int(1)and None +float(1.)and None +bool(True)and() 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 14e31351ca..c9bb0f03ee 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -161,13 +161,14 @@ pub(crate) fn native_literals( keywords, range: _, }, - range: _, + range: call_range, } = 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 { @@ -244,7 +245,20 @@ pub(crate) fn native_literals( let arg_code = checker.locator().slice(arg); - let content = match (parent_expr, literal_type, has_unary_op) { + let mut needs_space = false; + // Look for the `Rpar` token of the call expression and check if there is a keyword token right + // next to it without any space separating them. Without this check, the fix for this + // rule would create a syntax error. + // Ex) `bool(True)and None` no space between `)` and the keyword `and`. + // + // Subtract 1 from the end of the range to include `Rpar` token in the slice. + if let [paren_token, next_token, ..] = tokens.after(call_range.sub_end(1.into()).end()) + { + needs_space = next_token.kind().is_keyword() + && paren_token.range().end() == next_token.range().start(); + } + + let mut content = match (parent_expr, literal_type, has_unary_op) { // Expressions including newlines must be parenthesised to be valid syntax (_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"), @@ -265,6 +279,10 @@ pub(crate) fn native_literals( _ => arg_code.to_string(), }; + if needs_space { + content.push(' '); + } + let applicability = if checker.comment_ranges().intersects(call.range) { Applicability::Unsafe } else { 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 1c04500c3d..d57eac6e12 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 @@ -602,6 +602,8 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal) 85 | | ipsum''' # Comment 86 | | ).foo | |_^ UP018 +87 | +88 | # https://github.com/astral-sh/ruff/issues/17606 | = help: Replace with string literal @@ -615,3 +617,80 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal) 86 |-).foo 83 |+'''Lorem 84 |+ ipsum'''.foo +87 85 | +88 86 | # https://github.com/astral-sh/ruff/issues/17606 +89 87 | bool(True)and None + +UP018.py:89:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal) + | +88 | # https://github.com/astral-sh/ruff/issues/17606 +89 | bool(True)and None + | ^^^^^^^^^^ UP018 +90 | int(1)and None +91 | float(1.)and None + | + = help: Replace with boolean literal + +ℹ Safe fix +86 86 | ).foo +87 87 | +88 88 | # https://github.com/astral-sh/ruff/issues/17606 +89 |-bool(True)and None + 89 |+True and None +90 90 | int(1)and None +91 91 | float(1.)and None +92 92 | bool(True)and() + +UP018.py:90:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +88 | # https://github.com/astral-sh/ruff/issues/17606 +89 | bool(True)and None +90 | int(1)and None + | ^^^^^^ UP018 +91 | float(1.)and None +92 | bool(True)and() + | + = help: Replace with integer literal + +ℹ Safe fix +87 87 | +88 88 | # https://github.com/astral-sh/ruff/issues/17606 +89 89 | bool(True)and None +90 |-int(1)and None + 90 |+1 and None +91 91 | float(1.)and None +92 92 | bool(True)and() + +UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal) + | +89 | bool(True)and None +90 | int(1)and None +91 | float(1.)and None + | ^^^^^^^^^ UP018 +92 | bool(True)and() + | + = help: Replace with float literal + +ℹ Safe fix +88 88 | # https://github.com/astral-sh/ruff/issues/17606 +89 89 | bool(True)and None +90 90 | int(1)and None +91 |-float(1.)and None + 91 |+1. and None +92 92 | bool(True)and() + +UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal) + | +90 | int(1)and None +91 | float(1.)and None +92 | bool(True)and() + | ^^^^^^^^^^ UP018 + | + = help: Replace with boolean literal + +ℹ Safe fix +89 89 | bool(True)and None +90 90 | int(1)and None +91 91 | float(1.)and None +92 |-bool(True)and() + 92 |+True and()