all2md.linter

Document linter for the all2md AST.

The linter inspects a Document (regardless of the source format it was parsed from) and reports structural, heading, link, and typography issues via a rule-based engine.

Typical use:

from all2md import to_ast
from all2md.linter import lint_document, LintConfig

doc = to_ast("whitepaper.pdf")
result = lint_document(doc)
for violation in result.violations:
    print(violation)

The CLI entry point is exposed as all2md lint.

class all2md.linter.AppliedFix

Bases: object

Record of a fix that was (or would have been) applied.

rule_code: str
line: int | None
description: str
safety: FixSafety
to_dict() dict

Serialise to a plain dict for the JSON reporter.

__init__(rule_code: str, line: int | None, description: str, safety: FixSafety) None
class all2md.linter.FixContext

Bases: object

Mutation primitives passed to LintFix.apply.

The parent map is built lazily on first use, so fixes that only mutate Text.content never pay for it.

Initialise a context for document.

The parent map is not built until the first call to parent_of(), remove(), or replace().

__init__(document: Document) None

Initialise a context for document.

The parent map is not built until the first call to parent_of(), remove(), or replace().

parent_of(node: Node) Node | None

Return the parent of node in this context’s document, or None.

remove(node: Node) bool

Detach node from its parent.

Returns True on success, False if the node has no parent or the parent’s container does not hold it (e.g. the node has already been detached by an earlier fix in the same run).

replace(old: Node, new: Node) bool

Replace old with new in its parent’s container.

Returns True on success. Plumbed for future structural fixes; none of the v2.0 SAFE fixes use it.

class all2md.linter.FixSafety

Bases: IntEnum

Safety classification for an auto-fix.

Lower numeric value means safer / less invasive. apply_fixes uses <= max_safety to decide which fixes to apply, so SAFE fixes run under both --fix and --fix-unsafe while SUGGESTED fixes run only under --fix-unsafe.

SAFE = 1
SUGGESTED = 2
MANUAL = 3
property label: str

Lowercase label suitable for human output (‘safe’, ‘suggested’, ‘manual’).

__new__(value)
class all2md.linter.LintConfig

Bases: CloneFrozenMixin

Frozen configuration for a lint run.

Parameters:
  • enabled_rules (frozenset[str] or None) – When set, only rules whose code appears in this whitelist run. None (the default) means “every registered rule is enabled”.

  • disabled_rules (frozenset[str]) – Rules to skip. Applied after enabled_rules.

  • severity_overrides (dict[str, Severity]) – Map of rule code to severity, overriding the rule’s default_severity.

  • rule_options (dict[str, dict[str, Any]]) – Per-rule option dictionaries, keyed by rule code. Forwarded to rules via all2md.linter.rule.LintContext.config.

  • severity_threshold (Severity) – Minimum severity to report. Violations below this level are dropped by the runner before results are returned. Defaults to INFO (everything is reported).

enabled_rules: frozenset[str] | None = None
disabled_rules: frozenset[str]
severity_overrides: dict[str, Severity]
rule_options: dict[str, dict[str, Any]]
severity_threshold: Severity = 1
is_rule_enabled(code: str) bool

Return True if the rule with code should run under this config.

get_severity(rule_cls: type[LintRule]) Severity

Return the effective severity for rule_cls, honouring overrides.

get_rule_options(code: str) dict[str, Any]

Return the option dict for code (empty dict if none configured).

classmethod from_dict(data: dict[str, Any] | None) LintConfig

Build a LintConfig from a plain dict (as produced by the TOML loader).

Recognised keys:

  • enable / enabled_rules : list of rule codes (whitelist)

  • disable / disabled_rules : list of rule codes (blacklist)

  • severity : dict of rule code -> severity name

  • rules : dict of rule code -> per-rule options dict

  • severity_threshold : severity name (info/warning/error)

Unknown keys are silently ignored so config files can evolve without breaking older installs.

__init__(enabled_rules: frozenset[str] | None = None, disabled_rules: frozenset[str] = <factory>, severity_overrides: dict[str, ~all2md.linter.violations.Severity]=<factory>, rule_options: dict[str, dict[str, ~typing.Any]]=<factory>, severity_threshold: Severity = Severity.INFO) None
class all2md.linter.LintContext

Bases: object

Input passed to every LintRule.check() call.

document: Document
file_path: str | None
config: dict[str, Any]
__init__(document: Document, file_path: str | None = None, config: dict[str, Any]=<factory>) None
class all2md.linter.LintFix

Bases: object

A fix attached to a Violation.

Parameters:
  • target (Node) – The AST node this fix mutates or removes. Used for conflict detection (two fixes targeting the same node — the first one wins).

  • apply (Callable[[FixContext], None]) – In-place mutation of the AST. For text mutations the callback usually closes over target and rewrites target.content. For structural fixes the callback uses ctx.remove(target) or ctx.replace(old, new).

  • safety (FixSafety) – How aggressively to apply this fix.

  • description (str) – Short human-readable description of what the fix does, for reporters and logs.

target: Node
apply: Callable[[FixContext], None]
safety: FixSafety
description: str
__init__(target: Node, apply: Callable[[FixContext], None], safety: FixSafety, description: str = '') None
class all2md.linter.LintFixResult

Bases: object

Result of running lint --fix against a single document.

Carries both the pre-fix and post-fix lint results so reporters can show “applied N fixes, M remaining” without recomputing.

file_path: str | None
initial: LintResult
final: LintResult
applied: list[AppliedFix]
skipped_conflicts: list[AppliedFix]
rewritten: bool = False
property violations: list[Violation]

Post-fix violations — the ones the user still needs to address.

property rules_checked: int

Forward to the post-fix lint result.

property error_count: int

Forward to the post-fix lint result.

property warning_count: int

Forward to the post-fix lint result.

property info_count: int

Forward to the post-fix lint result.

property total: int

Forward to the post-fix lint result.

__init__(file_path: str | None, initial: LintResult, final: LintResult, applied: list[AppliedFix] = <factory>, skipped_conflicts: list[AppliedFix] = <factory>, rewritten: bool = False) None
class all2md.linter.LintResult

Bases: object

Result of linting a single document.

file_path: str | None
violations: list[Violation]
rules_checked: int = 0
property error_count: int

Return the number of ERROR-severity violations.

property warning_count: int

Return the number of WARNING-severity violations.

property info_count: int

Return the number of INFO-severity violations.

property total: int

Return the total number of violations in this result.

__init__(file_path: str | None, violations: list[Violation] = <factory>, rules_checked: int = 0) None
class all2md.linter.LintRule

Bases: ABC

Abstract base class for lint rules.

Subclasses must set the class-level metadata attributes (code, name, category, description, default_severity) and implement check().

code: str
name: str
category: str
description: str
default_severity: Severity
abstractmethod check(ctx: LintContext) list[Violation]

Inspect ctx.document and return any violations found.

build_violation(message: str, *, severity: Severity | None = None, line: int | None = None, column: int | None = None, node_type: str | None = None, suggestion: str | None = None, context: str | None = None, fix: 'LintFix' | None = None) Violation

Construct a Violation pre-populated with this rule’s metadata.

class all2md.linter.LintRunner

Bases: object

Run a LintConfig over one or more documents.

Initialise the runner with a config and a registry.

Either argument can be omitted; defaults are a blank LintConfig and the global rule_registry.

__init__(config: LintConfig | None = None, registry: RuleRegistry | None = None) None

Initialise the runner with a config and a registry.

Either argument can be omitted; defaults are a blank LintConfig and the global rule_registry.

lint_document(doc: Document, file_path: str | None = None) LintResult

Run all enabled rules against doc and return a LintResult.

lint_file(file_path: str | Path) LintResult

Parse file_path to an AST and lint it.

lint_files(file_paths: list[str | Path]) list[LintResult]

Run lint_file() against every path in the list.

lint_and_fix_document(doc: Document, *, file_path: str | None = None, max_safety: FixSafety = FixSafety.SAFE, max_passes: int = 5) LintFixResult

Lint doc, apply attached fixes (in place) to fixpoint, re-lint.

When two fixes target the same node, apply_fixes() skips the later one — so a single pass may leave correctable violations untouched. The runner re-runs the lint+fix cycle until no fixes apply (or max_passes is reached), which converts the per-call “first-wins” policy into “run to fixpoint” at the user level.

max_passes is a safety cap that surfaces non-idempotent fixes (the loop would otherwise oscillate forever). Five passes is plenty for the v2.0 SAFE fixes — the deepest natural cascade is TYP001 → TYP002 on the same node, which converges in two.

The document is mutated in place. Callers that need to write the result back to disk should serialise via the markdown renderer when LintFixResult.rewritten is true.

lint_and_fix_file(file_path: str | Path, *, max_safety: FixSafety = FixSafety.SAFE, write: bool = True) LintFixResult

Parse file_path, lint+fix, and (optionally) rewrite the file.

When write=True (the default) and at least one fix was applied, the mutated AST is serialised back to file_path via all2md.renderers.markdown.MarkdownRenderer. A clean file (no fixes applied) is never rewritten, sidestepping spurious renderer-canonicalisation drift.

class all2md.linter.RuleRegistry

Bases: object

Singleton registry mapping rule codes to LintRule classes.

Return the process-wide singleton instance.

static __new__(cls) RuleRegistry

Return the process-wide singleton instance.

register(rule_cls: type[LintRule]) None

Register a rule class by its code attribute.

unregister(code: str) bool

Remove a registered rule by code. Returns True if found.

get_rule(code: str) type[LintRule]

Return the rule class for code or raise KeyError.

has_rule(code: str) bool

Return True if code is registered.

list_rules(category: str | None = None) list[str]

List all registered rule codes, optionally filtered by category.

get_all_rules() list[type[LintRule]]

Return all registered rule classes, sorted by code.

iter_rules() Iterable[type[LintRule]]

Iterate over registered rule classes.

clear() None

Remove every registered rule and reset initialisation state.

Primarily intended for tests that need a clean slate.

discover_plugins() int

Load rule classes from the all2md.lint_rules entry point group.

Each entry point is expected to return a LintRule subclass. Failures are logged but do not abort discovery.

class all2md.linter.Severity

Bases: IntEnum

Severity levels for lint violations.

Higher numeric value means more severe. The ordering enables simple threshold filtering: drop any violation whose severity is below the configured threshold.

INFO = 1
WARNING = 2
ERROR = 3
classmethod from_name(name: str) Severity

Parse a severity from a case-insensitive name (info/warning/error).

property label: str

Lowercase label suitable for human output (‘error’, ‘warning’, ‘info’).

__new__(value)
class all2md.linter.Violation

Bases: object

A single lint violation emitted by a rule.

rule_code: str
rule_name: str
message: str
severity: Severity
line: int | None
column: int | None
node_type: str | None
suggestion: str | None
context: str | None
fix: 'LintFix' | None
property fixable: bool

True iff an auto-fix is attached to this violation.

to_dict() dict

Serialize the violation to a plain dict (used by the JSON reporter).

__init__(rule_code: str, rule_name: str, message: str, severity: Severity, line: int | None = None, column: int | None = None, node_type: str | None = None, suggestion: str | None = None, context: str | None = None, fix: 'LintFix' | None = None) None
all2md.linter.apply_fixes(doc: Document, violations: list['Violation'], max_safety: FixSafety) tuple[list[AppliedFix], list[AppliedFix]]

Apply every fix attached to violations whose safety is <= max_safety.

Conflict policy: when two fixes target the same node (by id()), the first one (in deterministic outer-to-inner, top-to-bottom order) is applied; subsequent fixes targeting that node are deferred and returned in skipped_conflicts. Users re-run --fix to converge.

Returns:

Two lists of AppliedFix records — the first describes fixes that ran, the second describes fixes that were deferred because an earlier fix already touched their target.

Return type:

(applied, skipped_conflicts)

all2md.linter.lint_and_fix_document(doc: Document, config: LintConfig | None = None, *, file_path: str | None = None, max_safety: FixSafety = FixSafety.SAFE) LintFixResult

Lint and fix doc in place; re-lint and return the combined result.

all2md.linter.lint_and_fix_file(file_path: str | Path, config: LintConfig | None = None, *, max_safety: FixSafety = FixSafety.SAFE, write: bool = True) LintFixResult

Parse, lint+fix, and rewrite file_path.

all2md.linter.lint_document(doc: Document, config: LintConfig | None = None, file_path: str | None = None) LintResult

Lint an already-parsed Document.

all2md.linter.lint_file(file_path: str | Path, config: LintConfig | None = None) LintResult

Parse file_path into an AST and lint it with config.

For a high-level overview and worked examples, see Linter.