From ddf3af3a5b45e20c8d18684627d44184b75a5001 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:18:49 +0800 Subject: [PATCH] Keep --ignore-regex from shifting match offsets _ignore_word_sub() replaced each ignored region with a single space, which changes the length of the text. Matches are found in that substituted text but several later checks index back into the original line, so every offset after an ignored region was wrong: - the escape-sequence check reads line[match.start() - 1] - --ignore-sic matches the line from match.end() - the interactive context display slices the line The result was false positives from an --ignore-regex that does not overlap the reported word at all. With `var = 'IGNOREME \nwe are here'`, codespell is quiet by default (the \n escape suppresses "nwe"), but --ignore-regex=IGNOREME made it report `nwe ==> new`. Pad each ignored region to its own length instead. This also matches what the option is documented to do -- ignore by treating the region as whitespace -- and the extra spaces do not affect word splitting, so detection is unchanged. Co-Authored-By: Claude Opus 4.8 --- codespell_lib/_codespell.py | 4 +++- codespell_lib/tests/test_basic.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 994c5c4a66..bd5ca00380 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -896,7 +896,9 @@ def _ignore_word_sub( ignore_word_regex: Optional[Pattern[str]], ) -> str: if ignore_word_regex: - text = ignore_word_regex.sub(" ", text) + # Pad to the match length: callers match against this text but index + # back into the original, so the substitution must not move offsets. + text = ignore_word_regex.sub(lambda m: " " * (m.end() - m.start()), text) return text diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index 930f09f14a..6777aa9631 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -1054,6 +1054,28 @@ def test_ignore_regex_option( assert cs.main(fname, r"--ignore-regex=\bdonn\b") == 1 +def test_ignore_regex_does_not_shift_offsets( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A non-overlapping --ignore-regex must not affect other checks. + + Matches are found in the ignore-substituted text but indexed against the + original line, so the substitution has to preserve offsets. + """ + # The escape-sequence check reads the character before the match. + fname = tmp_path / "esc.txt" + fname.write_text("var = 'IGNOREME \\nwe are here'\n") + assert cs.main(fname) == 0 + assert cs.main(fname, "--ignore-regex=IGNOREME") == 0 + + # The "[sic]" check matches the line from the end of the match. + fname = tmp_path / "sic.txt" + fname.write_text("IGNOREME padding wrod [sic]\n") + assert cs.main(fname, "--ignore-sic") == 0 + assert cs.main(fname, "--ignore-sic", "--ignore-regex=IGNOREME") == 0 + + def test_ignore_multiline_regex_option( tmp_path: Path, capsys: pytest.CaptureFixture[str],