[pyupgrade] Comments within parenthesized value ranges should not affect applicability (UP040) (#16027)

## Summary

Follow-up to #16026.

Previously, the fix for this would be marked as unsafe, even though all
comments are preserved:

```python
# .pyi
T: TypeAlias = (  # Comment
	int | str
)
```

Now it is safe: comments within the parenthesized range no longer affect
applicability.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Dylan <53534755+dylwil3@users.noreply.github.com>
This commit is contained in:
InSync 2025-02-08 03:44:33 +07:00 committed by GitHub
parent 19f3424a1a
commit a29009e4ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 22 deletions

View file

@ -12,3 +12,13 @@ x: TypeAlias = tuple[
int, # preserved
float,
]
T: TypeAlias = ( # comment0
# comment1
int # comment2
# comment3
| # comment4
# comment5
str # comment6
# comment7
) # comment8

View file

@ -170,25 +170,12 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
return;
};
// it would be easier to check for comments in the whole `stmt.range`, but because
// `create_diagnostic` uses the full source text of `value`, comments within `value` are
// actually preserved. thus, we have to check for comments in `stmt` but outside of `value`
let pre_value = TextRange::new(stmt.start(), value.start());
let post_value = TextRange::new(value.end(), stmt.end());
let comment_ranges = checker.comment_ranges();
let safety = if comment_ranges.intersects(pre_value) || comment_ranges.intersects(post_value) {
Applicability::Unsafe
} else {
Applicability::Safe
};
checker.report_diagnostic(create_diagnostic(
checker,
stmt.into(),
&target_name.id,
value,
&vars,
safety,
TypeAliasKind::TypeAliasType,
));
}
@ -248,13 +235,6 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
name,
value,
&vars,
// The fix is only safe in a type stub because new-style aliases have different runtime behavior
// See https://github.com/astral-sh/ruff/issues/6434
if checker.source_type.is_stub() {
Applicability::Safe
} else {
Applicability::Unsafe
},
TypeAliasKind::TypeAlias,
));
}
@ -266,19 +246,45 @@ fn create_diagnostic(
name: &Name,
value: &Expr,
type_vars: &[TypeVar],
applicability: Applicability,
type_alias_kind: TypeAliasKind,
) -> Diagnostic {
let source = checker.source();
let comment_ranges = checker.comment_ranges();
let range_with_parentheses =
parenthesized_range(value.into(), stmt.into(), checker.comment_ranges(), source)
parenthesized_range(value.into(), stmt.into(), comment_ranges, source)
.unwrap_or(value.range());
let content = format!(
"type {name}{type_params} = {value}",
type_params = DisplayTypeVars { type_vars, source },
value = &source[range_with_parentheses]
);
let edit = Edit::range_replacement(content, stmt.range());
let applicability =
if type_alias_kind == TypeAliasKind::TypeAlias && !checker.source_type.is_stub() {
// The fix is always unsafe in non-stubs
// because new-style aliases have different runtime behavior.
// See https://github.com/astral-sh/ruff/issues/6434
Applicability::Unsafe
} else {
// In stub files, or in non-stub files for `TypeAliasType` assignments,
// the fix is only unsafe if it would delete comments.
//
// it would be easier to check for comments in the whole `stmt.range`, but because
// `create_diagnostic` uses the full source text of `value`, comments within `value` are
// actually preserved. thus, we have to check for comments in `stmt` but outside of `value`
let pre_value = TextRange::new(stmt.start(), range_with_parentheses.start());
let post_value = TextRange::new(range_with_parentheses.end(), stmt.end());
if comment_ranges.intersects(pre_value) || comment_ranges.intersects(post_value) {
Applicability::Unsafe
} else {
Applicability::Safe
}
};
Diagnostic::new(
NonPEP695TypeAlias {
name: name.to_string(),

View file

@ -48,6 +48,8 @@ UP040.pyi:11:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of
13 | | float,
14 | | ]
| |_^ UP040
15 |
16 | T: TypeAlias = ( # comment0
|
= help: Use the `type` keyword
@ -60,3 +62,30 @@ UP040.pyi:11:1: UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of
12 12 | int, # preserved
13 13 | float,
14 14 | ]
UP040.pyi:16:1: UP040 [*] Type alias `T` uses `TypeAlias` annotation instead of the `type` keyword
|
14 | ]
15 |
16 | / T: TypeAlias = ( # comment0
17 | | # comment1
18 | | int # comment2
19 | | # comment3
20 | | | # comment4
21 | | # comment5
22 | | str # comment6
23 | | # comment7
24 | | ) # comment8
| |_^ UP040
|
= help: Use the `type` keyword
Safe fix
13 13 | float,
14 14 | ]
15 15 |
16 |-T: TypeAlias = ( # comment0
16 |+type T = ( # comment0
17 17 | # comment1
18 18 | int # comment2
19 19 | # comment3