summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/instructions/doc-linter.instructions.md46
-rw-r--r--scripts/doc-linter.py57
2 files changed, 75 insertions, 28 deletions
diff --git a/.github/instructions/doc-linter.instructions.md b/.github/instructions/doc-linter.instructions.md
index 5e7d359e..cc56f8fe 100644
--- a/.github/instructions/doc-linter.instructions.md
+++ b/.github/instructions/doc-linter.instructions.md
@@ -4,7 +4,10 @@ applyTo: "**/*.md,**/*.rst,**/*.txt"
## Doc Linter Rules
-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.
+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
@@ -33,23 +36,29 @@ The linter rejects public IP addresses that are not reserved for documentation.
| Example | Why suppression is needed |
|---------|--------------------------|
-| `8.8.8.8` (Google DNS) | Real public IP, not documentation-reserved |
+| Google Public DNS | Real public IP, not documentation-reserved |
| `64:ff9b::/96` (NAT64) | Well-known prefix, not in doc ranges |
| Real provider IPs in examples | Authenticity matters for the example |
### Line Length Rules
-Maximum 80 characters per line. The line-length check is **skipped** inside fenced code:
+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.).
+- 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. The marker form depends on the surrounding parser:
+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:
+% stop_vyoslinter
+
````md
% stop_vyoslinter
@@ -60,8 +69,12 @@ set system name-server '8.8.8.8'
% start_vyoslinter
````
+% start_vyoslinter
+
**RST (`.rst`) and `.txt` includes** — use the RST comment form:
+% stop_vyoslinter
+
```rst
.. stop_vyoslinter
@@ -72,19 +85,30 @@ set system name-server '8.8.8.8'
.. 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.
+% 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 marker lines only in the parser context
+where they apply, including RST markers inside `{eval-rst}` blocks.
**Rules for suppression markers:**
- 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.
+- 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.
+- 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).
+- 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).
+- 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 ec00472c..81c9baca 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -31,15 +31,31 @@ 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:
+MD_FENCE_RE = re.compile(r'^(\s*)(`{3,}|:{3,})(.*)$')
+
+SUPPRESSION_MARKER_RE = {
+ 'rst': {
+ 'stop': re.compile(r'^\s*\.\.\s+stop_vyoslinter\s*$'),
+ 'start': re.compile(r'^\s*\.\.\s+start_vyoslinter\s*$'),
+ },
+ 'md': {
+ 'stop': re.compile(r'^\s*%\s+stop_vyoslinter\s*$'),
+ 'start': re.compile(r'^\s*%\s+start_vyoslinter\s*$'),
+ },
+}
+
+
+def is_suppression_marker(line, kind, in_md_fence, in_rst_codeblock, md_fence_is_eval_rst):
+ """Detect valid stop/start markers in the parser context where they apply."""
+ if SUPPRESSION_MARKER_RE['md'][kind].match(line):
+ return not in_md_fence
+ if not SUPPRESSION_MARKER_RE['rst'][kind].match(line):
return False
- return f".. {token}" in line or f"% {token}" in line
+ if in_rst_codeblock:
+ return False
+ if in_md_fence:
+ return md_fence_is_eval_rst
+ return True
def lint_mac(cnt, line):
@@ -94,6 +110,7 @@ def lint_linelen(cnt, line):
def handle_file_action(filepath):
errors = []
in_md_fence = False
+ md_fence_is_eval_rst = False
md_fence_char = None
md_fence_min_len = 0
in_rst_codeblock = False
@@ -103,15 +120,7 @@ def handle_file_action(filepath):
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 :::)
+ # MD/MyST fenced code block tracking (``` or :::).
fence_match = MD_FENCE_RE.match(line)
if fence_match:
fence = fence_match.group(2)
@@ -121,8 +130,10 @@ def handle_file_action(filepath):
in_md_fence = True
md_fence_char = fence_char
md_fence_min_len = fence_len
+ md_fence_is_eval_rst = fence_match.group(3).strip().startswith('{eval-rst}')
elif fence_char == md_fence_char and fence_len >= md_fence_min_len:
in_md_fence = False
+ md_fence_is_eval_rst = False
# RST `.. code-block::` tracking (existing semantics for .rst/.txt).
if in_rst_codeblock:
@@ -137,6 +148,18 @@ def handle_file_action(filepath):
else:
break
+ if is_suppression_marker(
+ line, 'stop', in_md_fence, in_rst_codeblock, md_fence_is_eval_rst
+ ):
+ start_vyoslinter = False
+ if is_suppression_marker(
+ line, 'start', in_md_fence, in_rst_codeblock, md_fence_is_eval_rst
+ ):
+ start_vyoslinter = True
+
+ if not start_vyoslinter:
+ continue
+
test_line_length = not (in_md_fence or in_rst_codeblock)
err_mac = lint_mac(cnt, line.strip())