diff options
| author | Yuriy Andamasov <yuriy@vyos.io> | 2026-05-14 00:47:20 +0300 |
|---|---|---|
| committer | Mergify <37929162+mergify[bot]@users.noreply.github.com> | 2026-05-14 05:08:54 +0000 |
| commit | f86a67230b63ddb99a75a70d7ba0fed24514f379 (patch) | |
| tree | 3e1754b28eca3cc15af38cb9fbe90621c70ab2ae | |
| parent | 8bdb54adec6a985a698cbaef9df005c77b29ac99 (diff) | |
| download | vyos-documentation-f86a67230b63ddb99a75a70d7ba0fed24514f379.tar.gz vyos-documentation-f86a67230b63ddb99a75a70d7ba0fed24514f379.zip | |
ci(doc-linter): distinguish prose-bearing directive fences from code blocks
The line-length skip was
test_line_length = not (in_md_fence or in_rst_codeblock)
`in_md_fence` was True for every MyST/Markdown fence regardless of
content type. That includes admonition directives like `:::{note}`,
`:::{warning}`, `:::{tip}` whose content is normal prose, not
preformatted code. Long lines in admonitions were silently skipped,
contradicting the documented 80-char rule which exempts code blocks
only.
Track an `is_code` property on each fence-stack entry. A fence is
code-bearing when:
- info string is empty (plain ``` per CommonMark), OR
- info string doesn't start with `{` (bare language tag like
`python`, `bash`, `yaml`), OR
- info string is `{<directive>}` and `<directive>` is in the
CODE_BEARING_DIRECTIVES set (`code-block`, `code`, `sourcecode`,
`cfgcmd`, `opcmd`, `cmdinclude`, `cmdincludemd`, `literalinclude`,
`parsed-literal`, `raw`, `command-output`, `eval-rst`).
Anything else is prose-bearing (`{note}`, `{warning}`, `{tip}`,
`{deprecated}`, `{seealso}`, …) and its content gets line-length
checked.
`in_md_code_fence` checks the topmost stack entry — the innermost
fence wins, so a `{note}` containing an inner `{code-block}` lints
the outer prose lines and skips the inner code-block body. The
classic `is_suppression_marker()` call still uses `in_md_fence`
because suppression markers are about "any fence depth" not
"code-bearing depth".
`{eval-rst}` is kept in CODE_BEARING_DIRECTIVES to preserve current
behavior — its body is RST and any line-length on nested
`.. code-block::` is handled by the separate RST tracker. Tightening
eval-rst is a separate change if wanted.
Test coverage:
- `_fence_is_code` classifier: 16 cases (code-like vs prose-like)
all pass.
- Integration: long line in `{note}` flagged ✓; long line in
```python``` not flagged ✓; long line in `{cfgcmd}` not flagged ✓;
nested `{note}` > ```text``` — inner skipped ✓; nested `{note}` >
prose — flagged ✓.
Sweep over current `docs/` tree: 28 new warnings surface across the
existing pages (long prose inside admonition directives that the
previous logic had been silently hiding). CI on PR scope is changed
files only, so the new findings appear only when contributors touch
those pages — they won't break this PR or future infra PRs.
Tracked as item 5 of the rolling-side cleanup backlog from PR #2014
/ #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
(cherry picked from commit cd5759f26a1c18abc3c137234cf1ab503e2b26b7)
| -rw-r--r-- | scripts/doc-linter.py | 50 |
1 files changed, 44 insertions, 6 deletions
diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py index fe262d98..1da215a5 100644 --- a/scripts/doc-linter.py +++ b/scripts/doc-linter.py @@ -55,6 +55,38 @@ def is_docs_path(path): # Same character and length-or-greater closes. MD_FENCE_RE = re.compile(r'^(\s*)(`{3,}|:{3,})(.*)$') +# MyST directive names whose body is code-like — line-length exception applies. +# Anything else with a `{directive}` info string is treated as prose-bearing +# (`{note}`, `{warning}`, `{tip}`, `{deprecated}`, …); their content is normal +# Markdown and gets line-length checked. Plain fences with no info string OR a +# bare language tag (`python`, `bash`, `yaml`, …) are code blocks per +# CommonMark and always skip line-length. +CODE_BEARING_DIRECTIVES = frozenset({ + 'code-block', 'code', 'sourcecode', + 'cfgcmd', 'opcmd', 'cmdinclude', 'cmdincludemd', + 'literalinclude', 'parsed-literal', 'raw', + 'command-output', + 'eval-rst', # body is RST; its own line-length is handled via the + # `.. code-block::` tracker for nested code blocks. +}) + + +def _fence_is_code(info): + """Return True iff the fence opener `info` string designates a code-like block. + + Used to gate the line-length-check skip — only code-like fences should + suppress line-length linting; prose-bearing directive fences like + `{note}` / `{warning}` must still have their content checked. + """ + info = info.strip() + if not info: + return True # plain ``` is code per CommonMark + if not info.startswith('{'): + return True # python, bash, yaml, text, etc. + # `{code-block} python` -> `code-block`; `{note}` -> `note`. + name = info[1:].split('}', 1)[0].strip() + return name in CODE_BEARING_DIRECTIVES + SUPPRESSION_MARKER_RE = { 'rst': { 'stop': re.compile(r'^\s*\.\.\s+stop_vyoslinter\s*$'), @@ -136,10 +168,12 @@ def handle_file_action(filepath): """Run all lint checks on one file, respecting fence/code-block and suppression context.""" errors = [] file_ext = os.path.splitext(filepath)[1].lower() - # Stack of open MD/MyST fences: (char, min_len). Supports nesting like - # `:::{note}` containing `::::{code-block}`, where the inner opener does - # not close the outer note (CommonMark closing rule: matching closer must - # have no info string after the fence chars). + # Stack of open MD/MyST fences: (char, min_len, is_code). Supports + # nesting like `:::{note}` containing `::::{code-block}`, where the + # inner opener does not close the outer note (CommonMark closing rule: + # matching closer must have no info string after the fence chars). + # `is_code` records whether THAT fence is a code-bearing one — gates the + # line-length skip per the innermost (top-of-stack) entry. md_fence_stack = [] md_fence_is_eval_rst = False in_rst_codeblock = False @@ -172,9 +206,10 @@ def handle_file_action(filepath): else: if not md_fence_stack: md_fence_is_eval_rst = info.startswith('{eval-rst}') - md_fence_stack.append((fence_char, fence_len)) + md_fence_stack.append((fence_char, fence_len, _fence_is_code(info))) in_md_fence = bool(md_fence_stack) + in_md_code_fence = bool(md_fence_stack) and md_fence_stack[-1][2] # RST `.. code-block::` tracking (existing semantics for .rst/.txt). # Each `.. code-block::` directive resets the tracked indent so a @@ -219,7 +254,10 @@ def handle_file_action(filepath): if not start_vyoslinter: continue - test_line_length = not (in_md_fence or in_rst_codeblock) + # Only code-bearing fences (plain ```python```, `{code-block}`, + # `{cfgcmd}`, etc.) skip line-length. Prose-bearing directives + # like `{note}` and `{warning}` have their content checked. + test_line_length = not (in_md_code_fence or in_rst_codeblock) err_ip4 = lint_ipv4(cnt, line.strip()) err_ip6 = lint_ipv6(cnt, line.strip()) |
