mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 04:19:18 +00:00
[flake8-return] Consider exception suppress for unnecessary assignment (#9673)
## Summary This review contains a fix for [RET504](https://docs.astral.sh/ruff/rules/unnecessary-assign/) (unnecessary-assign) The problem is that Ruff suggests combining a return statement inside contextlib.suppress. Even though it is an unsafe fix it might lead to an invalid code that is not equivalent to the original one. See: https://github.com/astral-sh/ruff/issues/5909 ## Test Plan ```bash cargo test ```
This commit is contained in:
parent
0045032905
commit
ad2cfa3dba
4 changed files with 147 additions and 17 deletions
|
@ -363,3 +363,46 @@ def foo():
|
||||||
def mavko_debari(P_kbar):
|
def mavko_debari(P_kbar):
|
||||||
D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
||||||
return D
|
return D
|
||||||
|
|
||||||
|
|
||||||
|
# contextlib suppress in with statement
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
x = 2
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
x = x + 1
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def foo(data):
|
||||||
|
with open("in.txt") as file_out, contextlib.suppress(IOError):
|
||||||
|
file_out.write(data)
|
||||||
|
data = 10
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def foo(data):
|
||||||
|
with open("in.txt") as file_out:
|
||||||
|
file_out.write(data)
|
||||||
|
with contextlib.suppress(IOError):
|
||||||
|
data = 10
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
y = 1
|
||||||
|
x = 2
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
x = 1
|
||||||
|
y = y + 2
|
||||||
|
return y # RET504
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
y = 1
|
||||||
|
if y > 0:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
y = 2
|
||||||
|
return y
|
||||||
|
|
|
@ -737,7 +737,7 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
|
||||||
|
|
||||||
// Traverse the function body, to collect the stack.
|
// Traverse the function body, to collect the stack.
|
||||||
let stack = {
|
let stack = {
|
||||||
let mut visitor = ReturnVisitor::default();
|
let mut visitor = ReturnVisitor::new(checker.semantic());
|
||||||
for stmt in body {
|
for stmt in body {
|
||||||
visitor.visit_stmt(stmt);
|
visitor.visit_stmt(stmt);
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,5 +217,28 @@ RET504.py:365:12: RET504 [*] Unnecessary assignment to `D` before `return` state
|
||||||
364 |- D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
364 |- D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
||||||
365 |- return D
|
365 |- return D
|
||||||
364 |+ return 0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
364 |+ return 0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
|
||||||
|
366 365 |
|
||||||
|
367 366 |
|
||||||
|
368 367 | # contextlib suppress in with statement
|
||||||
|
|
||||||
|
RET504.py:400:12: RET504 [*] Unnecessary assignment to `y` before `return` statement
|
||||||
|
|
|
||||||
|
398 | x = 1
|
||||||
|
399 | y = y + 2
|
||||||
|
400 | return y # RET504
|
||||||
|
| ^ RET504
|
||||||
|
|
|
||||||
|
= help: Remove unnecessary assignment
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
396 396 | x = 2
|
||||||
|
397 397 | with contextlib.suppress(Exception):
|
||||||
|
398 398 | x = 1
|
||||||
|
399 |- y = y + 2
|
||||||
|
400 |- return y # RET504
|
||||||
|
399 |+ return y + 2
|
||||||
|
401 400 |
|
||||||
|
402 401 |
|
||||||
|
403 402 | def foo():
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,34 +3,48 @@ use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use ruff_python_ast::visitor;
|
use ruff_python_ast::visitor;
|
||||||
use ruff_python_ast::visitor::Visitor;
|
use ruff_python_ast::visitor::Visitor;
|
||||||
|
use ruff_python_semantic::SemanticModel;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(super) struct Stack<'a> {
|
pub(super) struct Stack<'data> {
|
||||||
/// The `return` statements in the current function.
|
/// The `return` statements in the current function.
|
||||||
pub(super) returns: Vec<&'a ast::StmtReturn>,
|
pub(super) returns: Vec<&'data ast::StmtReturn>,
|
||||||
/// The `elif` or `else` statements in the current function.
|
/// The `elif` or `else` statements in the current function.
|
||||||
pub(super) elifs_elses: Vec<(&'a [Stmt], &'a ElifElseClause)>,
|
pub(super) elifs_elses: Vec<(&'data [Stmt], &'data ElifElseClause)>,
|
||||||
/// The non-local variables in the current function.
|
/// The non-local variables in the current function.
|
||||||
pub(super) non_locals: FxHashSet<&'a str>,
|
pub(super) non_locals: FxHashSet<&'data str>,
|
||||||
/// Whether the current function is a generator.
|
/// Whether the current function is a generator.
|
||||||
pub(super) is_generator: bool,
|
pub(super) is_generator: bool,
|
||||||
/// The `assignment`-to-`return` statement pairs in the current function.
|
/// The `assignment`-to-`return` statement pairs in the current function.
|
||||||
/// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement
|
/// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement
|
||||||
/// removal for the `return` statement.
|
/// removal for the `return` statement.
|
||||||
pub(super) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>,
|
pub(super) assignment_return:
|
||||||
|
Vec<(&'data ast::StmtAssign, &'data ast::StmtReturn, &'data Stmt)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
pub(super) struct ReturnVisitor<'semantic, 'data> {
|
||||||
pub(super) struct ReturnVisitor<'a> {
|
/// The semantic model of the current file.
|
||||||
|
semantic: &'semantic SemanticModel<'data>,
|
||||||
/// The current stack of nodes.
|
/// The current stack of nodes.
|
||||||
pub(super) stack: Stack<'a>,
|
pub(super) stack: Stack<'data>,
|
||||||
/// The preceding sibling of the current node.
|
/// The preceding sibling of the current node.
|
||||||
sibling: Option<&'a Stmt>,
|
sibling: Option<&'data Stmt>,
|
||||||
/// The parent nodes of the current node.
|
/// The parent nodes of the current node.
|
||||||
parents: Vec<&'a Stmt>,
|
parents: Vec<&'data Stmt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
impl<'semantic, 'data> ReturnVisitor<'semantic, 'data> {
|
||||||
|
pub(super) fn new(semantic: &'semantic SemanticModel<'data>) -> Self {
|
||||||
|
Self {
|
||||||
|
semantic,
|
||||||
|
stack: Stack::default(),
|
||||||
|
sibling: None,
|
||||||
|
parents: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'semantic, 'a> Visitor<'a> for ReturnVisitor<'semantic, 'a> {
|
||||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::ClassDef(ast::StmtClassDef { decorator_list, .. }) => {
|
Stmt::ClassDef(ast::StmtClassDef { decorator_list, .. }) => {
|
||||||
|
@ -95,11 +109,17 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||||
// x = f.read()
|
// x = f.read()
|
||||||
// return x
|
// return x
|
||||||
// ```
|
// ```
|
||||||
Stmt::With(ast::StmtWith { body, .. }) => {
|
Stmt::With(with) => {
|
||||||
if let Some(stmt_assign) = body.last().and_then(Stmt::as_assign_stmt) {
|
if let Some(stmt_assign) =
|
||||||
self.stack
|
with.body.last().and_then(Stmt::as_assign_stmt)
|
||||||
.assignment_return
|
{
|
||||||
.push((stmt_assign, stmt_return, stmt));
|
if !has_conditional_body(with, self.semantic) {
|
||||||
|
self.stack.assignment_return.push((
|
||||||
|
stmt_assign,
|
||||||
|
stmt_return,
|
||||||
|
stmt,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -142,3 +162,47 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||||
self.sibling = sibling;
|
self.sibling = sibling;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RET504
|
||||||
|
/// If the last statement is a `return` statement, and the second-to-last statement is a
|
||||||
|
/// `with` statement that suppresses an exception, then we should not analyze the `return`
|
||||||
|
/// statement for unnecessary assignments. Otherwise we will suggest removing the assignment
|
||||||
|
/// and the `with` statement, which would change the behavior of the code.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```python
|
||||||
|
/// def foo(data):
|
||||||
|
/// with suppress(JSONDecoderError):
|
||||||
|
/// data = data.decode()
|
||||||
|
/// return data
|
||||||
|
|
||||||
|
/// Returns `true` if the [`With`] statement is known to have a conditional body. In other words:
|
||||||
|
/// if the [`With`] statement's body may or may not run.
|
||||||
|
///
|
||||||
|
/// For example, in the following, it's unsafe to inline the `return` into the `with`, since if
|
||||||
|
/// `data.decode()` fails, the behavior of the program will differ. (As-is, the function will return
|
||||||
|
/// the input `data`; if we inline the `return`, the function will return `None`.)
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// def func(data):
|
||||||
|
/// with suppress(JSONDecoderError):
|
||||||
|
/// data = data.decode()
|
||||||
|
/// return data
|
||||||
|
/// ```
|
||||||
|
fn has_conditional_body(with: &ast::StmtWith, semantic: &SemanticModel) -> bool {
|
||||||
|
with.items.iter().any(|item| {
|
||||||
|
let ast::WithItem {
|
||||||
|
context_expr: Expr::Call(ast::ExprCall { func, .. }),
|
||||||
|
..
|
||||||
|
} = item
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if let Some(call_path) = semantic.resolve_call_path(func) {
|
||||||
|
if call_path.as_slice() == ["contextlib", "suppress"] {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue