mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 14:21:24 +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)
|
||||
# error
|
||||
obj.startswith(foo) or obj.startswith("foo")
|
||||
# error
|
||||
obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
|
||||
|
||||
# ok
|
||||
obj.startswith(("foo", "bar"))
|
||||
|
|
|
@ -3435,7 +3435,7 @@ where
|
|||
pylint::rules::merge_isinstance(self, expr, op, values);
|
||||
}
|
||||
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) {
|
||||
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 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::{Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
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_stdlib::identifiers::is_identifier;
|
||||
use ruff_python_stdlib::keyword::KWLIST;
|
||||
|
@ -120,12 +126,17 @@ pub struct SingleStartsEndsWith {
|
|||
pub attr: String,
|
||||
}
|
||||
|
||||
impl Violation for SingleStartsEndsWith {
|
||||
impl AlwaysAutofixableViolation for SingleStartsEndsWith {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let SingleStartsEndsWith { attr } = self;
|
||||
format!("Call `{attr}` once with a `tuple`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let SingleStartsEndsWith { attr } = self;
|
||||
format!("Merge into a single `{attr}` call")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
|
@ -392,39 +403,116 @@ pub fn no_unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[
|
|||
}
|
||||
|
||||
/// PIE810
|
||||
pub fn single_starts_ends_with(checker: &mut Checker, values: &[Expr], node: &Boolop) {
|
||||
if *node != Boolop::Or {
|
||||
pub fn single_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
let ExprKind::BoolOp { op: Boolop::Or, values } = &expr.node else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Given `foo.startswith`, insert ("foo", "startswith") into the set.
|
||||
let mut seen = FxHashSet::default();
|
||||
for expr in values {
|
||||
if let ExprKind::Call {
|
||||
let mut duplicates = BTreeMap::new();
|
||||
for (index, call) in values.iter().enumerate() {
|
||||
let ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
..
|
||||
} = &expr.node
|
||||
{
|
||||
if !(args.len() == 1 && keywords.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
if let ExprKind::Attribute { value, attr, .. } = &func.node {
|
||||
if attr != "startswith" && attr != "endswith" {
|
||||
continue;
|
||||
}
|
||||
if let ExprKind::Name { id, .. } = &value.node {
|
||||
if !seen.insert((id, attr)) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
SingleStartsEndsWith {
|
||||
attr: attr.to_string(),
|
||||
},
|
||||
Range::from(value),
|
||||
));
|
||||
}
|
||||
}
|
||||
} = &call.node else {
|
||||
continue
|
||||
};
|
||||
|
||||
if !(args.len() == 1 && keywords.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ExprKind::Attribute { value, attr, .. } = &func.node else {
|
||||
continue
|
||||
};
|
||||
|
||||
if attr != "startswith" && attr != "endswith" {
|
||||
continue;
|
||||
}
|
||||
|
||||
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:
|
||||
name: SingleStartsEndsWith
|
||||
body: "Call `startswith` once with a `tuple`"
|
||||
suggestion: ~
|
||||
fixable: false
|
||||
suggestion: "Merge into a single `startswith` call"
|
||||
fixable: true
|
||||
location:
|
||||
row: 2
|
||||
column: 25
|
||||
column: 0
|
||||
end_location:
|
||||
row: 2
|
||||
column: 28
|
||||
fix: ~
|
||||
column: 46
|
||||
fix:
|
||||
content: "obj.startswith((\"foo\", \"bar\"))"
|
||||
location:
|
||||
row: 2
|
||||
column: 0
|
||||
end_location:
|
||||
row: 2
|
||||
column: 46
|
||||
parent: ~
|
||||
- kind:
|
||||
name: SingleStartsEndsWith
|
||||
body: "Call `endswith` once with a `tuple`"
|
||||
suggestion: ~
|
||||
fixable: false
|
||||
suggestion: "Merge into a single `endswith` call"
|
||||
fixable: true
|
||||
location:
|
||||
row: 4
|
||||
column: 23
|
||||
column: 0
|
||||
end_location:
|
||||
row: 4
|
||||
column: 26
|
||||
fix: ~
|
||||
column: 42
|
||||
fix:
|
||||
content: "obj.endswith((\"foo\", \"bar\"))"
|
||||
location:
|
||||
row: 4
|
||||
column: 0
|
||||
end_location:
|
||||
row: 4
|
||||
column: 42
|
||||
parent: ~
|
||||
- kind:
|
||||
name: SingleStartsEndsWith
|
||||
body: "Call `startswith` once with a `tuple`"
|
||||
suggestion: ~
|
||||
fixable: false
|
||||
suggestion: "Merge into a single `startswith` call"
|
||||
fixable: true
|
||||
location:
|
||||
row: 6
|
||||
column: 23
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 26
|
||||
fix: ~
|
||||
column: 42
|
||||
fix:
|
||||
content: "obj.startswith((foo, bar))"
|
||||
location:
|
||||
row: 6
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 42
|
||||
parent: ~
|
||||
- kind:
|
||||
name: SingleStartsEndsWith
|
||||
body: "Call `startswith` once with a `tuple`"
|
||||
suggestion: ~
|
||||
fixable: false
|
||||
suggestion: "Merge into a single `startswith` call"
|
||||
fixable: true
|
||||
location:
|
||||
row: 8
|
||||
column: 23
|
||||
column: 0
|
||||
end_location:
|
||||
row: 8
|
||||
column: 26
|
||||
fix: ~
|
||||
column: 44
|
||||
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: ~
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue