use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::PathBuf;
use smol_str::SmolStr;
use crate::linter::check::lint_document;
use crate::linter::diagnostic::{Diagnostic, Fix};
use crate::linter::fix::apply_fixes;
use crate::linter::render::{OutputMode, render_findings};
use crate::linter::rules::{Rule, all_rules};
use crate::parser::parse;
use crate::project::include::BibTarget;
use crate::project::labels::document_label_names;
use crate::project::{
CiteFileFacts, FileFacts, IncludeGraph, ResolvedCitations, ResolvedLabels,
collect_bib_resource_targets, collect_include_edge_keys,
};
use crate::semantic::SemanticModel;
use crate::syntax::SyntaxNode;
fn example_path() -> PathBuf {
PathBuf::from("example.tex")
}
pub fn demo_diagnostics(source: &str) -> Vec<Diagnostic> {
let path = example_path();
let root = SyntaxNode::new_root(parse(source).green);
let model = SemanticModel::build(&root);
let facts = [FileFacts {
path: path.clone(),
include_edges: collect_include_edge_keys(&root, None),
}];
let graph = IncludeGraph::build(&facts, None);
let labels = [(path.clone(), document_label_names(&model), true)];
let resolved_labels = ResolvedLabels::build(&labels, &graph);
let bib_targets = collect_bib_resource_targets(&root, None);
let mut bib_keys: HashMap<PathBuf, Vec<SmolStr>> = HashMap::new();
for target in &bib_targets {
if let BibTarget::Path(p) = target {
bib_keys.entry(p.clone()).or_default();
}
}
let cite_facts = [CiteFileFacts {
path: path.clone(),
bib_targets,
nocite_all: model.has_wildcard_nocite(),
is_document_root: true,
}];
let resolved_citations = ResolvedCitations::build(&cite_facts, &graph, &bib_keys);
lint_document(
&path,
&root,
&model,
Some(&resolved_labels),
Some(&resolved_citations),
)
}
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}");
}
for example in rule.examples() {
let _ = writeln!(out);
if !example.caption.is_empty() {
let _ = writeln!(out, "{}", example.caption);
let _ = writeln!(out);
}
fenced(&mut out, "tex", example.source);
let diagnostics: Vec<Diagnostic> = demo_diagnostics(example.source)
.into_iter()
.filter(|d| d.rule == id)
.collect();
let source = example.source.to_string();
let rendered = render_findings(&diagnostics, OutputMode::Pretty, &|path| {
(path == example_path().as_path()).then(|| source.clone())
});
let _ = writeln!(out);
fenced(&mut out, "text", &rendered);
let fixes: Vec<Fix> = diagnostics.iter().filter_map(|d| d.fix.clone()).collect();
let after = apply_fixes(&source, &fixes, false);
if after.applied > 0 {
let _ = writeln!(out);
let _ = writeln!(out, "After applying the fix:");
let _ = writeln!(out);
fenced(&mut out, "tex", &after.output);
}
}
out
}
pub fn explain_rule(id: &str) -> Option<String> {
all_rules()
.iter()
.find(|rule| rule.id() == id)
.map(|rule| render_rule_doc(rule.as_ref()))
}
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
}
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, "```");
}
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. -->
# Linter Rules
`badness lint` runs a set of built-in rules over each 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**. That id is what appears in a
diagnostic, what `[lint]` `select`/`ignore` (and `--select`/`--ignore`) target,
and what a `% badness-ignore <id>` comment suppresses.
Every rule is **on by default**; narrowing happens only through `select`/`ignore`
(see [Configuration](#configuration)). 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`; an *unsafe* fix, one that may change output
such as inserting a line-breaking tie, 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 diagnostic and fixed output, so
this page never drifts from the rules' actual behavior.
This page covers the **LaTeX** linter. BibTeX files have a parallel set of rules
(a separate `BibRule` registry under `src/bib/linter/`), selectable through the
same `[lint]` config but not yet catalogued here.
";
const FOOTER: &str = "\
## Configuration
Rules are selected through the `[lint]` table in `badness.toml`, or the matching
CLI flags:
```toml
[lint]
# When present, an allowlist: only these rules run.
select = [\"deprecated-command\", \"dollar-display-math\"]
# Applied on top of select (or the default set): these are turned off.
ignore = [\"missing-nonbreaking-space\"]
```
An unknown rule id is reported at lint time, not rejected at config-parse time.
To suppress a rule at a single site, use a comment directive:
```tex
% badness-ignore deprecated-command: legacy code, leave as-is
{\\bf here}
```
`% badness-ignore-file <id>: ...` suppresses one rule file-wide, and
`% badness-ignore-file: ...` suppresses all rules file-wide. Parse diagnostics
(rule id `parse`) are never suppressed by `select`/`ignore`.
";