Skip to content

Commit f2ae12b

Browse files
authored
[flake8-return] Fix false-positive for variables used inside nested functions in RET504 (astral-sh#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 astral-sh#17656. I accidentally deleted the branch of that PR, so I'm creating a new one. Fixes astral-sh#14052 ## Test Plan Add regression tests <!-- How was it tested? -->
1 parent 965f415 commit f2ae12b

File tree

6 files changed

+142
-50
lines changed

6 files changed

+142
-50
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,35 @@ def func(a: dict[str, int]) -> list[dict[str, int]]:
422422
services = a["services"]
423423
return services
424424

425+
426+
# See: https://github.com/astral-sh/ruff/issues/14052
427+
def outer() -> list[object]:
428+
@register
429+
async def inner() -> None:
430+
print(layout)
431+
432+
layout = [...]
433+
return layout
434+
435+
def outer() -> list[object]:
436+
with open("") as f:
437+
async def inner() -> None:
438+
print(layout)
439+
440+
layout = [...]
441+
return layout
442+
443+
444+
def outer() -> list[object]:
445+
def inner():
446+
with open("") as f:
447+
async def inner_inner() -> None:
448+
print(layout)
449+
450+
layout = [...]
451+
return layout
452+
453+
425454
# See: https://github.com/astral-sh/ruff/issues/18411
426455
def f():
427456
(#=

crates/ruff_linter/src/checkers/ast/analyze/bindings.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use crate::Fix;
44
use crate::checkers::ast::Checker;
55
use crate::codes::Rule;
66
use crate::rules::{
7-
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
8-
pylint, pyupgrade, refurb, ruff,
7+
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return,
8+
flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff,
99
};
1010

1111
/// Run lint rules over the [`Binding`]s.
@@ -25,11 +25,20 @@ pub(crate) fn bindings(checker: &Checker) {
2525
Rule::ForLoopWrites,
2626
Rule::CustomTypeVarForSelf,
2727
Rule::PrivateTypeParameter,
28+
Rule::UnnecessaryAssign,
2829
]) {
2930
return;
3031
}
3132

3233
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
34+
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
35+
if binding.kind.is_function_definition() {
36+
flake8_return::rules::unnecessary_assign(
37+
checker,
38+
binding.statement(checker.semantic()).unwrap(),
39+
);
40+
}
41+
}
3342
if checker.is_rule_enabled(Rule::UnusedVariable) {
3443
if binding.kind.is_bound_exception()
3544
&& binding.is_unused()

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
207207
Rule::UnnecessaryReturnNone,
208208
Rule::ImplicitReturnValue,
209209
Rule::ImplicitReturn,
210-
Rule::UnnecessaryAssign,
211210
Rule::SuperfluousElseReturn,
212211
Rule::SuperfluousElseRaise,
213212
Rule::SuperfluousElseContinue,

crates/ruff_linter/src/rules/flake8_return/rules/function.rs

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,21 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt:
539539
}
540540

541541
/// RET504
542-
fn unnecessary_assign(checker: &Checker, stack: &Stack) {
542+
pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
543+
let Stmt::FunctionDef(function_def) = function_stmt else {
544+
return;
545+
};
546+
let Some(stack) = create_stack(checker, function_def) else {
547+
return;
548+
};
549+
550+
if !result_exists(&stack.returns) {
551+
return;
552+
}
553+
554+
let Some(function_scope) = checker.semantic().function_scope(function_def) else {
555+
return;
556+
};
543557
for (assign, return_, stmt) in &stack.assignment_return {
544558
// Identify, e.g., `return x`.
545559
let Some(value) = return_.value.as_ref() else {
@@ -583,6 +597,22 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) {
583597
continue;
584598
}
585599

600+
let Some(assigned_binding) = function_scope
601+
.get(assigned_id)
602+
.map(|binding_id| checker.semantic().binding(binding_id))
603+
else {
604+
continue;
605+
};
606+
// Check if there's any reference made to `assigned_binding` in another scope, e.g, nested
607+
// functions. If there is, ignore them.
608+
if assigned_binding
609+
.references()
610+
.map(|reference_id| checker.semantic().reference(reference_id))
611+
.any(|reference| reference.scope_id() != assigned_binding.scope)
612+
{
613+
continue;
614+
}
615+
586616
let mut diagnostic = checker.report_diagnostic(
587617
UnnecessaryAssign {
588618
name: assigned_id.to_string(),
@@ -665,24 +695,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) {
665695
}
666696
}
667697

668-
/// Run all checks from the `flake8-return` plugin.
669-
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
670-
let ast::StmtFunctionDef {
671-
decorator_list,
672-
returns,
673-
body,
674-
..
675-
} = function_def;
698+
fn create_stack<'a>(
699+
checker: &'a Checker,
700+
function_def: &'a ast::StmtFunctionDef,
701+
) -> Option<Stack<'a>> {
702+
let ast::StmtFunctionDef { body, .. } = function_def;
676703

677704
// Find the last statement in the function.
678705
let Some(last_stmt) = body.last() else {
679706
// Skip empty functions.
680-
return;
707+
return None;
681708
};
682709

683710
// Skip functions that consist of a single return statement.
684711
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
685-
return;
712+
return None;
686713
}
687714

688715
// Traverse the function body, to collect the stack.
@@ -696,9 +723,29 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
696723

697724
// Avoid false positives for generators.
698725
if stack.is_generator {
699-
return;
726+
return None;
700727
}
701728

729+
Some(stack)
730+
}
731+
732+
/// Run all checks from the `flake8-return` plugin, but `RET504` which is ran
733+
/// after the semantic model is fully built.
734+
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
735+
let ast::StmtFunctionDef {
736+
decorator_list,
737+
returns,
738+
body,
739+
..
740+
} = function_def;
741+
742+
let Some(stack) = create_stack(checker, function_def) else {
743+
return;
744+
};
745+
let Some(last_stmt) = body.last() else {
746+
return;
747+
};
748+
702749
if checker.any_rule_enabled(&[
703750
Rule::SuperfluousElseReturn,
704751
Rule::SuperfluousElseRaise,
@@ -721,10 +768,6 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
721768
if checker.is_rule_enabled(Rule::ImplicitReturn) {
722769
implicit_return(checker, function_def, last_stmt);
723770
}
724-
725-
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
726-
unnecessary_assign(checker, &stack);
727-
}
728771
} else {
729772
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
730773
// Skip functions that have a return annotation that is not `None`.

crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,6 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
247247
422 | services = a["services"]
248248
423 | return services
249249
| ^^^^^^^^ RET504
250-
424 |
251-
425 | # See: https://github.com/astral-sh/ruff/issues/18411
252250
|
253251
= help: Remove unnecessary assignment
254252

@@ -260,46 +258,46 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
260258
423 |- return services
261259
422 |+ return a["services"]
262260
424 423 |
263-
425 424 | # See: https://github.com/astral-sh/ruff/issues/18411
264-
426 425 | def f():
261+
425 424 |
262+
426 425 | # See: https://github.com/astral-sh/ruff/issues/14052
265263

266-
RET504.py:429:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
264+
RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
267265
|
268-
427 | (#=
269-
428 | x) = 1
270-
429 | return x
266+
456 | (#=
267+
457 | x) = 1
268+
458 | return x
271269
| ^ RET504
272-
430 |
273-
431 | def f():
270+
459 |
271+
460 | def f():
274272
|
275273
= help: Remove unnecessary assignment
276274

277275
Unsafe fix
278-
424 424 |
279-
425 425 | # See: https://github.com/astral-sh/ruff/issues/18411
280-
426 426 | def f():
281-
427 |- (#=
282-
428 |- x) = 1
283-
429 |- return x
284-
427 |+ return 1
285-
430 428 |
286-
431 429 | def f():
287-
432 430 | x = (1
276+
453 453 |
277+
454 454 | # See: https://github.com/astral-sh/ruff/issues/18411
278+
455 455 | def f():
279+
456 |- (#=
280+
457 |- x) = 1
281+
458 |- return x
282+
456 |+ return 1
283+
459 457 |
284+
460 458 | def f():
285+
461 459 | x = (1
288286

289-
RET504.py:434:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
287+
RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
290288
|
291-
432 | x = (1
292-
433 | )
293-
434 | return x
289+
461 | x = (1
290+
462 | )
291+
463 | return x
294292
| ^ RET504
295293
|
296294
= help: Remove unnecessary assignment
297295

298296
Unsafe fix
299-
429 429 | return x
300-
430 430 |
301-
431 431 | def f():
302-
432 |- x = (1
303-
432 |+ return (1
304-
433 433 | )
305-
434 |- return x
297+
458 458 | return x
298+
459 459 |
299+
460 460 | def f():
300+
461 |- x = (1
301+
461 |+ return (1
302+
462 462 | )
303+
463 |- return x

crates/ruff_python_semantic/src/model.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,20 @@ impl<'a> SemanticModel<'a> {
20942094
None
20952095
})
20962096
}
2097+
2098+
/// Finds and returns the [`Scope`] corresponding to a given [`ast::StmtFunctionDef`].
2099+
///
2100+
/// This method searches all scopes created by a function definition, comparing the
2101+
/// [`TextRange`] of the provided `function_def` with the the range of the function
2102+
/// associated with the scope.
2103+
pub fn function_scope(&self, function_def: &ast::StmtFunctionDef) -> Option<&Scope> {
2104+
self.scopes.iter().find(|scope| {
2105+
let Some(function) = scope.kind.as_function() else {
2106+
return false;
2107+
};
2108+
function.range() == function_def.range()
2109+
})
2110+
}
20972111
}
20982112

20992113
pub struct ShadowedBinding {

0 commit comments

Comments
 (0)