Skip to content

Commit d006976

Browse files
authored
[ruff] Fix false positives and negatives in RUF010 (#18690)
1 parent 76619b9 commit d006976

File tree

4 files changed

+269
-71
lines changed

4 files changed

+269
-71
lines changed

crates/ruff_linter/resources/test/fixtures/ruff/RUF010.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,19 @@ def ascii(arg):
3636
)
3737

3838

39-
# OK
39+
# https://github.com/astral-sh/ruff/issues/16325
4040
f"{str({})}"
41+
42+
f"{str({} | {})}"
43+
44+
import builtins
45+
46+
f"{builtins.repr(1)}"
47+
48+
f"{repr(1)=}"
49+
50+
f"{repr(lambda: 1)}"
51+
52+
f"{repr(x := 2)}"
53+
54+
f"{str(object=3)}"

crates/ruff_linter/src/cst/matchers.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use anyhow::{Result, bail};
33
use libcst_native::{
44
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString,
55
FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import,
6-
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name,
7-
SmallStatement, Statement, Suite, Tuple, With,
6+
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, SmallStatement,
7+
Statement, Suite, Tuple, With,
88
};
99
use ruff_python_codegen::Stylist;
1010

@@ -104,14 +104,6 @@ pub(crate) fn match_attribute<'a, 'b>(
104104
}
105105
}
106106

107-
pub(crate) fn match_name<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Name<'b>> {
108-
if let Expression::Name(name) = expression {
109-
Ok(name)
110-
} else {
111-
bail!("Expected Expression::Name")
112-
}
113-
}
114-
115107
pub(crate) fn match_arg<'a, 'b>(call: &'a Call<'b>) -> Result<&'a Arg<'b>> {
116108
if let Some(arg) = call.args.first() {
117109
Ok(arg)
Lines changed: 121 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
use anyhow::{Result, bail};
1+
use std::fmt::Display;
22

3+
use anyhow::Result;
4+
5+
use libcst_native::{LeftParen, ParenthesizedNode, RightParen};
36
use ruff_macros::{ViolationMetadata, derive_message_formats};
4-
use ruff_python_ast::{self as ast, Arguments, Expr};
5-
use ruff_python_codegen::Stylist;
7+
use ruff_python_ast::{self as ast, Expr, OperatorPrecedence};
8+
use ruff_python_parser::TokenKind;
69
use ruff_text_size::Ranged;
710

8-
use crate::Locator;
911
use crate::checkers::ast::Checker;
12+
use crate::cst::helpers::space;
1013
use crate::cst::matchers::{
11-
match_call_mut, match_formatted_string, match_formatted_string_expression, match_name,
12-
transform_expression,
14+
match_call_mut, match_formatted_string, match_formatted_string_expression, transform_expression,
1315
};
14-
use crate::{AlwaysFixableViolation, Edit, Fix};
16+
use crate::{Edit, Fix, FixAvailability, Violation};
1517

1618
/// ## What it does
1719
/// Checks for uses of `str()`, `repr()`, and `ascii()` as explicit type
@@ -40,14 +42,16 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
4042
#[derive(ViolationMetadata)]
4143
pub(crate) struct ExplicitFStringTypeConversion;
4244

43-
impl AlwaysFixableViolation for ExplicitFStringTypeConversion {
45+
impl Violation for ExplicitFStringTypeConversion {
46+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
47+
4448
#[derive_message_formats]
4549
fn message(&self) -> String {
4650
"Use explicit conversion flag".to_string()
4751
}
4852

49-
fn fix_title(&self) -> String {
50-
"Replace with conversion flag".to_string()
53+
fn fix_title(&self) -> Option<String> {
54+
Some("Replace with conversion flag".to_string())
5155
}
5256
}
5357

@@ -68,84 +72,142 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &as
6872
continue;
6973
}
7074

71-
let Expr::Call(ast::ExprCall {
72-
func,
73-
arguments:
74-
Arguments {
75-
args,
76-
keywords,
77-
range: _,
78-
node_index: _,
79-
},
80-
..
81-
}) = expression.as_ref()
82-
else {
75+
let Expr::Call(call) = expression.as_ref() else {
8376
continue;
8477
};
8578

86-
// Can't be a conversion otherwise.
87-
if !keywords.is_empty() {
79+
let Some(conversion) = checker
80+
.semantic()
81+
.resolve_builtin_symbol(&call.func)
82+
.and_then(Conversion::from_str)
83+
else {
8884
continue;
89-
}
85+
};
86+
let arg = match conversion {
87+
// Handles the cases: `f"{str(object=arg)}"` and `f"{str(arg)}"`
88+
Conversion::Str if call.arguments.len() == 1 => {
89+
let Some(arg) = call.arguments.find_argument_value("object", 0) else {
90+
continue;
91+
};
92+
arg
93+
}
94+
Conversion::Str | Conversion::Repr | Conversion::Ascii => {
95+
// Can't be a conversion otherwise.
96+
if !call.arguments.keywords.is_empty() {
97+
continue;
98+
}
9099

91-
// Can't be a conversion otherwise.
92-
let [arg] = &**args else {
93-
continue;
100+
// Can't be a conversion otherwise.
101+
let [arg] = call.arguments.args.as_ref() else {
102+
continue;
103+
};
104+
arg
105+
}
94106
};
95107

96-
// Avoid attempting to rewrite, e.g., `f"{str({})}"`; the curly braces are problematic.
97-
if matches!(
98-
arg,
99-
Expr::Dict(_) | Expr::Set(_) | Expr::DictComp(_) | Expr::SetComp(_)
100-
) {
101-
continue;
108+
// Suppress lint for starred expressions.
109+
if arg.is_starred_expr() {
110+
return;
102111
}
103112

104-
if !checker
105-
.semantic()
106-
.resolve_builtin_symbol(func)
107-
.is_some_and(|builtin| matches!(builtin, "str" | "repr" | "ascii"))
113+
let mut diagnostic =
114+
checker.report_diagnostic(ExplicitFStringTypeConversion, expression.range());
115+
116+
// Don't support fixing f-string with debug text.
117+
if element
118+
.as_interpolation()
119+
.is_some_and(|interpolation| interpolation.debug_text.is_some())
108120
{
109-
continue;
121+
return;
110122
}
111123

112-
let mut diagnostic =
113-
checker.report_diagnostic(ExplicitFStringTypeConversion, expression.range());
114124
diagnostic.try_set_fix(|| {
115-
convert_call_to_conversion_flag(f_string, index, checker.locator(), checker.stylist())
125+
convert_call_to_conversion_flag(checker, conversion, f_string, index, arg)
116126
});
117127
}
118128
}
119129

120130
/// Generate a [`Fix`] to replace an explicit type conversion with a conversion flag.
121131
fn convert_call_to_conversion_flag(
132+
checker: &Checker,
133+
conversion: Conversion,
122134
f_string: &ast::FString,
123135
index: usize,
124-
locator: &Locator,
125-
stylist: &Stylist,
136+
arg: &Expr,
126137
) -> Result<Fix> {
127-
let source_code = locator.slice(f_string);
128-
transform_expression(source_code, stylist, |mut expression| {
138+
let source_code = checker.locator().slice(f_string);
139+
transform_expression(source_code, checker.stylist(), |mut expression| {
129140
let formatted_string = match_formatted_string(&mut expression)?;
130141
// Replace the formatted call expression at `index` with a conversion flag.
131142
let formatted_string_expression =
132143
match_formatted_string_expression(&mut formatted_string.parts[index])?;
133144
let call = match_call_mut(&mut formatted_string_expression.expression)?;
134-
let name = match_name(&call.func)?;
135-
match name.value {
136-
"str" => {
137-
formatted_string_expression.conversion = Some("s");
138-
}
139-
"repr" => {
140-
formatted_string_expression.conversion = Some("r");
141-
}
142-
"ascii" => {
143-
formatted_string_expression.conversion = Some("a");
144-
}
145-
_ => bail!("Unexpected function call: `{:?}`", name.value),
145+
146+
formatted_string_expression.conversion = Some(conversion.as_str());
147+
148+
if starts_with_brace(checker, arg) {
149+
formatted_string_expression.whitespace_before_expression = space();
146150
}
147-
formatted_string_expression.expression = call.args[0].value.clone();
151+
152+
formatted_string_expression.expression = if needs_paren(OperatorPrecedence::from_expr(arg))
153+
{
154+
call.args[0]
155+
.value
156+
.clone()
157+
.with_parens(LeftParen::default(), RightParen::default())
158+
} else {
159+
call.args[0].value.clone()
160+
};
161+
148162
Ok(expression)
149163
})
150164
.map(|output| Fix::safe_edit(Edit::range_replacement(output, f_string.range())))
151165
}
166+
167+
fn starts_with_brace(checker: &Checker, arg: &Expr) -> bool {
168+
checker
169+
.tokens()
170+
.in_range(arg.range())
171+
.iter()
172+
// Skip the trivia tokens
173+
.find(|token| !token.kind().is_trivia())
174+
.is_some_and(|token| matches!(token.kind(), TokenKind::Lbrace))
175+
}
176+
177+
fn needs_paren(precedence: OperatorPrecedence) -> bool {
178+
precedence <= OperatorPrecedence::Lambda
179+
}
180+
181+
/// Represents the three built-in Python conversion functions that can be replaced
182+
/// with f-string conversion flags.
183+
#[derive(Copy, Clone)]
184+
enum Conversion {
185+
Ascii,
186+
Str,
187+
Repr,
188+
}
189+
190+
impl Conversion {
191+
fn from_str(value: &str) -> Option<Self> {
192+
Some(match value {
193+
"ascii" => Self::Ascii,
194+
"str" => Self::Str,
195+
"repr" => Self::Repr,
196+
_ => return None,
197+
})
198+
}
199+
200+
fn as_str(self) -> &'static str {
201+
match self {
202+
Conversion::Ascii => "a",
203+
Conversion::Str => "s",
204+
Conversion::Repr => "r",
205+
}
206+
}
207+
}
208+
209+
impl Display for Conversion {
210+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211+
write!(f, "{}", self.as_str())
212+
}
213+
}

0 commit comments

Comments
 (0)