diff --git a/CHANGELOG.md b/CHANGELOG.md index e23bf1a32..6de2ac4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - #852 Implement patchedast handlers for TypeAlias - #853 Implement patchedast handlers TypeVar - #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf) -- #819 supports MatchOr, MatchSequence, MatchStar (@jheld) +- #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) # Release 1.14.0 diff --git a/rope/base/codeanalyze.py b/rope/base/codeanalyze.py index 992ff478c..25203cced 100644 --- a/rope/base/codeanalyze.py +++ b/rope/base/codeanalyze.py @@ -70,6 +70,25 @@ def get_line_start(self, lineno): def get_line_end(self, lineno): return self.starts[lineno] - 1 + def __getitem__(self, subscript): + start_offset = self._calculate_offset(subscript.start) + stop_offset = self._calculate_offset(subscript.stop) + return self.code[start_offset:stop_offset] + + def _calculate_offset(self, coord: tuple[int, int]) -> int: + lineno, col_offset = coord + lineno = self._clamp(0, self.length(), lineno) + col_offset = self._clamp( + 0, + self.get_line_end(lineno) - self.get_line_start(lineno), + col_offset, + ) + + return self.get_line_start(lineno) + col_offset + + def _clamp(self, min_value, max_value, value): + return max(min_value, min(max_value, value)) + class ArrayLinesAdapter: def __init__(self, lines): diff --git a/rope/refactor/patchedast.py b/rope/refactor/patchedast.py index 8ebfda398..a10350ae2 100644 --- a/rope/refactor/patchedast.py +++ b/rope/refactor/patchedast.py @@ -804,9 +804,43 @@ def _match_case(self, node): self._handle(node, children) def _MatchSequence(self, node): - children = ["[", *self._child_nodes(node.patterns, ","), "]"] + if node.patterns: + opening_paren, closing_paren = self._get_surrounding_parens(node) + + children = [ + *opening_paren, + *self._child_nodes(node.patterns, ","), + *closing_paren, + ] + else: + node_start = (node.lineno, node.col_offset) + node_end = (node.end_lineno, node.end_col_offset) + children = [self.lines[node_start:node_end]] self._handle(node, children) + def _get_surrounding_parens(self, node: ast.MatchSequence): + node_start = (node.lineno, node.col_offset) + first_pattern_start = (node.patterns[0].lineno, node.patterns[0].col_offset) + opening_paren = self.lines[node_start:first_pattern_start].strip() + if opening_paren not in ["[", "(", ""]: + warnings.warn( + f"Unexpected character in MatchSequence's opening_paren <{opening_paren}>; please report!", + RuntimeWarning, + ) + + last_pattern_end = ( + node.patterns[-1].end_lineno, + node.patterns[-1].end_col_offset, + ) + node_end = (node.end_lineno, node.end_col_offset) + closing_paren = self.lines[last_pattern_end:node_end].strip() + if closing_paren not in ["]", ")", ""]: + warnings.warn( + f"Unexpected character in MatchSequence's closing_paren <{closing_paren}>; please report!", + RuntimeWarning, + ) + return opening_paren, closing_paren + def _MatchStar(self, node): self._handle(node, ["*", node.name or "_"]) diff --git a/ropetest/codeanalyzetest.py b/ropetest/codeanalyzetest.py index 88a4caf96..a73f607c6 100644 --- a/ropetest/codeanalyzetest.py +++ b/ropetest/codeanalyzetest.py @@ -38,6 +38,26 @@ def test_source_lines_last_line_with_no_new_line(self): to_lines = SourceLinesAdapter("line1") self.assertEqual(1, to_lines.get_line_number(5)) + def test_source_lines_getitem_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual('ne2\nli', to_lines[(2, 2):(3, 2)]) + + def test_source_lines_getitem_start_lineno_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual("", to_lines[(100, 2):(3, 2)]) + + def test_source_lines_getitem_start_col_offset_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual('\nli', to_lines[(2, 100):(3, 2)]) + + def test_source_lines_getitem_end_lineno_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual("ne2\nline3\nline4\n", to_lines[(2, 2):(100, 2)]) + + def test_source_lines_getitem_end_col_offset_out_of_range(self): + to_lines = SourceLinesAdapter("line1\nline2\nline3\nline4\n") + self.assertEqual('ne2\nline3', to_lines[(2, 2):(3, 100)]) + class WordRangeFinderTest(unittest.TestCase): def _find_primary(self, code, offset): diff --git a/ropetest/refactor/patchedasttest.py b/ropetest/refactor/patchedasttest.py index da6c993a1..67a521b73 100644 --- a/ropetest/refactor/patchedasttest.py +++ b/ropetest/refactor/patchedasttest.py @@ -1426,13 +1426,11 @@ def test_match_node_with_wildcard(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_or(self): - source = dedent( - """\ + source = dedent("""\ match x: case 'v'|'z': print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchOr") @@ -1440,13 +1438,11 @@ def test_match_node_with_match_or(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_singleton_true(self): - source = dedent( - """\ + source = dedent("""\ match x: case True: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSingleton") @@ -1454,13 +1450,11 @@ def test_match_node_with_match_singleton_true(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_singleton_none(self): - source = dedent( - """\ + source = dedent("""\ match x: case None: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSingleton") @@ -1468,13 +1462,11 @@ def test_match_node_with_match_singleton_none(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_star_wildcard(self): - source = dedent( - """\ + source = dedent("""\ match x: case [*_]: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSequence") @@ -1482,13 +1474,11 @@ def test_match_node_with_match_sequence_with_star_wildcard(self): @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_tail_capture(self): - source = dedent( - """\ + source = dedent("""\ match x: case [1, 2, *rest]: print(rest) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSequence") @@ -1496,15 +1486,108 @@ def test_match_node_with_match_sequence_with_tail_capture(self): "[", "", "MatchValue", "", ",", " ", "MatchValue", "", ",", " ", "MatchStar", "", "]", ]) + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_no_parens(self): + source = dedent("""\ + match x: + case 1, 2: + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "MatchValue", "", ",", " ", "MatchValue", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_square_parens(self): + source = dedent("""\ + match x: + case [1, 2]: + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "[", "", "MatchValue", "", ",", " ", "MatchValue", "", "]", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_round_parens(self): + source = dedent("""\ + match x: + case (1, 2): + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "(", "", "MatchValue", "", ",", " ", "MatchValue", "", ")", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_spaces_around_parens(self): + source = dedent("""\ + match x: + case ( 1, 2 + ): + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "(", " ", "MatchValue", "", ",", " ", "MatchValue", "\n", ")", + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_with_internal_parens(self): + source = dedent("""\ + match x: + case [1], [2]: + print(rest) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", [ + "MatchSequence", "", ",", " ", "MatchSequence" + ]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_empty_round_parens(self): + source = dedent("""\ + match x: + case ( ): + print(x) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", ["( )"]) + + @testutils.only_for_versions_higher("3.10") + def test_match_node_with_match_sequence_empty_square_parens(self): + source = dedent("""\ + match x: + case []: + print(x) + """) + ast_frag = patchedast.get_patched_ast(source, True) + checker = _ResultChecker(self, ast_frag) + self.assert_single_case_match_block(checker, "MatchSequence") + checker.check_children("MatchSequence", ["[]"]) + @testutils.only_for_versions_higher("3.10") def test_match_node_with_match_sequence_with_star_and_value(self): - source = dedent( - """\ + source = dedent("""\ match x: case [*_, "something"]: print(x) - """ - ) + """) ast_frag = patchedast.get_patched_ast(source, True) checker = _ResultChecker(self, ast_frag) self.assert_single_case_match_block(checker, "MatchSequence")