diff --git a/CHANGELOG.md b/CHANGELOG.md index ef219ac7..0dadd387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # **Upcoming release** +- Fix `patchedast` aborting on a `'''` sequence inside a double-quoted + f-string (`end_quote_char` picked the longest quote in the body instead of + the matching delimiter), which raised `MismatchedTokenError` and broke + rename/inline for the whole module (@marlon-costa-dc) - ... # Release 1.14.0 diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 4e842c04..65d942f0 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -383,14 +383,16 @@ def start_quote_char(): return self.source[start : quote_pos + len(quote_char)] def end_quote_char(): - possible_quotes = [ - (self.source.source.rfind(q, start, end), q) - for q in reversed(QUOTE_CHARS) - ] - _, quote_pos, quote_char = max( - (len(q), pos, q) for pos, q in possible_quotes if pos != -1 - ) - return self.source[end - len(quote_char) : end] + # The closing delimiter is the quote char the f-string literally + # ends with -- NOT the longest quote char anywhere in the body. + # A ''' appearing inside a "..." f-string body must not be picked + # as the delimiter (issue: end_quote_char used max(len(q)) and + # mis-matched, producing a token that consume_joined_string could + # not locate). + for quote_char in QUOTE_CHARS: + if self.source.source[end - len(quote_char) : end] == quote_char: + return self.source[end - len(quote_char) : end] + return self.source[end - 1 : end] QUOTE_CHARS = ['"""', "'''", '"', "'"] offset = self.source.offset diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index 3ac341f2..109d07c8 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -272,6 +272,19 @@ def test_handling_format_strings_basic(self): checker.check_children("JoinedStr", ['f"', "abc", "FormattedValue", "", '"']) checker.check_children("FormattedValue", ["{", "", "Name", "", "}"]) + @testutils.only_for_versions_higher("3.6") + def test_handling_format_strings_with_triple_quote_in_body(self): + # A double-quoted f-string whose body contains a ''' sequence must not + # confuse the end-quote detection (end_quote_char picked the longest + # quote in the body instead of the matching delimiter). + source = 'f"abc = \'\'\'{a}"\n' + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + checker.check_children( + "JoinedStr", ['f"', "abc = '''", "FormattedValue", "", '"'] + ) + checker.check_children("FormattedValue", ["{", "", "Name", "", "}"]) + @testutils.only_for_versions_higher("3.6") def test_handling_format_strings_with_implicit_join(self): source = '''"1" + rf'abc{a}' f"""xxx{b} """\n'''