diff --git a/Makefile b/Makefile index 09c1c3037..a09927090 100644 --- a/Makefile +++ b/Makefile @@ -85,4 +85,4 @@ static: $(HTML_BUILD_DIR)/_static/style.css cp -r static/* $(HTML_BUILD_DIR)/_static/ test: - @python tests/rst_style.py $(SOURCE_DIR)/administration $(SOURCE_DIR)/applications $(SOURCE_DIR)/contributing $(SOURCE_DIR)/developer $(SOURCE_DIR)/services redirects + @python tests/main.py $(SOURCE_DIR)/administration $(SOURCE_DIR)/applications $(SOURCE_DIR)/contributing $(SOURCE_DIR)/developer $(SOURCE_DIR)/services redirects diff --git a/tests/checkers/__init__.py b/tests/checkers/__init__.py new file mode 100644 index 000000000..e05e75af2 --- /dev/null +++ b/tests/checkers/__init__.py @@ -0,0 +1,3 @@ +from . import redirect_rules +from . import resource_files +from . import rst_style diff --git a/tests/checkers/redirect_rules.py b/tests/checkers/redirect_rules.py new file mode 100644 index 000000000..2ca6ee89a --- /dev/null +++ b/tests/checkers/redirect_rules.py @@ -0,0 +1,55 @@ +import re +from pathlib import Path + +import sphinxlint + + +REDIRECT_RULE_RE = re.compile(r'^[ \t]*([\w\-/]+\.rst)[ \t]+([\w\-/]+\.rst)[ \t]*(?:#.*)?$') +REDIRECTS_FILE_VERSION_RE = re.compile('(?:redirects/)?(?:saas-)?(\d\d\.\d)\.txt') + + +@sphinxlint.checker('.txt') +def check_redirect_rules_format(file, lines, options=None): + """ Check that redirect rules are correctly formatted. """ + if file.startswith('redirects/'): # Only check text files in the /redirects folder. + for lno, line in enumerate(lines): + if not line.rstrip() or line.startswith('#'): + continue + if not REDIRECT_RULE_RE.search(line): + yield lno + 1, "invalid redirect rule format; learn more at redirects/MANUAL.md" + + +@sphinxlint.checker('.txt') +def check_redirect_rules_target(file, lines, options=None): + """ Check that redirect rules refer to existing files. """ + def get_redirects_file_version(file_name_): + match_ = REDIRECTS_FILE_VERSION_RE.search(file_name_) + if match_: + return float(match_.group(1)) + return -1.0 + + if file.startswith('redirects/'): # Only check text files in the /redirects folder. + # Find the current version, which is that of the file with the latest version. + redirects_dir = Path('redirects') + latest_redirects_version = 0.0 + for redirect_file in redirects_dir.iterdir(): + if redirect_file.is_dir() or redirect_file.suffix != '.txt': + continue + latest_redirects_version = max( + latest_redirects_version, get_redirects_file_version(redirect_file.name) + ) + + # Only check the existence of the redirection target if we are in the right version. + current_file_version = get_redirects_file_version(file) + if current_file_version < latest_redirects_version: + return + + for lno, line in enumerate(lines): + if not line.rstrip() or line.startswith('#'): + continue + match_res = REDIRECT_RULE_RE.search(line) + if match_res: + _old_path, new_path = match_res.groups() + redirect_file = Path('content') / new_path + if not redirect_file.is_file(): + yield lno + 1, f"the redirect rule targets the non-existing file {new_path}" diff --git a/tests/checkers/resource_files.py b/tests/checkers/resource_files.py new file mode 100644 index 000000000..82ad0145f --- /dev/null +++ b/tests/checkers/resource_files.py @@ -0,0 +1,7 @@ +import sphinxlint + + +@sphinxlint.checker('') +def check_file_extensions(file, lines, options=None): + """ Check that there is no file without extension. """ + yield 0, "the file does not have an extension" diff --git a/tests/rst_style.py b/tests/checkers/rst_style.py similarity index 50% rename from tests/rst_style.py rename to tests/checkers/rst_style.py index f414fb6f1..79e5b15de 100644 --- a/tests/rst_style.py +++ b/tests/checkers/rst_style.py @@ -1,18 +1,8 @@ import re -import sys -from pathlib import Path -from unittest.mock import patch import sphinxlint -CUSTOM_RST_DIRECTIVES = [ - 'card', 'cards', # cards - 'example', 'exercise', # custom_admonitions - 'spoiler', # spoilers - 'tab', 'tabs', 'group-tab', 'code-tab', # sphinx_tabs -] - ALLOWED_HEADING_CHARS = ['=', '-', '~', '*', '^'] # In the same order as in the guidelines. MAIN_HEADING_CHAR = ALLOWED_HEADING_CHARS[0] MAIN_HEADING_RE = re.compile(rf'{MAIN_HEADING_CHAR}+\n[^\n]+\n{MAIN_HEADING_CHAR}+\n') @@ -26,70 +16,8 @@ FORBIDDEN_HEADING_DELIMITER_RE = re.compile( '^(' + '|'.join(rf'\{char}+' for char in FORBIDDEN_HEADING_CHARS) + ')\n$' ) GIT_CONFLICT_MARKERS = ['<' * 7, '>' * 7] -REDIRECT_RULE_RE = re.compile(r'^[ \t]*([\w\-/]+\.rst)[ \t]+([\w\-/]+\.rst)[ \t]*(?:#.*)?$') -REDIRECTS_FILE_VERSION_RE = re.compile('(?:redirects/)?(?:saas-)?(\d\d\.\d)\.txt') -@sphinxlint.checker('.rst', '.py', '.js', '.xml', '.css', '.sass', '.less', '.po', '.pot') -def check_git_conflict_markers(file, lines, options=None): - """ Check that there are no conflict markers. """ - for lno, line in enumerate(lines): - if any(marker in line for marker in GIT_CONFLICT_MARKERS): - yield lno + 1, "the git conflict should be resolved" - - -@sphinxlint.checker('') -def check_file_extensions(file, lines, options=None): - """ Check that there is no file without extension. """ - yield 0, "the file does not have an extension" - - -@sphinxlint.checker('.txt') -def check_redirect_rules_format(file, lines, options=None): - """ Check that redirect rules are correctly formatted. """ - if file.startswith('redirects/'): # Only check text files in the /redirects folder. - for lno, line in enumerate(lines): - if not line.rstrip() or line.startswith('#'): - continue - if not REDIRECT_RULE_RE.search(line): - yield lno + 1, "invalid redirect rule format; learn more at redirects/MANUAL.md" - - -@sphinxlint.checker('.txt') -def check_redirect_rules_target(file, lines, options=None): - """ Check that redirect rules refer to existing files. """ - def get_redirects_file_version(file_name_): - match_ = REDIRECTS_FILE_VERSION_RE.search(file_name_) - if match_: - return float(match_.group(1)) - return -1.0 - - if file.startswith('redirects/'): # Only check text files in the /redirects folder. - # Find the current version, which is that of the file with the latest version. - redirects_dir = Path('redirects') - latest_redirects_version = 0.0 - for redirect_file in redirects_dir.iterdir(): - if redirect_file.is_dir() or redirect_file.suffix != '.txt': - continue - latest_redirects_version = max( - latest_redirects_version, get_redirects_file_version(redirect_file.name) - ) - - # Only check the existence of the redirection target if we are in the right version. - current_file_version = get_redirects_file_version(file) - if current_file_version < latest_redirects_version: - return - - for lno, line in enumerate(lines): - if not line.rstrip() or line.startswith('#'): - continue - match_res = REDIRECT_RULE_RE.search(line) - if match_res: - _old_path, new_path = match_res.groups() - redirect_file = Path('content') / new_path - if not redirect_file.is_file(): - yield lno + 1, f"the redirect rule targets the non-existing file {new_path}" - @sphinxlint.checker('.rst') def check_heading_delimiters_characters(file, lines, options=None): """ Check that heading delimiters use only allowed characters. """ @@ -175,58 +103,9 @@ def check_heading_spacing(file, lines, options=None): yield heading_lno + 1, "the heading should be followed by a blank line" -""" -The following checkers are selected. - -Base checkers: -- backtick-before-role: Search for roles preceded by a backtick. -- bad-dedent: Check for mis-alignment in indentation in code blocks. -- carriage-return: Check for carriage returns (\r) in lines. -- directive-missing-colons: Search for directive wrongly typed as comments. -- directive-with-three-dots: Search for directives with three dots instead of two. -- horizontal-tab: Check for horizontal tabs (\t) in lines. -- hyperlink-reference-missing-backtick: Search for missing backticks in front of hyperlink - references. -- missing-backtick-after-role: Search for roles missing their closing backticks. -- missing-colon-in-role: Search for missing colons in roles. -- missing-final-newline: Check that the last line of the file ends with a newline. -- missing-space-after-literal: Search for inline literals immediately followed by a character. -- missing-space-after-role: Search for roles immediately followed by a character. -- missing-space-before-default-role: Search for missing spaces before default role. -- missing-space-before-role: Search for missing spaces before roles. -- missing-space-in-hyperlink: Search for hyperlinks missing a space. -- missing-underscore-after-hyperlink: Search for hyperlinks missing underscore after their closing - backtick. -- python-syntax: Search invalid syntax in Python examples. -- role-with-double-backticks: Search for roles with double backticks. -- role-without-backticks: Search roles without backticks. -- trailing-whitespace: Check for trailing whitespaces at end of lines. -- unbalanced-inline-literals-delimiters: Search for unbalanced inline literals delimiters. - -Optional checkers: -- line-too-long: Check for line length; this checker is not run by default. - -Custom checkers: -- all the checkers defined in this file. -""" -if __name__ == '__main__': - # Patch sphinxlint's global constants to include our custom directives and parse their content. - with patch( - 'sphinxlint.DIRECTIVES_CONTAINING_RST', - sphinxlint.DIRECTIVES_CONTAINING_RST + CUSTOM_RST_DIRECTIVES, - ), patch( - 'sphinxlint.DIRECTIVES_CONTAINING_RST_RE', - '(' + '|'.join(sphinxlint.DIRECTIVES_CONTAINING_RST) + ')', - ), patch( - 'sphinxlint.ALL_DIRECTIVES', - '(' + '|'.join(sphinxlint.DIRECTIVES_CONTAINING_RST - + sphinxlint.DIRECTIVES_CONTAINING_ARBITRARY_CONTENT) - + ')', - ), patch( - 'sphinxlint.seems_directive_re', - re.compile(rf"^\s*(?