[pyupgrade] Preserve parenthesis when fixing native literals containing newlines (UP018) (#17220)

This commit is contained in:
Max Mynter 2025-04-24 08:48:02 +02:00 committed by GitHub
parent 48a85c4ed4
commit a01f25107a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 128 additions and 0 deletions

View file

@ -0,0 +1,5 @@
# These rules test for reformatting with specific line endings.
# Using a formatter fixes that and breaks the tests
[UP018_{CR,LF}.py]
generated_code = true
ij_formatter_enabled = false

View file

@ -0,0 +1 @@
# Keep parenthesis around preserved CR int(- 1) int(+ 1)

View file

@ -0,0 +1,7 @@
# Keep parentheses around preserved \n
int(-
1)
int(+
1)

View file

@ -0,0 +1,5 @@
# These rules test for reformatting with specific line endings.
# Using a formatter fixes that and breaks the tests
[RUF046_{CR,LF}.py]
generated_code = true
ij_formatter_enabled = false

View file

@ -0,0 +1 @@
int(- 1) # Carriage return as newline

View file

@ -0,0 +1,3 @@
# \n as newline
int(-
1)

View file

@ -36,6 +36,8 @@ mod tests {
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))]
#[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))]
#[test_case(Rule::NativeLiterals, Path::new("UP018.py"))]
#[test_case(Rule::NativeLiterals, Path::new("UP018_CR.py"))]
#[test_case(Rule::NativeLiterals, Path::new("UP018_LF.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_1.py"))]
#[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))]

View file

@ -4,6 +4,7 @@ use std::str::FromStr;
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, OperatorPrecedence, UnaryOp};
use ruff_source_file::find_newline;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@ -244,6 +245,9 @@ pub(crate) fn native_literals(
let arg_code = checker.locator().slice(arg);
let 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})"),
// 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

View file

@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP018_CR.py:2:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
1 | # Keep parenthesis around preserved CR int(- 1) int(+ 1)
| ^^^^^^^^^^^ UP018
|
= help: Replace with integer literal
Safe fix
1 1 | # Keep parenthesis around preserved CR 2 |-int(- 2 |+(- 3 3 | 1) 4 4 | int(+ 5 5 | 1)
UP018_CR.py:4:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
2 | int(- 1) int(+ 1)
| ^^^^^^^^^^^ UP018
|
= help: Replace with integer literal
Safe fix
1 1 | # Keep parenthesis around preserved CR 2 2 | int(- 3 3 | 1) 4 |-int(+ 4 |+(+ 5 5 | 1)

View file

@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP018_LF.py:3:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
1 | # Keep parentheses around preserved \n
2 |
3 | / int(-
4 | | 1)
| |______^ UP018
5 |
6 | int(+
|
= help: Replace with integer literal
Safe fix
1 1 | # Keep parentheses around preserved \n
2 2 |
3 |-int(-
3 |+(-
4 4 | 1)
5 5 |
6 6 | int(+
UP018_LF.py:6:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
4 | 1)
5 |
6 | / int(+
7 | | 1)
| |______^ UP018
|
= help: Replace with integer literal
Safe fix
3 3 | int(-
4 4 | 1)
5 5 |
6 |-int(+
6 |+(+
7 7 | 1)

View file

@ -84,6 +84,8 @@ mod tests {
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))]
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_CR.py"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_LF.py"))]
#[test_case(Rule::NeedlessElse, Path::new("RUF047_if.py"))]
#[test_case(Rule::NeedlessElse, Path::new("RUF047_for.py"))]
#[test_case(Rule::NeedlessElse, Path::new("RUF047_while.py"))]

View file

@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF046_CR.py:1:1: RUF046 [*] Value being cast to `int` is already an integer
|
1 | int(- 1) # Carriage return as newline
| ^^^^^^^^^^^ RUF046
|
= help: Remove unnecessary `int` call
Safe fix
1 |-int(- 1 |+(- 2 2 | 1) # Carriage return as newline

View file

@ -0,0 +1,17 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF046_LF.py:2:1: RUF046 [*] Value being cast to `int` is already an integer
|
1 | # \n as newline
2 | / int(-
3 | | 1)
| |______^ RUF046
|
= help: Remove unnecessary `int` call
Safe fix
1 1 | # \n as newline
2 |-int(-
2 |+(-
3 3 | 1)