From 517c87108d590583ac61511dcddfed7000c881ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 9 Nov 2026 11:28:54 +0100 Subject: [PATCH] refactor: report similar extraction matches Expose get_similar_occurrences() so clients can decide whether to offer similar-code extraction without constructing the resulting Change. Reuse the existing validation and match collection, and document the count for API callers. Fixes: #168 --- docs/library.rst | 6 ++++++ rope/refactor/extract.py | 33 +++++++++++++++++++++++------- ropetest/refactor/extracttest.py | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 35d497337..e613070e4 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -142,6 +142,12 @@ the changes it made. Calculating those changes can be time consuming. See the `rope.base.taskhandle.TaskHandle`_ section for measuring its progress or interrupting it. +To offer a similar-code extraction only when it finds other matches, call +``extractor.get_similar_occurrences()`` before asking the user to confirm +that option. It returns the number of regions similar extraction would +process, without creating a ``Change``. In the usual case, a result greater +than one means that similar extraction would affect additional regions. + Previewing And Performing Changes --------------------------------- diff --git a/rope/refactor/extract.py b/rope/refactor/extract.py index ea373613d..5d581c10f 100644 --- a/rope/refactor/extract.py +++ b/rope/refactor/extract.py @@ -68,21 +68,36 @@ def get_changes(self, extracted_name, similar=False, global_=False, kind=None): """ extracted_name, kind = self._get_kind_from_name(extracted_name, kind) + info = self._create_info(extracted_name, similar, global_, kind) + new_contents = _ExtractPerformer(info).extract() + changes = ChangeSet(f"Extract {info.kind} <{extracted_name}>") + changes.add_change(ChangeContents(self.resource, new_contents)) + return changes + + def get_similar_occurrences(self, global_=False, kind=None): + """Return the number of regions similar extraction would affect. + + This checks the same extraction constraints and match rules as + :meth:`get_changes` without creating changes. ``global_`` and ``kind`` + have the same meaning as in :meth:`get_changes`. + """ + info = self._create_info("", similar=True, global_=global_, kind=kind) + return len(_ExtractPerformer(info)._collect_matches().matches) + + def _create_info(self, extracted_name, similar, global_, kind): + kind = self._get_kind(kind) info = _ExtractInfo( self.project, self.resource, self.start_offset, self.end_offset, extracted_name, - variable=self._get_kind(kind) == "variable", + variable=kind == "variable", similar=similar, make_global=global_, ) - info.kind = self._get_kind(kind) - new_contents = _ExtractPerformer(info).extract() - changes = ChangeSet(f"Extract {info.kind} <{extracted_name}>") - changes.add_change(ChangeContents(self.resource, new_contents)) - return changes + info.kind = kind + return info def _get_kind_from_name(self, extracted_name, kind): for sign, selected_kind in self.kind_prefixes.items(): @@ -308,10 +323,14 @@ def _replace_occurrences(self, content, extract_info): region = match.get_region() content.add_change(region[0], region[1], replacement.substitute(mapping)) - def _collect_info(self): + def _collect_matches(self): extract_collector = _ExtractCollector(self.info) self._find_definition(extract_collector) self._find_matches(extract_collector) + return extract_collector + + def _collect_info(self): + extract_collector = self._collect_matches() self._find_definition_location(extract_collector) return extract_collector diff --git a/ropetest/refactor/extracttest.py b/ropetest/refactor/extracttest.py index 53a225d6e..bbd32894d 100644 --- a/ropetest/refactor/extracttest.py +++ b/ropetest/refactor/extracttest.py @@ -1439,6 +1439,41 @@ def test_definition_should_appear_before_the_first_use(self): """) self.assertEqual(expected, refactored) + def test_get_similar_occurrences_for_variable(self): + code = dedent("""\ + a = 1 + b = 1 + """) + testmod = testutils.create_module(self.project, "testmod") + testmod.write(code) + start = code.index("1") + extractor = extract.ExtractVariable(self.project, testmod, start, start + 1) + + self.assertEqual(2, extractor.get_similar_occurrences()) + + def test_get_similar_occurrences_for_method(self): + code = dedent("""\ + a = 1 + b = 1 + """) + testmod = testutils.create_module(self.project, "testmod") + testmod.write(code) + start = code.index("1") + extractor = extract.ExtractMethod(self.project, testmod, start, start + 1) + + self.assertEqual(2, extractor.get_similar_occurrences()) + + def test_get_similar_occurrences_with_no_other_matches(self): + code = dedent("""\ + a = 1 + """) + testmod = testutils.create_module(self.project, "testmod") + testmod.write(code) + start = code.index("1") + extractor = extract.ExtractMethod(self.project, testmod, start, start + 1) + + self.assertEqual(1, extractor.get_similar_occurrences()) + def test_extract_method_and_similar_expressions(self): code = dedent("""\ a = 1