[ruff] Parenthesize arguments to int when removing int would change semantics in unnecessary-cast-to-int (RUF046) (#15277)

When removing `int` in calls like `int(expr)` we may need to keep
parentheses around `expr` even when it is a function call or subscript,
since there may be newlines in between the function/value name and the
opening parentheses/bracket of the argument.

This PR implements that logic.

Closes #15263

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Dylan 2025-01-07 15:43:50 -06:00 committed by GitHub
parent 3b3c2c5aa4
commit 71ad9a2ab1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 350 additions and 7 deletions

View file

@ -160,3 +160,49 @@ int(1 +
int(round(1,
0))
# function calls may need to retain parentheses
# if the parentheses for the call itself
# lie on the next line.
# See https://github.com/astral-sh/ruff/issues/15263
int(round
(1))
int(round # a comment
# and another comment
(10)
)
int(round (17)) # this is safe without parens
int( round (
17
)) # this is also safe without parens
int((round) # Comment
(42)
)
int((round # Comment
)(42)
)
int( # Unsafe fix because of this comment
( # Comment
(round
) # Comment
)(42)
)
int(
round(
42
) # unsafe fix because of this comment
)
int(
round(
42
)
# unsafe fix because of this comment
)

View file

@ -4,9 +4,9 @@ use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{Arguments, Expr, ExprCall};
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{lines_after_ignoring_trivia, CommentRanges};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::unnecessary_round::{
@ -114,12 +114,36 @@ fn unwrap_int_expression(
let parenthesize = semantic.current_expression_parent().is_some()
|| argument.is_named_expr()
|| locator.count_lines(argument.range()) > 0;
if parenthesize && !has_own_parentheses(argument) {
if parenthesize && !has_own_parentheses(argument, comment_ranges, source) {
format!("({})", locator.slice(argument.range()))
} else {
locator.slice(argument.range()).to_string()
}
};
// Since we're deleting the complement of the argument range within
// the call range, we have to check both ends for comments.
//
// For example:
// ```python
// int( # comment
// round(
// 42.1
// ) # comment
// )
// ```
let applicability = {
let call_to_arg_start = TextRange::new(call.start(), argument.start());
let arg_to_call_end = TextRange::new(argument.end(), call.end());
if comment_ranges.intersects(call_to_arg_start)
|| comment_ranges.intersects(arg_to_call_end)
{
Applicability::Unsafe
} else {
applicability
}
};
let edit = Edit::range_replacement(content, call.range());
Fix::applicable_edit(edit, applicability)
}
@ -229,16 +253,49 @@ fn round_applicability(arguments: &Arguments, semantic: &SemanticModel) -> Optio
}
/// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`).
fn has_own_parentheses(expr: &Expr) -> bool {
fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str) -> bool {
match expr {
Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
| Expr::Subscript(_)
| Expr::List(_)
| Expr::Set(_)
| Expr::Dict(_)
| Expr::Call(_) => true,
| Expr::Dict(_) => true,
Expr::Call(call_expr) => {
// A call where the function and parenthesized
// argument(s) appear on separate lines
// requires outer parentheses. That is:
// ```
// (f
// (10))
// ```
// is different than
// ```
// f
// (10)
// ```
let func_end = parenthesized_range(
call_expr.func.as_ref().into(),
call_expr.into(),
comment_ranges,
source,
)
.unwrap_or(call_expr.func.range())
.end();
lines_after_ignoring_trivia(func_end, source) == 0
}
Expr::Subscript(subscript_expr) => {
// Same as above
let subscript_end = parenthesized_range(
subscript_expr.value.as_ref().into(),
subscript_expr.into(),
comment_ranges,
source,
)
.unwrap_or(subscript_expr.value.range())
.end();
lines_after_ignoring_trivia(subscript_end, source) == 0
}
Expr::Generator(generator) => generator.parenthesized,
Expr::Tuple(tuple) => tuple.parenthesized,
_ => false,

View file

@ -1005,6 +1005,8 @@ RUF046.py:161:1: RUF046 [*] Value being cast to `int` is already an integer
161 | / int(round(1,
162 | | 0))
| |_____________^ RUF046
163 |
164 | # function calls may need to retain parentheses
|
= help: Remove unnecessary `int` call
@ -1016,3 +1018,241 @@ RUF046.py:161:1: RUF046 [*] Value being cast to `int` is already an integer
162 |- 0))
161 |+round(1,
162 |+ 0)
163 163 |
164 164 | # function calls may need to retain parentheses
165 165 | # if the parentheses for the call itself
RUF046.py:168:1: RUF046 [*] Value being cast to `int` is already an integer
|
166 | # lie on the next line.
167 | # See https://github.com/astral-sh/ruff/issues/15263
168 | / int(round
169 | | (1))
| |____^ RUF046
170 |
171 | int(round # a comment
|
= help: Remove unnecessary `int` call
Safe fix
165 165 | # if the parentheses for the call itself
166 166 | # lie on the next line.
167 167 | # See https://github.com/astral-sh/ruff/issues/15263
168 |-int(round
168 |+(round
169 169 | (1))
170 170 |
171 171 | int(round # a comment
RUF046.py:171:1: RUF046 [*] Value being cast to `int` is already an integer
|
169 | (1))
170 |
171 | / int(round # a comment
172 | | # and another comment
173 | | (10)
174 | | )
| |_^ RUF046
175 |
176 | int(round (17)) # this is safe without parens
|
= help: Remove unnecessary `int` call
Safe fix
168 168 | int(round
169 169 | (1))
170 170 |
171 |-int(round # a comment
171 |+(round # a comment
172 172 | # and another comment
173 |-(10)
174 |-)
173 |+(10))
175 174 |
176 175 | int(round (17)) # this is safe without parens
177 176 |
RUF046.py:176:1: RUF046 [*] Value being cast to `int` is already an integer
|
174 | )
175 |
176 | int(round (17)) # this is safe without parens
| ^^^^^^^^^^^^^^^ RUF046
177 |
178 | int( round (
|
= help: Remove unnecessary `int` call
Safe fix
173 173 | (10)
174 174 | )
175 175 |
176 |-int(round (17)) # this is safe without parens
176 |+round (17) # this is safe without parens
177 177 |
178 178 | int( round (
179 179 | 17
RUF046.py:178:1: RUF046 [*] Value being cast to `int` is already an integer
|
176 | int(round (17)) # this is safe without parens
177 |
178 | / int( round (
179 | | 17
180 | | )) # this is also safe without parens
| |______________^ RUF046
181 |
182 | int((round) # Comment
|
= help: Remove unnecessary `int` call
Safe fix
175 175 |
176 176 | int(round (17)) # this is safe without parens
177 177 |
178 |-int( round (
178 |+round (
179 179 | 17
180 |- )) # this is also safe without parens
180 |+ ) # this is also safe without parens
181 181 |
182 182 | int((round) # Comment
183 183 | (42)
RUF046.py:182:1: RUF046 [*] Value being cast to `int` is already an integer
|
180 | )) # this is also safe without parens
181 |
182 | / int((round) # Comment
183 | | (42)
184 | | )
| |_^ RUF046
185 |
186 | int((round # Comment
|
= help: Remove unnecessary `int` call
Safe fix
179 179 | 17
180 180 | )) # this is also safe without parens
181 181 |
182 |-int((round) # Comment
183 |-(42)
184 |-)
182 |+((round) # Comment
183 |+(42))
185 184 |
186 185 | int((round # Comment
187 186 | )(42)
RUF046.py:186:1: RUF046 [*] Value being cast to `int` is already an integer
|
184 | )
185 |
186 | / int((round # Comment
187 | | )(42)
188 | | )
| |_^ RUF046
189 |
190 | int( # Unsafe fix because of this comment
|
= help: Remove unnecessary `int` call
Safe fix
183 183 | (42)
184 184 | )
185 185 |
186 |-int((round # Comment
186 |+(round # Comment
187 187 | )(42)
188 |-)
189 188 |
190 189 | int( # Unsafe fix because of this comment
191 190 | ( # Comment
RUF046.py:190:1: RUF046 [*] Value being cast to `int` is already an integer
|
188 | )
189 |
190 | / int( # Unsafe fix because of this comment
191 | | ( # Comment
192 | | (round
193 | | ) # Comment
194 | | )(42)
195 | | )
| |_^ RUF046
196 |
197 | int(
|
= help: Remove unnecessary `int` call
Unsafe fix
187 187 | )(42)
188 188 | )
189 189 |
190 |-int( # Unsafe fix because of this comment
191 190 | ( # Comment
192 191 | (round
193 192 | ) # Comment
194 193 | )(42)
195 |-)
196 194 |
197 195 | int(
198 196 | round(
RUF046.py:197:1: RUF046 [*] Value being cast to `int` is already an integer
|
195 | )
196 |
197 | / int(
198 | | round(
199 | | 42
200 | | ) # unsafe fix because of this comment
201 | | )
| |_^ RUF046
202 |
203 | int(
|
= help: Remove unnecessary `int` call
Unsafe fix
194 194 | )(42)
195 195 | )
196 196 |
197 |-int(
198 |- round(
197 |+round(
199 198 | 42
200 |- ) # unsafe fix because of this comment
201 |-)
199 |+ )
202 200 |
203 201 | int(
204 202 | round(
RUF046.py:203:1: RUF046 [*] Value being cast to `int` is already an integer
|
201 | )
202 |
203 | / int(
204 | | round(
205 | | 42
206 | | )
207 | | # unsafe fix because of this comment
208 | | )
| |_^ RUF046
|
= help: Remove unnecessary `int` call
Unsafe fix
200 200 | ) # unsafe fix because of this comment
201 201 | )
202 202 |
203 |-int(
204 |- round(
203 |+round(
205 204 | 42
206 |- )
207 |-# unsafe fix because of this comment
208 |-)
205 |+ )