refactor(edit): unify fuzzy uniqueness check with the fuzzy matcher

The uniqueness guard counted occurrences via normalize_for_fuzzy_match,
while fuzzy_find_text located matches with a whitespace-flexible regex,
so the two could disagree. Extract the pattern builder as a single
source of truth (_build_fuzzy_pattern) and add count_matches, which
counts with the same exact-then-fuzzy strategy used to locate and
replace. This is the optional follow-up suggested in the review of #2942.

Adds regression tests for exact and fuzzy multi-match rejection.
This commit is contained in:
weijun-xia
2026-07-08 11:13:30 +08:00
parent ce09efe640
commit ed36ca99c0
4 changed files with 101 additions and 27 deletions

View File

@@ -98,6 +98,41 @@ class TestEditFuzzyPreservesWhitespace(unittest.TestCase):
"def foo():\n x = 100\n y = 2\n return x + y\n",
)
def test_exact_match_rejects_multiple_occurrences(self):
# Two byte-identical statements; the exact-match path applies and the
# uniqueness guard counts exact occurrences, so the ambiguous edit is
# rejected instead of silently editing only the first.
with open(self.path, "w", encoding="utf-8") as f:
f.write("a = 1\nb = 2\na = 1\n")
result = self.tool.execute({
"path": self.path,
"oldText": "a = 1",
"newText": "a = 9",
})
self.assertEqual(result.status, "error", result.result)
self.assertIn("occurrences", result.result)
# An ambiguous match must leave the file untouched.
self.assertEqual(self._read(), "a = 1\nb = 2\na = 1\n")
def test_fuzzy_match_rejects_multiple_occurrences(self):
# oldText uses loose spacing, so the exact match fails and the fuzzy
# path runs. The uniqueness guard now counts with the SAME regex used
# to match/replace, so an ambiguous fuzzy match (two hits) is rejected
# rather than silently editing the first one.
with open(self.path, "w", encoding="utf-8") as f:
f.write("def foo():\n x = 1\n y = 2\n x = 1\n")
result = self.tool.execute({
"path": self.path,
"oldText": "x = 1",
"newText": "x = 99",
})
self.assertEqual(result.status, "error", result.result)
self.assertIn("occurrences", result.result)
self.assertEqual(
self._read(),
"def foo():\n x = 1\n y = 2\n x = 1\n",
)
if __name__ == "__main__":
unittest.main()