From f70e8a7524b255c20e3dc966141b6e80d9697958 Mon Sep 17 00:00:00 2001
From: konsti <konstin@mailbox.org>
Date: Mon, 2 Oct 2023 10:43:39 +0200
Subject: [PATCH] Fix PLE251 rules with f-string escaping (#7741)

**Summary** The `value` of the `FStringMiddle` for `f"""}}ab"""` is
`}ab`, i.e. the curly brace escaping is decoded. If we iterate over
string this gives us false indices causing exploding fixes for PLE251
rules (PLE2510, PLE2512, PLE2513, PLE2514, PLE2515). Instead, we now use
the source range.

Handles
https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
Handles
https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256

**Test Plan** Minimized fuzzing cases as fixtures.
---
 .../fixtures/pylint/invalid_characters.py     | Bin 1461 -> 1639 bytes
 .../pylint/rules/invalid_string_characters.rs |   5 ++--
 ..._tests__PLE2510_invalid_characters.py.snap |   5 ++++
 ..._tests__PLE2512_invalid_characters.py.snap |  24 ++++++++++++++++++
 ..._tests__PLE2513_invalid_characters.py.snap |  21 +++++++++++++++
 ..._tests__PLE2515_invalid_characters.py.snap |   2 ++
 6 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py b/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py
index e1864fa38175502c3dcdc92b2b38f0d924a1cb34..79c9307695a9153b9945e2976c45335259630bfa 100644
GIT binary patch
diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs
index 5c00ef89a1..aa5c1954e8 100644
--- a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs
+++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs
@@ -179,8 +179,9 @@ pub(crate) fn invalid_string_characters(
     locator: &Locator,
 ) {
     let text = match tok {
-        Tok::String { .. } => locator.slice(range),
-        Tok::FStringMiddle { value, .. } => value.as_str(),
+        // We can't use the `value` field since it's decoded and e.g. for f-strings removed a curly
+        // brace that escaped another curly brace, which would gives us wrong column information.
+        Tok::String { .. } | Tok::FStringMiddle { .. } => locator.slice(range),
         _ => return,
     };
 
diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap
index 25e9c12e47..b472b75871 100644
--- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap
+++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap
@@ -48,6 +48,8 @@ invalid_characters.py:55:21: PLE2510 [*] Invalid unescaped character backspace,
 54 | 
 55 | nested_fstrings = f'{f'{f''}'}'
    |                      PLE2510
+56 | 
+57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
    |
    = help: Replace with escape sequence
 
@@ -57,5 +59,8 @@ invalid_characters.py:55:21: PLE2510 [*] Invalid unescaped character backspace,
 54 54 | 
 55    |-nested_fstrings = f'{f'{f''}'}'
    55 |+nested_fstrings = f'\b{f'{f''}'}'
+56 56 | 
+57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
+58 58 | x = f"""}}ab"""
 
 
diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap
index efda03729d..aca79fabc8 100644
--- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap
+++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap
@@ -47,6 +47,8 @@ invalid_characters.py:55:25: PLE2512 [*] Invalid unescaped character SUB, use "\
 54 | 
 55 | nested_fstrings = f'{f'{f''}'}'
    |                         PLE2512
+56 | 
+57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
    |
    = help: Replace with escape sequence
 
@@ -56,5 +58,27 @@ invalid_characters.py:55:25: PLE2512 [*] Invalid unescaped character SUB, use "\
 54 54 | 
 55    |-nested_fstrings = f'{f'{f''}'}'
    55 |+nested_fstrings = f'{f'\x1A{f''}'}'
+56 56 | 
+57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
+58 58 | x = f"""}}ab"""
+
+invalid_characters.py:58:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+   |
+57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
+58 | x = f"""}}ab"""
+   |             PLE2512
+59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
+60 | x = f"""}}ab"""
+   |
+   = help: Replace with escape sequence
+
+ℹ Fix
+55 55 | nested_fstrings = f'{f'{f''}'}'
+56 56 | 
+57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
+58    |-x = f"""}}ab"""
+   58 |+x = f"""}}a\x1Ab"""
+59 59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
+60 60 | x = f"""}}ab"""
 
 
diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap
index fbfd76cfa2..87dd00ef51 100644
--- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap
+++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap
@@ -47,6 +47,8 @@ invalid_characters.py:55:29: PLE2513 [*] Invalid unescaped character ESC, use "\
 54 | 
 55 | nested_fstrings = f'{f'{f''}'}'
    |                            PLE2513
+56 | 
+57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
    |
    = help: Replace with escape sequence
 
@@ -56,5 +58,24 @@ invalid_characters.py:55:29: PLE2513 [*] Invalid unescaped character ESC, use "\
 54 54 | 
 55    |-nested_fstrings = f'{f'{f''}'}'
    55 |+nested_fstrings = f'{f'{f'\x1B'}'}'
+56 56 | 
+57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
+58 58 | x = f"""}}ab"""
+
+invalid_characters.py:60:12: PLE2513 [*] Invalid unescaped character ESC, use "\x1B" instead
+   |
+58 | x = f"""}}ab"""
+59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
+60 | x = f"""}}ab"""
+   |             PLE2513
+   |
+   = help: Replace with escape sequence
+
+ℹ Fix
+57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
+58 58 | x = f"""}}ab"""
+59 59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
+60    |-x = f"""}}ab"""
+   60 |+x = f"""}}a\x1Bb"""
 
 
diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap
index 3a9ba051ee..7ef2c89169 100644
--- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap
+++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap
@@ -141,6 +141,7 @@ invalid_characters.py:53:61: PLE2515 [*] Invalid unescaped character zero-width-
    53 |+zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b​"
 54 54 | 
 55 55 | nested_fstrings = f'{f'{f''}'}'
+56 56 | 
 
 invalid_characters.py:53:62: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
    |
@@ -161,5 +162,6 @@ invalid_characters.py:53:62: PLE2515 [*] Invalid unescaped character zero-width-
    53 |+zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ ​\u200b"
 54 54 | 
 55 55 | nested_fstrings = f'{f'{f''}'}'
+56 56 | 
 
 
