diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de2ac4e5..9b6cc0d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - #853 Implement patchedast handlers TypeVar - #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf) - #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) +- #861 Recognize pattern captures for rename and reject unsupported inline operations (@yangfan-yf-yf) # Release 1.14.0 diff --git a/rope/base/oi/soi.py b/rope/base/oi/soi.py index 090b10337..62ce84a99 100644 --- a/rope/base/oi/soi.py +++ b/rope/base/oi/soi.py @@ -6,7 +6,7 @@ """ import rope.base.builtins # Use full qualification for clarity. -from rope.base import arguments, evaluate, pynames, pyobjects, utils +from rope.base import arguments, ast, evaluate, pynames, pyobjects, utils from rope.base.oi.type_hinting.factory import get_type_hinting_factory _ignore_inferred = utils.ignore_exception(pyobjects.IsBeingInferredError) @@ -141,6 +141,9 @@ def _parameter_objects(pyobject): @_ignore_inferred def _infer_assignment(assignment, pymodule): + assign_node = assignment.type_hint or assignment.ast_node + if isinstance(assign_node, ast.pattern): + return pyobjects.get_unknown() result = _follow_pyname(assignment, pymodule) if result is None: return None diff --git a/rope/base/pyobjectsdef.py b/rope/base/pyobjectsdef.py index 95bd15691..1a2510e68 100644 --- a/rope/base/pyobjectsdef.py +++ b/rope/base/pyobjectsdef.py @@ -454,6 +454,22 @@ def _AnnAssign(self, node): def _AugAssign(self, node): pass + def _add_match_capture(self, node, name): + if name is not None: + assignment = pynamesdef.AssignmentValue(node) + self._assigned(name, assignment) + + def _MatchAs(self, node): + self._add_match_capture(node, node.name) + self.generic_visit(node) + + def _MatchStar(self, node): + self._add_match_capture(node, node.name) + + def _MatchMapping(self, node): + self._add_match_capture(node, node.rest) + self.generic_visit(node) + def _For(self, node): self._update_evaluated(node.target, node.iter, ".__iter__().next()") for child in node.body + node.orelse: diff --git a/rope/refactor/inline.py b/rope/refactor/inline.py index 768d30b7d..52627ef5a 100644 --- a/rope/refactor/inline.py +++ b/rope/refactor/inline.py @@ -21,6 +21,7 @@ import rope.base.builtins # Use fully qualified names for clarity. from rope.base import ( + ast, codeanalyze, evaluate, exceptions, @@ -245,6 +246,13 @@ def __init__(self, *args, **kwds): self._init_imports() def _check_exceptional_conditions(self): + if any( + isinstance(assignment.ast_node, ast.pattern) + for assignment in self.pyname.assignments + ): + raise exceptions.RefactoringError( + "Pattern matching captures cannot be inlined." + ) if len(self.pyname.assignments) != 1: raise exceptions.RefactoringError( "Local variable should be assigned once for inlining." diff --git a/ropetest/objectinfertest.py b/ropetest/objectinfertest.py index a46a7c018..023552d39 100644 --- a/ropetest/objectinfertest.py +++ b/ropetest/objectinfertest.py @@ -2,7 +2,7 @@ from textwrap import dedent import rope.base.builtins # Use fully-qualified names for clarity. -from rope.base import libutils +from rope.base import libutils, pyobjects from ropetest import testutils @@ -324,6 +324,17 @@ class C(object): a_var = mod["a_var"].get_object() self.assertEqual(c_class, a_var.get_type()) + @testutils.only_for_versions_higher("3.10") + def test_match_capture_is_unknown(self): + code = dedent("""\ + captured = 1 + match value: + case [1, *_] as captured: + pass + """) + scope = libutils.get_string_scope(self.project, code) + self.assertEqual(pyobjects.get_unknown(), scope["captured"].get_object()) + def test_basic_list_comprehensions(self): code = dedent("""\ class C(object): diff --git a/ropetest/pyscopestest.py b/ropetest/pyscopestest.py index d60702b32..278ccfde5 100644 --- a/ropetest/pyscopestest.py +++ b/ropetest/pyscopestest.py @@ -112,6 +112,25 @@ def test_inline_assignment(self): ["a_var", "values"], ) + @testutils.only_for_versions_higher("3.10") + def test_match_capture_names(self): + code = dedent("""\ + def func(value): + match value: + case [_, *rest] as whole: + pass + case {"key": item, **remaining}: + pass + """) + scope = libutils.get_string_scope(self.project, code) + function_scope = scope.get_scopes()[0] + defined_names = set(function_scope.get_names()) + + self.assertLessEqual( + {"item", "remaining", "rest", "whole"}, defined_names + ) + self.assertNotIn("_", defined_names) + @testutils.only_for_versions_higher("3.8") def test_inline_assignment_in_comprehensions(self): code = dedent("""\ diff --git a/ropetest/refactor/inlinetest.py b/ropetest/refactor/inlinetest.py index 585214b41..c0be4d3e1 100644 --- a/ropetest/refactor/inlinetest.py +++ b/ropetest/refactor/inlinetest.py @@ -113,6 +113,22 @@ def test_on_unknown_vars(self): with self.assertRaises(rope.base.exceptions.RefactoringError): self._inline(code, code.index("another_var") + 1) + @testutils.only_for_versions_higher("3.10") + def test_pattern_capture_cannot_be_inlined(self): + code = dedent("""\ + old_name = 0 + match value: + case old_name: + print(old_name) + print(old_name) + """) + capture_offset = code.index("old_name", code.index("case")) + 1 + with self.assertRaisesRegex( + rope.base.exceptions.RefactoringError, + "Pattern matching captures cannot be inlined", + ): + self._inline(code, capture_offset) + def test_attribute_inlining(self): code = dedent("""\ class A(object): diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index 78bc38d82..842b92401 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -1,10 +1,10 @@ import sys import unittest from textwrap import dedent -from rope.base import exceptions import rope.base.codeanalyze import rope.refactor.occurrences +from rope.base import exceptions from rope.refactor import rename from rope.refactor.rename import Rename from ropetest import testutils @@ -242,6 +242,63 @@ def test_renaming_inline_assignment(self): refactored, ) + @testutils.only_for_versions_higher("3.10") + def test_renaming_match_as_capture(self): + code = dedent("""\ + match value: + case [1, *_] as old_name: + print(old_name) + """) + refactored = self._local_rename( + code, code.rindex("old_name") + 1, "new_name" + ) + self.assertEqual( + dedent("""\ + match value: + case [1, *_] as new_name: + print(new_name) + """), + refactored, + ) + + @testutils.only_for_versions_higher("3.10") + def test_renaming_match_star_capture(self): + code = dedent("""\ + match value: + case [1, *old_name] if old_name: + print(old_name) + """) + refactored = self._local_rename( + code, code.index("old_name") + 1, "new_name" + ) + self.assertEqual( + dedent("""\ + match value: + case [1, *new_name] if new_name: + print(new_name) + """), + refactored, + ) + + @testutils.only_for_versions_higher("3.10") + def test_renaming_match_mapping_rest_capture(self): + code = dedent("""\ + match value: + case {"key": item, **old_name}: + print(old_name) + """) + refactored = self._local_rename( + code, code.index("old_name") + 1, "new_name" + ) + self.assertEqual( + dedent("""\ + match value: + case {"key": item, **new_name}: + print(new_name) + """), + refactored, + ) + def test_renaming_arguments_for_normal_args_changing_calls(self): code = dedent("""\ def a_func(p1=None, p2=None):