Skip to content

Commit

Permalink
Extend reimplemented-starmap (FURB140) to catch calls with a sing…
Browse files Browse the repository at this point in the history
…le and starred argument (#7768)
  • Loading branch information
tjkuson committed Oct 3, 2023
1 parent 3ccd1d5 commit e129f77
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 23 deletions.
12 changes: 12 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/refurb/FURB140.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ def zipped():
# FURB140
{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.

[print(x, int) for x, _ in zipped()]
Expand All @@ -41,3 +47,9 @@ def zipped():
[print() 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()]
57 changes: 42 additions & 15 deletions crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub(crate) fn reimplemented_starmap(checker: &mut Checker, target: &StarmapCandi
// ```
//
// `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;
};

Expand All @@ -99,13 +99,30 @@ pub(crate) fn reimplemented_starmap(checker: &mut Checker, target: &StarmapCandi
return;
};

// Here we want to check that `args` and `elts` are the same (same length, same elements,
// same order).
if elts.len() != args.len()
|| !std::iter::zip(elts, args)
.all(|(x, y)| ComparableExpr::from(x) == ComparableExpr::from(y))
{
return;
match value {
// Ex) `f(*x) for x in iter`
ComprehensionTarget::Name(name) => {
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))
{
return;
}
}
}

let mut diagnostic = Diagnostic::new(ReimplementedStarmap, target.range());
Expand All @@ -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`
// correspondingly.
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(),
);
Ok(Fix::suggested_edits(import_edit, [main_edit]))
Expand Down Expand Up @@ -252,7 +269,7 @@ fn try_construct_call(
// We can only do our fix if `builtin` identifier is still bound to
// the built-in type.
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:
Expand Down Expand Up @@ -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`.
fn match_comprehension(comprehension: &ast::Comprehension) -> Option<(&[Expr], &Expr)> {
#[derive(Debug)]
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() {
return None;
}

let ast::ExprTuple { elts, .. } = comprehension.target.as_tuple_expr()?;
Some((elts, &comprehension.iter))
match &comprehension.target {
Expr::Tuple(tuple) => Some(ComprehensionTarget::Tuple(tuple)),
Expr::Name(name) => Some(ComprehensionTarget::Name(name)),
_ => None,
}
}

/// Match that the given expression is `func(x, y, z, ...)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
26 |
27 | # Non-errors.
27 | # FURB140 (check it still flags starred arguments).
|
= help: Replace with `itertools.starmap`

Expand All @@ -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 |+set(sm(print, zipped()))
26 26 |
27 27 | # Non-errors.
28 28 |
27 27 | # FURB140 (check it still flags starred arguments).
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 |


14 changes: 9 additions & 5 deletions crates/ruff_python_ast/src/comparable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,11 +927,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
}) => Self::Starred(ExprStarred {
value: value.into(),
}),
ast::Expr::Name(ast::ExprName {
id,
ctx: _,
range: _,
}) => Self::Name(ExprName { id: id.as_str() }),
ast::Expr::Name(name) => name.into(),
ast::Expr::List(ast::ExprList {
elts,
ctx: _,
Expand Down Expand Up @@ -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)]
pub struct StmtFunctionDef<'a> {
is_async: bool,
Expand Down

0 comments on commit e129f77

Please sign in to comment.