Skip to content

Commit 1682f12

Browse files
committed
fix: Preserve multiline UP040 alias grouping
Parenthesize multiline binary and boolean expressions when converting TypeAliasType assignments to type statements. The call syntax previously provided grouping that was lost by the fix, producing invalid Python. See #28102
1 parent 2c3fa3e commit 1682f12

4 files changed

Lines changed: 51 additions & 5 deletions

File tree

‎crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,11 @@ class Foo:
132132
# Test case for TypeVar with default - should be converted when preview mode is enabled
133133
T_default = TypeVar("T_default", default=int)
134134
DefaultList: TypeAlias = list[T_default]
135+
136+
# Preserve parentheses around a multiline union in a TypeAliasType call.
137+
LongAlias = TypeAliasType(
138+
"LongAlias",
139+
int
140+
| str
141+
| float
142+
)

‎crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
193193
value,
194194
&vars,
195195
TypeAliasKind::TypeAliasType,
196+
parenthesized_range(value.into(), arguments.into(), checker.tokens())
197+
.unwrap_or(value.range()),
196198
);
197199
}
198200

@@ -247,6 +249,7 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
247249
value,
248250
&vars,
249251
TypeAliasKind::TypeAlias,
252+
parenthesized_range(value.into(), stmt.into(), checker.tokens()).unwrap_or(value.range()),
250253
);
251254
}
252255

@@ -255,9 +258,10 @@ fn create_diagnostic(
255258
checker: &Checker,
256259
stmt: StmtRef,
257260
name: &Name,
258-
value: &Expr,
261+
value_expr: &Expr,
259262
type_vars: &[TypeVar],
260263
type_alias_kind: TypeAliasKind,
264+
range_with_parentheses: TextRange,
261265
) {
262266
// If any type variables have defaults, skip the rule unless
263267
// running with preview mode enabled and targeting Python 3.13+.
@@ -273,16 +277,22 @@ fn create_diagnostic(
273277
}
274278

275279
let source = checker.source();
276-
let tokens = checker.tokens();
277280
let comment_ranges = checker.comment_ranges();
278281

279-
let range_with_parentheses =
280-
parenthesized_range(value.into(), stmt.into(), tokens).unwrap_or(value.range());
282+
let value = &source[range_with_parentheses];
283+
let value = if type_alias_kind == TypeAliasKind::TypeAliasType
284+
&& value.contains('\n')
285+
&& matches!(value_expr, Expr::BinOp(_) | Expr::BoolOp(_))
286+
{
287+
format!("({value})")
288+
} else {
289+
value.to_string()
290+
};
281291

282292
let content = format!(
283293
"type {name}{type_params} = {value}",
284294
type_params = DisplayTypeVars { type_vars, source },
285-
value = &source[range_with_parentheses]
295+
value = value,
286296
);
287297
let edit = Edit::range_replacement(content, stmt.range());
288298

‎crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,27 @@ help: Use the `type` keyword
436436
123 | # comment1
437437
|
438438
note: This is an unsafe fix and may change runtime behavior
439+
440+
UP040 [*] Type alias `LongAlias` uses `TypeAliasType` assignment instead of the `type` keyword
441+
--> UP040.py:137:1
442+
|
443+
136 | # Preserve parentheses around a multiline union in a TypeAliasType call.
444+
137 | / LongAlias = TypeAliasType(
445+
138 | | "LongAlias",
446+
139 | | int
447+
140 | | | str
448+
141 | | | float
449+
142 | | )
450+
| |_^
451+
help: Use the `type` keyword
452+
|
453+
136 | # Preserve parentheses around a multiline union in a TypeAliasType call.
454+
- LongAlias = TypeAliasType(
455+
- "LongAlias",
456+
- int
457+
137 + type LongAlias = (int
458+
138 | | str
459+
- | float
460+
- )
461+
139 + | float)
462+
|

‎crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `t
5656
133 | T_default = TypeVar("T_default", default=int)
5757
134 | DefaultList: TypeAlias = list[T_default]
5858
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59+
135 |
60+
136 | # Preserve parentheses around a multiline union in a TypeAliasType call.
61+
|
5962
help: Use the `type` keyword
6063
|
6164
133 | T_default = TypeVar("T_default", default=int)
6265
- DefaultList: TypeAlias = list[T_default]
6366
134 + type DefaultList[T_default = int] = list[T_default]
67+
135 |
6468
|
6569
note: This is an unsafe fix and may change runtime behavior

0 commit comments

Comments
 (0)