Skip to content

Commit

Permalink
Implement autofix for RET504
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jun 10, 2023
1 parent 7275c16 commit 17a2887
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 31 deletions.
31 changes: 28 additions & 3 deletions crates/ruff/resources/test/fixtures/flake8_return/RET504.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def x():
return a


# ignore unpacking
# Ignore unpacking
def x():
b, a = [1, 2]
return a
Expand Down Expand Up @@ -109,7 +109,8 @@ def x():

# Considered OK, since functions can have side effects.
def x():
b, a = 1, 2
a = 1
b = 2
print(b)
return a

Expand Down Expand Up @@ -317,7 +318,7 @@ class Foo:
def foo():
with open("foo.txt", "r") as f:
x = f.read()
return x
return x # RET504


def foo():
Expand All @@ -332,3 +333,27 @@ def foo():
x = f.read()
print(x)
return x


# Autofix cases
def foo():
a = 1
b=a
return b # RET504


def foo():
a = 1
b =a
return b # RET504


def foo():
a = 1
b= a
return b # RET504


def foo():
a = 1 # Comment
return a
69 changes: 62 additions & 7 deletions crates/ruff/src/rules/flake8_return/rules/function.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::ops::Add;

use ruff_text_size::{TextRange, TextSize};
use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt};

use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
Expand All @@ -9,6 +12,7 @@ use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_semantic::model::SemanticModel;

use crate::autofix::edits;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_return::helpers::end_of_last_statement;
Expand Down Expand Up @@ -161,12 +165,16 @@ pub struct UnnecessaryAssign {
name: String,
}

impl Violation for UnnecessaryAssign {
impl AlwaysAutofixableViolation for UnnecessaryAssign {
#[derive_message_formats]
fn message(&self) -> String {
let UnnecessaryAssign { name } = self;
format!("Unnecessary assignment to `{name}` before `return` statement")
}

fn autofix_title(&self) -> String {
"Remove unnecessary assignment".to_string()
}
}

/// ## What it does
Expand Down Expand Up @@ -504,9 +512,9 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) {

/// RET504
fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
for (stmt_assign, stmt_return) in &stack.assignment_return {
for (assign, return_, stmt) in &stack.assignment_return {
// Identify, e.g., `return x`.
let Some(value) = stmt_return.value.as_ref() else {
let Some(value) = return_.value.as_ref() else {
continue;
};

Expand All @@ -515,11 +523,11 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
};

// Identify, e.g., `x = 1`.
if stmt_assign.targets.len() > 1 {
if assign.targets.len() > 1 {
continue;
}

let Some(target) = stmt_assign.targets.first() else {
let Some(target) = assign.targets.first() else {
continue;
};

Expand All @@ -535,12 +543,59 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
continue;
}

checker.diagnostics.push(Diagnostic::new(
let mut diagnostic = Diagnostic::new(
UnnecessaryAssign {
name: assigned_id.to_string(),
},
value.range(),
));
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
// Delete the `return` statement. There's no need to treat this as an isolated
// edit, since we're editing the preceding statement, so no conflicting edit would
// be allowed to remove that preceding statement.
let delete_return = edits::delete_stmt(
stmt,
None,
checker.locator,
checker.indexer,
checker.stylist,
);

// Replace the `x = 1` statement with `return 1`.
let content = checker.locator.slice(assign.range());
let equals_index = content
.find('=')
.ok_or(anyhow::anyhow!("expected '=' in assignment statement"))?;
let after_equals = equals_index + 1;

let replace_assign = Edit::range_replacement(
// If necessary, add whitespace after the `return` keyword.
// Ex) Convert `x=y` to `return y` (instead of `returny`).
if content[after_equals..]
.chars()
.next()
.map_or(false, char::is_alphabetic)
{
"return ".to_string()
} else {
"return".to_string()
},
// Replace from the start of the assignment statement to the end of the equals
// sign.
TextRange::new(
assign.range().start(),
assign
.range()
.start()
.add(TextSize::try_from(after_equals)?),
),
);

Ok(Fix::suggested_edits(replace_assign, [delete_return]))
});
}
checker.diagnostics.push(diagnostic);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,201 @@
---
source: crates/ruff/src/rules/flake8_return/mod.rs
---
RET504.py:6:12: RET504 Unnecessary assignment to `a` before `return` statement
RET504.py:6:12: RET504 [*] Unnecessary assignment to `a` before `return` statement
|
4 | def x():
5 | a = 1
6 | return a # RET504
| ^ RET504
|
= help: Remove unnecessary assignment

RET504.py:23:12: RET504 Unnecessary assignment to `formatted` before `return` statement
Suggested fix
2 2 | # Errors
3 3 | ###
4 4 | def x():
5 |- a = 1
6 |- return a # RET504
5 |+ return 1
7 6 |
8 7 |
9 8 | # Can be refactored false positives

RET504.py:23:12: RET504 [*] Unnecessary assignment to `formatted` before `return` statement
|
21 | # clean up after any blank components
22 | formatted = formatted.replace("()", "").replace(" ", " ").strip()
23 | return formatted
| ^^^^^^^^^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
19 19 | def x():
20 20 | formatted = _USER_AGENT_FORMATTER.format(format_string, **values)
21 21 | # clean up after any blank components
22 |- formatted = formatted.replace("()", "").replace(" ", " ").strip()
23 |- return formatted
22 |+ return formatted.replace("()", "").replace(" ", " ").strip()
24 23 |
25 24 |
26 25 | # https://github.com/afonasev/flake8-return/issues/47#issue-641117366

RET504.py:245:12: RET504 Unnecessary assignment to `queryset` before `return` statement
RET504.py:246:12: RET504 [*] Unnecessary assignment to `queryset` before `return` statement
|
243 | queryset = Model.filter(a=1)
244 | queryset = queryset.filter(c=3)
245 | return queryset
244 | queryset = Model.filter(a=1)
245 | queryset = queryset.filter(c=3)
246 | return queryset
| ^^^^^^^^ RET504
|
= help: Remove unnecessary assignment

RET504.py:250:12: RET504 Unnecessary assignment to `queryset` before `return` statement
Suggested fix
242 242 |
243 243 | def get_queryset():
244 244 | queryset = Model.filter(a=1)
245 |- queryset = queryset.filter(c=3)
246 |- return queryset
245 |+ return queryset.filter(c=3)
247 246 |
248 247 |
249 248 | def get_queryset():

RET504.py:251:12: RET504 [*] Unnecessary assignment to `queryset` before `return` statement
|
248 | def get_queryset():
249 | queryset = Model.filter(a=1)
250 | return queryset # RET504
249 | def get_queryset():
250 | queryset = Model.filter(a=1)
251 | return queryset # RET504
| ^^^^^^^^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
247 247 |
248 248 |
249 249 | def get_queryset():
250 |- queryset = Model.filter(a=1)
251 |- return queryset # RET504
250 |+ return Model.filter(a=1)
252 251 |
253 252 |
254 253 | # Function arguments

RET504.py:268:12: RET504 Unnecessary assignment to `val` before `return` statement
RET504.py:269:12: RET504 [*] Unnecessary assignment to `val` before `return` statement
|
266 | return val
267 | val = 1
268 | return val # RET504
267 | return val
268 | val = 1
269 | return val # RET504
| ^^^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
265 265 | def str_to_bool(val):
266 266 | if isinstance(val, bool):
267 267 | return val
268 |- val = 1
269 |- return val # RET504
268 |+ return 1
270 269 |
271 270 |
272 271 | def str_to_bool(val):

RET504.py:321:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
319 | with open("foo.txt", "r") as f:
320 | x = f.read()
321 | return x # RET504
| ^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
317 317 | # `with` statements
318 318 | def foo():
319 319 | with open("foo.txt", "r") as f:
320 |- x = f.read()
321 |- return x # RET504
320 |+ return f.read()
322 321 |
323 322 |
324 323 | def foo():

RET504.py:342:12: RET504 [*] Unnecessary assignment to `b` before `return` statement
|
340 | a = 1
341 | b=a
342 | return b # RET504
| ^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
338 338 | # Autofix cases
339 339 | def foo():
340 340 | a = 1
341 |- b=a
342 |- return b # RET504
341 |+ return a
343 342 |
344 343 |
345 344 | def foo():

RET504.py:348:12: RET504 [*] Unnecessary assignment to `b` before `return` statement
|
346 | a = 1
347 | b =a
348 | return b # RET504
| ^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
344 344 |
345 345 | def foo():
346 346 | a = 1
347 |- b =a
348 |- return b # RET504
347 |+ return a
349 348 |
350 349 |
351 350 | def foo():

RET504.py:320:12: RET504 Unnecessary assignment to `x` before `return` statement
RET504.py:354:12: RET504 [*] Unnecessary assignment to `b` before `return` statement
|
318 | with open("foo.txt", "r") as f:
319 | x = f.read()
320 | return x
352 | a = 1
353 | b= a
354 | return b # RET504
| ^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
350 350 |
351 351 | def foo():
352 352 | a = 1
353 |- b= a
354 |- return b # RET504
353 |+ return a
355 354 |
356 355 |
357 356 | def foo():

RET504.py:359:12: RET504 [*] Unnecessary assignment to `a` before `return` statement
|
357 | def foo():
358 | a = 1 # Comment
359 | return a
| ^ RET504
|
= help: Remove unnecessary assignment

Suggested fix
355 355 |
356 356 |
357 357 | def foo():
358 |- a = 1 # Comment
359 |- return a
358 |+ return 1 # Comment


Loading

0 comments on commit 17a2887

Please sign in to comment.