Extend reimplemented-starmap (FURB140) to catch calls with a single and starred argument (#7768)

This commit is contained in:
Tom Kuson 2023-10-03 02:38:05 +01:00 committed by GitHub
parent 3ccd1d580d
commit e129f77bcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 23 deletions

View file

@ -24,6 +24,12 @@ from itertools import starmap as sm
# FURB140 # FURB140
{print(x, y) for x, y in zipped()} {print(x, y) for x, y in zipped()}
# FURB140 (check it still flags starred arguments).
# See https://github.com/astral-sh/ruff/issues/7636
[foo(*t) for t in [(85, 60), (100, 80)]]
(foo(*t) for t in [(85, 60), (100, 80)])
{foo(*t) for t in [(85, 60), (100, 80)]}
# Non-errors. # Non-errors.
[print(x, int) for x, _ in zipped()] [print(x, int) for x, _ in zipped()]
@ -41,3 +47,9 @@ from itertools import starmap as sm
[print() for x, y in zipped()] [print() for x, y in zipped()]
[print(x, end=y) for x, y in zipped()] [print(x, end=y) for x, y in zipped()]
[print(*x, y) for x, y in zipped()]
[print(x, *y) for x, y in zipped()]
[print(*x, *y) for x, y in zipped()]

View file

@ -82,7 +82,7 @@ pub(crate) fn reimplemented_starmap(checker: &mut Checker, target: &StarmapCandi
// ``` // ```
// //
// `x, y, z, ...` are what we call `elts` for short. // `x, y, z, ...` are what we call `elts` for short.
let Some((elts, iter)) = match_comprehension(comprehension) else { let Some(value) = match_comprehension_target(comprehension) else {
return; return;
}; };
@ -99,14 +99,31 @@ pub(crate) fn reimplemented_starmap(checker: &mut Checker, target: &StarmapCandi
return; return;
}; };
// Here we want to check that `args` and `elts` are the same (same length, same elements, match value {
// same order). // Ex) `f(*x) for x in iter`
if elts.len() != args.len() ComprehensionTarget::Name(name) => {
|| !std::iter::zip(elts, args) let [arg] = args else {
return;
};
let Expr::Starred(ast::ExprStarred { value, .. }) = arg else {
return;
};
if ComparableExpr::from(value.as_ref()) != ComparableExpr::from(name) {
return;
}
}
// Ex) `f(x, y, z) for x, y, z in iter`
ComprehensionTarget::Tuple(tuple) => {
if tuple.elts.len() != args.len()
|| !std::iter::zip(&tuple.elts, args)
.all(|(x, y)| ComparableExpr::from(x) == ComparableExpr::from(y)) .all(|(x, y)| ComparableExpr::from(x) == ComparableExpr::from(y))
{ {
return; return;
} }
}
}
let mut diagnostic = Diagnostic::new(ReimplementedStarmap, target.range()); let mut diagnostic = Diagnostic::new(ReimplementedStarmap, target.range());
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
@ -127,7 +144,7 @@ pub(crate) fn reimplemented_starmap(checker: &mut Checker, target: &StarmapCandi
// - For list and set comprehensions, we'd want to wrap it with `list` and `set` // - For list and set comprehensions, we'd want to wrap it with `list` and `set`
// correspondingly. // correspondingly.
let main_edit = Edit::range_replacement( let main_edit = Edit::range_replacement(
target.try_make_suggestion(starmap_name, iter, func, checker)?, target.try_make_suggestion(starmap_name, &comprehension.iter, func, checker)?,
target.range(), target.range(),
); );
Ok(Fix::suggested_edits(import_edit, [main_edit])) Ok(Fix::suggested_edits(import_edit, [main_edit]))
@ -252,7 +269,7 @@ fn try_construct_call(
// We can only do our fix if `builtin` identifier is still bound to // We can only do our fix if `builtin` identifier is still bound to
// the built-in type. // the built-in type.
if !checker.semantic().is_builtin(builtin) { if !checker.semantic().is_builtin(builtin) {
bail!(format!("Can't use built-in `{builtin}` constructor")) bail!("Can't use built-in `{builtin}` constructor")
} }
// In general, we replace: // In general, we replace:
@ -306,14 +323,24 @@ fn wrap_with_call_to(call: ast::ExprCall, func_name: &str) -> ast::ExprCall {
} }
} }
/// Match that the given comprehension is `(x, y, z, ...) in iter`. #[derive(Debug)]
fn match_comprehension(comprehension: &ast::Comprehension) -> Option<(&[Expr], &Expr)> { enum ComprehensionTarget<'a> {
/// E.g., `(x, y, z, ...)` in `(x, y, z, ...) in iter`.
Tuple(&'a ast::ExprTuple),
/// E.g., `x` in `x in iter`.
Name(&'a ast::ExprName),
}
/// Extract the target from the comprehension (e.g., `(x, y, z)` in `(x, y, z, ...) in iter`).
fn match_comprehension_target(comprehension: &ast::Comprehension) -> Option<ComprehensionTarget> {
if comprehension.is_async || !comprehension.ifs.is_empty() { if comprehension.is_async || !comprehension.ifs.is_empty() {
return None; return None;
} }
match &comprehension.target {
let ast::ExprTuple { elts, .. } = comprehension.target.as_tuple_expr()?; Expr::Tuple(tuple) => Some(ComprehensionTarget::Tuple(tuple)),
Some((elts, &comprehension.iter)) Expr::Name(name) => Some(ComprehensionTarget::Name(name)),
_ => None,
}
} }
/// Match that the given expression is `func(x, y, z, ...)`. /// Match that the given expression is `func(x, y, z, ...)`.

View file

@ -119,7 +119,7 @@ FURB140.py:25:1: FURB140 [*] Use `itertools.starmap` instead of the generator
25 | {print(x, y) for x, y in zipped()} 25 | {print(x, y) for x, y in zipped()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
26 | 26 |
27 | # Non-errors. 27 | # FURB140 (check it still flags starred arguments).
| |
= help: Replace with `itertools.starmap` = help: Replace with `itertools.starmap`
@ -130,7 +130,69 @@ FURB140.py:25:1: FURB140 [*] Use `itertools.starmap` instead of the generator
25 |-{print(x, y) for x, y in zipped()} 25 |-{print(x, y) for x, y in zipped()}
25 |+set(sm(print, zipped())) 25 |+set(sm(print, zipped()))
26 26 | 26 26 |
27 27 | # Non-errors. 27 27 | # FURB140 (check it still flags starred arguments).
28 28 | 28 28 | # See https://github.com/astral-sh/ruff/issues/7636
FURB140.py:29:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
27 | # FURB140 (check it still flags starred arguments).
28 | # See https://github.com/astral-sh/ruff/issues/7636
29 | [foo(*t) for t in [(85, 60), (100, 80)]]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
30 | (foo(*t) for t in [(85, 60), (100, 80)])
31 | {foo(*t) for t in [(85, 60), (100, 80)]}
|
= help: Replace with `itertools.starmap`
Suggested fix
26 26 |
27 27 | # FURB140 (check it still flags starred arguments).
28 28 | # See https://github.com/astral-sh/ruff/issues/7636
29 |-[foo(*t) for t in [(85, 60), (100, 80)]]
29 |+list(sm(foo, [(85, 60), (100, 80)]))
30 30 | (foo(*t) for t in [(85, 60), (100, 80)])
31 31 | {foo(*t) for t in [(85, 60), (100, 80)]}
32 32 |
FURB140.py:30:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
28 | # See https://github.com/astral-sh/ruff/issues/7636
29 | [foo(*t) for t in [(85, 60), (100, 80)]]
30 | (foo(*t) for t in [(85, 60), (100, 80)])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
31 | {foo(*t) for t in [(85, 60), (100, 80)]}
|
= help: Replace with `itertools.starmap`
Suggested fix
27 27 | # FURB140 (check it still flags starred arguments).
28 28 | # See https://github.com/astral-sh/ruff/issues/7636
29 29 | [foo(*t) for t in [(85, 60), (100, 80)]]
30 |-(foo(*t) for t in [(85, 60), (100, 80)])
30 |+sm(foo, [(85, 60), (100, 80)])
31 31 | {foo(*t) for t in [(85, 60), (100, 80)]}
32 32 |
33 33 | # Non-errors.
FURB140.py:31:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
29 | [foo(*t) for t in [(85, 60), (100, 80)]]
30 | (foo(*t) for t in [(85, 60), (100, 80)])
31 | {foo(*t) for t in [(85, 60), (100, 80)]}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
32 |
33 | # Non-errors.
|
= help: Replace with `itertools.starmap`
Suggested fix
28 28 | # See https://github.com/astral-sh/ruff/issues/7636
29 29 | [foo(*t) for t in [(85, 60), (100, 80)]]
30 30 | (foo(*t) for t in [(85, 60), (100, 80)])
31 |-{foo(*t) for t in [(85, 60), (100, 80)]}
31 |+set(sm(foo, [(85, 60), (100, 80)]))
32 32 |
33 33 | # Non-errors.
34 34 |

View file

@ -927,11 +927,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
}) => Self::Starred(ExprStarred { }) => Self::Starred(ExprStarred {
value: value.into(), value: value.into(),
}), }),
ast::Expr::Name(ast::ExprName { ast::Expr::Name(name) => name.into(),
id,
ctx: _,
range: _,
}) => Self::Name(ExprName { id: id.as_str() }),
ast::Expr::List(ast::ExprList { ast::Expr::List(ast::ExprList {
elts, elts,
ctx: _, ctx: _,
@ -968,6 +964,14 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
} }
} }
impl<'a> From<&'a ast::ExprName> for ComparableExpr<'a> {
fn from(expr: &'a ast::ExprName) -> Self {
Self::Name(ExprName {
id: expr.id.as_str(),
})
}
}
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
pub struct StmtFunctionDef<'a> { pub struct StmtFunctionDef<'a> {
is_async: bool, is_async: bool,