Skip to content

Commit afcd781

Browse files
authored
[pyupgrade] Fix syntax error on string with newline escape and comment (UP037) (#22968)
## Summary Fixes a syntax error introduced by `UP037` when a quoted annotation contains a trailing comment. Fixes #19835. ## Problem The logic for detecting trailing comments in quoted annotations was brittle, relying on a fixed offset (`len() - 2`) from the end of the token stream. This failed to correctly identify comments in strings like `"A\n#"`, leading to invalid code generation where the closing parenthesis of the function definition was commented out. ## Approach Updated the logic in `crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs` to detect trailing comments. It now iterates backwards through the tokens of the annotation string, skipping any newline tokens, to find if the last significant token is a comment. ## Test Plan Added regression tests to `crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_0.py`. Verified with: `cargo test -p ruff_linter pyupgrade::tests`
1 parent 0b7971a commit afcd781

4 files changed

Lines changed: 86 additions & 5 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,7 @@ def f() -> """Literal[0]
120120
121121
""":
122122
return 0
123+
124+
# https://github.com/astral-sh/ruff/issues/19835
125+
def foo(bar: "A\n#"): ...
126+
def foo(bar: "A\n#\n"): ...

crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,8 @@ pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: Text
115115

116116
let last_token_is_comment = checker
117117
.tokens()
118-
// The actual last token will always be a logical newline,
119-
// so we check the second to last
120-
.get(checker.tokens().len().saturating_sub(2))
118+
.iter()
119+
.rfind(|tok| !tok.kind().is_any_newline())
121120
.is_some_and(|tok| tok.kind() == TokenKind::Comment);
122121

123122
let new_content = match (spans_multiple_lines, last_token_is_comment) {

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,5 +590,44 @@ help: Remove quotes
590590
119 | #
591591
120 |
592592
- """:
593-
121 + ):
593+
121 +
594+
122 + ):
595+
123 | return 0
596+
124 |
597+
125 | # https://github.com/astral-sh/ruff/issues/19835
598+
599+
UP037 [*] Remove quotes from type annotation
600+
--> UP037_0.py:125:14
601+
|
602+
124 | # https://github.com/astral-sh/ruff/issues/19835
603+
125 | def foo(bar: "A\n#"): ...
604+
| ^^^^^^
605+
126 | def foo(bar: "A\n#\n"): ...
606+
|
607+
help: Remove quotes
594608
122 | return 0
609+
123 |
610+
124 | # https://github.com/astral-sh/ruff/issues/19835
611+
- def foo(bar: "A\n#"): ...
612+
125 + def foo(bar: (A
613+
126 + #
614+
127 + )): ...
615+
128 | def foo(bar: "A\n#\n"): ...
616+
617+
UP037 [*] Remove quotes from type annotation
618+
--> UP037_0.py:126:14
619+
|
620+
124 | # https://github.com/astral-sh/ruff/issues/19835
621+
125 | def foo(bar: "A\n#"): ...
622+
126 | def foo(bar: "A\n#\n"): ...
623+
| ^^^^^^^^
624+
|
625+
help: Remove quotes
626+
123 |
627+
124 | # https://github.com/astral-sh/ruff/issues/19835
628+
125 | def foo(bar: "A\n#"): ...
629+
- def foo(bar: "A\n#\n"): ...
630+
126 + def foo(bar: (A
631+
127 + #
632+
128 +
633+
129 + )): ...

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,5 +590,44 @@ help: Remove quotes
590590
119 | #
591591
120 |
592592
- """:
593-
121 + ):
593+
121 +
594+
122 + ):
595+
123 | return 0
596+
124 |
597+
125 | # https://github.com/astral-sh/ruff/issues/19835
598+
599+
UP037 [*] Remove quotes from type annotation
600+
--> UP037_0.py:125:14
601+
|
602+
124 | # https://github.com/astral-sh/ruff/issues/19835
603+
125 | def foo(bar: "A\n#"): ...
604+
| ^^^^^^
605+
126 | def foo(bar: "A\n#\n"): ...
606+
|
607+
help: Remove quotes
594608
122 | return 0
609+
123 |
610+
124 | # https://github.com/astral-sh/ruff/issues/19835
611+
- def foo(bar: "A\n#"): ...
612+
125 + def foo(bar: (A
613+
126 + #
614+
127 + )): ...
615+
128 | def foo(bar: "A\n#\n"): ...
616+
617+
UP037 [*] Remove quotes from type annotation
618+
--> UP037_0.py:126:14
619+
|
620+
124 | # https://github.com/astral-sh/ruff/issues/19835
621+
125 | def foo(bar: "A\n#"): ...
622+
126 | def foo(bar: "A\n#\n"): ...
623+
| ^^^^^^^^
624+
|
625+
help: Remove quotes
626+
123 |
627+
124 | # https://github.com/astral-sh/ruff/issues/19835
628+
125 | def foo(bar: "A\n#"): ...
629+
- def foo(bar: "A\n#\n"): ...
630+
126 + def foo(bar: (A
631+
127 + #
632+
128 +
633+
129 + )): ...

0 commit comments

Comments
 (0)