-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[flake8-return] Fix false-positive for variables used inside nested functions in RET504
#18433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
5251666
b1afafb
f0254c8
16c055b
92d8387
dccc22d
31f62a1
6b83c67
f23a3ef
e5fc4a7
48814a2
927178d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -556,7 +556,18 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt: | |
| } | ||
|
|
||
| /// RET504 | ||
| fn unnecessary_assign(checker: &Checker, stack: &Stack) { | ||
| pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) { | ||
| let Stmt::FunctionDef(function_def) = function_stmt else { | ||
| unreachable!(); | ||
| }; | ||
| let Some(stack) = create_stack(checker, function_def) else { | ||
| return; | ||
| }; | ||
|
|
||
| if !result_exists(&stack.returns) { | ||
| return; | ||
| } | ||
|
|
||
| for (assign, return_, stmt) in &stack.assignment_return { | ||
| // Identify, e.g., `return x`. | ||
| let Some(value) = return_.value.as_ref() else { | ||
|
|
@@ -600,6 +611,25 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) { | |
| continue; | ||
| } | ||
|
|
||
| let Some(assigned_binding) = checker | ||
| .semantic() | ||
| .bindings | ||
| .iter() | ||
| .filter(|binding| binding.kind.is_assignment()) | ||
| .find(|binding| binding.name(checker.source()) == assigned_id.as_str()) | ||
| else { | ||
| continue; | ||
| }; | ||
| // Check if there's any reference made to `assigned_binding` in another scope, e.g, nested | ||
| // functions. If there is, ignore them. | ||
| if assigned_binding | ||
| .references() | ||
| .map(|reference_id| checker.semantic().reference(reference_id)) | ||
| .any(|reference| reference.scope_id() != assigned_binding.scope) | ||
| { | ||
| continue; | ||
| } | ||
|
Comment on lines
+608
to
+614
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about this, but I suspect that this is the cause of the new false negatives. Could this be checking other scopes not nested within the current function? Both of the false negative cases have instances of the same variable names in other functions in the same file. I don't know of an API off the top of my head, but we may need to restrict the search to children of the current scope.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might actually be corroborated by your comment. I think other functions in the file might be leaking into the current check, but I could still be wrong.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the error is in the code above, where the search for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've come up with this solution where it tries to find the scope of the function by looking up its name. Then it looks up diff --git a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs
index 299acbc6c..29d759dcd 100644
--- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs
+++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs
@@ -7,8 +7,8 @@ use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, Decorator, ElifElseClause, Expr, Stmt};
use ruff_python_parser::TokenKind;
-use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::visibility::is_property;
+use ruff_python_semantic::{BindingKind, SemanticModel};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, is_python_whitespace};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -568,6 +568,19 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
return;
}
+ let Some(function_scope) = checker
+ .semantic()
+ .lookup_symbol(&function_def.name)
+ .map(|binding_id| checker.semantic().binding(binding_id))
+ .and_then(|binding| {
+ let BindingKind::FunctionDefinition(scope_id) = &binding.kind else {
+ return None;
+ };
+ Some(&checker.semantic().scopes[*scope_id])
+ })
+ else {
+ return;
+ };
for (assign, return_, stmt) in &stack.assignment_return {
// Identify, e.g., `return x`.
let Some(value) = return_.value.as_ref() else {
@@ -611,12 +624,9 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
continue;
}
- let Some(assigned_binding) = checker
- .semantic()
- .bindings
- .iter()
- .filter(|binding| binding.kind.is_assignment())
- .find(|binding| binding.name(checker.source()) == assigned_id.as_str())
+ let Some(assigned_binding) = function_scope
+ .get(assigned_id)
+ .map(|binding_id| checker.semantic().binding(binding_id))
else {
continue;
};
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how helpful this is, but I spent some time minimizing the ecosystem hit that was causing problems: # minimized from an ecosystem hit in
# https://github.com/astral-sh/ruff/pull/18433#issuecomment-2932216413
def foo():
safe_dag_prefix = 1
[safe_dag_prefix for _ in range(5)]
def bar():
safe_dag_prefix = 2
return safe_dag_prefix # Supposed to trigger RET504It seems that it might have something to do with the shared variable name appearing in a list comprehension. The Airflow example has it in a position like I think you're on the right track with looking up the scope. Is there not a way to look them up by something other than their name? |
||
|
|
||
| let mut diagnostic = checker.report_diagnostic( | ||
| UnnecessaryAssign { | ||
| name: assigned_id.to_string(), | ||
|
|
@@ -682,24 +712,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) { | |
| } | ||
| } | ||
|
|
||
| /// Run all checks from the `flake8-return` plugin. | ||
| pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { | ||
| let ast::StmtFunctionDef { | ||
| decorator_list, | ||
| returns, | ||
| body, | ||
| .. | ||
| } = function_def; | ||
| fn create_stack<'a>( | ||
| checker: &'a Checker, | ||
| function_def: &'a ast::StmtFunctionDef, | ||
| ) -> Option<Stack<'a>> { | ||
| let ast::StmtFunctionDef { body, .. } = function_def; | ||
|
|
||
| // Find the last statement in the function. | ||
| let Some(last_stmt) = body.last() else { | ||
| // Skip empty functions. | ||
| return; | ||
| return None; | ||
| }; | ||
|
|
||
| // Skip functions that consist of a single return statement. | ||
| if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) { | ||
| return; | ||
| return None; | ||
| } | ||
|
|
||
| // Traverse the function body, to collect the stack. | ||
|
|
@@ -713,9 +740,29 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { | |
|
|
||
| // Avoid false positives for generators. | ||
| if stack.is_generator { | ||
| return; | ||
| return None; | ||
| } | ||
|
|
||
| Some(stack) | ||
| } | ||
|
|
||
| /// Run all checks from the `flake8-return` plugin, but `RET504` which is ran | ||
| /// after the semantic model is fully built. | ||
| pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { | ||
| let ast::StmtFunctionDef { | ||
| decorator_list, | ||
| returns, | ||
| body, | ||
| .. | ||
| } = function_def; | ||
|
|
||
| let Some(stack) = create_stack(checker, function_def) else { | ||
| return; | ||
| }; | ||
|
|
||
| // SAFETY: `create_stack` checks if the function has the last statement. | ||
| let last_stmt = body.last().unwrap(); | ||
LaBatata101 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if checker.any_enabled(&[ | ||
| Rule::SuperfluousElseReturn, | ||
| Rule::SuperfluousElseRaise, | ||
|
|
@@ -738,10 +785,6 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { | |
| if checker.enabled(Rule::ImplicitReturn) { | ||
| implicit_return(checker, function_def, last_stmt); | ||
| } | ||
|
|
||
| if checker.enabled(Rule::UnnecessaryAssign) { | ||
| unnecessary_assign(checker, &stack); | ||
| } | ||
| } else { | ||
| if checker.enabled(Rule::UnnecessaryReturnNone) { | ||
| // Skip functions that have a return annotation that is not `None`. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.