Module

linting.rules

The three starter lint rules for issue #56 Phase-1.

Each rule is a stateless@dataclass(frozen=True, slots=True)implementing theLintRuleprotocol. All per-run state lives in local variables insidecheck— nothing is stored on the rule instance — so a single rule instance is safe to share across threads.

Thread Safety:

All rules are stateless frozen dataclasses. ``check`` holds only local
state. Safe to call concurrently.

Classes

HeadingIncrementRule 3
Flag headings that skip a level (e.g. h1 -> h3). Mirrors markdownlint MD001. Operates on the flat,…

Flag headings that skip a level (e.g. h1 -> h3).

Mirrors markdownlint MD001. Operates on the flat, document-order sequence of all headings (including nested ones) fromctx.headings(), comparing each heading to its immediate predecessor. The first heading is never flagged and may start at any level; only an upward jump of more than one is a violation.

Attributes

Name Type Description
rule_id ClassVar[str]
default_severity ClassVar[Severity]

Methods

check 1 Iterable[Diagnostic]
Yield a diagnostic for each heading that skips a level.
def check(self, ctx: LintContext) -> Iterable[Diagnostic]
Parameters
Name Type Description
ctx
Returns
Iterable[Diagnostic]
NoEmptyLinkRule 3
Flag links with no visible text. A link is empty when it has no children OR all descendant content…

Flag links with no visible text.

A link is empty when it has no children OR all descendant content is whitespace-only. An Image descendant, a non-empty CodeSpan, or non-whitespace inline HTML counts as visible content, so icon links and code links are not flagged. Image nodes themselves are never inspected (Image.alt is a flat string; Image is not a Link).

Inline nodes share the enclosing block's location, so the diagnostic is reported at the start of the enclosing block; the url is included in the message to disambiguate multiple empty links within one block.

Attributes

Name Type Description
rule_id ClassVar[str]
default_severity ClassVar[Severity]

Methods

check 1 Iterable[Diagnostic]
Yield a diagnostic for each link with no visible text.
def check(self, ctx: LintContext) -> Iterable[Diagnostic]
Parameters
Name Type Description
ctx
Returns
Iterable[Diagnostic]
TrailingWhitespaceRule 4
Flag source lines ending in space/tab, excluding code-block content. A pure source scan over ``ctx…

Flag source lines ending in space/tab, excluding code-block content.

A pure source scan over ctx.lines (source split on "\n"). Lines inside a top-level fenced or indented code block are skipped, since trailing whitespace there is legitimate code content (this keeps the repo's own docs clean when dogfooded). The whitespace signal is unrecoverable from the AST (trailing spaces are stripped during parsing), so it is detected from raw source; the code-block ranges are read from the AST.

Hard-line-break lines (exactly two trailing spaces) are still flagged — at INFO severity, matching markdownlint MD009's default — so they never trip a default error-only CLI exit code.

Attributes

Name Type Description
rule_id ClassVar[str]
default_severity ClassVar[Severity]

Methods

check 1 Iterable[Diagnostic]
Yield a diagnostic for each line with trailing whitespace.
def check(self, ctx: LintContext) -> Iterable[Diagnostic]
Parameters
Name Type Description
ctx
Returns
Iterable[Diagnostic]
Internal Methods 1
_code_block_lines 1 frozenset[int]
Compute the set of 1-indexed line numbers inside top-level code. The parser fi…
staticmethod
def _code_block_lines(ctx: LintContext) -> frozenset[int]

Compute the set of 1-indexed line numbers inside top-level code.

The parser fixes location.end_lineno at lineno + 1for both FencedCode and IndentedCode regardless of the block's real length, so it cannot be used to derive the span. Instead the true span is recovered from data already on each node:

  • FencedCode (top-level): the opening fence is location.lineno and source_end (a byte offset into ctx.source) maps to the closing fence line, so the inclusive span islocation.lineno.. source[:source_end].count("\n") + 1. Nested fences carry a content_override and source offsets relative to a sub-parser, so they fall back to the location span (best-effort, as documented).
  • IndentedCode: code.count("\n")gives the number of line breaks; the inclusive end islocation.lineno + countless one if codeends in a trailing newline.

Nested code blocks may carry inner-buffer relative line numbers, so exclusion is best-effort for top-level code.

Parameters
Name Type Description
ctx
Returns
frozenset[int]

Functions

_has_visible_content 1 bool
Return True if ``node`` (or any descendant) renders visible content. Visible c…
def _has_visible_content(node: Node) -> bool

Return True if node (or any descendant) renders visible content.

Visible content is any non-whitespace Text; any Image (a visible, clickable target); a CodeSpan with non-empty code; or non-whitespace inline HTML. Recurses through emphasis/strong/strikethrough/link wrappers that carry achildrentuple.

Parameters
Name Type Description
node Node
Returns
bool