badness 0.11.0

A language server, formatter, and linter for LaTeX
//! Rendering the bib linter-rules reference from rule metadata.
//!
//! The bib analog of [`crate::linter::docs`], sharing its section renderer
//! ([`render_rule_section`]) so the two references cannot drift in shape.
//! [`render_rule_doc`] and [`render_reference_page`] are the single source of
//! truth shared by the snapshot test (`tests/bib_rule_docs.rs`) and the docs
//! generator (`examples/docgen.rs`), so the committed
//! `docs/src/reference/bib-linter-rules.md` and the pinned snapshots can never
//! diverge from behavior. Every example is linted by the *real* bib driver
//! ([`demo_diagnostics`]), so the rendered diagnostics and the autofix
//! before/after always reflect the current rules.

use std::path::PathBuf;

use crate::bib::linter::check::lint_document;
use crate::bib::linter::rules::{BibRule, all_rules};
use crate::bib::parse;
use crate::bib::semantic::Model;
use crate::linter::diagnostic::Diagnostic;
use crate::linter::docs::{RuleDocSection, render_rule_section};

/// The synthetic path an example snippet is linted under. Every bib rule is
/// single-file-sound, so one fixed name serves all of them (no per-rule
/// `example_path` the way path-gated LaTeX rules need).
fn example_path() -> PathBuf {
    PathBuf::from("references.bib")
}

/// Lint `source` as a self-contained `.bib` file: parse, build the semantic
/// [`Model`], and run every built-in bib rule.
pub fn demo_diagnostics(source: &str) -> Vec<Diagnostic> {
    let path = example_path();
    let root = parse(source).syntax();
    let model = Model::build(&root);
    lint_document(&path, &root, &model)
}

/// Render the reference *section* for a single bib rule: an `## \`id\``
/// heading, the rule's `description()`, and each example rendered with its live
/// diagnostics and (for a safe autofix) the after state.
pub fn render_rule_doc(rule: &dyn BibRule) -> String {
    let path = example_path();
    render_rule_section(
        &RuleDocSection {
            id: rule.id(),
            description: rule.description(),
            examples: rule.examples(),
            companions: &[],
            example_path: &path,
            lang: "bib",
        },
        &demo_diagnostics,
    )
}

/// Render the reference section for the bib rule with `id`, or `None` if no
/// built-in bib rule has that id. Backs `badness lint --explain <rule>` (after
/// the LaTeX registry misses), reusing the same live-linted rendering as the
/// docs page.
pub fn explain_rule(id: &str) -> Option<String> {
    all_rules()
        .iter()
        .find(|rule| rule.id() == id)
        .map(|rule| render_rule_doc(rule.as_ref()))
}

/// The full `bib-linter-rules.md` reference page: a static preamble, one
/// generated section per rule (registry order), and a static configuration
/// footer.
pub fn render_reference_page() -> String {
    let mut out = String::from(PREAMBLE);
    for rule in all_rules() {
        out.push('\n');
        out.push_str(&render_rule_doc(rule.as_ref()));
    }
    out.push('\n');
    out.push_str(FOOTER);
    out
}

const PREAMBLE: &str = "\
<!-- Generated by `cargo run --example docgen`. Do not edit by hand: edit each \
rule's `description()`/`examples()` in `src/bib/linter/rules/` and regenerate. -->

# BibTeX Linter Rules

`badness lint` runs a parallel set of built-in rules over each `.bib` file's
parse tree and reports a diagnostic for every finding. This page is the
catalogue: one section per rule, keyed by its stable **rule id**. Bib rules
share one id namespace with the [LaTeX rules](linter-rules.md), so the same
`[lint]` `select`/`ignore` (and `--select`/`--ignore`) target both.

Every rule is **on by default**; narrowing happens only through `select`/`ignore`
in the `[lint]` table (see the
[Configuration reference](configuration.md#lint)). Where a rewrite is unambiguous a rule
carries an **auto-fix**: a *safe* fix (shown below as \"After applying the fix\")
is applied by `badness lint --fix`.

Each example below is linted live to produce its diagnostic and fixed output, so
this page never drifts from the rules' actual behavior.
";

const FOOTER: &str = "\
## Suppression

BibTeX has no line-comment token, so per-site suppression rides a structured
`@comment` entry instead of the LaTeX `%` directive. A plain directive
suppresses one rule on the **next entry**:

```bib
@comment{badness-ignore missing-required-field: publisher long gone}
@book{oldbook, title = {An Orphaned Book}}
```

`@comment{badness-ignore-file <id>: ...}` suppresses one rule file-wide, and
`@comment{badness-ignore-file: ...}` suppresses all rules file-wide. Parse
diagnostics (rule id `parse`) are never suppressed.
";