diff options
| -rw-r--r-- | .github/instructions/doc-linter.instructions.md (renamed from .github/instructions/rst-linter.instructions.md) | 46 | ||||
| -rw-r--r-- | scripts/doc-linter.py | 133 |
2 files changed, 106 insertions, 73 deletions
diff --git a/.github/instructions/rst-linter.instructions.md b/.github/instructions/doc-linter.instructions.md index 419f094a..5e7d359e 100644 --- a/.github/instructions/rst-linter.instructions.md +++ b/.github/instructions/doc-linter.instructions.md @@ -1,10 +1,10 @@ --- -applyTo: "**/*.rst" +applyTo: "**/*.md,**/*.rst,**/*.txt" --- -## RST Linter Rules +## Doc Linter Rules -Every pull request is automatically linted by `doc-linter.py` (from the `vyos/.github` repository). It checks **only changed files** for two things: IP address usage and line length. +Every pull request is automatically linted by `scripts/doc-linter.py`. It checks **only changed files** for two things: IP address usage and line length. MyST Markdown (`.md`), legacy RST (`.rst`), and `.txt` includes are all linted. ### IP Address Rules @@ -39,11 +39,28 @@ The linter rejects public IP addresses that are not reserved for documentation. ### Line Length Rules -Maximum 80 characters per line. Lines inside `.. code-block::` directives are **exempt** from the line length limit (they render with `<pre>` tags and preserve source formatting). +Maximum 80 characters per line. The line-length check is **skipped** inside fenced code: + +- MyST / Markdown: ` ```...``` ` and `:::...:::` fenced blocks (any info string, including ` ```{eval-rst} `, ` ```{cfgcmd} `, etc.). +- RST: lines indented under `.. code-block::` directives. ### Suppression Syntax -When real public IPs or long lines are unavoidable, wrap the block: +When real public IPs or long lines are unavoidable, wrap the block. The marker form depends on the surrounding parser: + +**MyST Markdown (`.md`)** — use the MyST comment form: + +````md +% stop_vyoslinter + +``` +set system name-server '8.8.8.8' +``` + +% start_vyoslinter +```` + +**RST (`.rst`) and `.txt` includes** — use the RST comment form: ```rst .. stop_vyoslinter @@ -55,16 +72,19 @@ When real public IPs or long lines are unavoidable, wrap the block: .. start_vyoslinter ``` +**Inside a `{eval-rst}` block in MyST** — the embedded content is parsed as RST, so the RST form (`.. stop_vyoslinter`) matches the surrounding parser. The linter recognizes both forms regardless of context, but use the form that fits the parser of the enclosing block. + **Rules for suppression markers:** -- Place `.. stop_vyoslinter` on its own line, immediately before the content -- Place `.. start_vyoslinter` on its own line, immediately after the content -- Always re-enable the linter — never leave it stopped for the rest of the file -- Suppress the smallest possible region -- Do NOT use suppression for content that can use documentation-reserved addresses instead +- Place the stop marker on its own line, immediately before the content. +- Place the start marker on its own line, immediately after the content. +- Always re-enable the linter — never leave it stopped for the rest of the file. +- Suppress the smallest possible region. +- Do NOT use suppression for content that can use documentation-reserved addresses instead. ### Common Mistakes -- Removing `stop/start_vyoslinter` markers without fixing the underlying issue (exposes the line to the linter and fails CI) -- Using real public IPs when documentation addresses would work just as well -- Adding suppression markers around content that only has private (RFC 1918) addresses or `0.0.0.0/0` — these are allowed and don't need suppression +- Removing `stop/start_vyoslinter` markers without fixing the underlying issue (exposes the line to the linter and fails CI). +- Using real public IPs when documentation addresses would work just as well. +- Adding suppression markers around content that only has private (RFC 1918) addresses or `0.0.0.0/0` — these are allowed and don't need suppression. +- Mixing marker forms (using `% stop_vyoslinter` inside an `{eval-rst}` block, or `.. stop_vyoslinter` in plain MyST prose). diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py index 3dc7c2fc..ec00472c 100644 --- a/scripts/doc-linter.py +++ b/scripts/doc-linter.py @@ -27,6 +27,20 @@ MAC = r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})' NUMBER = r"([\s']\d+[\s'])" +SUPPORTED_EXTS = ('.md', '.rst', '.txt') + +# MyST / Markdown fenced code block: leading whitespace + 3+ backticks or 3+ colons. +# Same character and length-or-greater closes. +MD_FENCE_RE = re.compile(r'^(\s*)(`{3,}|:{3,})') + + +def is_suppression_marker(line, kind): + """Detect `.. stop_vyoslinter` (RST/txt) or `% stop_vyoslinter` (MyST).""" + token = f"{kind}_vyoslinter" + if token not in line: + return False + return f".. {token}" in line or f"% {token}" in line + def lint_mac(cnt, line): mac = re.search(MAC, line, re.I) @@ -79,65 +93,64 @@ def lint_linelen(cnt, line): def handle_file_action(filepath): errors = [] - try: - with open(filepath) as fp: - line = fp.readline() - cnt = 1 - test_line_lenght = True - start_vyoslinter = True - indentation = 0 - while line: - # search for ignore linter comments in lines - if ".. stop_vyoslinter" in line: - start_vyoslinter = False - if ".. start_vyoslinter" in line: - start_vyoslinter = True - if start_vyoslinter: - # ignore every '.. code-block::' for line lenght - # rst code-block have its own style in html the format in rst - # and the build page must be the same - if test_line_lenght is False: - if len(line) > indentation: - #print(f"'{line}'") - #print(indentation) - if line[indentation].isspace() is False: - test_line_lenght = True - - if ".. code-block::" in line: - test_line_lenght = False - indentation = 0 - for i in line: - if i.isspace(): - indentation = indentation + 1 - else: - break - - err_mac = lint_mac(cnt, line.strip()) - # disable mac detection for the moment, too many false positives - err_mac = None - err_ip4 = lint_ipv4(cnt, line.strip()) - err_ip6 = lint_ipv6(cnt, line.strip()) - if test_line_lenght: - err_len = lint_linelen(cnt, line) + in_md_fence = False + md_fence_char = None + md_fence_min_len = 0 + in_rst_codeblock = False + rst_codeblock_indent = 0 + start_vyoslinter = True + cnt = 0 + + with open(filepath) as fp: + for cnt, line in enumerate(fp, start=1): + if is_suppression_marker(line, 'stop'): + start_vyoslinter = False + if is_suppression_marker(line, 'start'): + start_vyoslinter = True + + if not start_vyoslinter: + continue + + # MD/MyST fenced code block tracking (``` or :::) + fence_match = MD_FENCE_RE.match(line) + if fence_match: + fence = fence_match.group(2) + fence_char = fence[0] + fence_len = len(fence) + if not in_md_fence: + in_md_fence = True + md_fence_char = fence_char + md_fence_min_len = fence_len + elif fence_char == md_fence_char and fence_len >= md_fence_min_len: + in_md_fence = False + + # RST `.. code-block::` tracking (existing semantics for .rst/.txt). + if in_rst_codeblock: + if len(line) > rst_codeblock_indent and not line[rst_codeblock_indent].isspace(): + in_rst_codeblock = False + if not in_rst_codeblock and ".. code-block::" in line: + in_rst_codeblock = True + rst_codeblock_indent = 0 + for ch in line: + if ch.isspace(): + rst_codeblock_indent += 1 else: - err_len = None - if err_mac: - errors.append(err_mac) - if err_ip4: - errors.append(err_ip4) - if err_ip6: - errors.append(err_ip6) - if err_len: - errors.append(err_len) - - line = fp.readline() - cnt += 1 - - # ensure linter was not stop on top and forgot to tun on again - if start_vyoslinter == False: - errors.append((f"Don't forgett to turn linter back on", cnt, 'error')) - finally: - fp.close() + break + + test_line_length = not (in_md_fence or in_rst_codeblock) + + err_mac = lint_mac(cnt, line.strip()) + # disable mac detection for the moment, too many false positives + err_mac = None + err_ip4 = lint_ipv4(cnt, line.strip()) + err_ip6 = lint_ipv6(cnt, line.strip()) + err_len = lint_linelen(cnt, line) if test_line_length else None + for e in (err_mac, err_ip4, err_ip6, err_len): + if e: + errors.append(e) + + if not start_vyoslinter: + errors.append(("Don't forget to turn linter back on", cnt, 'error')) if len(errors) > 0: ''' @@ -156,14 +169,14 @@ def main(): try: files = ast.literal_eval(sys.argv[1]) for file in files: - if file[-4:] in [".rst", ".txt"] and "_build" not in file: + if file.endswith(SUPPORTED_EXTS) and "_build" not in file: if handle_file_action(file) is False: bool_error = False except Exception as e: for root, dirs, files in os.walk("docs"): path = root.split(os.sep) for file in files: - if file[-4:] in [".rst", ".txt"] and "_build" not in path: + if file.endswith(SUPPORTED_EXTS) and "_build" not in path: fpath = '/'.join(path) filepath = f"{fpath}/{file}" if handle_file_action(filepath) is False: |
