diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 994c5c4a66..2cab3808cc 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -310,22 +310,32 @@ def open_with_internal( def get_lines(self, f: TextIO) -> list[tuple[bool, int, list[str]]]: fragments = [] - line_number = 0 if self.ignore_multiline_regex: text = f.read() pos = 0 for m in re.finditer(self.ignore_multiline_regex, text): - lines = text[pos : m.start()].splitlines(True) - fragments.append((False, line_number, lines)) - line_number += len(lines) - lines = m.group().splitlines(True) - fragments.append((True, line_number, lines)) - line_number += len(lines) - 1 + # A fragment can start mid-line, so its line number has to come + # from its offset rather than from counting the lines before it. + fragments.append( + ( + False, + text.count("\n", 0, pos), + text[pos : m.start()].splitlines(True), + ) + ) + fragments.append( + ( + True, + text.count("\n", 0, m.start()), + m.group().splitlines(True), + ) + ) pos = m.end() - lines = text[pos:].splitlines(True) - fragments.append((False, line_number, lines)) + fragments.append( + (False, text.count("\n", 0, pos), text[pos:].splitlines(True)) + ) else: - fragments.append((False, line_number, f.readlines())) + fragments.append((False, 0, f.readlines())) return fragments diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index 930f09f14a..48846187e2 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -1054,6 +1054,46 @@ def test_ignore_regex_option( assert cs.main(fname, r"--ignore-regex=\bdonn\b") == 1 +def test_ignore_multiline_regex_line_numbers( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """Misspellings after an ignored region keep their real line numbers.""" + fname = tmp_path / "ml.txt" + fname.write_text( + "line one is fine\n" + "# codespell:ignore-begin\n" + "abandonned inside the ignored region\n" + "# codespell:ignore-end\n" + "line five is fine\n" + "here is the abandonned we report\n" + ) + + # The example from --help: the match ends on a line boundary. + result = cs.main( + fname, + "--ignore-multiline-regex", + "# codespell:ignore-begin *\n.*# codespell:ignore-end *\n", + std=True, + ) + assert isinstance(result, tuple) + code, stdout, _ = result + assert code == 1 + assert f"{fname}:6: abandonned" in stdout + + # A match that starts and ends mid-line. + result = cs.main( + fname, + "--ignore-multiline-regex", + "codespell:ignore-begin.*codespell:ignore-end", + std=True, + ) + assert isinstance(result, tuple) + code, stdout, _ = result + assert code == 1 + assert f"{fname}:6: abandonned" in stdout + + def test_ignore_multiline_regex_option( tmp_path: Path, capsys: pytest.CaptureFixture[str],