Skip to content

Commit be3307e

Browse files
Make unnecessary-paren-on-raise-exception an unsafe edit (#8231)
## Summary This rule is now unsafe if we can't verify that the `obj` in `raise obj()` is a class or builtin. (If we verify that it's a function, we don't raise at all, as before.) See the documentation change for motivation behind the unsafe edit. Closes #8228.
1 parent 317d3dd commit be3307e

File tree

3 files changed

+67
-16
lines changed

3 files changed

+67
-16
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_raise/RSE102.py

+3
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,6 @@ def error():
7979
raise IndexError() from ZeroDivisionError
8080

8181
raise IndexError();
82+
83+
# RSE102
84+
raise Foo()

crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs

+43-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
22
use ruff_macros::{derive_message_formats, violation};
33
use ruff_python_ast::{self as ast, Expr};
4+
use ruff_python_semantic::BindingKind;
45
use ruff_text_size::Ranged;
56

67
use crate::checkers::ast::Checker;
@@ -15,6 +16,13 @@ use crate::checkers::ast::Checker;
1516
///
1617
/// Removing the parentheses makes the code more concise.
1718
///
19+
/// ## Known problems
20+
/// Parentheses can only be omitted if the exception is a class, as opposed to
21+
/// a function call. This rule isn't always capable of distinguishing between
22+
/// the two. For example, if you define a method `get_exception` that itself
23+
/// returns an exception object, this rule will falsy mark the parentheses
24+
/// in `raise get_exception()` as unnecessary.
25+
///
1826
/// ## Example
1927
/// ```python
2028
/// raise TypeError()
@@ -54,25 +62,32 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
5462

5563
if arguments.is_empty() {
5664
// `raise func()` still requires parentheses; only `raise Class()` does not.
57-
if checker
58-
.semantic()
59-
.lookup_attribute(func)
60-
.is_some_and(|id| checker.semantic().binding(id).kind.is_function_definition())
61-
{
62-
return;
63-
}
65+
let exception_type = if let Some(id) = checker.semantic().lookup_attribute(func) {
66+
match checker.semantic().binding(id).kind {
67+
BindingKind::FunctionDefinition(_) => return,
68+
BindingKind::ClassDefinition(_) => Some(ExceptionType::Class),
69+
BindingKind::Builtin => Some(ExceptionType::Builtin),
70+
_ => None,
71+
}
72+
} else {
73+
None
74+
};
6475

6576
// `ctypes.WinError()` is a function, not a class. It's part of the standard library, so
6677
// we might as well get it right.
67-
if checker
68-
.semantic()
69-
.resolve_call_path(func)
70-
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
78+
if exception_type
79+
.as_ref()
80+
.is_some_and(ExceptionType::is_builtin)
81+
&& checker
82+
.semantic()
83+
.resolve_call_path(func)
84+
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
7185
{
7286
return;
7387
}
7488

7589
let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, arguments.range());
90+
7691
// If the arguments are immediately followed by a `from`, insert whitespace to avoid
7792
// a syntax error, as in:
7893
// ```python
@@ -85,13 +100,25 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
85100
.next()
86101
.is_some_and(char::is_alphanumeric)
87102
{
88-
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
89-
" ".to_string(),
90-
arguments.range(),
91-
)));
103+
diagnostic.set_fix(if exception_type.is_some() {
104+
Fix::safe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
105+
} else {
106+
Fix::unsafe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
107+
});
92108
} else {
93-
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(arguments.range())));
109+
diagnostic.set_fix(if exception_type.is_some() {
110+
Fix::safe_edit(Edit::range_deletion(arguments.range()))
111+
} else {
112+
Fix::unsafe_edit(Edit::range_deletion(arguments.range()))
113+
});
94114
}
115+
95116
checker.diagnostics.push(diagnostic);
96117
}
97118
}
119+
120+
#[derive(Debug, is_macro::Is)]
121+
enum ExceptionType {
122+
Class,
123+
Builtin,
124+
}

crates/ruff_linter/src/rules/flake8_raise/snapshots/ruff_linter__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap

+21
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,16 @@ RSE102.py:79:17: RSE102 [*] Unnecessary parentheses on raised exception
238238
79 |+raise IndexError from ZeroDivisionError
239239
80 80 |
240240
81 81 | raise IndexError();
241+
82 82 |
241242

242243
RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
243244
|
244245
79 | raise IndexError() from ZeroDivisionError
245246
80 |
246247
81 | raise IndexError();
247248
| ^^ RSE102
249+
82 |
250+
83 | # RSE102
248251
|
249252
= help: Remove unnecessary parentheses
250253

@@ -254,5 +257,23 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
254257
80 80 |
255258
81 |-raise IndexError();
256259
81 |+raise IndexError;
260+
82 82 |
261+
83 83 | # RSE102
262+
84 84 | raise Foo()
263+
264+
RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
265+
|
266+
83 | # RSE102
267+
84 | raise Foo()
268+
| ^^ RSE102
269+
|
270+
= help: Remove unnecessary parentheses
271+
272+
Suggested fix
273+
81 81 | raise IndexError();
274+
82 82 |
275+
83 83 | # RSE102
276+
84 |-raise Foo()
277+
84 |+raise Foo
257278

258279

0 commit comments

Comments
 (0)