diff --git a/sphinx-lint.1 b/sphinx-lint.1 new file mode 100644 index 000000000..fa411cf2a --- /dev/null +++ b/sphinx-lint.1 @@ -0,0 +1,74 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" (C) Copyright 2025 Julien Palard +.\" +.TH sphinx-lint 1 "November 16 2025" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.SH NAME +sphinx-lint \- proofreads \fB.rst\fP files +.SH SYNOPSIS +.B sphinx-lint +[-h] [-v] [-i IGNORE] [-d DISABLE] [-e ENABLE] [--list] +[--max-line-length MAX_LINE_LENGTH] [-s SORT_BY] [-j N] [-V] [paths ...] +.br +.SH DESCRIPTION +\fBsphinx-lint\fP searches for stylistic and formal issues in \fB.rst\fP +and \fB.py\fP files. +.PP +If no paths are given, \fBsphinx-lint\fP searches files starting from the +current working directory. +.SH OPTIONS +These programs follow the usual GNU command line syntax, with long +options starting with two dashes ('\-'). +A summary of options is included below. +.TP +.B \-h, \-\-help +Show a summary of options. +.TP +.B \-v, \-\-verbose +Print all checked file names and additional information. +.TP +.B -i, --ignore IGNORE +Ignore the specified subdirectory or file path. +.TP +.B -d, --disable DISABLE +Comma-separated list of checks to disable. Use \fIall\fP to disable all checks. +Can be used in conjunction with \fB--enable\fP (evaluated left-to-right). +For example, \fB--disable all --enable trailing-whitespace\fP enables a +single check while disabling all others. +.TP +.B -e, --enable ENABLE +Comma-separated list of checks to enable. Use \fIall\fP to enable all checks. +Can be used in conjunction with \fB--disable\fP (evaluated left-to-right). +For example, \fB--enable all --disable trailing-whitespace\fP enables all +checks except the specified one. +.TP +.B --list +List enabled checkers and exit. Useful to see which checkers would be +used with a given set of \fB--enable\fP and \fB--disable\fP options. +.TP +.B --max-line-length MAX_LINE_LENGTH +Maximum number of characters allowed on a single line. +.TP +.B -s, --sort-by SORT_BY +Comma-separated list of fields used to sort errors. Available fields: +\fIfilename\fP, \fIline\fP, \fIerror_type\fP. +.TP +.B -j, --jobs N +Run in parallel with \fBN\fP processes. Defaults to \fIauto\fP, which sets +\fBN\fP to the number of logical CPUs. Values less than one are treated as \fB1\fP. +.TP +.B -V, --version +Show the program's version number and exit. +.SH SEE ALSO +.BR sphinx-build (1) diff --git a/sphinxlint/checkers.py b/sphinxlint/checkers.py index 9b5879247..ec66adcac 100644 --- a/sphinxlint/checkers.py +++ b/sphinxlint/checkers.py @@ -246,17 +246,22 @@ def check_missing_space_in_hyperlink(file, lines, options=None): if "`" not in line: continue for match in rst.SEEMS_HYPERLINK_RE.finditer(line): - if not match.group(1): + if not match.group(2): yield lno, "missing space before < in hyperlink" @checker(".rst", ".po") def check_missing_underscore_after_hyperlink(file, lines, options=None): - """Search for hyperlinks missing underscore after their closing backtick. + """Search for hyperlinks with incorrect underscore usage after closing backtick. + For regular hyperlinks: Bad: `Link text ` Good: `Link text `_ + For hyperlinks within download directives: + Bad: :download:`file `_ + Good: :download:`file ` + Note: URLs within download directives don't need trailing underscores. https://www.sphinx-doc.org/en/master/usage/referencing.html#role-download @@ -265,19 +270,15 @@ def check_missing_underscore_after_hyperlink(file, lines, options=None): if "`" not in line: continue for match in rst.SEEMS_HYPERLINK_RE.finditer(line): - if not match.group(2): - # Check if this is within any download directive on the line - # Include optional underscore in pattern to handle both cases - is_in_download = False - for download_match in rst.HYPERLINK_WITHIN_DOWNLOAD_RE.finditer(line): - if ( - match.start() >= download_match.start() - and match.end() <= download_match.end() - ): - is_in_download = True - break - if not is_in_download: - yield lno, "missing underscore after closing backtick in hyperlink" + is_in_download = bool(match.group(1)) + has_underscore = bool(match.group(3)) + + if is_in_download and has_underscore: + yield lno, "unnecessary underscore after closing backtick in hyperlink" + elif not is_in_download and not has_underscore: + yield lno, "missing underscore after closing backtick in hyperlink" + else: + continue @checker(".rst", ".po") @@ -555,3 +556,20 @@ def check_unnecessary_parentheses(filename, lines, options): for lno, line in enumerate(lines, start=1): for match in rst.ROLE_WITH_UNNECESSARY_PARENTHESES_RE.finditer(line): yield lno, f"Unnecessary parentheses in {match.group(0).strip()!r}" + + +@checker(".rst", ".po") +def check_exclamation_and_tilde(file, lines, options): + """Check for roles that start with an exclamation mark and tilde (`!~`). + + Bad: :meth:`!~list.pop` + Good: :meth:`!pop` + """ + for lno, line in enumerate(lines, start=1): + if not ("~" in line and "!" in line and "`" in line): + continue + for match in rst.ROLE_WITH_EXCLAMATION_AND_TILDE_RE.finditer(line): + yield ( + lno, + f"Found a role with both `!` and `~` in {match.group(0).strip()!r}.", + ) diff --git a/sphinxlint/rst.py b/sphinxlint/rst.py index eac031c4e..5ff2e0fe4 100644 --- a/sphinxlint/rst.py +++ b/sphinxlint/rst.py @@ -280,9 +280,7 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): # The :issue`123` is ... ROLE_MISSING_RIGHT_COLON_RE = re.compile(rf"(^|\s):{SIMPLENAME}`(?!`)") -SEEMS_HYPERLINK_RE = re.compile(r"`[^`]+?(\s?)`(_?)") - -HYPERLINK_WITHIN_DOWNLOAD_RE = re.compile(r":download:`[^`]*`_?") +SEEMS_HYPERLINK_RE = re.compile(r"(:download:)?`[^`]+?(\s?)`(_?)") LEAKED_MARKUP_RE = re.compile(r"[a-z]::\s|`|\.\.\s*\w+:") @@ -293,3 +291,5 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): ROLE_MISSING_CLOSING_BACKTICK_RE = re.compile(rf"({ROLE_HEAD}`[^`]+?)[^`]*$") ROLE_WITH_UNNECESSARY_PARENTHESES_RE = re.compile(r"(^|\s):(func|meth):`[^`]+\(\)`") + +ROLE_WITH_EXCLAMATION_AND_TILDE_RE = re.compile(rf"{ROLE_HEAD}`[!~]{{2}}[^`]*`") diff --git a/tests/fixtures/xfail/download-hyperlink-unnecessary-underscore.rst b/tests/fixtures/xfail/download-hyperlink-unnecessary-underscore.rst new file mode 100644 index 000000000..30903a431 --- /dev/null +++ b/tests/fixtures/xfail/download-hyperlink-unnecessary-underscore.rst @@ -0,0 +1,7 @@ +.. expect: unnecessary underscore after closing backtick in hyperlink + +Download directives should not have trailing underscores after hyperlinks. + +:download:`Download this file `_ + +This is wrong because download directives don't need trailing underscores. diff --git a/tests/fixtures/xfail/role-with-exclamation-and-tilde.rst b/tests/fixtures/xfail/role-with-exclamation-and-tilde.rst new file mode 100644 index 000000000..520afc032 --- /dev/null +++ b/tests/fixtures/xfail/role-with-exclamation-and-tilde.rst @@ -0,0 +1,7 @@ +.. expect: Found a role with both `!` and `~` in ':meth:`!~list.pop`'. (exclamation-and-tilde) +.. expect: Found a role with both `!` and `~` in ':meth:`~!list.pop`'. (exclamation-and-tilde) + +:meth:`!~list.pop` cannot be used to display ``pop()`` and avoid +reference warnings; instead, it renders as ``~list.pop()``. +We should instead write :meth:`!pop` to correctly display ``pop()``. +:meth:`~!list.pop` doesn’t work either.