Contributing to Badness
Thanks for your interest in Badness, a formatter, linter, and language server for LaTeX. This guide covers everything you need to build the project, run the tests, and get a change merged. Contributions of all sizes are welcome, from typo fixes to new lint rules and parser features.
Getting set up
Badness is a single-crate Rust project (edition 2024). The toolchain is pinned
by rust-toolchain.toml, so a stable rustup install picks up the right
version automatically.
git clone https://github.com/jolars/badness
cd badness
cargo build
If you use Nix with devenv, the dev
shell provides the full toolchain plus the profiling and benchmarking tools
(perf, cargo-flamegraph, hyperfine, cargo-show-asm, cargo-llvm-cov)
and the go-task runner. It loads automatically with direnv.
The task runner is go-task; task --list shows every
available task. The most common ones are below, but every task maps to a plain
cargo invocation if you’d rather not install it.
Building and testing
| Task | Equivalent | What it does |
|---|---|---|
task build | cargo build | Dev build. |
task test | cargo test | Run the whole test suite. |
task fmt | cargo fmt | Format the code. |
task lint | cargo clippy --all-targets --all-features -- -D warnings | Clippy, warnings as errors. |
task check | Everything CI runs: fmt-check, lint, test. |
Run task check before opening a pull request; it mirrors CI exactly.
Badness uses insta for snapshot tests. When a change
deliberately alters formatter or parser output, review and accept the new
snapshots with task snapshots (cargo insta review).
Project layout
Badness parses LaTeX into a lossless concrete syntax tree and builds three tools on top of it:
- a formatter (
badness format) that lays out source deterministically, - a linter (
badness lint) that reports diagnostics, and - a language server (
badness lsp).
The architecture follows rust-analyzer:
- A hand-written, error-tolerant lexer and parser turn LaTeX into a flat
token stream, then an event stream (
Start/Tok/Finish), which a tree builder re-attaches trivia to and feeds into rowan to produce the lossless tree. - A semantic layer (a signature database) assigns meaning (arity, verbatim-ness, sectioning) on top of the generic tree. Meaning never leaks into the parser.
- The formatter lowers the tree into a Wadler-style
DocIR, laid out under a flat/break fit model. - Incremental recomputation is salsa-first.
The source lives in one crate, organized into module folders: parser/,
formatter/, linter/, semantic/, project/, text/, plus syntax.rs and
incremental.rs.
Invariants
Three properties are held by construction and enforced as test oracles. A change that breaks any of them is a bug, not a trade-off:
- Losslessness:
reconstruct(text) == text, byte-for-byte. The parser never loses or corrupts input. - Idempotence:
format(format(x)) == format(x). - Protected regions: verbatim-like content (
verbatim,lstlisting,\verb, comments) is never altered by the formatter.
The formatter may normalize structure on purpose (for example, x^{2} becomes
x^2); it preserves meaning, not the exact parse tree.
A couple of ground rules keep the design coherent:
- Keep the syntactic layer free of semantic knowledge. Parsing is the parser’s job; layout is the formatter’s job.
- New parser features need corpus and snapshot tests and a losslessness assertion.
Making a change
- Prefer trunk-based development and atomic commits. Branch first for
substantial changes; small fixes can go straight to
main. - Follow Conventional Commits, for
example
feat(linter): add missing-required-argument ruleorfix(parser): recover at unbalanced brace. TheCHANGELOG.mdis generated from the commit history by versionary, so a clear, well-scoped commit message is what shows up in the release notes. Don’t hand-editCHANGELOG.md. - Keep commit subjects short (imperative mood, ideally under 60 characters) and
use the body for rationale. Close issues with
Fixes #123in the body. - A rustfmt git hook rewrites unformatted files and aborts the commit, so run
cargo fmtfirst. Clippy warnings are treated as errors.
Adding a lint rule
New lint rules implement the Rule trait, register in the rule list, ship unit
and integration tests with a losslessness-safe fix, and regenerate the rules
reference with task docs:rules. Look at the existing rules in
src/linter/rules/ for the pattern.
Documentation
User-facing docs are an mdBook under
docs/. Preview them locally with task docs:serve (live reload) or build them
with task docs. The linter-rules reference and the benchmark page are
generated; regenerate them with task docs:rules and task bench respectively
rather than editing the rendered pages by hand.
A note on AGENTS.md
The repo also contains an AGENTS.md file. It is a detailed,
decision-by-decision record of the architecture aimed at AI coding agents.
Humans are welcome to read it for the deep rationale behind a design choice, but
this file is the contributor guide you should start from.
License
By contributing, you agree that your contributions are licensed under the project’s MIT License.