Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/refurb/FURB122.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,43 @@ def _():
global global_foo
for [a, b, (global_foo, c)] in d:
f.write((a, b))


# Test cases for lambda and ternary expressions - https://github.com/astral-sh/ruff/issues/18590

def _():
with Path("file.txt").open("w", encoding="utf-8") as f:
for l in lambda: 0:
f.write(f"[{l}]")


def _():
with Path("file.txt").open("w", encoding="utf-8") as f:
for l in (1,) if True else (2,):
f.write(f"[{l}]")


# don't need to add parentheses when making a function argument
def _():
with open("file", "w") as f:
for line in lambda: 0:
f.write(line)


def _():
with open("file", "w") as f:
for line in (1,) if True else (2,):
f.write(line)


# don't add extra parentheses if they're already parenthesized
def _():
with open("file", "w") as f:
for line in (lambda: 0):
f.write(f"{line}")


def _():
with open("file", "w") as f:
for line in ((1,) if True else (2,)):
f.write(f"{line}")
25 changes: 25 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/refurb/FURB142.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,28 @@ async def f(y):
def g():
for x in (set(),):
x.add(x)


# Test cases for lambda and ternary expressions - https://github.com/astral-sh/ruff/issues/18590

s = set()

for x in lambda: 0:
s.discard(-x)

for x in (1,) if True else (2,):
s.add(-x)

# don't add extra parens
for x in (lambda: 0):
s.discard(-x)

for x in ((1,) if True else (2,)):
s.add(-x)

# don't add parens directly in function call
for x in lambda: 0:
s.discard(x)

for x in (1,) if True else (2,):
s.add(x)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use ruff_python_ast::{Expr, Stmt, StmtFor};
use ruff_python_semantic::analyze::typing;

use crate::checkers::ast::Checker;
use crate::rules::refurb::rules::helpers::IterLocation;
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};

use super::helpers::parenthesize_loop_iter_if_necessary;
Expand Down Expand Up @@ -106,15 +107,15 @@ pub(crate) fn for_loop_set_mutations(checker: &Checker, for_stmt: &StmtFor) {
format!(
"{}.{batch_method_name}({})",
set.id,
parenthesize_loop_iter_if_necessary(for_stmt, checker),
parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Call),
)
}
(for_target, arg) => format!(
"{}.{batch_method_name}({} for {} in {})",
set.id,
locator.slice(arg),
locator.slice(for_target),
parenthesize_loop_iter_if_necessary(for_stmt, checker),
parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Comprehension),
),
};

Expand Down
5 changes: 3 additions & 2 deletions crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use ruff_python_semantic::{Binding, ScopeId, SemanticModel, TypingOnlyBindingsSt
use ruff_text_size::{Ranged, TextRange, TextSize};

use crate::checkers::ast::Checker;
use crate::rules::refurb::rules::helpers::IterLocation;
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};

use super::helpers::parenthesize_loop_iter_if_necessary;
Expand Down Expand Up @@ -182,7 +183,7 @@ fn for_loop_writes(
format!(
"{}.writelines({})",
locator.slice(io_object_name),
parenthesize_loop_iter_if_necessary(for_stmt, checker),
parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Call),
)
}
(for_target, write_arg) => {
Expand All @@ -191,7 +192,7 @@ fn for_loop_writes(
locator.slice(io_object_name),
locator.slice(write_arg),
locator.slice(for_target),
parenthesize_loop_iter_if_necessary(for_stmt, checker),
parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Comprehension),
)
}
};
Expand Down
31 changes: 25 additions & 6 deletions crates/ruff_linter/src/rules/refurb/rules/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ use ruff_python_ast::{self as ast, parenthesize::parenthesized_range};

use crate::checkers::ast::Checker;

/// A helper function that extracts the `iter` from a [`ast::StmtFor`] node and,
/// if the `iter` is an unparenthesized tuple, adds parentheses:
/// A helper function that extracts the `iter` from a [`ast::StmtFor`] node and
/// adds parentheses if needed.
///
/// - `for x in z: ...` -> `"x"`
/// - `for (x, y) in z: ...` -> `"(x, y)"`
/// - `for [x, y] in z: ...` -> `"[x, y]"`
/// - `for x, y in z: ...` -> `"(x, y)"` # <-- Parentheses added only for this example
/// These cases are okay and will not be modified:
///
/// - `for x in z: ...` -> `"z"`
/// - `for x in (y, z): ...` -> `"(y, z)"`
/// - `for x in [y, z]: ...` -> `"[y, z]"`
///
/// While these cases require parentheses:
///
/// - `for x in y, z: ...` -> `"(y, z)"`
/// - `for x in lambda: 0: ...` -> `"(lambda: 0)"`
/// - `for x in (1,) if True else (2,): ...` -> `"((1,) if True else (2,))"`
pub(super) fn parenthesize_loop_iter_if_necessary<'a>(
for_stmt: &'a ast::StmtFor,
checker: &'a Checker,
location: IterLocation,
) -> Cow<'a, str> {
let locator = checker.locator();
let iter = for_stmt.iter.as_ref();
Expand All @@ -35,6 +43,17 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>(
ast::Expr::Tuple(tuple) if !tuple.parenthesized => {
Cow::Owned(format!("({iter_in_source})"))
}
ast::Expr::Lambda(_) | ast::Expr::If(_)
if matches!(location, IterLocation::Comprehension) =>
{
Cow::Owned(format!("({iter_in_source})"))
}
_ => Cow::Borrowed(iter_in_source),
}
}

#[derive(Copy, Clone)]
pub(super) enum IterLocation {
Call,
Comprehension,
}
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,126 @@ FURB122.py:93:9: FURB122 [*] Use of `f.write` in a for loop
98 97 |
99 98 |
100 99 | # OK

FURB122.py:183:9: FURB122 [*] Use of `f.write` in a for loop
|
181 | def _():
182 | with Path("file.txt").open("w", encoding="utf-8") as f:
183 | / for l in lambda: 0:
184 | | f.write(f"[{l}]")
| |_____________________________^ FURB122
|
= help: Replace with `f.writelines`

ℹ Safe fix
180 180 |
181 181 | def _():
182 182 | with Path("file.txt").open("w", encoding="utf-8") as f:
183 |- for l in lambda: 0:
184 |- f.write(f"[{l}]")
183 |+ f.writelines(f"[{l}]" for l in (lambda: 0))
185 184 |
186 185 |
187 186 | def _():

FURB122.py:189:9: FURB122 [*] Use of `f.write` in a for loop
|
187 | def _():
188 | with Path("file.txt").open("w", encoding="utf-8") as f:
189 | / for l in (1,) if True else (2,):
190 | | f.write(f"[{l}]")
| |_____________________________^ FURB122
|
= help: Replace with `f.writelines`

ℹ Safe fix
186 186 |
187 187 | def _():
188 188 | with Path("file.txt").open("w", encoding="utf-8") as f:
189 |- for l in (1,) if True else (2,):
190 |- f.write(f"[{l}]")
189 |+ f.writelines(f"[{l}]" for l in ((1,) if True else (2,)))
191 190 |
192 191 |
193 192 | # don't need to add parentheses when making a function argument

FURB122.py:196:9: FURB122 [*] Use of `f.write` in a for loop
|
194 | def _():
195 | with open("file", "w") as f:
196 | / for line in lambda: 0:
197 | | f.write(line)
| |_________________________^ FURB122
|
= help: Replace with `f.writelines`

ℹ Safe fix
193 193 | # don't need to add parentheses when making a function argument
194 194 | def _():
195 195 | with open("file", "w") as f:
196 |- for line in lambda: 0:
197 |- f.write(line)
196 |+ f.writelines(lambda: 0)
198 197 |
199 198 |
200 199 | def _():

FURB122.py:202:9: FURB122 [*] Use of `f.write` in a for loop
|
200 | def _():
201 | with open("file", "w") as f:
202 | / for line in (1,) if True else (2,):
203 | | f.write(line)
| |_________________________^ FURB122
|
= help: Replace with `f.writelines`

ℹ Safe fix
199 199 |
200 200 | def _():
201 201 | with open("file", "w") as f:
202 |- for line in (1,) if True else (2,):
203 |- f.write(line)
202 |+ f.writelines((1,) if True else (2,))
204 203 |
205 204 |
206 205 | # don't add extra parentheses if they're already parenthesized

FURB122.py:209:9: FURB122 [*] Use of `f.write` in a for loop
|
207 | def _():
208 | with open("file", "w") as f:
209 | / for line in (lambda: 0):
210 | | f.write(f"{line}")
| |______________________________^ FURB122
|
= help: Replace with `f.writelines`

ℹ Safe fix
206 206 | # don't add extra parentheses if they're already parenthesized
207 207 | def _():
208 208 | with open("file", "w") as f:
209 |- for line in (lambda: 0):
210 |- f.write(f"{line}")
209 |+ f.writelines(f"{line}" for line in (lambda: 0))
211 210 |
212 211 |
213 212 | def _():

FURB122.py:215:9: FURB122 [*] Use of `f.write` in a for loop
|
213 | def _():
214 | with open("file", "w") as f:
215 | / for line in ((1,) if True else (2,)):
216 | | f.write(f"{line}")
| |______________________________^ FURB122
|
= help: Replace with `f.writelines`

ℹ Safe fix
212 212 |
213 213 | def _():
214 214 | with open("file", "w") as f:
215 |- for line in ((1,) if True else (2,)):
216 |- f.write(f"{line}")
215 |+ f.writelines(f"{line}" for line in ((1,) if True else (2,)))
Loading