Fix --write-changes silently not fixing a trailing-apostrophe typo#3978
Fix --write-changes silently not fixing a trailing-apostrophe typo#3978chuenchen309 wants to merge 1 commit into
Conversation
The word regex admits a trailing apostrophe, so word can end in one. A \b after that can never hold -- it needs a word/non-word transition and the apostrophe is already non-word -- so re.sub matched nothing while codespell still reported FIXED and exited 0. Use lookarounds instead, and escape the interpolated word. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I always hesitate to modify regexes because I fear it might have an unexpected impact on performance. Unfortunately we don't have tests for that 😟 |
|
Totally fair concern — I measured it so we're not guessing. The key point is where this regex runs: it's on the Timing the two patterns directly (
So it's a fixed ~0.5 µs adder from swapping one No backtracking risk either: the middle is now a If you'd rather touch even less, the minimal variant And since you mentioned there's no perf coverage — I'm glad to add a small |
Problem
codespell -wreportsFIXEDand exits 0 for a misspelling that ends in an apostrophe, but the file is unchanged — the typo survives silently.It's also non-idempotent: running
codespell -wthree times reportsFIXEDall three times and the file never changes. Since the exit code is 0 for both a real fix and a phantom one, nothing downstream can tell them apart — a CI that runscodespell -wand thencodespellstays red with no tool-side remedy.Cause
_codespell.py:1097builds the substitution pattern with\banchors:The word regex (
_codespell.py:51) is[\w\-'’]+, so it admits a trailing apostrophe andwordcan end in one. A trailing\bthen needs a word→non-word transition, but the preceding character is already non-word, so the assertion can never hold:Detection uses the plain word regex and is unaffected, which is why the misspelling is still reported — only the write-back is lost.
This affects 23 keys in the shipped dictionaries (
dont',cant',isnt',wasnt',wouldnt',thats',aircrafts', …).build_dictalso auto-generates a U+2019 variant of each viaalt_chars, and’is non-word too, so the typographic form fails identically.Mid-word apostrophes are fine (
woudn't→wouldn'twrites correctly) because the trailing\bthere follows a word character.Fix
Lookarounds express the intended boundary without depending on the previous character's class. Verified equivalent on the boundary-sensitive cases:
dont'dont' go theredont' go there❌don't go there✅dont’dont’ godont’ go❌don’t go✅tehteh cat tehthe cat thethe cat the✅tehtheteh xteh_teh xtehteh'sthe'sthe's✅The
re.escapeisn't fixing a live bug — no dictionary key currently contains a regex metacharacter (I checked) — but the interpolation was only safe by accident, and escaping is free here.Testing
Added
test_write_changes_trailing_apostrophe, next to the existingtest_apostrophe. The existing test only asserts detection (cs.main(fname) == 1) and useswoudn't— a mid-word apostrophe — so it never exercised the broken path; none of the-wtests use an apostrophe-terminated word either.Verified it fails first (
assert "dont' go\n" == "don't go\n") and passes after.Full
codespell_lib/tests/— 124 passed, 16 skipped. The 3 failures (test_command,test_stdin,test_args_from_file) are identical before and after my change; they'reFileNotFoundErrorfrom my sandbox not having the CLI on PATH, unrelated to this.ruff checkandruff format --checkclean.This change was AI-assisted (Claude). I reproduced the phantom fix and the non-idempotence myself with the real CLI before writing anything, confirmed the regex mechanism directly in a REPL, counted the affected dictionary keys, and ran the tests and baseline comparison above myself.