diff --git a/download-more-tests.sh b/download-more-tests.sh index c0dc6b3af..d515d07c5 100644 --- a/download-more-tests.sh +++ b/download-more-tests.sh @@ -30,6 +30,9 @@ # https://github.com/sympy/sympy doc # https://github.com/sphinx-doc/sphinx doc --enable line-too-long --max-line-length 85 +# This one could be enabled soon: +## https://github.com/python/python-docs-fr . --enable all --disable line-too-long + grep '^# https://' "$0" | while read -r _ repo directory flags do @@ -56,3 +59,4 @@ grep '^# https://' "$0" | rm -f tests/fixtures/friends/cpython/Doc/README.rst rm -fr tests/fixtures/friends/peps/pep_sphinx_extensions +find tests/fixtures/friends/ '(' -name 'test_*.py' -o -name '*_test.py' ')' -delete diff --git a/pyproject.toml b/pyproject.toml index 87ecb13ec..86dbe868a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ requires-python = ">= 3.7" dependencies = [ "regex", + "polib", ] dynamic = ["version"] diff --git a/sphinxlint.py b/sphinxlint.py index 33e238c89..b4843e6c5 100755 --- a/sphinxlint.py +++ b/sphinxlint.py @@ -10,9 +10,10 @@ """Sphinx rst linter.""" -__version__ = "0.6.6" +__version__ = "0.6.7" import argparse +import io import multiprocessing import os import sys @@ -21,9 +22,9 @@ from itertools import chain, starmap from os.path import exists, isfile, join, splitext +from polib import pofile, POFile import regex as re - # The following chars groups are from docutils: CLOSING_DELIMITERS = "\\\\.,;!?" DELIMITERS = ( @@ -76,34 +77,50 @@ ) # fmt: off -DIRECTIVES = [ +DIRECTIVES_CONTAINING_RST = [ # standard docutils ones 'admonition', 'attention', 'caution', 'class', 'compound', 'container', - 'contents', 'csv-table', 'danger', 'date', 'default-role', 'epigraph', - 'error', 'figure', 'footer', 'header', 'highlights', 'hint', 'image', - 'important', 'include', 'line-block', 'list-table', 'meta', 'note', - 'parsed-literal', 'pull-quote', 'raw', 'replace', - 'restructuredtext-test-directive', 'role', 'rubric', 'sectnum', 'sidebar', - 'table', 'target-notes', 'tip', 'title', 'topic', 'unicode', 'warning', + 'danger', 'epigraph', 'error', 'figure', 'footer', 'header', 'highlights', + 'hint', 'image', 'important', 'include', 'line-block', 'list-table', 'meta', + 'note', 'parsed-literal', 'pull-quote', 'replace', 'sidebar', 'tip', 'topic', + 'warning', # Sphinx and Python docs custom ones 'acks', 'attribute', 'autoattribute', 'autoclass', 'autodata', 'autoexception', 'autofunction', 'automethod', 'automodule', 'availability', 'centered', 'cfunction', 'class', 'classmethod', 'cmacro', - 'cmdoption', 'cmember', 'code-block', 'confval', 'cssclass', 'ctype', + 'cmdoption', 'cmember', 'confval', 'cssclass', 'ctype', 'currentmodule', 'cvar', 'data', 'decorator', 'decoratormethod', 'deprecated-removed', 'deprecated(?!-removed)', 'describe', 'directive', 'doctest', 'envvar', 'event', 'exception', 'function', 'glossary', 'highlight', 'highlightlang', 'impl-detail', 'index', 'literalinclude', 'method', 'miscnews', 'module', 'moduleauthor', 'opcode', 'pdbcommand', - 'productionlist', 'program', 'role', 'sectionauthor', 'seealso', + 'program', 'role', 'sectionauthor', 'seealso', 'sourcecode', 'staticmethod', 'tabularcolumns', 'testcode', 'testoutput', 'testsetup', 'toctree', 'todo', 'todolist', 'versionadded', 'versionchanged', 'c:function', 'coroutinefunction' ] + +DIRECTIVES_CONTAINING_ARBITRARY_CONTENT = [ + # standard docutils ones + 'contents', 'csv-table', 'date', 'default-role', 'include', 'raw', + 'restructuredtext-test-directive','role', 'rubric', 'sectnum', 'table', + 'target-notes', 'title', 'unicode', + # Sphinx and Python docs custom ones + 'productionlist', 'code-block', +] + # fmt: on -ALL_DIRECTIVES = "(" + "|".join(DIRECTIVES) + ")" +DIRECTIVES_CONTAINING_ARBITRARY_CONTENT_RE = ( + "(" + "|".join(DIRECTIVES_CONTAINING_ARBITRARY_CONTENT) + ")" +) +DIRECTIVES_CONTAINING_RST_RE = "(" + "|".join(DIRECTIVES_CONTAINING_RST) + ")" +ALL_DIRECTIVES = ( + "(" + + "|".join(DIRECTIVES_CONTAINING_RST + DIRECTIVES_CONTAINING_ARBITRARY_CONTENT) + + ")" +) BEFORE_ROLE = r"(^|(?<=[\s(/'{\[*-]))" SIMPLENAME = r"(?:(?!_)\w)+(?:[-._+:](?:(?!_)\w)+)*" ROLE_TAG = rf":{SIMPLENAME}:" @@ -141,8 +158,8 @@ # issue:`123` # instead of: # :issue:`123` -role_glued_with_word = re.compile(rf"(^|\s)(?!:){SIMPLENAME}:`(?!`)") +role_glued_with_word = re.compile(rf"(^|\s)(?`(_?)") leaked_markup_re = re.compile(r"[a-z]::\s|`|\.\.\s*\w+:") @@ -192,7 +210,7 @@ def check_python_syntax(file, lines, options=None): role_missing_closing_backtick = re.compile(rf"({ROLE_HEAD}`[^`]+?)[^`]*$") -@checker(".rst") +@checker(".rst", ".po") def check_missing_backtick_after_role(file, lines, options=None): """Search for roles missing their closing backticks. @@ -245,7 +263,7 @@ def clean_paragraph(paragraph): return paragraph.replace("\x00", "\\") -@checker(".rst") +@checker(".rst", ".po") def check_missing_space_after_literal(file, lines, options=None): r"""Search for inline literals immediately followed by a character. @@ -266,7 +284,7 @@ def check_missing_space_after_literal(file, lines, options=None): ) -@checker(".rst") +@checker(".rst", ".po") def check_unbalanced_inline_literals_delimiters(file, lines, options=None): r"""Search for unbalanced inline literals delimiters. @@ -367,6 +385,11 @@ def paragraphs(lines): + ")" ) +ascii_allowed_before_inline_markup = r"""[-:/'"<(\[{]""" +unicode_allowed_before_inline_markup = r"[\p{Ps}\p{Pi}\p{Pf}\p{Pd}\p{Po}]" +ascii_allowed_after_inline_markup = r"""[-.,:;!?/'")\]}>]""" +unicode_allowed_after_inline_markup = r"[\p{Pe}\p{Pi}\p{Pf}\p{Pd}\p{Po}]" + def inline_markup_gen(start_string, end_string, extra_allowed_before=""): """Generate a regex matching an inline markup. @@ -374,10 +397,6 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): inline_markup_gen('**', '**') geneates a regex matching strong emphasis inline markup. """ - ascii_allowed_before = r"""[-:/'"<(\[{]""" - unicode_allowed_before = r"[\p{Ps}\p{Pi}\p{Pf}\p{Pd}\p{Po}]" - ascii_allowed_after = r"""[-.,:;!?/'")\]}>]""" - unicode_allowed_after = r"[\p{Pe}\p{Pi}\p{Pf}\p{Pd}\p{Po}]" if extra_allowed_before: extra_allowed_before = "|" + extra_allowed_before return re.compile( @@ -388,8 +407,8 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): (?<= # Inline markup start-strings must: ^| # start a text block \s| # or be immediately preceded by whitespace, - {ascii_allowed_before}| # one of the ASCII characters - {unicode_allowed_before} # or a similar non-ASCII punctuation character. + {ascii_allowed_before_inline_markup}| # one of the ASCII characters + {unicode_allowed_before_inline_markup} # or a similar non-ASCII punctuation character. {extra_allowed_before} ) @@ -409,8 +428,8 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): $| # end a text block or \s| # be immediately followed by whitespace, \x00| - {ascii_allowed_after}| # one of the ASCII characters - {unicode_allowed_after} # or a similar non-ASCII punctuation character. + {ascii_allowed_after_inline_markup}| # one of the ASCII characters + {unicode_allowed_after_inline_markup} # or a similar non-ASCII punctuation character. ) """, flags=re.VERBOSE | re.DOTALL, @@ -424,14 +443,26 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): anonymous_hyperlink_references_re = inline_markup_gen("`", "`__") inline_literal_re = inline_markup_gen("``", "``") normal_role_re = re.compile( - f":{SIMPLENAME}:{interpreted_text_re.pattern}", flags=re.VERBOSE | re.DOTALL + rf""" + (? 4: + return # we don't handle tables yet. + paragraph = clean_paragraph(paragraph) + match = role_glued_with_word.search(paragraph) + if match: + error_offset = paragraph[: match.start()].count("\n") + if looks_like_glued(match): + yield paragraph_lno + error_offset, f"missing space before role ({match.group(0)})." + else: + yield paragraph_lno + error_offset, f"role missing opening tag colon ({match.group(0)})." -@checker(".rst") +@checker(".rst", ".po") def check_missing_space_before_default_role(file, lines, options=None): """Search for missing spaces before default role. @@ -631,7 +698,7 @@ def check_missing_space_before_default_role(file, lines, options=None): ) -@checker(".rst") +@checker(".rst", ".po") def check_hyperlink_reference_missing_backtick(file, lines, options=None): """Search for missing backticks in front of hyperlink references. @@ -652,7 +719,7 @@ def check_hyperlink_reference_missing_backtick(file, lines, options=None): ) -@checker(".rst") +@checker(".rst", ".po") def check_missing_colon_in_role(file, lines, options=None): """Search for missing colons in roles. @@ -660,11 +727,12 @@ def check_missing_colon_in_role(file, lines, options=None): Good: :issue:`123` """ for lno, line in enumerate(lines, start=1): - if role_missing_right_colon.search(line): - yield lno, "role missing colon before first backtick." + match = role_missing_right_colon.search(line) + if match: + yield lno, f"role missing colon before first backtick ({match.group(0)})." -@checker(".py", ".rst", rst_only=False) +@checker(".py", ".rst", ".po", rst_only=False) def check_carriage_return(file, lines, options=None): r"""Check for carriage returns (\r) in lines.""" for lno, line in enumerate(lines): @@ -672,7 +740,7 @@ def check_carriage_return(file, lines, options=None): yield lno + 1, "\\r in line" -@checker(".py", ".rst", rst_only=False) +@checker(".py", ".rst", ".po", rst_only=False) def check_horizontal_tab(file, lines, options=None): r"""Check for horizontal tabs (\t) in lines.""" for lno, line in enumerate(lines): @@ -680,7 +748,7 @@ def check_horizontal_tab(file, lines, options=None): yield lno + 1, "OMG TABS!!!1" -@checker(".py", ".rst", rst_only=False) +@checker(".py", ".rst", ".po", rst_only=False) def check_trailing_whitespace(file, lines, options=None): """Check for trailing whitespaces at end of lines.""" for lno, line in enumerate(lines): @@ -689,14 +757,14 @@ def check_trailing_whitespace(file, lines, options=None): yield lno + 1, "trailing whitespace" -@checker(".py", ".rst", rst_only=False) +@checker(".py", ".rst", ".po", rst_only=False) def check_missing_final_newline(file, lines, options=None): """Check that the last line of the file ends with a newline.""" if lines and not lines[-1].endswith("\n"): yield len(lines), "No newline at end of file." -@checker(".rst", enabled=False, rst_only=True) +@checker(".rst", ".po", enabled=False, rst_only=True) def check_line_too_long(file, lines, options=None): """Check for line length; this checker is not run by default.""" for lno, line in enumerate(lines): @@ -728,16 +796,18 @@ def check_leaked_markup(file, lines, options=None): def is_multiline_non_rst_block(line): """Returns True if the next lines are an indented literal block.""" - if line.endswith("..\n"): + if re.match(r"^\s*\.\.$", line): # it's the start of a comment block. return True - if line.endswith("::\n"): - return True - if re.match(r"^ *\.\. code-block::", line): + if re.match(rf"^ *\.\. {DIRECTIVES_CONTAINING_RST_RE}::", line): + return False + if re.match(rf"^ *\.\. {DIRECTIVES_CONTAINING_ARBITRARY_CONTENT_RE}::", line): return True if re.match(r"^ *.. productionlist::", line): return True if re.match(r"^ *\.\. ", line) and type_of_explicit_markup(line) == "comment": return True + if line.endswith("::\n"): # It's a literal block + return True return False @@ -798,7 +868,7 @@ def type_of_explicit_markup(line): ) -@checker(".rst", enabled=False) +@checker(".rst", ".po", enabled=False) def check_triple_backticks(file, lines, options=None): """Check for triple backticks, like ```Point``` (but it's a valid syntax). @@ -815,7 +885,7 @@ def check_triple_backticks(file, lines, options=None): yield lno + 1, "There's no rst syntax using triple backticks" -@checker(".rst", rst_only=False) +@checker(".rst", ".po", rst_only=False) def check_bad_dedent(file, lines, options=None): """Check for mis-alignment in indentation in code blocks. @@ -972,6 +1042,19 @@ def check_text(filename, text, checkers, options=None): return errors +def po2rst(text): + """Extract msgstr entries from a po content, keeping linenos.""" + output = [] + po = pofile(text, encoding="UTF-8") + for entry in po.translated_entries(): + # Don't check original msgid, assume it's checked directly. + while len(output) + 1 < entry.linenum: + output.append("\n") + for line in entry.msgstr.splitlines(): + output.append(line + "\n") + return "".join(output) + + def check_file(filename, checkers, options: CheckersOptions = None): ext = splitext(filename)[1] if not any(ext in checker.suffixes for checker in checkers): @@ -979,6 +1062,8 @@ def check_file(filename, checkers, options: CheckersOptions = None): try: with open(filename, encoding="utf-8") as f: text = f.read() + if filename.endswith(".po"): + text = po2rst(text) except OSError as err: print(f"{filename}: cannot open: {err}") return Counter({4: 1}) diff --git a/tests/fixtures/xfail/backslash-space.rst b/tests/fixtures/xfail/backslash-space.rst index b53276df7..b40dbb298 100644 --- a/tests/fixtures/xfail/backslash-space.rst +++ b/tests/fixtures/xfail/backslash-space.rst @@ -1,2 +1,6 @@ +.. expect: inline literal missing (escaped) space after literal: '``Callable``s' (missing-space-after-literal) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + Here ``Callable``s the "s" should be separated from ``Callable`` using a "backslash-space". diff --git a/tests/fixtures/xfail/bad-dedent-in-code-block.rst b/tests/fixtures/xfail/bad-dedent-in-code-block.rst index 6c6fc4b35..b362dd827 100644 --- a/tests/fixtures/xfail/bad-dedent-in-code-block.rst +++ b/tests/fixtures/xfail/bad-dedent-in-code-block.rst @@ -1,3 +1,5 @@ +.. expect: Bad dedent in block (bad-dedent) + Here's a single big code block, but the author intent was to write two:: >>> from enum import Enum diff --git a/tests/fixtures/xfail/bad-directive.rst b/tests/fixtures/xfail/bad-directive.rst index 5c704054a..ab14c701e 100644 --- a/tests/fixtures/xfail/bad-directive.rst +++ b/tests/fixtures/xfail/bad-directive.rst @@ -1,3 +1,5 @@ +.. expect: directive should start with two dots, not three. (directive-with-three-dots) + ... versionchanged:: 3.11 Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* is not a context manager. diff --git a/tests/fixtures/xfail/code-sample-glued-with-plural.rst b/tests/fixtures/xfail/code-sample-glued-with-plural.rst index c289b5d45..c7e5e39b4 100644 --- a/tests/fixtures/xfail/code-sample-glued-with-plural.rst +++ b/tests/fixtures/xfail/code-sample-glued-with-plural.rst @@ -1 +1,5 @@ - all 4 ``'a'``s, but, when the final ``'a'`` is encountered, the +.. expect: inline literal missing (escaped) space after literal: "``'a'``s" (missing-space-after-literal) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + +All 4 ``'a'``s, but, when the final ``'a'`` is encountered, the diff --git a/tests/fixtures/xfail/default-role-between-brackets.rst b/tests/fixtures/xfail/default-role-between-brackets.rst index 963f7e807..c1cebcfcb 100644 --- a/tests/fixtures/xfail/default-role-between-brackets.rst +++ b/tests/fixtures/xfail/default-role-between-brackets.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + Even between brackets, this is a [`default role`] diff --git a/tests/fixtures/xfail/default-role-can-end-with-backslash.rst b/tests/fixtures/xfail/default-role-can-end-with-backslash.rst index 6f5e9f156..522767849 100644 --- a/tests/fixtures/xfail/default-role-can-end-with-backslash.rst +++ b/tests/fixtures/xfail/default-role-can-end-with-backslash.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + This is a legitimate default role: `item`\ s. diff --git a/tests/fixtures/xfail/default-role-double-quoted.rst b/tests/fixtures/xfail/default-role-double-quoted.rst index 17bc10f23..3e3cfa792 100644 --- a/tests/fixtures/xfail/default-role-double-quoted.rst +++ b/tests/fixtures/xfail/default-role-double-quoted.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + The only supported backend is `"perf"`. diff --git a/tests/fixtures/xfail/default-role-glued.rst b/tests/fixtures/xfail/default-role-glued.rst new file mode 100644 index 000000000..c48d70632 --- /dev/null +++ b/tests/fixtures/xfail/default-role-glued.rst @@ -0,0 +1,3 @@ +.. expect: missing space before default role + +Lines containing`'RESTART'` mean that the user execution process has been. diff --git a/tests/fixtures/xfail/default-role-hidden-in-function.rst b/tests/fixtures/xfail/default-role-hidden-in-function.rst index 096cf257b..b536a212f 100644 --- a/tests/fixtures/xfail/default-role-hidden-in-function.rst +++ b/tests/fixtures/xfail/default-role-hidden-in-function.rst @@ -1,3 +1,5 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + .. c:function:: void PyThread_tss_free(Py_tss_t *key) Even if hidden in a function, this default-role should be spotted: diff --git a/tests/fixtures/xfail/default-role-no-alpha.rst b/tests/fixtures/xfail/default-role-no-alpha.rst index aac809690..b13223b69 100644 --- a/tests/fixtures/xfail/default-role-no-alpha.rst +++ b/tests/fixtures/xfail/default-role-no-alpha.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + Assignment expressions using the walrus operator `:=` assign a variable. diff --git a/tests/fixtures/xfail/default-role-parenthesed.rst b/tests/fixtures/xfail/default-role-parenthesed.rst index 02c4a71ea..c7059586e 100644 --- a/tests/fixtures/xfail/default-role-parenthesed.rst +++ b/tests/fixtures/xfail/default-role-parenthesed.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + This is really a default role: (`foo`). diff --git a/tests/fixtures/xfail/default-role-quote.rst b/tests/fixtures/xfail/default-role-quote.rst index 8d37df4f5..55fbb2686 100644 --- a/tests/fixtures/xfail/default-role-quote.rst +++ b/tests/fixtures/xfail/default-role-quote.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + Lines containing `'RESTART'` mean that the user execution process has been. diff --git a/tests/fixtures/xfail/default-role-separated-by-commas.rst b/tests/fixtures/xfail/default-role-separated-by-commas.rst index 6f9042c76..510c79b54 100644 --- a/tests/fixtures/xfail/default-role-separated-by-commas.rst +++ b/tests/fixtures/xfail/default-role-separated-by-commas.rst @@ -1 +1,3 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + This is not detected: `foo`, `bar`. diff --git a/tests/fixtures/xfail/default-role.rst b/tests/fixtures/xfail/default-role.rst index d26bcc2d1..ee8ea3e4d 100644 --- a/tests/fixtures/xfail/default-role.rst +++ b/tests/fixtures/xfail/default-role.rst @@ -1,2 +1,4 @@ +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + Unless it has been set to something explicitly, the default role is `emphasis` and normally remains unused. diff --git a/tests/fixtures/xfail/error-hidden-in-note.rst b/tests/fixtures/xfail/error-hidden-in-note.rst new file mode 100644 index 000000000..344eb4965 --- /dev/null +++ b/tests/fixtures/xfail/error-hidden-in-note.rst @@ -0,0 +1,5 @@ +.. expect: default role used + +.. note:: + This is a note, but it's still parsed as rst, so errors should be spotted. + Like using a `default role`. diff --git a/tests/fixtures/xfail/hyperlink-missing-backtick.rst b/tests/fixtures/xfail/hyperlink-missing-backtick.rst index b6e1f53fe..7a661ad9a 100644 --- a/tests/fixtures/xfail/hyperlink-missing-backtick.rst +++ b/tests/fixtures/xfail/hyperlink-missing-backtick.rst @@ -1,3 +1,5 @@ +.. expect: missing backtick before hyperlink reference: 'Misc/NEWS `_'. (hyperlink-reference-missing-backtick) + In the following line, the hyperlink reference misses its opening backtick Misc/NEWS `_ that's bad. diff --git a/tests/fixtures/xfail/hyperlink-missing-space.rst b/tests/fixtures/xfail/hyperlink-missing-space.rst index 10f531912..a8693bef7 100644 --- a/tests/fixtures/xfail/hyperlink-missing-space.rst +++ b/tests/fixtures/xfail/hyperlink-missing-space.rst @@ -1,3 +1,5 @@ +.. expect: missing space before < in hyperlink (missing-space-in-hyperlink) + A hyperlink needs whitespace before the opening bracket. Wrong: `Link text`_ diff --git a/tests/fixtures/xfail/hyperlink-missing-underscore-and-space.rst b/tests/fixtures/xfail/hyperlink-missing-underscore-and-space.rst new file mode 100644 index 000000000..d47d2aa03 --- /dev/null +++ b/tests/fixtures/xfail/hyperlink-missing-underscore-and-space.rst @@ -0,0 +1,10 @@ +.. expect: missing space before < in hyperlink +.. expect: missing underscore after closing backtick in hyperlink + +.. and yes, it very looks like a default role, so we get this too: +.. expect: default role used + +This combines two mistakes: missing an underscore at the end and +forgetting the space before the opening bracket: + +`Link text` diff --git a/tests/fixtures/xfail/hyperlink-missing-underscore.rst b/tests/fixtures/xfail/hyperlink-missing-underscore.rst index 67de35d16..7294c7601 100644 --- a/tests/fixtures/xfail/hyperlink-missing-underscore.rst +++ b/tests/fixtures/xfail/hyperlink-missing-underscore.rst @@ -1,9 +1,9 @@ +.. expect: missing underscore after closing backtick in hyperlink +And, as it looks a lot like a default role, it also triggers: +.. expect: default role used + + A hyperlink is marked with an underscore after the closing backtick. Wrong (this is taken as a use of the default role): `Link text ` - -This combines this mistake with the mistake of forgetting the space -before the opening bracket: - -`Link text` diff --git a/tests/fixtures/xfail/hyperlinks-several.rst b/tests/fixtures/xfail/hyperlinks-several.rst index 00bb297b0..52ce21615 100644 --- a/tests/fixtures/xfail/hyperlinks-several.rst +++ b/tests/fixtures/xfail/hyperlinks-several.rst @@ -1,3 +1,7 @@ +.. expect: missing underscore after closing backtick in hyperlink (missing-underscore-after-hyperlink) +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) +.. expect: missing space before < in hyperlink (missing-space-in-hyperlink) + Several malformed hyperlinks on the same line yield several errors. `Link text`_ `Link text 2 ` diff --git a/tests/fixtures/xfail/missing-backtick-on-role.rst b/tests/fixtures/xfail/missing-backtick-on-role.rst index 708d844ad..340559ead 100644 --- a/tests/fixtures/xfail/missing-backtick-on-role.rst +++ b/tests/fixtures/xfail/missing-backtick-on-role.rst @@ -1,3 +1,5 @@ +.. expect: role missing closing backtick: ':class:`foo but no other issues,\nand there is other lines too.\n' (missing-backtick-after-role) + In this paragraph there is a missing backtick at the end of a role, :class:`foo but no other issues, diff --git a/tests/fixtures/xfail/missing-column-1.rst b/tests/fixtures/xfail/missing-colon-1.rst similarity index 56% rename from tests/fixtures/xfail/missing-column-1.rst rename to tests/fixtures/xfail/missing-colon-1.rst index a6f06470d..2a72d7361 100644 --- a/tests/fixtures/xfail/missing-column-1.rst +++ b/tests/fixtures/xfail/missing-colon-1.rst @@ -1 +1,3 @@ +.. expect: role missing opening tag colon + The same format as the c:macro:`PY_VERSION_HEX` macro. diff --git a/tests/fixtures/xfail/missing-column-2.rst b/tests/fixtures/xfail/missing-colon-2.rst similarity index 50% rename from tests/fixtures/xfail/missing-column-2.rst rename to tests/fixtures/xfail/missing-colon-2.rst index 052c448b0..7c964b0bf 100644 --- a/tests/fixtures/xfail/missing-column-2.rst +++ b/tests/fixtures/xfail/missing-colon-2.rst @@ -1,2 +1,7 @@ +.. expect: role missing opening tag colon + +.. and yes, it looks like a default role, so: +.. expect: default role used + New Linux constants ``TCP_USER_TIMEOUT`` and ``TCP_CONGESTION`` were added. (Contributed by Omar Sandoval, issue:`26273`). diff --git a/tests/fixtures/xfail/missing-colon.rst b/tests/fixtures/xfail/missing-colon.rst new file mode 100644 index 000000000..0080cd6c9 --- /dev/null +++ b/tests/fixtures/xfail/missing-colon.rst @@ -0,0 +1,5 @@ +.. expect: role missing opening tag colon +.. expect: default role used (hint: for inline literals, use double backticks) (default-role) + +This attribute is to match :attr:`importlib.machinery.ModuleSpec.loader` +as stored in the attr:`__spec__` object. diff --git a/tests/fixtures/xfail/missing-column.rst b/tests/fixtures/xfail/missing-column.rst deleted file mode 100644 index 8521ebb5d..000000000 --- a/tests/fixtures/xfail/missing-column.rst +++ /dev/null @@ -1,2 +0,0 @@ -This attribute is to match :attr:`importlib.machinery.ModuleSpec.loader` -as stored in the attr:`__spec__` object. diff --git a/tests/fixtures/xfail/missing-newline-at-end-of-file.rst b/tests/fixtures/xfail/missing-newline-at-end-of-file.rst new file mode 100644 index 000000000..abb24a69f --- /dev/null +++ b/tests/fixtures/xfail/missing-newline-at-end-of-file.rst @@ -0,0 +1,3 @@ +.. expect: No newline at end of file. + + Hell o \ No newline at end of file diff --git a/tests/fixtures/xfail/missing-right-role-column.rst b/tests/fixtures/xfail/missing-right-role-colon.rst similarity index 51% rename from tests/fixtures/xfail/missing-right-role-column.rst rename to tests/fixtures/xfail/missing-right-role-colon.rst index a78540acb..28ac80f66 100644 --- a/tests/fixtures/xfail/missing-right-role-column.rst +++ b/tests/fixtures/xfail/missing-right-role-colon.rst @@ -1,3 +1,10 @@ +.. expect: role missing colon before first backtick + +.. It also looks like: +.. expect: missing space before default role +.. even it's not. + + Default: value of the ``PLATLIBDIR`` macro which is set by the :option`configure --with-platlibdir option <--with-platlibdir>` (default: ``"lib"``). diff --git a/tests/fixtures/xfail/missing-space-1.rst b/tests/fixtures/xfail/missing-space-1.rst index e9185f502..6cc47509e 100644 --- a/tests/fixtures/xfail/missing-space-1.rst +++ b/tests/fixtures/xfail/missing-space-1.rst @@ -1 +1,3 @@ +.. expect: missing space before role + by the:c:func:`PyThreadState_EnterTracing` function. diff --git a/tests/fixtures/xfail/missing-space-2.rst b/tests/fixtures/xfail/missing-space-2.rst index d3b10121a..c0c80ddbd 100644 --- a/tests/fixtures/xfail/missing-space-2.rst +++ b/tests/fixtures/xfail/missing-space-2.rst @@ -1 +1,3 @@ +.. expect: missing space before role + Resume them using the:c:func:`PyThreadState_LeaveTracing` function. diff --git a/tests/fixtures/xfail/missing-space-around-inline-literals.rst b/tests/fixtures/xfail/missing-space-around-inline-literals.rst index d05e19f39..7da268963 100644 --- a/tests/fixtures/xfail/missing-space-around-inline-literals.rst +++ b/tests/fixtures/xfail/missing-space-around-inline-literals.rst @@ -1,3 +1,10 @@ +.. expect: missing space before default role: 'ace``here``'. (missing-space-before-default-role) +.. expect: inline literal missing (escaped) space after literal: '``space``h' (missing-space-after-literal) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + A missing space``here``. Another missing ``space``here. diff --git a/tests/fixtures/xfail/missing-space-before-default-role.rst b/tests/fixtures/xfail/missing-space-before-default-role.rst index e7bbf6da6..13b7b6070 100644 --- a/tests/fixtures/xfail/missing-space-before-default-role.rst +++ b/tests/fixtures/xfail/missing-space-before-default-role.rst @@ -1,3 +1,5 @@ +.. expect: missing space before default role: "ing`'RESTART'`". (missing-space-before-default-role) + Lines containing`'RESTART'` mean that the user execution process has been re-started. This occurs when the user execution process has crashed, when one requests a restart on the Shell menu, or when one runs code diff --git a/tests/fixtures/xfail/missing-space.rst b/tests/fixtures/xfail/missing-space.rst index d3b10121a..c0c80ddbd 100644 --- a/tests/fixtures/xfail/missing-space.rst +++ b/tests/fixtures/xfail/missing-space.rst @@ -1 +1,3 @@ +.. expect: missing space before role + Resume them using the:c:func:`PyThreadState_LeaveTracing` function. diff --git a/tests/fixtures/xfail/missing-surrogate-space.rst b/tests/fixtures/xfail/missing-surrogate-space.rst index f5db640a0..c6eb3a8c4 100644 --- a/tests/fixtures/xfail/missing-surrogate-space.rst +++ b/tests/fixtures/xfail/missing-surrogate-space.rst @@ -1,2 +1,4 @@ +.. expect: role missing (escaped) space after role: ':exc:`ExceptionGroup`s' (missing-space-after-role) + Here the ``s`` should not be prefixed by backslask space: :exc:`ExceptionGroup`s. diff --git a/tests/fixtures/xfail/role-inside-literal-role-missing-surrogate-escape.rst b/tests/fixtures/xfail/role-inside-literal-role-missing-surrogate-escape.rst index be06a74db..3a31898c0 100644 --- a/tests/fixtures/xfail/role-inside-literal-role-missing-surrogate-escape.rst +++ b/tests/fixtures/xfail/role-inside-literal-role-missing-surrogate-escape.rst @@ -1 +1,4 @@ +.. expect: role missing (escaped) space after role: ':literal:`:manpage:`man(1)``s' (missing-space-after-role) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + The :literal:`:manpage:`man(1)``s roles... diff --git a/tests/fixtures/xfail/role-missing-colon.rst b/tests/fixtures/xfail/role-missing-colon.rst new file mode 100644 index 000000000..478151086 --- /dev/null +++ b/tests/fixtures/xfail/role-missing-colon.rst @@ -0,0 +1,3 @@ +.. expect: role missing opening tag colon + +The c:macro:`PY_VERSION_HEX` miss a colon. diff --git a/tests/fixtures/xfail/role-with-double-backticks.rst b/tests/fixtures/xfail/role-with-double-backticks.rst index d75be3650..e6d5a04f8 100644 --- a/tests/fixtures/xfail/role-with-double-backticks.rst +++ b/tests/fixtures/xfail/role-with-double-backticks.rst @@ -1 +1,3 @@ +.. expect: role use a single backtick, double backtick found. (role-with-double-backticks) + :const:``None`` should be written :const:`None`. diff --git a/tests/fixtures/xfail/role-without-backticks.rst b/tests/fixtures/xfail/role-without-backticks.rst index 9741afd3b..c4c15d9ce 100644 --- a/tests/fixtures/xfail/role-without-backticks.rst +++ b/tests/fixtures/xfail/role-without-backticks.rst @@ -1,3 +1,5 @@ +.. expect: role with no backticks: ':func:pdb.main\n' (role-without-backticks) + This role has no backticks at all: :func:pdb.main diff --git a/tests/fixtures/xfail/space-inside-inline-literal.rst b/tests/fixtures/xfail/space-inside-inline-literal.rst index 71dd24ca6..a7904ff24 100644 --- a/tests/fixtures/xfail/space-inside-inline-literal.rst +++ b/tests/fixtures/xfail/space-inside-inline-literal.rst @@ -1 +1,4 @@ +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + That space make the intended inline literal not being one: `` __repr__``. diff --git a/tests/fixtures/xfail/superfluous-backtick-in-front-of-role.rst b/tests/fixtures/xfail/superfluous-backtick-in-front-of-role.rst index 773ea747e..5e9a634f5 100644 --- a/tests/fixtures/xfail/superfluous-backtick-in-front-of-role.rst +++ b/tests/fixtures/xfail/superfluous-backtick-in-front-of-role.rst @@ -1,3 +1,11 @@ +.. expect: 15: superfluous backtick in front of role (backtick-before-role) +.. expect: 23: superfluous backtick in front of role (backtick-before-role) + +.. and as erroneous roles may greatly looks like default roles, sphinx-lint sees: +.. expect: default role used +.. expect: default role used + + Right: :c:func:`PyFrame_GetLocals` instead. diff --git a/tests/fixtures/xfail/trailing-double-backticks.rst b/tests/fixtures/xfail/trailing-double-backticks.rst index 4ef1b88d6..07b68981c 100644 --- a/tests/fixtures/xfail/trailing-double-backticks.rst +++ b/tests/fixtures/xfail/trailing-double-backticks.rst @@ -1,2 +1,4 @@ +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + The ``double backticks`` at the end of this line `` should clearly ``not be there``. diff --git a/tests/fixtures/xfail/trailing-whitespaces.rst b/tests/fixtures/xfail/trailing-whitespaces.rst index 8a13e2bf8..e373c0b75 100644 --- a/tests/fixtures/xfail/trailing-whitespaces.rst +++ b/tests/fixtures/xfail/trailing-whitespaces.rst @@ -1 +1,3 @@ +.. expect: trailing whitespace (trailing-whitespace) + This line should have a trailing whitespace and a newline: diff --git a/tests/fixtures/xfail/triple-dot-directive.rst b/tests/fixtures/xfail/triple-dot-directive.rst index daf3ee69b..d32dbd48b 100644 --- a/tests/fixtures/xfail/triple-dot-directive.rst +++ b/tests/fixtures/xfail/triple-dot-directive.rst @@ -1,2 +1,4 @@ +.. expect: directive should start with two dots, not three. (directive-with-three-dots) + ... versionchanged:: 3.11 This directive has three dots instead of two. diff --git a/tests/fixtures/xfail/zero-width-no-break-space-before-inline-literal.rst b/tests/fixtures/xfail/zero-width-no-break-space-before-inline-literal.rst index c45c077e1..5f1168518 100644 --- a/tests/fixtures/xfail/zero-width-no-break-space-before-inline-literal.rst +++ b/tests/fixtures/xfail/zero-width-no-break-space-before-inline-literal.rst @@ -1,6 +1,11 @@ +.. expect: missing space before default role: 'e \ufeff``i would like to be an inline literal``'. (missing-space-before-default-role) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) +.. expect: found an unbalanced inline literal markup. (unbalanced-inline-literals-delimiters) + See https://github.com/spyder-ide/spyder-docs/pull/332 for context. -Some words then a ZWNBSP here ``i would like to be an inline literal`` (but it is not). +Some words then a ZWNBSP here ``i would like to be an inline literal`` +(but it is not). This inline literal will **not** be parsed by docutils because of the ZERO WIDTH NO-BREAK SPACE that you may don't see, place right before diff --git a/tests/fixtures/xpass/multiple-inline-literals.rst b/tests/fixtures/xpass/multiple-inline-literals.rst new file mode 100644 index 000000000..14cad099e --- /dev/null +++ b/tests/fixtures/xpass/multiple-inline-literals.rst @@ -0,0 +1,2 @@ +``_PyUnicode_Name_CAPI`` de l'API PyCapsule ``unicodedata.ucnhash_CAPI`` +a été déplacée dans l'API C interne. Contribution par Victor Stinner. diff --git a/tests/fixtures/xpass/roles-may-not-be-hardcoded.rst b/tests/fixtures/xpass/roles-may-not-be-hardcoded.rst new file mode 100644 index 000000000..832789911 --- /dev/null +++ b/tests/fixtures/xpass/roles-may-not-be-hardcoded.rst @@ -0,0 +1 @@ +such as :std:doc:`PyPA build ` diff --git a/tests/fixtures/xpass/should-not-trigger-trailing-whistespace.rst b/tests/fixtures/xpass/should-not-trigger-trailing-whistespace.rst new file mode 100644 index 000000000..902d4d61f --- /dev/null +++ b/tests/fixtures/xpass/should-not-trigger-trailing-whistespace.rst @@ -0,0 +1 @@ +Hell o diff --git a/tests/test_filter_out_literal.py b/tests/test_filter_out_literal.py index 7d5b75ec3..22fcdaeaf 100644 --- a/tests/test_filter_out_literal.py +++ b/tests/test_filter_out_literal.py @@ -294,3 +294,80 @@ def test_consecutive_production_list(): for line in hide_non_rst_blocks(CONSECUTIVE_PRODUCTION_LIST.splitlines(True)): out.append(line) assert "".join(out) == CONSECUTIVE_PRODUCTION_LIST_EXPECTED + + +ATTENTION = """ +This is a test for an attention admonition. + +.. attention:: + An admonition can contain RST so it should **NOT** be dropped. + +and that's it. +""" + + +def test_filter_out_attention(): + out = [] + excluded = [] + for line in hide_non_rst_blocks( + ATTENTION.splitlines(True), + hidden_block_cb=lambda lineno, block: excluded.append((lineno, block)), + ): + out.append(line) + assert "".join(out) == ATTENTION + assert not excluded + + +NOTE = """ +This is a note, it contains rst, so it should **not** be dropped: + +.. note:: + + hello I am a not **I can** contain rst. + +End of it. +""" + + +def test_filter_out_note(): + out = [] + excluded = [] + for line in hide_non_rst_blocks( + NOTE.splitlines(True), + hidden_block_cb=lambda lineno, block: excluded.append((lineno, block)), + ): + out.append(line) + assert "".join(out) == NOTE + assert not excluded + + +UNKNOWN = """ +This is an unknown directive, to avoid false positives, just drop its content. + +.. this_is_not_a_known_directive:: + + So this can contain rst, or arbitary text. + +In the face of ambiguity, refuse the temptation to guess. +""" + +UNKNOWN_EXPECTED = """ +This is an unknown directive, to avoid false positives, just drop its content. + + + + + +In the face of ambiguity, refuse the temptation to guess. +""" + + +def test_filter_out_unknown(): + out = [] + excluded = [] + for line in hide_non_rst_blocks( + UNKNOWN.splitlines(True), + hidden_block_cb=lambda lineno, block: excluded.append((lineno, block)), + ): + out.append(line) + assert "".join(out) == UNKNOWN_EXPECTED diff --git a/tests/test_nonregression.py b/tests/test_nonregression.py deleted file mode 100644 index 58f292c18..000000000 --- a/tests/test_nonregression.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest - -from sphinxlint import check_text, checkers - - -@pytest.fixture -def check_str(capsys): - def _check_str(rst): - error_count = check_text("test.rst", rst, checkers.values()) - out, err = capsys.readouterr() - assert not err - return error_count, out - - yield _check_str - - -def test_role_missing_colon(check_str): - """sphinx-lint should find missing leading colon in roles. - - It's at the end the same as role glued with word. - """ - error_count, out = check_str("The c:macro:`PY_VERSION_HEX` miss a colon.\n") - assert "role" in out - assert error_count - - -def test_last_line(check_str): - """Check regression of last line ending with space, a char, and no newline. - - It wrongly raised a "trailing whitespace". - """ - _, out = check_str("Hell o") - assert "trailing whitespace" not in out - - -def test_last_line_has_no_newline(check_str): - error_count, out = check_str("Hello\nworld") - assert "No newline at end of file" in out - assert error_count - - -def test_roles_may_not_be_hardcoded(check_str): - error_count, out = check_str("such as :std:doc:`PyPA build `\n") - assert not out - assert not error_count diff --git a/tests/test_po2rst.py b/tests/test_po2rst.py new file mode 100644 index 000000000..1f2bf4c91 --- /dev/null +++ b/tests/test_po2rst.py @@ -0,0 +1,39 @@ +from sphinxlint import po2rst + + +def test_po2rst(): + po = """msgid "foo" +msgstr "bar" + +msgid "test1" +msgstr "test2" +""" + rst = """bar + + +test2 +""" + assert po2rst(po) == rst + + +def test_po2rst_more(): + po = """msgid "foo" +msgstr "bar" + +msgid "test1" +msgstr "" +"test2" + +msgid "test3" +msgstr "test4" +""" + rst = """bar + + +test2 + + + +test4 +""" + assert po2rst(po) == rst diff --git a/tests/test_sphinxlint.py b/tests/test_sphinxlint.py index c5d82dc91..b51dda738 100644 --- a/tests/test_sphinxlint.py +++ b/tests/test_sphinxlint.py @@ -11,36 +11,60 @@ @pytest.mark.parametrize("file", [str(f) for f in (FIXTURE_DIR / "xpass").iterdir()]) def test_sphinxlint_shall_pass(file, capsys): - error_count = main(["sphinxlint.py", "--enable", "all", str(file)]) + has_errors = main(["sphinxlint.py", "--enable", "all", str(file)]) out, err = capsys.readouterr() assert err == "" assert out == "No problems found.\n" - assert error_count == 0 + assert not has_errors @pytest.mark.parametrize( "file", [str(f) for f in (FIXTURE_DIR / "triggers-false-positive").iterdir()] ) def test_sphinxlint_shall_trigger_false_positive(file, capsys): - error_count = main(["sphinxlint.py", str(file)]) + has_errors = main(["sphinxlint.py", str(file)]) out, err = capsys.readouterr() assert out == "No problems found.\n" assert err == "" - assert error_count == 0 - error_count = main(["sphinxlint.py", "--enable", "all", str(file)]) + assert not has_errors + has_errors = main(["sphinxlint.py", "--enable", "all", str(file)]) out, err = capsys.readouterr() assert err == "" assert out != "No problems found.\n" - assert error_count > 0 + assert has_errors -@pytest.mark.parametrize("file", [str(f) for f in (FIXTURE_DIR / "xfail").iterdir()]) -def test_sphinxlint_shall_not_pass(file, capsys): - error_count = main(["sphinxlint.py", "--enable", "all", str(file)]) +def gather_xfail(): + """Find all rst files in the fixtures/xfail directory. + + Each file is searched for lines containing expcted errors, they + are starting with `.. expect: `. + """ + marker = ".. expect: " + for file in (FIXTURE_DIR / "xfail").iterdir(): + expected_errors = [] + for line in Path(file).read_text(encoding="UTF-8").splitlines(): + if line.startswith(marker): + expected_errors.append(line[len(marker) :]) + yield str(file), expected_errors + + +@pytest.mark.parametrize("file,expected_errors", gather_xfail()) +def test_sphinxlint_shall_not_pass(file, expected_errors, capsys): + has_errors = main(["sphinxlint.py", "--enable", "all", file]) out, err = capsys.readouterr() assert out != "No problems found.\n" assert err == "" - assert error_count > 0 + assert has_errors + assert expected_errors, ( + "That's not OK not to tell which errors are expected, " + """add one using a ".. expect: " line.""" + ) + for expected_error in expected_errors: + assert expected_error in out + number_of_expected_errors = len(expected_errors) + number_of_reported_errors = len(out.splitlines()) + assert number_of_expected_errors == number_of_reported_errors @pytest.mark.parametrize("file", [str(FIXTURE_DIR / "paragraphs.rst")]) @@ -58,10 +82,10 @@ def test_paragraphs(file): @pytest.mark.parametrize("file", [str(FIXTURE_DIR / "paragraphs.rst")]) def test_line_no_in_error_msg(file, capsys): - error_count = main(["sphinxlint.py", file]) + has_errors = main(["sphinxlint.py", file]) out, err = capsys.readouterr() assert err == "" assert "paragraphs.rst:76: role missing colon before" in out assert "paragraphs.rst:70: role use a single backtick" in out assert "paragraphs.rst:65: inline literal missing (escaped) space" in out - assert error_count > 0 + assert has_errors diff --git a/tests/test_friends.py b/tests/test_xpass_friends.py similarity index 86% rename from tests/test_friends.py rename to tests/test_xpass_friends.py index 9fa99127d..80a825912 100644 --- a/tests/test_friends.py +++ b/tests/test_xpass_friends.py @@ -22,8 +22,8 @@ ) def test_sphinxlint_friend_projects_shall_pass(file, capsys): flags = (Path(file) / "flags").read_text(encoding="UTF-8") - error_count = main(["sphinxlint.py", file] + shlex.split(flags)) + has_errors = main(["sphinxlint.py", file] + shlex.split(flags)) out, err = capsys.readouterr() assert err == "" assert out == "No problems found.\n" - assert error_count == 0 + assert not has_errors