mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:16 +00:00
Prefer itertools.pairwise()
over zip()
for successive pairs (RUF007
) (#3501)
This commit is contained in:
parent
373a77e8c2
commit
33d2457909
9 changed files with 273 additions and 5 deletions
19
crates/ruff/resources/test/fixtures/ruff/RUF007.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/ruff/RUF007.py
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
input = [1, 2, 3]
|
||||||
|
otherInput = [2, 3, 4]
|
||||||
|
|
||||||
|
# OK
|
||||||
|
zip(input, otherInput) # different inputs
|
||||||
|
zip(input, otherInput[1:]) # different inputs
|
||||||
|
zip(input, input[2:]) # not successive
|
||||||
|
zip(input[:-1], input[2:]) # not successive
|
||||||
|
list(zip(input, otherInput)) # nested call
|
||||||
|
zip(input, input[1::2]) # not successive
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
zip(input, input[1:])
|
||||||
|
zip(input, input[1::1])
|
||||||
|
zip(input[:-1], input[1:])
|
||||||
|
zip(input[1:], input[2:])
|
||||||
|
zip(input[1:-1], input[2:])
|
||||||
|
list(zip(input, input[1:]))
|
||||||
|
list(zip(input[:-1], input[1:]))
|
|
@ -2866,6 +2866,12 @@ where
|
||||||
flake8_pytest_style::rules::fail_call(self, func, args, keywords);
|
flake8_pytest_style::rules::fail_call(self, func, args, keywords);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.settings.rules.enabled(Rule::PairwiseOverZipped) {
|
||||||
|
if self.settings.target_version >= PythonVersion::Py310 {
|
||||||
|
ruff::rules::pairwise_over_zipped(self, func, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// flake8-simplify
|
// flake8-simplify
|
||||||
if self
|
if self
|
||||||
.settings
|
.settings
|
||||||
|
|
|
@ -661,6 +661,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||||
(Ruff, "003") => Rule::AmbiguousUnicodeCharacterComment,
|
(Ruff, "003") => Rule::AmbiguousUnicodeCharacterComment,
|
||||||
(Ruff, "005") => Rule::UnpackInsteadOfConcatenatingToCollectionLiteral,
|
(Ruff, "005") => Rule::UnpackInsteadOfConcatenatingToCollectionLiteral,
|
||||||
(Ruff, "006") => Rule::AsyncioDanglingTask,
|
(Ruff, "006") => Rule::AsyncioDanglingTask,
|
||||||
|
(Ruff, "007") => Rule::PairwiseOverZipped,
|
||||||
(Ruff, "100") => Rule::UnusedNOQA,
|
(Ruff, "100") => Rule::UnusedNOQA,
|
||||||
|
|
||||||
// flake8-django
|
// flake8-django
|
||||||
|
|
|
@ -602,6 +602,7 @@ ruff_macros::register_rules!(
|
||||||
rules::ruff::rules::UnpackInsteadOfConcatenatingToCollectionLiteral,
|
rules::ruff::rules::UnpackInsteadOfConcatenatingToCollectionLiteral,
|
||||||
rules::ruff::rules::AsyncioDanglingTask,
|
rules::ruff::rules::AsyncioDanglingTask,
|
||||||
rules::ruff::rules::UnusedNOQA,
|
rules::ruff::rules::UnusedNOQA,
|
||||||
|
rules::ruff::rules::PairwiseOverZipped,
|
||||||
// flake8-django
|
// flake8-django
|
||||||
rules::flake8_django::rules::NullableModelStringField,
|
rules::flake8_django::rules::NullableModelStringField,
|
||||||
rules::flake8_django::rules::LocalsInRenderFunction,
|
rules::flake8_django::rules::LocalsInRenderFunction,
|
||||||
|
|
|
@ -142,4 +142,14 @@ mod tests {
|
||||||
assert_yaml_snapshot!(diagnostics);
|
assert_yaml_snapshot!(diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ruff_pairwise_over_zipped() -> Result<()> {
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("ruff/RUF007.py"),
|
||||||
|
&settings::Settings::for_rules(vec![Rule::PairwiseOverZipped]),
|
||||||
|
)?;
|
||||||
|
assert_yaml_snapshot!(diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
|
mod ambiguous_unicode_character;
|
||||||
|
mod asyncio_dangling_task;
|
||||||
|
mod pairwise_over_zipped;
|
||||||
|
mod unpack_instead_of_concatenating_to_collection_literal;
|
||||||
|
mod unused_noqa;
|
||||||
|
|
||||||
pub use ambiguous_unicode_character::{
|
pub use ambiguous_unicode_character::{
|
||||||
ambiguous_unicode_character, AmbiguousUnicodeCharacterComment,
|
ambiguous_unicode_character, AmbiguousUnicodeCharacterComment,
|
||||||
AmbiguousUnicodeCharacterDocstring, AmbiguousUnicodeCharacterString,
|
AmbiguousUnicodeCharacterDocstring, AmbiguousUnicodeCharacterString,
|
||||||
};
|
};
|
||||||
pub use asyncio_dangling_task::{asyncio_dangling_task, AsyncioDanglingTask};
|
pub use asyncio_dangling_task::{asyncio_dangling_task, AsyncioDanglingTask};
|
||||||
|
pub use pairwise_over_zipped::{pairwise_over_zipped, PairwiseOverZipped};
|
||||||
pub use unpack_instead_of_concatenating_to_collection_literal::{
|
pub use unpack_instead_of_concatenating_to_collection_literal::{
|
||||||
unpack_instead_of_concatenating_to_collection_literal,
|
unpack_instead_of_concatenating_to_collection_literal,
|
||||||
UnpackInsteadOfConcatenatingToCollectionLiteral,
|
UnpackInsteadOfConcatenatingToCollectionLiteral,
|
||||||
};
|
};
|
||||||
pub use unused_noqa::{UnusedCodes, UnusedNOQA};
|
pub use unused_noqa::{UnusedCodes, UnusedNOQA};
|
||||||
|
|
||||||
mod ambiguous_unicode_character;
|
|
||||||
mod asyncio_dangling_task;
|
|
||||||
mod unpack_instead_of_concatenating_to_collection_literal;
|
|
||||||
mod unused_noqa;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Context {
|
pub enum Context {
|
||||||
String,
|
String,
|
||||||
|
|
132
crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs
Normal file
132
crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use num_traits::ToPrimitive;
|
||||||
|
use rustpython_parser::ast::{Constant, Expr, ExprKind, Unaryop};
|
||||||
|
|
||||||
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
use crate::Range;
|
||||||
|
|
||||||
|
#[violation]
|
||||||
|
pub struct PairwiseOverZipped;
|
||||||
|
|
||||||
|
impl Violation for PairwiseOverZipped {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
format!("Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SliceInfo {
|
||||||
|
arg_name: String,
|
||||||
|
slice_start: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SliceInfo {
|
||||||
|
pub fn new(arg_name: String, slice_start: Option<i64>) -> Self {
|
||||||
|
Self {
|
||||||
|
arg_name,
|
||||||
|
slice_start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the argument name, lower bound, and upper bound for an expression, if it's a slice.
|
||||||
|
fn match_slice_info(expr: &Expr) -> Option<SliceInfo> {
|
||||||
|
let ExprKind::Subscript { value, slice, .. } = &expr.node else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ExprKind::Name { id: arg_id, .. } = &value.node else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ExprKind::Slice { lower, step, .. } = &slice.node else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid false positives for slices with a step.
|
||||||
|
if let Some(step) = step {
|
||||||
|
if let Some(step) = to_bound(step) {
|
||||||
|
if step != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(SliceInfo::new(
|
||||||
|
arg_id.to_string(),
|
||||||
|
lower.as_ref().and_then(|expr| to_bound(expr)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bound(expr: &Expr) -> Option<i64> {
|
||||||
|
match &expr.node {
|
||||||
|
ExprKind::Constant {
|
||||||
|
value: Constant::Int(value),
|
||||||
|
..
|
||||||
|
} => value.to_i64(),
|
||||||
|
ExprKind::UnaryOp {
|
||||||
|
op: Unaryop::USub | Unaryop::Invert,
|
||||||
|
operand,
|
||||||
|
} => {
|
||||||
|
if let ExprKind::Constant {
|
||||||
|
value: Constant::Int(value),
|
||||||
|
..
|
||||||
|
} = &operand.node
|
||||||
|
{
|
||||||
|
value.to_i64().map(|v| -v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RUF007
|
||||||
|
pub fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[Expr]) {
|
||||||
|
let ExprKind::Name { id, .. } = &func.node else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(args.len() > 1 && id == "zip" && checker.ctx.is_builtin(id)) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow the first argument to be a `Name` or `Subscript`.
|
||||||
|
let Some(first_arg_info) = ({
|
||||||
|
if let ExprKind::Name { id, .. } = &args[0].node {
|
||||||
|
Some(SliceInfo::new(id.to_string(), None))
|
||||||
|
} else {
|
||||||
|
match_slice_info(&args[0])
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Require second argument to be a `Subscript`.
|
||||||
|
let ExprKind::Subscript { .. } = &args[1].node else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(second_arg_info) = match_slice_info(&args[1]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify that the arguments match the same name.
|
||||||
|
if first_arg_info.arg_name != second_arg_info.arg_name {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the arguments are successive.
|
||||||
|
if second_arg_info.slice_start.unwrap_or(0) - first_arg_info.slice_start.unwrap_or(0) != 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checker
|
||||||
|
.diagnostics
|
||||||
|
.push(Diagnostic::new(PairwiseOverZipped, Range::from(func)));
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/ruff/mod.rs
|
||||||
|
expression: diagnostics
|
||||||
|
---
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 13
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 13
|
||||||
|
column: 3
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 14
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 14
|
||||||
|
column: 3
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 15
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 15
|
||||||
|
column: 3
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 16
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 16
|
||||||
|
column: 3
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 17
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 17
|
||||||
|
column: 3
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 18
|
||||||
|
column: 5
|
||||||
|
end_location:
|
||||||
|
row: 18
|
||||||
|
column: 8
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
name: PairwiseOverZipped
|
||||||
|
body: "Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs"
|
||||||
|
suggestion: ~
|
||||||
|
fixable: false
|
||||||
|
location:
|
||||||
|
row: 19
|
||||||
|
column: 5
|
||||||
|
end_location:
|
||||||
|
row: 19
|
||||||
|
column: 8
|
||||||
|
fix: ~
|
||||||
|
parent: ~
|
||||||
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -2013,6 +2013,7 @@
|
||||||
"RUF003",
|
"RUF003",
|
||||||
"RUF005",
|
"RUF005",
|
||||||
"RUF006",
|
"RUF006",
|
||||||
|
"RUF007",
|
||||||
"RUF1",
|
"RUF1",
|
||||||
"RUF10",
|
"RUF10",
|
||||||
"RUF100",
|
"RUF100",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue