Skip to content

Commit a0356f1

Browse files
authored
[flake8-errmsg] Avoid shadowing existing msg in fix for EM101 (astral-sh#24363)
Closes astral-sh#24335 As suggested, we use the new `fresh_binding` helper from astral-sh#24316 Note that the issue with this fix was already brought up in the discussion in astral-sh#9052 (see also astral-sh#9059), where it was decided that it was okay because the fix was already marked as unsafe.
1 parent 37f5d61 commit a0356f1

6 files changed

Lines changed: 70 additions & 29 deletions

File tree

‎crates/ruff_linter/resources/test/fixtures/flake8_errmsg/EM.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,11 @@ def f_typing_cast_excluded_aliased():
110110
raise my_cast(RuntimeError, "This should not trigger EM101")
111111

112112

113+
# Regression test for https://github.com/astral-sh/ruff/issues/24335
114+
# (Do not shadow existing `msg`)
115+
def f():
116+
msg = "."
117+
try:
118+
raise RuntimeError("!")
119+
except RuntimeError:
120+
return msg

‎crates/ruff_linter/src/fix/edits.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
use anyhow::{Context, Result};
44

55
use ruff_python_ast::AnyNodeRef;
6+
use ruff_python_ast::name::Name;
67
use ruff_python_ast::token::{self, Tokens, parenthesized_range};
78
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt};
89
use ruff_python_codegen::Stylist;
910
use ruff_python_index::Indexer;
11+
use ruff_python_semantic::SemanticModel;
1012
use ruff_python_trivia::textwrap::dedent_to;
1113
use ruff_python_trivia::{
1214
PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace,
@@ -397,6 +399,23 @@ pub(crate) fn add_parameter(
397399
}
398400
}
399401

402+
/// Return a fresh binding name derived from `base` that does not shadow an
403+
/// existing non-builtin symbol in the current semantic scope.
404+
pub(crate) fn fresh_binding_name(semantic: &SemanticModel<'_>, base: &str) -> Name {
405+
if semantic.is_available(base) {
406+
return Name::new(base);
407+
}
408+
409+
let mut index = 0;
410+
loop {
411+
let candidate = format!("{base}_{index}");
412+
if semantic.is_available(&candidate) {
413+
return Name::new(candidate);
414+
}
415+
index += 1;
416+
}
417+
}
418+
400419
/// Safely adjust the indentation of the indented block at [`TextRange`].
401420
///
402421
/// The [`TextRange`] is assumed to represent an entire indented block, including the leading

‎crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
22
use ruff_python_ast::whitespace;
33
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
44
use ruff_python_codegen::Stylist;
5+
use ruff_python_semantic::SemanticModel;
56
use ruff_source_file::LineRanges;
67
use ruff_text_size::Ranged;
78

89
use crate::Locator;
910
use crate::checkers::ast::Checker;
11+
use crate::fix::edits::fresh_binding_name;
1012
use crate::registry::Rule;
1113
use crate::{Edit, Fix, FixAvailability, Violation};
1214

@@ -211,6 +213,7 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
211213
indentation,
212214
checker.stylist(),
213215
checker.locator(),
216+
checker.semantic(),
214217
));
215218
}
216219
}
@@ -229,6 +232,7 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
229232
indentation,
230233
checker.stylist(),
231234
checker.locator(),
235+
checker.semantic(),
232236
));
233237
}
234238
}
@@ -246,6 +250,7 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
246250
indentation,
247251
checker.stylist(),
248252
checker.locator(),
253+
checker.semantic(),
249254
));
250255
}
251256
}
@@ -266,6 +271,7 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
266271
indentation,
267272
checker.stylist(),
268273
checker.locator(),
274+
checker.semantic(),
269275
));
270276
}
271277
}
@@ -293,19 +299,23 @@ fn generate_fix(
293299
stmt_indentation: &str,
294300
stylist: &Stylist,
295301
locator: &Locator,
302+
semantic: &SemanticModel,
296303
) -> Fix {
304+
let msg_name = fresh_binding_name(semantic, "msg");
297305
Fix::unsafe_edits(
298306
Edit::insertion(
299307
if locator.contains_line_break(exc_arg.range()) {
300308
format!(
301-
"msg = ({line_ending}{stmt_indentation}{indentation}{}{line_ending}{stmt_indentation}){line_ending}{stmt_indentation}",
309+
"{} = ({line_ending}{stmt_indentation}{indentation}{}{line_ending}{stmt_indentation}){line_ending}{stmt_indentation}",
310+
msg_name,
302311
locator.slice(exc_arg.range()),
303312
line_ending = stylist.line_ending().as_str(),
304313
indentation = stylist.indentation().as_str(),
305314
)
306315
} else {
307316
format!(
308-
"msg = {}{}{}",
317+
"{} = {}{}{}",
318+
msg_name,
309319
locator.slice(exc_arg.range()),
310320
stylist.line_ending().as_str(),
311321
stmt_indentation,
@@ -314,7 +324,7 @@ fn generate_fix(
314324
stmt.start(),
315325
),
316326
[Edit::range_replacement(
317-
String::from("msg"),
327+
msg_name.to_string(),
318328
exc_arg.range(),
319329
)],
320330
)

‎crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__custom.snap‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ help: Assign to variable; remove string literal
7272
30 | def f_msg_defined():
7373
31 | msg = "hello"
7474
- raise RuntimeError("This is an example exception")
75-
32 + msg = "This is an example exception"
76-
33 + raise RuntimeError(msg)
75+
32 + msg_0 = "This is an example exception"
76+
33 + raise RuntimeError(msg_0)
7777
34 |
7878
35 |
7979
36 | def f_msg_in_nested_scope():
@@ -111,8 +111,8 @@ help: Assign to variable; remove string literal
111111
44 |
112112
45 | def nested():
113113
- raise RuntimeError("This is an example exception")
114-
46 + msg = "This is an example exception"
115-
47 + raise RuntimeError(msg)
114+
46 + msg_0 = "This is an example exception"
115+
47 + raise RuntimeError(msg_0)
116116
48 |
117117
49 |
118118
50 | def f_fix_indentation_check(foo):

‎crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__defaults.snap‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ help: Assign to variable; remove string literal
110110
30 | def f_msg_defined():
111111
31 | msg = "hello"
112112
- raise RuntimeError("This is an example exception")
113-
32 + msg = "This is an example exception"
114-
33 + raise RuntimeError(msg)
113+
32 + msg_0 = "This is an example exception"
114+
33 + raise RuntimeError(msg_0)
115115
34 |
116116
35 |
117117
36 | def f_msg_in_nested_scope():
@@ -149,8 +149,8 @@ help: Assign to variable; remove string literal
149149
44 |
150150
45 | def nested():
151151
- raise RuntimeError("This is an example exception")
152-
46 + msg = "This is an example exception"
153-
47 + raise RuntimeError(msg)
152+
46 + msg_0 = "This is an example exception"
153+
47 + raise RuntimeError(msg_0)
154154
48 |
155155
49 |
156156
50 | def f_fix_indentation_check(foo):
@@ -348,3 +348,24 @@ help: Assign to variable; remove `.format()` string
348348
95 |
349349
96 | def raise_typing_cast_exception():
350350
note: This is an unsafe fix and may change runtime behavior
351+
352+
EM101 [*] Exception must not use a string literal, assign to variable first
353+
--> EM.py:118:28
354+
|
355+
116 | msg = "."
356+
117 | try:
357+
118 | raise RuntimeError("!")
358+
| ^^^
359+
119 | except RuntimeError:
360+
120 | return msg
361+
|
362+
help: Assign to variable; remove string literal
363+
115 | def f():
364+
116 | msg = "."
365+
117 | try:
366+
- raise RuntimeError("!")
367+
118 + msg_0 = "!"
368+
119 + raise RuntimeError(msg_0)
369+
120 | except RuntimeError:
370+
121 | return msg
371+
note: This is an unsafe fix and may change runtime behavior

‎crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs‎

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
use crate::fix::edits::fresh_binding_name;
12
use ruff_macros::{ViolationMetadata, derive_message_formats};
2-
use ruff_python_ast::name::Name;
33
use ruff_python_ast::{self as ast, Expr};
44
use ruff_python_semantic::{SemanticModel, analyze::typing::is_mutable_expr};
55

@@ -129,20 +129,3 @@ fn generate_dict_comprehension(
129129
};
130130
generator.expr(&dict_comp.into())
131131
}
132-
133-
/// Return a fresh binding name derived from `base` that does not shadow an
134-
/// existing non-builtin symbol in the current semantic scope.
135-
fn fresh_binding_name(semantic: &SemanticModel<'_>, base: &str) -> Name {
136-
if semantic.is_available(base) {
137-
return Name::new(base);
138-
}
139-
140-
let mut index = 0;
141-
loop {
142-
let candidate = format!("{base}_{index}");
143-
if semantic.is_available(&candidate) {
144-
return Name::new(candidate);
145-
}
146-
index += 1;
147-
}
148-
}

0 commit comments

Comments
 (0)