Implement useless-else-on-loop (#1031)

This commit is contained in:
Harutaka Kawamura 2022-12-04 23:22:04 +09:00 committed by GitHub
parent 0ae6890094
commit e070166409
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 4 deletions

View file

@ -772,6 +772,7 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
| PLR0206 | PropertyWithParameters | Cannot have defined parameters for properties | |
| PLR0402 | ConsiderUsingFromImport | Consider using `from ... import ...` | |
| PLR1701 | ConsiderMergingIsinstance | Consider merging these isinstance calls: `isinstance(..., (...))` | |
| PLW0120 | UselessElseOnLoop | Else clause on loop without a break statement, remove the else and de-indent all the code inside it. | |
### Ruff-specific rules

View file

@ -0,0 +1,103 @@
"""Check for else branches on loops with break and return only."""
def test_return_for():
"""else + return is not acceptable."""
for i in range(10):
if i % 2:
return i
else: # [useless-else-on-loop]
print("math is broken")
return None
def test_return_while():
"""else + return is not acceptable."""
while True:
return 1
else: # [useless-else-on-loop]
print("math is broken")
return None
while True:
def short_fun():
"""A function with a loop."""
for _ in range(10):
break
else: # [useless-else-on-loop]
print("or else!")
while True:
while False:
break
else: # [useless-else-on-loop]
print("or else!")
for j in range(10):
pass
else: # [useless-else-on-loop]
print("fat chance")
for j in range(10):
break
def test_return_for2():
"""no false positive for break in else
https://bitbucket.org/logilab/pylint/issue/117/useless-else-on-loop-false-positives
"""
for i in range(10):
for _ in range(i):
if i % 2:
break
else:
break
else:
print("great math")
def test_break_in_orelse_deep():
"""no false positive for break in else deeply nested"""
for _ in range(10):
if 1 < 2: # pylint: disable=comparison-of-constants
for _ in range(3):
if 3 < 2: # pylint: disable=comparison-of-constants
break
else:
break
else:
return True
return False
def test_break_in_orelse_deep2():
"""should rise a useless-else-on-loop message, as the break statement is only
for the inner for loop
"""
for _ in range(10):
if 1 < 2: # pylint: disable=comparison-of-constants
for _ in range(3):
if 3 < 2: # pylint: disable=comparison-of-constants
break
else:
print("all right")
else: # [useless-else-on-loop]
return True
return False
def test_break_in_orelse_deep3():
"""no false positive for break deeply nested in else"""
for _ in range(10):
for _ in range(3):
pass
else:
if 1 < 2: # pylint: disable=comparison-of-constants
break
else:
return True
return False

View file

@ -1009,16 +1009,27 @@ where
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
}
}
StmtKind::While { .. } => {
StmtKind::While { body, orelse, .. } => {
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Stmt(stmt));
}
if self.settings.enabled.contains(&CheckCode::PLW0120) {
pylint::plugins::useless_else_on_loop(self, stmt, body, orelse);
}
}
StmtKind::For {
target, body, iter, ..
target,
body,
iter,
orelse,
..
}
| StmtKind::AsyncFor {
target, body, iter, ..
target,
body,
iter,
orelse,
..
} => {
if self.settings.enabled.contains(&CheckCode::B007) {
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
@ -1029,6 +1040,9 @@ where
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Stmt(stmt));
}
if self.settings.enabled.contains(&CheckCode::PLW0120) {
pylint::plugins::useless_else_on_loop(self, stmt, body, orelse);
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {

View file

@ -99,6 +99,7 @@ pub enum CheckCode {
PLR0206,
PLR0402,
PLR1701,
PLW0120,
// flake8-builtins
A001,
A002,
@ -579,6 +580,7 @@ pub enum CheckKind {
PropertyWithParameters,
ConsiderUsingFromImport(String, String),
AwaitOutsideAsync,
UselessElseOnLoop,
// flake8-builtins
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
@ -881,6 +883,7 @@ impl CheckCode {
CheckCode::PLR1701 => {
CheckKind::ConsiderMergingIsinstance("...".to_string(), vec!["...".to_string()])
}
CheckCode::PLW0120 => CheckKind::UselessElseOnLoop,
// flake8-builtins
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
@ -1305,6 +1308,7 @@ impl CheckCode {
CheckCode::PLR0206 => CheckCategory::Pylint,
CheckCode::PLR0402 => CheckCategory::Pylint,
CheckCode::PLR1701 => CheckCategory::Pylint,
CheckCode::PLW0120 => CheckCategory::Pylint,
CheckCode::Q000 => CheckCategory::Flake8Quotes,
CheckCode::Q001 => CheckCategory::Flake8Quotes,
CheckCode::Q002 => CheckCategory::Flake8Quotes,
@ -1432,6 +1436,7 @@ impl CheckKind {
CheckKind::ConsiderMergingIsinstance(..) => &CheckCode::PLR1701,
CheckKind::PropertyWithParameters => &CheckCode::PLR0206,
CheckKind::ConsiderUsingFromImport(..) => &CheckCode::PLR0402,
CheckKind::UselessElseOnLoop => &CheckCode::PLW0120,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
@ -1831,6 +1836,9 @@ impl CheckKind {
CheckKind::AwaitOutsideAsync => {
"`await` should be used within an async function".to_string()
}
CheckKind::UselessElseOnLoop => "Else clause on loop without a break statement, \
remove the else and de-indent all the code inside it"
.to_string(),
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")

View file

@ -305,6 +305,11 @@ pub enum CheckCodePrefix {
PLR17,
PLR170,
PLR1701,
PLW,
PLW0,
PLW01,
PLW012,
PLW0120,
Q,
Q0,
Q00,
@ -1228,6 +1233,11 @@ impl CheckCodePrefix {
CheckCodePrefix::PLR17 => vec![CheckCode::PLR1701],
CheckCodePrefix::PLR170 => vec![CheckCode::PLR1701],
CheckCodePrefix::PLR1701 => vec![CheckCode::PLR1701],
CheckCodePrefix::PLW => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW0 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW01 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW012 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW0120 => vec![CheckCode::PLW0120],
CheckCodePrefix::Q => vec![
CheckCode::Q000,
CheckCode::Q001,
@ -1942,6 +1952,11 @@ impl CheckCodePrefix {
CheckCodePrefix::PLR17 => SuffixLength::Two,
CheckCodePrefix::PLR170 => SuffixLength::Three,
CheckCodePrefix::PLR1701 => SuffixLength::Four,
CheckCodePrefix::PLW => SuffixLength::Zero,
CheckCodePrefix::PLW0 => SuffixLength::One,
CheckCodePrefix::PLW01 => SuffixLength::Two,
CheckCodePrefix::PLW012 => SuffixLength::Three,
CheckCodePrefix::PLW0120 => SuffixLength::Four,
CheckCodePrefix::Q => SuffixLength::Zero,
CheckCodePrefix::Q0 => SuffixLength::One,
CheckCodePrefix::Q00 => SuffixLength::Two,
@ -2068,6 +2083,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::PLC,
CheckCodePrefix::PLE,
CheckCodePrefix::PLR,
CheckCodePrefix::PLW,
CheckCodePrefix::Q,
CheckCodePrefix::RET,
CheckCodePrefix::RUF,

View file

@ -17,6 +17,7 @@ mod tests {
#[test_case(CheckCode::PLR0206, Path::new("property_with_parameters.py"); "PLR0206")]
#[test_case(CheckCode::PLR0402, Path::new("import_aliasing.py"); "PLR0402")]
#[test_case(CheckCode::PLR1701, Path::new("consider_merging_isinstance.py"); "PLR1701")]
#[test_case(CheckCode::PLW0120, Path::new("useless_else_on_loop.py"); "PLW0120")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());
let mut checks = test_path(

View file

@ -1,6 +1,8 @@
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Alias, Arguments, Boolop, Cmpop, Expr, ExprKind, Stmt};
use rustpython_ast::{
Alias, Arguments, Boolop, Cmpop, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
};
use crate::ast::types::{FunctionScope, Range, ScopeKind};
use crate::autofix::Fix;
@ -162,3 +164,38 @@ pub fn consider_merging_isinstance(
}
}
}
fn loop_exits_early(body: &[Stmt]) -> bool {
body.iter().any(|stmt| match &stmt.node {
StmtKind::If { body, .. } => loop_exits_early(body),
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
..
} => {
loop_exits_early(body)
|| handlers.iter().any(|handler| match &handler.node {
ExcepthandlerKind::ExceptHandler { body, .. } => loop_exits_early(body),
})
|| loop_exits_early(orelse)
|| loop_exits_early(finalbody)
}
StmtKind::For { orelse, .. }
| StmtKind::AsyncFor { orelse, .. }
| StmtKind::While { orelse, .. } => loop_exits_early(orelse),
StmtKind::Break { .. } => true,
_ => false,
})
}
/// PLW0120
pub fn useless_else_on_loop(checker: &mut Checker, stmt: &Stmt, body: &[Stmt], orelse: &[Stmt]) {
if !orelse.is_empty() && !loop_exits_early(body) {
checker.add_check(Check::new(
CheckKind::UselessElseOnLoop,
Range::from_located(stmt),
));
}
}

View file

@ -0,0 +1,61 @@
---
source: src/pylint/mod.rs
expression: checks
---
- kind: UselessElseOnLoop
location:
row: 6
column: 4
end_location:
row: 11
column: 4
fix: ~
- kind: UselessElseOnLoop
location:
row: 16
column: 4
end_location:
row: 20
column: 4
fix: ~
- kind: UselessElseOnLoop
location:
row: 23
column: 0
end_location:
row: 34
column: 0
fix: ~
- kind: UselessElseOnLoop
location:
row: 34
column: 0
end_location:
row: 40
column: 0
fix: ~
- kind: UselessElseOnLoop
location:
row: 40
column: 0
end_location:
row: 48
column: 0
fix: ~
- kind: UselessElseOnLoop
location:
row: 81
column: 4
end_location:
row: 90
column: 4
fix: ~
- kind: UselessElseOnLoop
location:
row: 96
column: 8
end_location:
row: 101
column: 4
fix: ~