fatou 0.11.0

A language server, formatter, and linter for Julia
//! Rendering the lint-rule reference from rule metadata.
//!
//! [`render_rule_doc`] and [`render_reference_page`] are the single source of
//! truth shared by the snapshot test (`tests/rule_docs.rs`) and the docs
//! generator (`examples/docgen.rs`), so the committed
//! `docs/src/reference/rules.md` and the pinned snapshots can never diverge
//! from behavior. Every example is linted by the *real* linter, so the rendered
//! diagnostics and the autofix before/after always reflect current behavior.

use std::fmt::Write as _;
use std::path::PathBuf;

use crate::config::LintConfig;
use crate::linter::check::check_source_with_target;
use crate::linter::fix::apply_fixes;
use crate::linter::render::{OutputMode, render_findings};
use crate::linter::rules::Rule;

/// The synthetic path used when linting an example snippet. The same value keys
/// both the lint run and the `render_findings` source lookup.
fn example_path() -> PathBuf {
    PathBuf::from("example.jl")
}

/// Render the reference *section* for a single 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 Rule) -> String {
    let mut out = String::new();
    let id = rule.id();
    let _ = writeln!(out, "## `{id}`");

    let description = rule.description().trim();
    if !description.is_empty() {
        let _ = writeln!(out);
        let _ = writeln!(out, "{description}");
    }

    // Restrict to this rule so an example can't trip a different one.
    let config = LintConfig {
        select: Some(vec![id.to_string()]),
        ..Default::default()
    };
    let path = example_path();

    for example in rule.examples() {
        let _ = writeln!(out);
        if !example.caption.is_empty() {
            let _ = writeln!(out, "{}", example.caption);
            let _ = writeln!(out);
        }
        fenced(&mut out, "julia", example.source);

        let report = check_source_with_target(
            Some(&path),
            example.source,
            &config,
            rule.example_julia_target(),
        );
        let source = example.source.to_string();
        let rendered = render_findings(&report.diagnostics, OutputMode::Pretty, false, &|p| {
            (p == Some(path.as_path())).then(|| source.clone())
        });
        let _ = writeln!(out);
        fenced(&mut out, "text", &rendered);

        // Show the result of the safe fixes, when the example carries any.
        let fixed = apply_fixes(example.source, &report.diagnostics, false);
        if fixed.output != example.source {
            let _ = writeln!(out);
            let _ = writeln!(out, "After applying the fix:");
            let _ = writeln!(out);
            fenced(&mut out, "julia", &fixed.output);
        }
    }

    out
}

/// Write a fenced code block, normalizing the body to end with exactly one
/// newline so the closing fence always sits on its own line (idempotence).
fn fenced(out: &mut String, lang: &str, body: &str) {
    let _ = writeln!(out, "```{lang}");
    let _ = out.write_str(body);
    if !body.ends_with('\n') {
        let _ = out.write_str("\n");
    }
    let _ = writeln!(out, "```");
}

/// The full `rules.md` reference page: a static preamble followed by one section
/// per documented rule (one carrying at least one example), in registry order.
pub fn render_reference_page() -> String {
    let mut out = String::from(PREAMBLE);
    for rule in crate::linter::rules::all_rules() {
        if rule.examples().is_empty() {
            continue;
        }
        out.push('\n');
        out.push_str(&render_rule_doc(rule.as_ref()));
    }
    out
}

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

# Lint Rules

`fatou lint` runs a set of built-in rules over each file and reports a
diagnostic for every finding. This page is the catalogue: one section per rule,
keyed by its stable **rule ID**. That ID is what appears in a diagnostic, what
`[lint]` `select`/`ignore` (and `--select`/`--ignore`) target, what
`[lint.severity]` re-grades, and what a `# fatou-ignore <id>` comment
suppresses.

Most rules are on by default. The few that are opt-in say so in their
description; name one in `select` to run it.

Where a rewrite is unambiguous a rule carries an **autofix**: a *safe* fix
(shown below as \"After applying the fix\") is applied by `fatou lint --fix`; an
*unsafe* fix, one that may change behavior, is applied only with
`--unsafe-fixes` or as an editor code action, so it has no \"after\" block here.

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