From 7a5bc73faded443d73e90cba3824733e7b4e299f Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Wed, 13 May 2026 14:02:23 -0700 Subject: [PATCH] feat: support codespell:ignore-next-line directive --- README.rst | 16 +++++++++++ codespell_lib/_codespell.py | 31 ++++++++++++++++++++-- codespell_lib/tests/test_basic.py | 44 +++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a9562e8f2b..47c8032900 100644 --- a/README.rst +++ b/README.rst @@ -156,6 +156,22 @@ Words should be separated by a comma. def wrod(wrods) # codespell:ignore pass +3. ignore the following line (useful when a formatter pushes comments to a new line): + + .. code-block:: python + + # codespell:ignore-next-line wrod + def wrod(): + pass + + Use the bare form to ignore every misspelling on the next line: + + .. code-block:: python + + # codespell:ignore-next-line + def wrod(wrods): + pass + Using a config file ------------------- diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index c5b5b4e21b..603f56a507 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -56,8 +56,12 @@ r"(\b(?:https?|[ts]?ftp|file|git|smb)://[^\s]+(?=$|\s)|\b[\w.%+-]+@[\w.-]+\b)" ) codespell_ignore_tag = "codespell:ignore" +codespell_ignore_next_line_tag = "codespell:ignore-next-line" inline_ignore_regex = re.compile( - rf"[^\w\s]\s*{codespell_ignore_tag}\b(\s+(?P[\w,]*))?" + rf"[^\w\s]\s*{codespell_ignore_tag}(?!-)\b(\s+(?P[\w,]*))?" +) +ignore_next_line_regex = re.compile( + rf"[^\w\s]\s*{codespell_ignore_next_line_tag}\b(\s+(?P[\w,]*))?" ) USAGE = """ \t%prog [OPTIONS] [file1 file2 ... fileN] @@ -955,13 +959,28 @@ def parse_lines( _, fragment_line_number, lines = fragment + next_line_ignore_words: Optional[set[str]] = None + for i, line in enumerate(lines): line = line.rstrip() + # Apply any ignore-next-line directive carried from the previous line. + pending_next_line_ignore = next_line_ignore_words + next_line_ignore_words = None + + directive_words: set[str] = set() + if codespell_ignore_next_line_tag in line: + nl_match = ignore_next_line_regex.search(line) + if nl_match: + directive_words = set( + filter(None, (nl_match.group("words") or "").split(",")) + ) + next_line_ignore_words = directive_words + if not line or line in exclude_lines: continue line_number = fragment_line_number + i - extra_words_to_ignore = set() + extra_words_to_ignore: set[str] = set() match = ( inline_ignore_regex.search(line) if codespell_ignore_tag in line else None ) @@ -972,6 +991,14 @@ def parse_lines( if not extra_words_to_ignore: continue + # Words named in an ignore-next-line directive are ignored on its own line too. + extra_words_to_ignore |= directive_words + + if pending_next_line_ignore is not None: + if not pending_next_line_ignore: + continue + extra_words_to_ignore |= pending_next_line_ignore + fixed_words = set() asked_for = set() diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index 5120e1e8a1..fef55d9c03 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -493,6 +493,50 @@ def test_inline_ignores( assert cs.main(d) == expected_error_count +@pytest.mark.parametrize( + ("content", "expected_error_count"), + [ + # wildcard form: ignore all misspellings on the next line + ("# codespell:ignore-next-line\nabandonned abondon abilty\n", 0), + ("// codespell:ignore-next-line\nabandonned abondon abilty\n", 0), + # specific word form: ignore only listed words on the next line + ( + "# codespell:ignore-next-line abondon\nabandonned abondon abilty\n", + 2, + ), + ( + "# codespell:ignore-next-line abondon,abilty\nabandonned abondon abilty\n", + 1, + ), + # the directive does not affect the line it is on or subsequent lines + ( + "abandonned # codespell:ignore-next-line\nabondon\nabilty\n", + 2, + ), + # listing an unused ignore word still triggers a skip + ( + "# codespell:ignore-next-line nomenklatur\nabandonned abondon abilty\n", + 3, + ), + # invalid directives are not honored + ("# codespell:ignore-next-lin\nabandonned\n", 1), + ("codespell:ignore-next-line\nabandonned\n", 1), + # directive followed by a blank line still consumes the directive + ("# codespell:ignore-next-line\n\nabandonned\n", 1), + ], +) +def test_ignore_next_line( + tmpdir: pytest.TempPathFactory, + capsys: pytest.CaptureFixture[str], + content: str, + expected_error_count: int, +) -> None: + d = str(tmpdir) + with open(op.join(d, "bad.txt"), "w", encoding="utf-8") as f: + f.write(content) + assert cs.main(d) == expected_error_count + + def test_custom_regex( tmp_path: Path, capsys: pytest.CaptureFixture[str],