From a67995ce47206d6581e3ffbbac6694380e3255c7 Mon Sep 17 00:00:00 2001
From: Raphael Roberts <raphael.roberts48@gmail.com>
Date: Mon, 2 Dec 2024 18:28:20 -0600
Subject: [PATCH] Handle the edge case where an f-string looks like
 f"#{some_var}"

---
 rope/refactor/similarfinder.py | 52 +++++++++++++++++++++++++---------
 1 file changed, 39 insertions(+), 13 deletions(-)

diff --git a/rope/refactor/similarfinder.py b/rope/refactor/similarfinder.py
index dd5bec36..3c197e42 100644
--- a/rope/refactor/similarfinder.py
+++ b/rope/refactor/similarfinder.py
@@ -269,10 +269,26 @@ def get_region(self):
 
 
 class CodeTemplate:
+    _dollar_name_pattern = r"(?P<name>\$\{[^\s\$\}]*\})"
+
     def __init__(self, template):
         self.template = template
         self._find_names()
 
+    @classmethod
+    def _get_pattern(cls):
+        if cls._match_pattern is None:
+            pattern = "|".join(
+                (
+                    codeanalyze.get_comment_pattern(),
+                    codeanalyze.get_string_pattern(),
+                    f"(?P<fstring>{codeanalyze.get_formatted_string_pattern()})",
+                    cls._dollar_name_pattern,
+                )
+            )
+            cls._match_pattern = re.compile(pattern)
+        return cls._match_pattern
+
     def _find_names(self):
         self.names = {}
         for match in CodeTemplate._get_pattern().finditer(self.template):
@@ -283,6 +299,29 @@ def _find_names(self):
                     self.names[name] = []
                 self.names[name].append((start, end))
 
+            elif "fstring" in match.groupdict() and match.group("fstring") is not None:
+                self._fstring_case(match)
+
+    def _fstring_case(self, fstring_match: re.Match):
+        """Needed because CodeTemplate._match_pattern short circuits
+        as soon as it sees a '#', even if that '#' is inside a f-string
+        that has a ${variable}."""
+
+        string_start, string_end = fstring_match.span("fstring")
+
+        for match in re.finditer(self._dollar_name_pattern, self.template):
+            if match.start("name") < string_start:
+                continue
+
+            if match.end("name") > string_end:
+                break
+
+            start, end = match.span("name")
+            name = self.template[start + 2 : end - 1]
+            if name not in self.names:
+                self.names[name] = []
+            self.names[name].append((start, end))
+
     def get_names(self):
         return self.names.keys()
 
@@ -298,19 +337,6 @@ def substitute(self, mapping):
 
     _match_pattern = None
 
-    @classmethod
-    def _get_pattern(cls):
-        if cls._match_pattern is None:
-            pattern = (
-                codeanalyze.get_comment_pattern()
-                + "|"
-                + codeanalyze.get_string_pattern()
-                + "|"
-                + r"(?P<name>\$\{[^\s\$\}]*\})"
-            )
-            cls._match_pattern = re.compile(pattern)
-        return cls._match_pattern
-
 
 class _RopeVariable:
     """Transform and identify rope inserted wildcards"""