Skip to content

Commit f1418be

Browse files
authored
[pyupgrade] Reuse replacement logic from UP046 and UP047 (UP040) (#15840)
## Summary This is a follow-up to #15565, tracked in #15642, to reuse the string replacement logic from the other PEP 695 rules instead of the `Generator`, which has the benefit of preserving more comments. However, comments in some places are still dropped, so I added a check for this and update the fix safety accordingly. I also added a `## Fix safety` section to the docs to reflect this and the existing `isinstance` caveat. ## Test Plan Existing UP040 tests, plus some new cases.
1 parent 59be5f5 commit f1418be

File tree

6 files changed

+168
-45
lines changed

6 files changed

+168
-45
lines changed

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

+20
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,23 @@ class Foo:
9393
# `default` should be skipped for now, added in Python 3.13
9494
T = typing.TypeVar("T", default=Any)
9595
AnyList = TypeAliasType("AnyList", list[T], typep_params=(T,))
96+
97+
# unsafe fix if comments within the fix
98+
T = TypeVar("T")
99+
PositiveList = TypeAliasType( # eaten comment
100+
"PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
101+
)
102+
103+
T = TypeVar("T")
104+
PositiveList = TypeAliasType(
105+
"PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
106+
) # this comment should be okay
107+
108+
109+
# this comment will actually be preserved because it's inside the "value" part
110+
T = TypeVar("T")
111+
PositiveList = TypeAliasType(
112+
"PositiveList", list[
113+
Annotated[T, Gt(0)], # preserved comment
114+
], type_params=(T,)
115+
)

crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.pyi

+7
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ from typing import TypeAlias
55
# Fixes in type stub files should be safe to apply unlike in regular code where runtime behavior could change
66
x: typing.TypeAlias = int
77
x: TypeAlias = int
8+
9+
10+
# comments in the value are preserved
11+
x: TypeAlias = tuple[
12+
int, # preserved
13+
float,
14+
]

crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ struct DisplayTypeVars<'a> {
6060

6161
impl Display for DisplayTypeVars<'_> {
6262
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63-
f.write_str("[")?;
6463
let nvars = self.type_vars.len();
64+
if nvars == 0 {
65+
return Ok(());
66+
}
67+
f.write_str("[")?;
6568
for (i, tv) in self.type_vars.iter().enumerate() {
6669
write!(f, "{}", tv.display(self.source))?;
6770
if i < nvars - 1 {

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

+39-42
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Vi
44
use ruff_macros::{derive_message_formats, ViolationMetadata};
55
use ruff_python_ast::name::Name;
66
use ruff_python_ast::{
7-
self as ast, visitor::Visitor, Expr, ExprCall, ExprName, Keyword, Stmt, StmtAnnAssign,
8-
StmtAssign, StmtTypeAlias, TypeParam,
7+
visitor::Visitor, Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign,
98
};
10-
use ruff_python_codegen::Generator;
119
use ruff_text_size::{Ranged, TextRange};
1210

1311
use crate::checkers::ast::Checker;
1412
use crate::settings::types::PythonVersion;
1513

16-
use super::{expr_name_to_type_var, TypeParamKind, TypeVar, TypeVarReferenceVisitor};
14+
use super::{
15+
expr_name_to_type_var, DisplayTypeVars, TypeParamKind, TypeVar, TypeVarReferenceVisitor,
16+
};
1717

1818
/// ## What it does
1919
/// Checks for use of `TypeAlias` annotations and `TypeAliasType` assignments
@@ -50,6 +50,13 @@ use super::{expr_name_to_type_var, TypeParamKind, TypeVar, TypeVarReferenceVisit
5050
/// type PositiveInt = Annotated[int, Gt(0)]
5151
/// ```
5252
///
53+
/// ## Fix safety
54+
///
55+
/// This fix is marked unsafe for `TypeAlias` assignments outside of stub files because of the
56+
/// runtime behavior around `isinstance()` calls noted above. The fix is also unsafe for
57+
/// `TypeAliasType` assignments if there are any comments in the replacement range that would be
58+
/// deleted.
59+
///
5360
/// [PEP 695]: https://peps.python.org/pep-0695/
5461
#[derive(ViolationMetadata)]
5562
pub(crate) struct NonPEP695TypeAlias {
@@ -145,13 +152,25 @@ pub(crate) fn non_pep695_type_alias_type(checker: &mut Checker, stmt: &StmtAssig
145152
return;
146153
};
147154

155+
// it would be easier to check for comments in the whole `stmt.range`, but because
156+
// `create_diagnostic` uses the full source text of `value`, comments within `value` are
157+
// actually preserved. thus, we have to check for comments in `stmt` but outside of `value`
158+
let pre_value = TextRange::new(stmt.start(), value.start());
159+
let post_value = TextRange::new(value.end(), stmt.end());
160+
let comment_ranges = checker.comment_ranges();
161+
let safety = if comment_ranges.intersects(pre_value) || comment_ranges.intersects(post_value) {
162+
Applicability::Unsafe
163+
} else {
164+
Applicability::Safe
165+
};
166+
148167
checker.diagnostics.push(create_diagnostic(
149-
checker.generator(),
150-
stmt.range(),
151-
target_name.id.clone(),
168+
checker.source(),
169+
stmt.range,
170+
&target_name.id,
152171
value,
153172
&vars,
154-
Applicability::Safe,
173+
safety,
155174
TypeAliasKind::TypeAliasType,
156175
));
157176
}
@@ -184,8 +203,6 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
184203
return;
185204
};
186205

187-
// TODO(zanie): We should check for generic type variables used in the value and define them
188-
// as type params instead
189206
let vars = {
190207
let mut visitor = TypeVarReferenceVisitor {
191208
vars: vec![],
@@ -208,9 +225,9 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
208225
}
209226

210227
checker.diagnostics.push(create_diagnostic(
211-
checker.generator(),
228+
checker.source(),
212229
stmt.range(),
213-
name.clone(),
230+
name,
214231
value,
215232
&vars,
216233
// The fix is only safe in a type stub because new-style aliases have different runtime behavior
@@ -226,46 +243,26 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
226243

227244
/// Generate a [`Diagnostic`] for a non-PEP 695 type alias or type alias type.
228245
fn create_diagnostic(
229-
generator: Generator,
246+
source: &str,
230247
stmt_range: TextRange,
231-
name: Name,
248+
name: &Name,
232249
value: &Expr,
233-
vars: &[TypeVar],
250+
type_vars: &[TypeVar],
234251
applicability: Applicability,
235252
type_alias_kind: TypeAliasKind,
236253
) -> Diagnostic {
254+
let content = format!(
255+
"type {name}{type_params} = {value}",
256+
type_params = DisplayTypeVars { type_vars, source },
257+
value = &source[value.range()]
258+
);
259+
let edit = Edit::range_replacement(content, stmt_range);
237260
Diagnostic::new(
238261
NonPEP695TypeAlias {
239262
name: name.to_string(),
240263
type_alias_kind,
241264
},
242265
stmt_range,
243266
)
244-
.with_fix(Fix::applicable_edit(
245-
Edit::range_replacement(
246-
generator.stmt(&Stmt::from(StmtTypeAlias {
247-
range: TextRange::default(),
248-
name: Box::new(Expr::Name(ExprName {
249-
range: TextRange::default(),
250-
id: name,
251-
ctx: ast::ExprContext::Load,
252-
})),
253-
type_params: create_type_params(vars),
254-
value: Box::new(value.clone()),
255-
})),
256-
stmt_range,
257-
),
258-
applicability,
259-
))
260-
}
261-
262-
fn create_type_params(vars: &[TypeVar]) -> Option<ruff_python_ast::TypeParams> {
263-
if vars.is_empty() {
264-
return None;
265-
}
266-
267-
Some(ast::TypeParams {
268-
range: TextRange::default(),
269-
type_params: vars.iter().map(TypeParam::from).collect(),
270-
})
267+
.with_fix(Fix::applicable_edit(edit, applicability))
271268
}

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

+72-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
3-
snapshot_kind: text
43
---
54
UP040.py:5:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
65
|
@@ -360,3 +359,75 @@ UP040.py:85:1: UP040 [*] Type alias `PositiveInt` uses `TypeAliasType` assignmen
360359
86 86 |
361360
87 87 | # OK: Other name
362361
88 88 | T = TypeVar("T", bound=SupportGt)
362+
363+
UP040.py:99:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword
364+
|
365+
97 | # unsafe fix if comments within the fix
366+
98 | T = TypeVar("T")
367+
99 | / PositiveList = TypeAliasType( # eaten comment
368+
100 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
369+
101 | | )
370+
| |_^ UP040
371+
102 |
372+
103 | T = TypeVar("T")
373+
|
374+
= help: Use the `type` keyword
375+
376+
ℹ Unsafe fix
377+
96 96 |
378+
97 97 | # unsafe fix if comments within the fix
379+
98 98 | T = TypeVar("T")
380+
99 |-PositiveList = TypeAliasType( # eaten comment
381+
100 |- "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
382+
101 |-)
383+
99 |+type PositiveList[T] = list[Annotated[T, Gt(0)]]
384+
102 100 |
385+
103 101 | T = TypeVar("T")
386+
104 102 | PositiveList = TypeAliasType(
387+
388+
UP040.py:104:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword
389+
|
390+
103 | T = TypeVar("T")
391+
104 | / PositiveList = TypeAliasType(
392+
105 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
393+
106 | | ) # this comment should be okay
394+
| |_^ UP040
395+
|
396+
= help: Use the `type` keyword
397+
398+
ℹ Safe fix
399+
101 101 | )
400+
102 102 |
401+
103 103 | T = TypeVar("T")
402+
104 |-PositiveList = TypeAliasType(
403+
105 |- "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
404+
106 |-) # this comment should be okay
405+
104 |+type PositiveList[T] = list[Annotated[T, Gt(0)]] # this comment should be okay
406+
107 105 |
407+
108 106 |
408+
109 107 | # this comment will actually be preserved because it's inside the "value" part
409+
410+
UP040.py:111:1: UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword
411+
|
412+
109 | # this comment will actually be preserved because it's inside the "value" part
413+
110 | T = TypeVar("T")
414+
111 | / PositiveList = TypeAliasType(
415+
112 | | "PositiveList", list[
416+
113 | | Annotated[T, Gt(0)], # preserved comment
417+
114 | | ], type_params=(T,)
418+
115 | | )
419+
| |_^ UP040
420+
|
421+
= help: Use the `type` keyword
422+
423+
ℹ Safe fix
424+
108 108 |
425+
109 109 | # this comment will actually be preserved because it's inside the "value" part
426+
110 110 | T = TypeVar("T")
427+
111 |-PositiveList = TypeAliasType(
428+
112 |- "PositiveList", list[
429+
111 |+type PositiveList[T] = list[
430+
113 112 | Annotated[T, Gt(0)], # preserved comment
431+
114 |- ], type_params=(T,)
432+
115 |-)
433+
113 |+ ]

crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
3-
snapshot_kind: text
43
---
54
UP040.pyi:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
65
|
@@ -19,6 +18,8 @@ UP040.pyi:6:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t
1918
6 |-x: typing.TypeAlias = int
2019
6 |+type x = int
2120
7 7 | x: TypeAlias = int
21+
8 8 |
22+
9 9 |
2223
2324
UP040.pyi:7:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
2425
|
@@ -35,3 +36,27 @@ UP040.pyi:7:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of t
3536
6 6 | x: typing.TypeAlias = int
3637
7 |-x: TypeAlias = int
3738
7 |+type x = int
39+
8 8 |
40+
9 9 |
41+
10 10 | # comments in the value are preserved
42+
43+
UP040.pyi:11:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
44+
|
45+
10 | # comments in the value are preserved
46+
11 | / x: TypeAlias = tuple[
47+
12 | | int, # preserved
48+
13 | | float,
49+
14 | | ]
50+
| |_^ UP040
51+
|
52+
= help: Use the `type` keyword
53+
54+
ℹ Safe fix
55+
8 8 |
56+
9 9 |
57+
10 10 | # comments in the value are preserved
58+
11 |-x: TypeAlias = tuple[
59+
11 |+type x = tuple[
60+
12 12 | int, # preserved
61+
13 13 | float,
62+
14 14 | ]

0 commit comments

Comments
 (0)