From 741e993bed633088ccd687985b85f3d549e77775 Mon Sep 17 00:00:00 2001 From: Tanbir Hossain Ramim <96797470+TanbirRamim@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:11:30 +0200 Subject: [PATCH 1/2] Treat type alias statements as name assignments `type X = ...` statements were not registered in the scope, so rename and inline refactoring could not resolve the alias name. The scope visitor now records the alias name as an assigned name. Generic aliases get no assignment value, since their value refers to their own type parameters. Fixes #862 --- CHANGELOG.md | 1 + rope/base/pyobjectsdef.py | 10 +++++ ropetest/refactor/inlinetest.py | 26 +++++++++++++ ropetest/refactor/renametest.py | 68 +++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 010425d5d..2bb1425e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf) - #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) - #870 Add default implementation for is_dir() (@lieryan) +- #862 Recognize type alias statements as assignments in rename and inline refactoring (@TanbirRamim) # Release 1.14.0 diff --git a/rope/base/pyobjectsdef.py b/rope/base/pyobjectsdef.py index 95bd15691..c1e3f951f 100644 --- a/rope/base/pyobjectsdef.py +++ b/rope/base/pyobjectsdef.py @@ -371,6 +371,13 @@ def _Assign(self, node): self.visit(child_node) _ExpressionVisitor(self.scope_visitor).visit(node.value) + def _TypeAlias(self, node): + # the value of a generic alias refers to its own type parameters, + # so it cannot be used as a value outside of the alias + if not node.type_params: + self.assigned_ast = node.value + self.visit(node.name) + def _assigned(self, name, assignment=None): self.scope_visitor._assigned(name, assignment) @@ -451,6 +458,9 @@ def _Assign(self, node): def _AnnAssign(self, node): _AnnAssignVisitor(self).visit(node) + def _TypeAlias(self, node): + _AssignVisitor(self).visit(node) + def _AugAssign(self, node): pass diff --git a/ropetest/refactor/inlinetest.py b/ropetest/refactor/inlinetest.py index 585214b41..bc5f16d80 100644 --- a/ropetest/refactor/inlinetest.py +++ b/ropetest/refactor/inlinetest.py @@ -42,6 +42,32 @@ def test_empty_case(self): refactored = self._inline(code, code.index("a_var") + 1) self.assertEqual("", refactored) + @testutils.only_for_versions_higher("3.12") + def test_inlining_type_alias(self): + code = dedent("""\ + type an_alias = int + def a_func(param: an_alias) -> an_alias: + pass + """) + refactored = self._inline(code, code.index("an_alias") + 1) + self.assertEqual( + dedent("""\ + def a_func(param: int) -> int: + pass + """), + refactored, + ) + + @testutils.only_for_versions_higher("3.12") + def test_inlining_generic_type_alias(self): + code = dedent("""\ + type an_alias[T] = list[T] + def a_func(param: an_alias[int]): + pass + """) + with self.assertRaises(rope.base.exceptions.RefactoringError): + self._inline(code, code.index("an_alias") + 1) + def test_long_definition(self): code = dedent("""\ a_var = 10 + (10 + 10) diff --git a/ropetest/refactor/renametest.py b/ropetest/refactor/renametest.py index 78bc38d82..941e6fc13 100644 --- a/ropetest/refactor/renametest.py +++ b/ropetest/refactor/renametest.py @@ -242,6 +242,74 @@ def test_renaming_inline_assignment(self): refactored, ) + @testutils.only_for_versions_higher("3.12") + def test_renaming_type_alias(self): + code = dedent("""\ + type old_name = int + x: old_name = 1 + """) + refactored = self._local_rename(code, code.index("old_name") + 1, "new_name") + self.assertEqual( + dedent("""\ + type new_name = int + x: new_name = 1 + """), + refactored, + ) + + @testutils.only_for_versions_higher("3.12") + def test_renaming_type_alias_from_its_usage(self): + code = dedent("""\ + type old_name = int + def a_func(param: old_name) -> old_name: + pass + """) + offset = code.rindex("old_name") + 1 + refactored = self._local_rename(code, offset, "new_name") + self.assertEqual( + dedent("""\ + type new_name = int + def a_func(param: new_name) -> new_name: + pass + """), + refactored, + ) + + @testutils.only_for_versions_higher("3.12") + def test_renaming_type_alias_in_function_scope(self): + code = dedent("""\ + old_name = 1 + def a_func(): + type old_name = int + x: old_name = 1 + """) + offset = code.index("old_name", code.index("type")) + 1 + refactored = self._local_rename(code, offset, "new_name") + self.assertEqual( + dedent("""\ + old_name = 1 + def a_func(): + type new_name = int + x: new_name = 1 + """), + refactored, + ) + + @testutils.only_for_versions_higher("3.12") + def test_renaming_generic_type_alias(self): + code = dedent("""\ + type OldAlias[T] = list[T] + x: OldAlias[int] = [] + """) + refactored = self._local_rename(code, code.index("OldAlias") + 1, "NewAlias") + self.assertEqual( + dedent("""\ + type NewAlias[T] = list[T] + x: NewAlias[int] = [] + """), + refactored, + ) + def test_renaming_arguments_for_normal_args_changing_calls(self): code = dedent("""\ def a_func(p1=None, p2=None): From 739b17c30fb739116354add6be3fd2ebf730a9b3 Mon Sep 17 00:00:00 2001 From: Tanbir Date: Wed, 23 Sep 2026 15:35:25 +0200 Subject: [PATCH 2/2] Refuse inlining type aliases A type alias is a distinct TypeAliasType object whose value is evaluated lazily, so replacing the alias name with its value can change behaviour (e.g. Alias.__value__, or forward references). Keep the alias name resolution for rename, but do not record the value as an assignment. --- CHANGELOG.md | 2 +- rope/base/pyobjectsdef.py | 7 +++---- ropetest/refactor/inlinetest.py | 25 +++++++++++++++---------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e682758..f269f11d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) - #870 Add default implementation for is_dir() (@lieryan) - #872 Fix unicode handling in patchedast (@lieryan) -- #862 Recognize type alias statements as assignments in rename and inline refactoring (@TanbirRamim) +- #862 Recognize type alias statements as assignments in rename refactoring (@TanbirRamim) # Release 1.14.0 diff --git a/rope/base/pyobjectsdef.py b/rope/base/pyobjectsdef.py index c1e3f951f..e07f69f3e 100644 --- a/rope/base/pyobjectsdef.py +++ b/rope/base/pyobjectsdef.py @@ -372,10 +372,9 @@ def _Assign(self, node): _ExpressionVisitor(self.scope_visitor).visit(node.value) def _TypeAlias(self, node): - # the value of a generic alias refers to its own type parameters, - # so it cannot be used as a value outside of the alias - if not node.type_params: - self.assigned_ast = node.value + # the alias value is not recorded as an assigned value: a type + # alias is a distinct TypeAliasType object whose value is evaluated + # lazily, so substituting the value for the alias name is unsafe self.visit(node.name) def _assigned(self, name, assignment=None): diff --git a/ropetest/refactor/inlinetest.py b/ropetest/refactor/inlinetest.py index bc5f16d80..79d807aaf 100644 --- a/ropetest/refactor/inlinetest.py +++ b/ropetest/refactor/inlinetest.py @@ -43,20 +43,25 @@ def test_empty_case(self): self.assertEqual("", refactored) @testutils.only_for_versions_higher("3.12") - def test_inlining_type_alias(self): + def test_inlining_type_alias_is_refused(self): code = dedent("""\ type an_alias = int - def a_func(param: an_alias) -> an_alias: + value = an_alias.__value__ + """) + with self.assertRaises(rope.base.exceptions.RefactoringError): + self._inline(code, code.index("an_alias") + 1) + + @testutils.only_for_versions_higher("3.12") + def test_inlining_lazily_evaluated_type_alias_is_refused(self): + code = dedent("""\ + type an_alias = Later + def a_func(param: an_alias): + pass + class Later: pass """) - refactored = self._inline(code, code.index("an_alias") + 1) - self.assertEqual( - dedent("""\ - def a_func(param: int) -> int: - pass - """), - refactored, - ) + with self.assertRaises(rope.base.exceptions.RefactoringError): + self._inline(code, code.index("an_alias") + 1) @testutils.only_for_versions_higher("3.12") def test_inlining_generic_type_alias(self):