mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +00:00
[flake8-return
] Fix false-positive for variables used inside nested functions in RET504
(#18433)
<!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> This PR is the same as #17656. I accidentally deleted the branch of that PR, so I'm creating a new one. Fixes #14052 ## Test Plan Add regression tests <!-- How was it tested? -->
This commit is contained in:
parent
965f415212
commit
f2ae12bab3
6 changed files with 142 additions and 50 deletions
|
@ -422,6 +422,35 @@ def func(a: dict[str, int]) -> list[dict[str, int]]:
|
||||||
services = a["services"]
|
services = a["services"]
|
||||||
return services
|
return services
|
||||||
|
|
||||||
|
|
||||||
|
# See: https://github.com/astral-sh/ruff/issues/14052
|
||||||
|
def outer() -> list[object]:
|
||||||
|
@register
|
||||||
|
async def inner() -> None:
|
||||||
|
print(layout)
|
||||||
|
|
||||||
|
layout = [...]
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def outer() -> list[object]:
|
||||||
|
with open("") as f:
|
||||||
|
async def inner() -> None:
|
||||||
|
print(layout)
|
||||||
|
|
||||||
|
layout = [...]
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
def outer() -> list[object]:
|
||||||
|
def inner():
|
||||||
|
with open("") as f:
|
||||||
|
async def inner_inner() -> None:
|
||||||
|
print(layout)
|
||||||
|
|
||||||
|
layout = [...]
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
# See: https://github.com/astral-sh/ruff/issues/18411
|
# See: https://github.com/astral-sh/ruff/issues/18411
|
||||||
def f():
|
def f():
|
||||||
(#=
|
(#=
|
||||||
|
|
|
@ -4,8 +4,8 @@ use crate::Fix;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::codes::Rule;
|
use crate::codes::Rule;
|
||||||
use crate::rules::{
|
use crate::rules::{
|
||||||
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
|
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return,
|
||||||
pylint, pyupgrade, refurb, ruff,
|
flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Run lint rules over the [`Binding`]s.
|
/// Run lint rules over the [`Binding`]s.
|
||||||
|
@ -25,11 +25,20 @@ pub(crate) fn bindings(checker: &Checker) {
|
||||||
Rule::ForLoopWrites,
|
Rule::ForLoopWrites,
|
||||||
Rule::CustomTypeVarForSelf,
|
Rule::CustomTypeVarForSelf,
|
||||||
Rule::PrivateTypeParameter,
|
Rule::PrivateTypeParameter,
|
||||||
|
Rule::UnnecessaryAssign,
|
||||||
]) {
|
]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
|
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
|
||||||
|
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
|
||||||
|
if binding.kind.is_function_definition() {
|
||||||
|
flake8_return::rules::unnecessary_assign(
|
||||||
|
checker,
|
||||||
|
binding.statement(checker.semantic()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if checker.is_rule_enabled(Rule::UnusedVariable) {
|
if checker.is_rule_enabled(Rule::UnusedVariable) {
|
||||||
if binding.kind.is_bound_exception()
|
if binding.kind.is_bound_exception()
|
||||||
&& binding.is_unused()
|
&& binding.is_unused()
|
||||||
|
|
|
@ -207,7 +207,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||||
Rule::UnnecessaryReturnNone,
|
Rule::UnnecessaryReturnNone,
|
||||||
Rule::ImplicitReturnValue,
|
Rule::ImplicitReturnValue,
|
||||||
Rule::ImplicitReturn,
|
Rule::ImplicitReturn,
|
||||||
Rule::UnnecessaryAssign,
|
|
||||||
Rule::SuperfluousElseReturn,
|
Rule::SuperfluousElseReturn,
|
||||||
Rule::SuperfluousElseRaise,
|
Rule::SuperfluousElseRaise,
|
||||||
Rule::SuperfluousElseContinue,
|
Rule::SuperfluousElseContinue,
|
||||||
|
|
|
@ -539,7 +539,21 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt:
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RET504
|
/// 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 {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(stack) = create_stack(checker, function_def) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !result_exists(&stack.returns) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(function_scope) = checker.semantic().function_scope(function_def) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
for (assign, return_, stmt) in &stack.assignment_return {
|
for (assign, return_, stmt) in &stack.assignment_return {
|
||||||
// Identify, e.g., `return x`.
|
// Identify, e.g., `return x`.
|
||||||
let Some(value) = return_.value.as_ref() else {
|
let Some(value) = return_.value.as_ref() else {
|
||||||
|
@ -583,6 +597,22 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(assigned_binding) = function_scope
|
||||||
|
.get(assigned_id)
|
||||||
|
.map(|binding_id| checker.semantic().binding(binding_id))
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
let mut diagnostic = checker.report_diagnostic(
|
let mut diagnostic = checker.report_diagnostic(
|
||||||
UnnecessaryAssign {
|
UnnecessaryAssign {
|
||||||
name: assigned_id.to_string(),
|
name: assigned_id.to_string(),
|
||||||
|
@ -665,24 +695,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run all checks from the `flake8-return` plugin.
|
fn create_stack<'a>(
|
||||||
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
checker: &'a Checker,
|
||||||
let ast::StmtFunctionDef {
|
function_def: &'a ast::StmtFunctionDef,
|
||||||
decorator_list,
|
) -> Option<Stack<'a>> {
|
||||||
returns,
|
let ast::StmtFunctionDef { body, .. } = function_def;
|
||||||
body,
|
|
||||||
..
|
|
||||||
} = function_def;
|
|
||||||
|
|
||||||
// Find the last statement in the function.
|
// Find the last statement in the function.
|
||||||
let Some(last_stmt) = body.last() else {
|
let Some(last_stmt) = body.last() else {
|
||||||
// Skip empty functions.
|
// Skip empty functions.
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip functions that consist of a single return statement.
|
// Skip functions that consist of a single return statement.
|
||||||
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
|
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
|
||||||
return;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traverse the function body, to collect the stack.
|
// Traverse the function body, to collect the stack.
|
||||||
|
@ -696,9 +723,29 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||||
|
|
||||||
// Avoid false positives for generators.
|
// Avoid false positives for generators.
|
||||||
if stack.is_generator {
|
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;
|
||||||
|
};
|
||||||
|
let Some(last_stmt) = body.last() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if checker.any_rule_enabled(&[
|
if checker.any_rule_enabled(&[
|
||||||
Rule::SuperfluousElseReturn,
|
Rule::SuperfluousElseReturn,
|
||||||
Rule::SuperfluousElseRaise,
|
Rule::SuperfluousElseRaise,
|
||||||
|
@ -721,10 +768,6 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||||
if checker.is_rule_enabled(Rule::ImplicitReturn) {
|
if checker.is_rule_enabled(Rule::ImplicitReturn) {
|
||||||
implicit_return(checker, function_def, last_stmt);
|
implicit_return(checker, function_def, last_stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
|
|
||||||
unnecessary_assign(checker, &stack);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
|
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
|
||||||
// Skip functions that have a return annotation that is not `None`.
|
// Skip functions that have a return annotation that is not `None`.
|
||||||
|
|
|
@ -247,8 +247,6 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
|
||||||
422 | services = a["services"]
|
422 | services = a["services"]
|
||||||
423 | return services
|
423 | return services
|
||||||
| ^^^^^^^^ RET504
|
| ^^^^^^^^ RET504
|
||||||
424 |
|
|
||||||
425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
|
||||||
|
|
|
|
||||||
= help: Remove unnecessary assignment
|
= help: Remove unnecessary assignment
|
||||||
|
|
||||||
|
@ -260,46 +258,46 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
|
||||||
423 |- return services
|
423 |- return services
|
||||||
422 |+ return a["services"]
|
422 |+ return a["services"]
|
||||||
424 423 |
|
424 423 |
|
||||||
425 424 | # See: https://github.com/astral-sh/ruff/issues/18411
|
425 424 |
|
||||||
426 425 | def f():
|
426 425 | # See: https://github.com/astral-sh/ruff/issues/14052
|
||||||
|
|
||||||
RET504.py:429:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
||||||
|
|
|
|
||||||
427 | (#=
|
456 | (#=
|
||||||
428 | x) = 1
|
457 | x) = 1
|
||||||
429 | return x
|
458 | return x
|
||||||
| ^ RET504
|
| ^ RET504
|
||||||
430 |
|
459 |
|
||||||
431 | def f():
|
460 | def f():
|
||||||
|
|
|
|
||||||
= help: Remove unnecessary assignment
|
= help: Remove unnecessary assignment
|
||||||
|
|
||||||
ℹ Unsafe fix
|
ℹ Unsafe fix
|
||||||
424 424 |
|
453 453 |
|
||||||
425 425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
454 454 | # See: https://github.com/astral-sh/ruff/issues/18411
|
||||||
426 426 | def f():
|
455 455 | def f():
|
||||||
427 |- (#=
|
456 |- (#=
|
||||||
428 |- x) = 1
|
457 |- x) = 1
|
||||||
429 |- return x
|
458 |- return x
|
||||||
427 |+ return 1
|
456 |+ return 1
|
||||||
430 428 |
|
459 457 |
|
||||||
431 429 | def f():
|
460 458 | def f():
|
||||||
432 430 | x = (1
|
461 459 | x = (1
|
||||||
|
|
||||||
RET504.py:434:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
||||||
|
|
|
|
||||||
432 | x = (1
|
461 | x = (1
|
||||||
433 | )
|
462 | )
|
||||||
434 | return x
|
463 | return x
|
||||||
| ^ RET504
|
| ^ RET504
|
||||||
|
|
|
|
||||||
= help: Remove unnecessary assignment
|
= help: Remove unnecessary assignment
|
||||||
|
|
||||||
ℹ Unsafe fix
|
ℹ Unsafe fix
|
||||||
429 429 | return x
|
458 458 | return x
|
||||||
430 430 |
|
459 459 |
|
||||||
431 431 | def f():
|
460 460 | def f():
|
||||||
432 |- x = (1
|
461 |- x = (1
|
||||||
432 |+ return (1
|
461 |+ return (1
|
||||||
433 433 | )
|
462 462 | )
|
||||||
434 |- return x
|
463 |- return x
|
||||||
|
|
|
@ -2094,6 +2094,20 @@ impl<'a> SemanticModel<'a> {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds and returns the [`Scope`] corresponding to a given [`ast::StmtFunctionDef`].
|
||||||
|
///
|
||||||
|
/// This method searches all scopes created by a function definition, comparing the
|
||||||
|
/// [`TextRange`] of the provided `function_def` with the the range of the function
|
||||||
|
/// associated with the scope.
|
||||||
|
pub fn function_scope(&self, function_def: &ast::StmtFunctionDef) -> Option<&Scope> {
|
||||||
|
self.scopes.iter().find(|scope| {
|
||||||
|
let Some(function) = scope.kind.as_function() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
function.range() == function_def.range()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ShadowedBinding {
|
pub struct ShadowedBinding {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue