Skip to content

Commit 9eb73cb

Browse files
[pycodestyle] Preserve original value format (E731) (#15097)
Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent 03bb942 commit 9eb73cb

3 files changed

Lines changed: 170 additions & 31 deletions

File tree

crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,24 @@ def scope():
156156
class FilterDataclass:
157157
# OK
158158
filter: Callable[[str], bool] = lambda _: True
159+
160+
161+
# Regression tests for:
162+
# * https://github.com/astral-sh/ruff/issues/7720
163+
x = lambda: """
164+
a
165+
b
166+
"""
167+
168+
# * https://github.com/astral-sh/ruff/issues/10277
169+
at_least_one_million = lambda _: _ >= 1_000_000
170+
171+
x = lambda: (
172+
# comment
173+
5 + 10
174+
)
175+
176+
x = lambda: (
177+
# comment
178+
y := 10
179+
)

crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
22
use ruff_macros::{derive_message_formats, ViolationMetadata};
3+
use ruff_python_ast::parenthesize::parenthesized_range;
34
use ruff_python_ast::{
4-
self as ast, Expr, Identifier, Parameter, ParameterWithDefault, Parameters, Stmt,
5+
self as ast, AstNode, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter,
6+
ParameterWithDefault, Parameters, Stmt,
57
};
6-
use ruff_python_codegen::Generator;
78
use ruff_python_semantic::SemanticModel;
89
use ruff_python_trivia::{has_leading_content, has_trailing_content, leading_indentation};
910
use ruff_source_file::UniversalNewlines;
@@ -65,10 +66,7 @@ pub(crate) fn lambda_assignment(
6566
return;
6667
};
6768

68-
let Expr::Lambda(ast::ExprLambda {
69-
parameters, body, ..
70-
}) = value
71-
else {
69+
let Expr::Lambda(lambda) = value else {
7270
return;
7371
};
7472

@@ -85,16 +83,9 @@ pub(crate) fn lambda_assignment(
8583
let first_line = checker.locator().line_str(stmt.start());
8684
let indentation = leading_indentation(first_line);
8785
let mut indented = String::new();
88-
for (idx, line) in function(
89-
id,
90-
parameters.as_deref(),
91-
body,
92-
annotation,
93-
checker.semantic(),
94-
checker.generator(),
95-
)
96-
.universal_newlines()
97-
.enumerate()
86+
for (idx, line) in function(id, lambda, annotation, checker)
87+
.universal_newlines()
88+
.enumerate()
9889
{
9990
if idx == 0 {
10091
indented.push_str(&line);
@@ -186,19 +177,21 @@ fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec<Exp
186177
/// Generate a function definition from a `lambda` expression.
187178
fn function(
188179
name: &str,
189-
parameters: Option<&Parameters>,
190-
body: &Expr,
180+
lambda: &ExprLambda,
191181
annotation: Option<&Expr>,
192-
semantic: &SemanticModel,
193-
generator: Generator,
182+
checker: &Checker,
194183
) -> String {
184+
// Use a dummy body. It gets replaced at the end with the actual body.
185+
// This allows preserving the source formatting for the body.
195186
let body = Stmt::Return(ast::StmtReturn {
196-
value: Some(Box::new(body.clone())),
187+
value: Some(Box::new(Expr::EllipsisLiteral(
188+
ExprEllipsisLiteral::default(),
189+
))),
197190
range: TextRange::default(),
198191
});
199-
let parameters = parameters.cloned().unwrap_or_default();
192+
let parameters = lambda.parameters.as_deref().cloned().unwrap_or_default();
200193
if let Some(annotation) = annotation {
201-
if let Some((arg_types, return_type)) = extract_types(annotation, semantic) {
194+
if let Some((arg_types, return_type)) = extract_types(annotation, checker.semantic()) {
202195
// A `lambda` expression can only have positional-only and positional-or-keyword
203196
// arguments. The order is always positional-only first, then positional-or-keyword.
204197
let new_posonlyargs = parameters
@@ -243,10 +236,12 @@ fn function(
243236
type_params: None,
244237
range: TextRange::default(),
245238
});
246-
return generator.stmt(&func);
239+
let generated = checker.generator().stmt(&func);
240+
241+
return replace_trailing_ellipsis_with_original_expr(generated, lambda, checker);
247242
}
248243
}
249-
let func = Stmt::FunctionDef(ast::StmtFunctionDef {
244+
let function = Stmt::FunctionDef(ast::StmtFunctionDef {
250245
is_async: false,
251246
name: Identifier::new(name.to_string(), TextRange::default()),
252247
parameters: Box::new(parameters),
@@ -256,5 +251,32 @@ fn function(
256251
type_params: None,
257252
range: TextRange::default(),
258253
});
259-
generator.stmt(&func)
254+
let generated = checker.generator().stmt(&function);
255+
256+
replace_trailing_ellipsis_with_original_expr(generated, lambda, checker)
257+
}
258+
259+
fn replace_trailing_ellipsis_with_original_expr(
260+
mut generated: String,
261+
lambda: &ExprLambda,
262+
checker: &Checker,
263+
) -> String {
264+
let original_expr_range = parenthesized_range(
265+
(&lambda.body).into(),
266+
lambda.as_any_node_ref(),
267+
checker.comment_ranges(),
268+
checker.source(),
269+
)
270+
.unwrap_or(lambda.body.range());
271+
272+
let original_expr_in_source = checker.locator().slice(original_expr_range);
273+
274+
let placeholder_ellipsis_start = generated.rfind("...").unwrap();
275+
let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len();
276+
277+
generated.replace_range(
278+
placeholder_ellipsis_start..placeholder_ellipsis_end,
279+
original_expr_in_source,
280+
);
281+
generated
260282
}

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ E731.py:127:5: E731 [*] Do not assign a `lambda` expression, use a `def`
279279
126 126 |
280280
127 |- f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
281281
127 |+ def f(a: str, b: int) -> tuple[str, int]:
282-
128 |+ return a, b
282+
128 |+ return (a, b)
283283
128 129 |
284284
129 130 |
285285
130 131 | def scope():
@@ -364,7 +364,103 @@ E731.py:147:5: E731 [*] Do not assign a `lambda` expression, use a `def`
364364
148 |- i := 1,
365365
149 |- )
366366
147 |+ def f():
367-
148 |+ return (i := 1),
368-
150 149 |
369-
151 150 |
370-
152 151 | from dataclasses import dataclass
367+
148 |+ return (
368+
149 |+ i := 1,
369+
150 |+ )
370+
150 151 |
371+
151 152 |
372+
152 153 | from dataclasses import dataclass
373+
374+
E731.py:163:1: E731 [*] Do not assign a `lambda` expression, use a `def`
375+
|
376+
161 | # Regression tests for:
377+
162 | # * https://github.com/astral-sh/ruff/issues/7720
378+
163 | / x = lambda: """
379+
164 | | a
380+
165 | | b
381+
166 | | """
382+
| |___^ E731
383+
167 |
384+
168 | # * https://github.com/astral-sh/ruff/issues/10277
385+
|
386+
= help: Rewrite `x` as a `def`
387+
388+
Unsafe fix
389+
160 160 |
390+
161 161 | # Regression tests for:
391+
162 162 | # * https://github.com/astral-sh/ruff/issues/7720
392+
163 |-x = lambda: """
393+
163 |+def x():
394+
164 |+ return """
395+
164 165 | a
396+
165 166 | b
397+
166 167 | """
398+
399+
E731.py:169:1: E731 [*] Do not assign a `lambda` expression, use a `def`
400+
|
401+
168 | # * https://github.com/astral-sh/ruff/issues/10277
402+
169 | at_least_one_million = lambda _: _ >= 1_000_000
403+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731
404+
170 |
405+
171 | x = lambda: (
406+
|
407+
= help: Rewrite `at_least_one_million` as a `def`
408+
409+
Unsafe fix
410+
166 166 | """
411+
167 167 |
412+
168 168 | # * https://github.com/astral-sh/ruff/issues/10277
413+
169 |-at_least_one_million = lambda _: _ >= 1_000_000
414+
169 |+def at_least_one_million(_):
415+
170 |+ return _ >= 1_000_000
416+
170 171 |
417+
171 172 | x = lambda: (
418+
172 173 | # comment
419+
420+
E731.py:171:1: E731 [*] Do not assign a `lambda` expression, use a `def`
421+
|
422+
169 | at_least_one_million = lambda _: _ >= 1_000_000
423+
170 |
424+
171 | / x = lambda: (
425+
172 | | # comment
426+
173 | | 5 + 10
427+
174 | | )
428+
| |_^ E731
429+
175 |
430+
176 | x = lambda: (
431+
|
432+
= help: Rewrite `x` as a `def`
433+
434+
Unsafe fix
435+
168 168 | # * https://github.com/astral-sh/ruff/issues/10277
436+
169 169 | at_least_one_million = lambda _: _ >= 1_000_000
437+
170 170 |
438+
171 |-x = lambda: (
439+
171 |+def x():
440+
172 |+ return (
441+
172 173 | # comment
442+
173 174 | 5 + 10
443+
174 175 | )
444+
445+
E731.py:176:1: E731 [*] Do not assign a `lambda` expression, use a `def`
446+
|
447+
174 | )
448+
175 |
449+
176 | / x = lambda: (
450+
177 | | # comment
451+
178 | | y := 10
452+
179 | | )
453+
| |_^ E731
454+
|
455+
= help: Rewrite `x` as a `def`
456+
457+
Unsafe fix
458+
173 173 | 5 + 10
459+
174 174 | )
460+
175 175 |
461+
176 |-x = lambda: (
462+
176 |+def x():
463+
177 |+ return (
464+
177 178 | # comment
465+
178 179 | y := 10
466+
179 180 | )

0 commit comments

Comments
 (0)