-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[pylint
] Add fix for useless-else-on-loop
(PLW0120
)
#9590
Changes from all commits
dee62b0
a763159
f40fc2c
bfba477
7f07a67
bb001ed
3b9570d
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 |
---|---|---|
@@ -1,10 +1,16 @@ | ||
use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt}; | ||
use anyhow::Result; | ||
|
||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ast::whitespace::indentation; | ||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::identifier; | ||
use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt}; | ||
use ruff_python_codegen::Stylist; | ||
use ruff_source_file::Locator; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
use crate::rules::pyupgrade::fixes::adjust_indentation; | ||
|
||
/// ## What it does | ||
/// Checks for `else` clauses on loops without a `break` statement. | ||
|
@@ -42,15 +48,50 @@ use crate::checkers::ast::Checker; | |
pub struct UselessElseOnLoop; | ||
|
||
impl Violation for UselessElseOnLoop { | ||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!( | ||
"`else` clause on loop without a `break` statement; remove the `else` and de-indent all the \ | ||
code inside it" | ||
"`else` clause on loop without a `break` statement; remove the `else` and dedent its contents" | ||
) | ||
} | ||
|
||
fn fix_title(&self) -> Option<String> { | ||
Some("Remove `else`".to_string()) | ||
} | ||
} | ||
|
||
/// PLW0120 | ||
pub(crate) fn useless_else_on_loop( | ||
checker: &mut Checker, | ||
stmt: &Stmt, | ||
body: &[Stmt], | ||
orelse: &[Stmt], | ||
) { | ||
if orelse.is_empty() || loop_exits_early(body) { | ||
return; | ||
} | ||
|
||
let else_range = identifier::else_(stmt, checker.locator().contents()).expect("else clause"); | ||
|
||
let mut diagnostic = Diagnostic::new(UselessElseOnLoop, else_range); | ||
|
||
if checker.settings.preview.is_enabled() { | ||
diagnostic.try_set_fix(|| { | ||
remove_else( | ||
stmt, | ||
orelse, | ||
else_range, | ||
checker.locator(), | ||
checker.stylist(), | ||
) | ||
}); | ||
} | ||
|
||
checker.diagnostics.push(diagnostic); | ||
} | ||
|
||
/// Returns `true` if the given body contains a `break` statement. | ||
fn loop_exits_early(body: &[Stmt]) -> bool { | ||
body.iter().any(|stmt| match stmt { | ||
Stmt::If(ast::StmtIf { | ||
|
@@ -91,17 +132,50 @@ fn loop_exits_early(body: &[Stmt]) -> bool { | |
}) | ||
} | ||
|
||
/// PLW0120 | ||
pub(crate) fn useless_else_on_loop( | ||
checker: &mut Checker, | ||
/// Generate a [`Fix`] to remove the `else` clause from the given statement. | ||
fn remove_else( | ||
stmt: &Stmt, | ||
body: &[Stmt], | ||
orelse: &[Stmt], | ||
) { | ||
if !orelse.is_empty() && !loop_exits_early(body) { | ||
checker.diagnostics.push(Diagnostic::new( | ||
UselessElseOnLoop, | ||
identifier::else_(stmt, checker.locator().contents()).unwrap(), | ||
)); | ||
else_range: TextRange, | ||
locator: &Locator, | ||
stylist: &Stylist, | ||
) -> Result<Fix> { | ||
let Some(start) = orelse.first() else { | ||
return Err(anyhow::anyhow!("Empty `else` clause")); | ||
}; | ||
let Some(end) = orelse.last() else { | ||
return Err(anyhow::anyhow!("Empty `else` clause")); | ||
}; | ||
|
||
let start_indentation = indentation(locator, start); | ||
if start_indentation.is_none() { | ||
// Inline `else` block (e.g., `else: x = 1`). | ||
Ok(Fix::safe_edit(Edit::deletion( | ||
else_range.start(), | ||
start.start(), | ||
))) | ||
} else { | ||
// Identify the indentation of the loop itself (e.g., the `while` or `for`). | ||
let Some(desired_indentation) = indentation(locator, stmt) else { | ||
return Err(anyhow::anyhow!("Compound statement cannot be inlined")); | ||
}; | ||
|
||
// Dedent the content from the end of the `else` to the end of the loop. | ||
let indented = adjust_indentation( | ||
TextRange::new( | ||
locator.full_line_end(else_range.start()), | ||
locator.full_line_end(end.end()), | ||
), | ||
desired_indentation, | ||
locator, | ||
stylist, | ||
)?; | ||
|
||
// Replace the content from the start of the `else` to the end of the loop. | ||
Ok(Fix::safe_edit(Edit::replacement( | ||
indented, | ||
locator.line_start(else_range.start()), | ||
locator.full_line_end(end.end()), | ||
))) | ||
} | ||
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 simplified this a bit, but we now drop comments on the same line as the 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. @diceroll123 - Did you feel strongly about preserving these? I can see why it might be useful, but the comments do get moved, right? Like: else: # some comment
x = 1
y = 2 Was getting moved to: x = 1
y = 2 # some comment Which struct me as potentially-wrong. For example, in this case: def test_retain_comment():
"""Retain the comment within the `else` block"""
for j in range(10):
pass
else: # [useless-else-on-loop]
print("fat chance")
for j in range(10):
break The comment was moved to after the break. 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. (Merging to keep things moving but happy to revisit.) 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. No strong feelings either way! It's much simpler without preserving comments on the else line. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the handling out to its own function so that it can return
Result
and make all the error cases explicit (rather than usingunwrap
, which panics on failure).