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],