mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:21 +00:00
Autofix PIE810 rule violations (#3411)
This commit is contained in:
parent
872829ca72
commit
bb3bb24b59
4 changed files with 188 additions and 50 deletions
|
@ -6,6 +6,8 @@ obj.endswith("foo") or obj.endswith("bar")
|
||||||
obj.startswith(foo) or obj.startswith(bar)
|
obj.startswith(foo) or obj.startswith(bar)
|
||||||
# error
|
# error
|
||||||
obj.startswith(foo) or obj.startswith("foo")
|
obj.startswith(foo) or obj.startswith("foo")
|
||||||
|
# error
|
||||||
|
obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
|
||||||
|
|
||||||
# ok
|
# ok
|
||||||
obj.startswith(("foo", "bar"))
|
obj.startswith(("foo", "bar"))
|
||||||
|
|
|
@ -3435,7 +3435,7 @@ where
|
||||||
pylint::rules::merge_isinstance(self, expr, op, values);
|
pylint::rules::merge_isinstance(self, expr, op, values);
|
||||||
}
|
}
|
||||||
if self.settings.rules.enabled(&Rule::SingleStartsEndsWith) {
|
if self.settings.rules.enabled(&Rule::SingleStartsEndsWith) {
|
||||||
flake8_pie::rules::single_starts_ends_with(self, values, op);
|
flake8_pie::rules::single_starts_ends_with(self, expr);
|
||||||
}
|
}
|
||||||
if self.settings.rules.enabled(&Rule::DuplicateIsinstanceCall) {
|
if self.settings.rules.enabled(&Rule::DuplicateIsinstanceCall) {
|
||||||
flake8_simplify::rules::duplicate_isinstance_call(self, expr);
|
flake8_simplify::rules::duplicate_isinstance_call(self, expr);
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
use itertools::Either::{Left, Right};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::iter;
|
||||||
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use rustpython_parser::ast::{Boolop, Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
|
use rustpython_parser::ast::{
|
||||||
|
Boolop, Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind,
|
||||||
|
};
|
||||||
|
|
||||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
|
use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
|
||||||
use ruff_diagnostics::{Diagnostic, Fix};
|
use ruff_diagnostics::{Diagnostic, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::comparable::ComparableExpr;
|
use ruff_python_ast::comparable::ComparableExpr;
|
||||||
use ruff_python_ast::helpers::{match_trailing_comment, unparse_expr};
|
use ruff_python_ast::helpers::{create_expr, match_trailing_comment, unparse_expr};
|
||||||
use ruff_python_ast::types::{Range, RefEquality};
|
use ruff_python_ast::types::{Range, RefEquality};
|
||||||
use ruff_python_stdlib::identifiers::is_identifier;
|
use ruff_python_stdlib::identifiers::is_identifier;
|
||||||
use ruff_python_stdlib::keyword::KWLIST;
|
use ruff_python_stdlib::keyword::KWLIST;
|
||||||
|
@ -120,12 +126,17 @@ pub struct SingleStartsEndsWith {
|
||||||
pub attr: String,
|
pub attr: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Violation for SingleStartsEndsWith {
|
impl AlwaysAutofixableViolation for SingleStartsEndsWith {
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let SingleStartsEndsWith { attr } = self;
|
let SingleStartsEndsWith { attr } = self;
|
||||||
format!("Call `{attr}` once with a `tuple`")
|
format!("Call `{attr}` once with a `tuple`")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn autofix_title(&self) -> String {
|
||||||
|
let SingleStartsEndsWith { attr } = self;
|
||||||
|
format!("Merge into a single `{attr}` call")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[violation]
|
#[violation]
|
||||||
|
@ -392,39 +403,116 @@ pub fn no_unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PIE810
|
/// PIE810
|
||||||
pub fn single_starts_ends_with(checker: &mut Checker, values: &[Expr], node: &Boolop) {
|
pub fn single_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||||
if *node != Boolop::Or {
|
let ExprKind::BoolOp { op: Boolop::Or, values } = &expr.node else {
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Given `foo.startswith`, insert ("foo", "startswith") into the set.
|
let mut duplicates = BTreeMap::new();
|
||||||
let mut seen = FxHashSet::default();
|
for (index, call) in values.iter().enumerate() {
|
||||||
for expr in values {
|
let ExprKind::Call {
|
||||||
if let ExprKind::Call {
|
|
||||||
func,
|
func,
|
||||||
args,
|
args,
|
||||||
keywords,
|
keywords,
|
||||||
..
|
..
|
||||||
} = &expr.node
|
} = &call.node else {
|
||||||
{
|
continue
|
||||||
if !(args.len() == 1 && keywords.is_empty()) {
|
};
|
||||||
continue;
|
|
||||||
}
|
if !(args.len() == 1 && keywords.is_empty()) {
|
||||||
if let ExprKind::Attribute { value, attr, .. } = &func.node {
|
continue;
|
||||||
if attr != "startswith" && attr != "endswith" {
|
}
|
||||||
continue;
|
|
||||||
}
|
let ExprKind::Attribute { value, attr, .. } = &func.node else {
|
||||||
if let ExprKind::Name { id, .. } = &value.node {
|
continue
|
||||||
if !seen.insert((id, attr)) {
|
};
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
|
||||||
SingleStartsEndsWith {
|
if attr != "startswith" && attr != "endswith" {
|
||||||
attr: attr.to_string(),
|
continue;
|
||||||
},
|
}
|
||||||
Range::from(value),
|
|
||||||
));
|
let ExprKind::Name { id: arg_name, .. } = &value.node else {
|
||||||
}
|
continue
|
||||||
}
|
};
|
||||||
|
|
||||||
|
duplicates
|
||||||
|
.entry((attr.as_str(), arg_name.as_str()))
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a `Diagnostic` for each duplicate.
|
||||||
|
for ((attr_name, arg_name), indices) in duplicates {
|
||||||
|
if indices.len() > 1 {
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
SingleStartsEndsWith {
|
||||||
|
attr: attr_name.to_string(),
|
||||||
|
},
|
||||||
|
Range::from(expr),
|
||||||
|
);
|
||||||
|
if checker.patch(diagnostic.kind.rule()) {
|
||||||
|
let words: Vec<&Expr> = indices
|
||||||
|
.iter()
|
||||||
|
.map(|index| &values[*index])
|
||||||
|
.map(|expr| {
|
||||||
|
let ExprKind::Call { func: _, args, keywords: _} = &expr.node else {
|
||||||
|
unreachable!("{}", format!("Indices should only contain `{attr_name}` calls"))
|
||||||
|
};
|
||||||
|
args.get(0)
|
||||||
|
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument"))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let call = create_expr(ExprKind::Call {
|
||||||
|
func: Box::new(create_expr(ExprKind::Attribute {
|
||||||
|
value: Box::new(create_expr(ExprKind::Name {
|
||||||
|
id: arg_name.to_string(),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
})),
|
||||||
|
attr: attr_name.to_string(),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
})),
|
||||||
|
args: vec![create_expr(ExprKind::Tuple {
|
||||||
|
elts: words
|
||||||
|
.iter()
|
||||||
|
.flat_map(|value| {
|
||||||
|
if let ExprKind::Tuple { elts, .. } = &value.node {
|
||||||
|
Left(elts.iter())
|
||||||
|
} else {
|
||||||
|
Right(iter::once(*value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(Clone::clone)
|
||||||
|
.collect(),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
})],
|
||||||
|
keywords: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate the combined `BoolOp`.
|
||||||
|
let mut call = Some(call);
|
||||||
|
let bool_op = create_expr(ExprKind::BoolOp {
|
||||||
|
op: Boolop::Or,
|
||||||
|
values: values
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, elt)| {
|
||||||
|
if indices.contains(&index) {
|
||||||
|
std::mem::take(&mut call)
|
||||||
|
} else {
|
||||||
|
Some(elt.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
|
||||||
|
diagnostic.amend(Fix::replacement(
|
||||||
|
unparse_expr(&bool_op, checker.stylist),
|
||||||
|
expr.location,
|
||||||
|
expr.end_location.unwrap(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,53 +5,101 @@ expression: diagnostics
|
||||||
- kind:
|
- kind:
|
||||||
name: SingleStartsEndsWith
|
name: SingleStartsEndsWith
|
||||||
body: "Call `startswith` once with a `tuple`"
|
body: "Call `startswith` once with a `tuple`"
|
||||||
suggestion: ~
|
suggestion: "Merge into a single `startswith` call"
|
||||||
fixable: false
|
fixable: true
|
||||||
location:
|
location:
|
||||||
row: 2
|
row: 2
|
||||||
column: 25
|
column: 0
|
||||||
end_location:
|
end_location:
|
||||||
row: 2
|
row: 2
|
||||||
column: 28
|
column: 46
|
||||||
fix: ~
|
fix:
|
||||||
|
content: "obj.startswith((\"foo\", \"bar\"))"
|
||||||
|
location:
|
||||||
|
row: 2
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 2
|
||||||
|
column: 46
|
||||||
parent: ~
|
parent: ~
|
||||||
- kind:
|
- kind:
|
||||||
name: SingleStartsEndsWith
|
name: SingleStartsEndsWith
|
||||||
body: "Call `endswith` once with a `tuple`"
|
body: "Call `endswith` once with a `tuple`"
|
||||||
suggestion: ~
|
suggestion: "Merge into a single `endswith` call"
|
||||||
fixable: false
|
fixable: true
|
||||||
location:
|
location:
|
||||||
row: 4
|
row: 4
|
||||||
column: 23
|
column: 0
|
||||||
end_location:
|
end_location:
|
||||||
row: 4
|
row: 4
|
||||||
column: 26
|
column: 42
|
||||||
fix: ~
|
fix:
|
||||||
|
content: "obj.endswith((\"foo\", \"bar\"))"
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 42
|
||||||
parent: ~
|
parent: ~
|
||||||
- kind:
|
- kind:
|
||||||
name: SingleStartsEndsWith
|
name: SingleStartsEndsWith
|
||||||
body: "Call `startswith` once with a `tuple`"
|
body: "Call `startswith` once with a `tuple`"
|
||||||
suggestion: ~
|
suggestion: "Merge into a single `startswith` call"
|
||||||
fixable: false
|
fixable: true
|
||||||
location:
|
location:
|
||||||
row: 6
|
row: 6
|
||||||
column: 23
|
column: 0
|
||||||
end_location:
|
end_location:
|
||||||
row: 6
|
row: 6
|
||||||
column: 26
|
column: 42
|
||||||
fix: ~
|
fix:
|
||||||
|
content: "obj.startswith((foo, bar))"
|
||||||
|
location:
|
||||||
|
row: 6
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 6
|
||||||
|
column: 42
|
||||||
parent: ~
|
parent: ~
|
||||||
- kind:
|
- kind:
|
||||||
name: SingleStartsEndsWith
|
name: SingleStartsEndsWith
|
||||||
body: "Call `startswith` once with a `tuple`"
|
body: "Call `startswith` once with a `tuple`"
|
||||||
suggestion: ~
|
suggestion: "Merge into a single `startswith` call"
|
||||||
fixable: false
|
fixable: true
|
||||||
location:
|
location:
|
||||||
row: 8
|
row: 8
|
||||||
column: 23
|
column: 0
|
||||||
end_location:
|
end_location:
|
||||||
row: 8
|
row: 8
|
||||||
column: 26
|
column: 44
|
||||||
fix: ~
|
fix:
|
||||||
|
content: "obj.startswith((foo, \"foo\"))"
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 44
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: SingleStartsEndsWith
|
||||||
|
body: "Call `startswith` once with a `tuple`"
|
||||||
|
suggestion: "Merge into a single `startswith` call"
|
||||||
|
fixable: true
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 65
|
||||||
|
fix:
|
||||||
|
content: "obj.endswith(foo) or obj.startswith((foo, \"foo\"))"
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 65
|
||||||
parent: ~
|
parent: ~
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue