Imported from jolars/dprint-plugin-badness (
AGENTS.md). Install upstream withnpx skills add jolars/dprint-plugin-badness. Copyright stays with the author.
Agent instructions
This file provides guidance to coding agents when working with code in this repository.
What this is
A thin dprint Wasm plugin that wraps the
badness-formatter crate so the
badness LaTeX/BibTeX formatter can run inside dprint. The plugin holds no
formatting logic of its own; it resolves the file's kind from its path, maps
dprint configuration onto a badness_formatter::FormatStyle, and forwards the
file text.
badness-formatter is the only badness dependency: it re-exports
badness-parser's parser, syntax, semantic, and ast modules plus the
rowan it is built against, so everything the plugin needs is reachable through
it without a second version-locked dependency.
This crate is released independently of the main badness CLI (which lives in the
jolars/badness repo). The separate repo exists so the plugin.wasm release
asset does not pollute badness's v* GitHub release stream, which the VS Code
extension and install scripts resolve platform binaries from.
Build, lint, test
Only the wasm32-unknown-unknown target produces a usable plugin (the target is
pinned in rust-toolchain.toml). The crate also builds for the host target —
generate_plugin_code! is cfg-gated to target_arch = "wasm32" — so a native
cargo build/cargo test compiles the library without the plugin entrypoints.
That native build exists to run the tests; it is not a usable plugin artifact.
cargo build --release --target wasm32-unknown-unknown # target/wasm32-unknown-unknown/release/dprint_plugin_badness.wasm
cargo test # native; config, formatting, and schema tests
cargo fmt # rustfmt is a git hook
cargo clippy --all-targets -- -D warnings
mod schema_tests generates the config schema with
schemars::schema_for!(Configuration) and asserts the committed schema.json is
in sync (regenerate with UPDATE_SCHEMA=1 cargo test), that it advertises the
real defaults, and that every enum value it advertises is one the parse_*
helpers accept.
Beyond the unit tests, correctness is enforced in CI
(.github/workflows/ci.yml) by a parity + idempotence smoke test: it builds
the wasm plugin, downloads the latest badness CLI release, formats the same
samples through both, and diffs the outputs (they must be byte-identical), then
re-runs dprint fmt to confirm stability. When changing config mapping, mirror
this locally. The plugin must stay byte-for-byte identical to the CLI for
equivalent settings — that is the only invariant that matters here, subject to
the one documented exception below.
The smoke-test samples must not depend on a local .sty: the CLI folds in
signatures scanned from sibling .sty/.cls files (disk_scope_signatures),
which a sandboxed plugin cannot read, so such a sample would diverge legitimately
and turn the parity check into noise. That exception is documented in the README;
do not try to "fix" it here.
Architecture
Everything lives in src/lib.rs:
FileKind— mirrors badness's ownFileKind(src/file_discovery.rs), resolved fromrequest.file_path. Unlike a single-extension plugin this is load-bearing: it decides which pipeline runs (LaTeX vs BibTeX), theLexConfig(.sty/.cls/*.code.texlex under an implicit\makeatletter,.dtxruns the docstrip mode), and the default wrap mode.*.code.texis matched on the file name, sincePath::extensionsees onlytex. Unknown extensions fall back toTex, mirroringfile_kind_or_tex— dprint has already decided the file is ours.Configuration— the dprint-facing config struct (camelCase,deny_unknown_fields). Enum-valued options are stored asStringand parsed lazily, but their schema is borrowed from the formatter's own enums with#[schemars(with = "WrapMode")](badness-formatter'sschemafeature), soschema.jsonenumerates badness's real values instead of restating them. Those wire values arebadness.toml's kebab-case spellings (single-line), so the two config files agree on everything but the key casing. When the formatter grows an option, add the mirror here.parse_wrap/parse_math_wrap/parse_line_ending— map a string onto the formatter enum, pushing aConfigurationDiagnosticon an unknown value. Each runs twice: once inresolve_configpurely to collect diagnostics, and again inbuild_styleto produce the real value. These are still hand-written (the diagnostics enumerate the accepted values), but they can no longer drift from the schema:schema_tests::every_advertised_value_parsesfeeds every value the generated schema advertises through them, so a new upstream variant fails the test instead of being silently rejected at format time.validate_width— mirrorsbadness.toml's1..=1000bound on both widths.default_line_ending— seedslineEndingfrom dprint's globalnewLineKind. dprint has no equivalent of badness'snative, and itsautomeans what badness's does, so an unset global falls back toautoeither way.build_style— the whole config mapping.wrapis resolved per file: the config value if set, elsekind.default_wrap(). That per-file resolution is what every call site in the badness CLI does; a plugin that hard-defaultedwrapwould reflow package sources.expand_to_top_level_blocks+format_text_range— the range-format path, ported from badness's LSP (src/lsp.rs).format_node_range_with_signatures_sentenceassumes a block-aligned range, so the selection is first widened to the cover of everyROOTchild node it overlaps, and the splice covers that expanded range. A selection touching no block is a no-op. BibTeX has no range entry (badness's LSP does not offer one either), so.bibfalls back to a whole-file format.SyncPluginHandlerimpl —resolve_configreads the dprint globals and validates;formatdecodes UTF-8, resolves the kind, and dispatches tobib::format_with_style,format_with_style_flavored_sentence, orformat_text_range, returningOk(None)when the output equals the input. The whole thing is wrapped incatch_unwindso an unexpected panic becomes aFormatErrorrather than tearing down the wasm instance.generate_plugin_code!— the wasm entrypoints, cfg-gated totarget_arch = "wasm32".
Parse errors are surfaced as format errors, deliberately. badness's formatter
only operates on a clean parse (FormatError::ParseErrors), and badness format
refuses such a file too. Do not add a fallback that passes unparseable input
through — that would hide exactly the divergence the parity test exists to catch.
FILE_EXTENSIONS is the set the plugin claims in dprint. Keep it aligned with
what badness format itself walks (badness's src/file_discovery.rs,
lint_file_kind) — not a superset — so the plugin never formats something
the CLI would skip.
The sandbox constraint
dprint Wasm plugins get exactly these host imports: fd_write,
host_has_cancelled, host_write_buffer, host_format,
host_get_formatted_text, host_get_error_text. There is no filesystem
access. Everything the plugin needs must arrive through the config or the file
text; do not try to add file reading here — it is not a missing feature, it is a
hard platform limit. (A dprint process plugin would have OS access, but that is
a different, unsandboxed, per-platform-binary product.) This is also why
badness-formatter must stay wasm32-unknown-unknown-clean, an invariant
badness's own CI enforces, and why the local-package signature scope is
unavailable here.
Bootstrapping (remove once the release is out)
Cargo.toml depends on the sibling checkout by path, not on crates.io, because
the published badness-formatter 0.2.0 has no schema feature — the plugin
needs it to borrow the formatter's enum schemas. This does not build in CI;
switch to badness-formatter = { version = "0.3", features = ["schema"] } once
that release is out, and delete this section.
Releasing
Versioning is managed by versionary
(versionary.jsonc, release-type: rust). Pushing a v* tag triggers
publish-dprint-wasm.yml, which builds the wasm, names it plugin.wasm, writes
a plugin.wasm.sha256, copies the generated schema.json, and uploads all three
to the matching GitHub release. The asset must be named plugin.wasm: that is
the name the plugins.dprint.dev service resolves
plugins.dprint.dev/jolars/badness-<tag>.wasm to. The version the plugin
reports, its update_url, and its config_schema_url all come from
CARGO_PKG_VERSION, so the crate version must match the release tag.
bump-badness-formatter.yml watches crates.io daily and opens a releasable
feat:/fix: PR when a new badness-formatter lands (dependabot deliberately
ignores that crate). When bumping, expect build_style and the parse_* helpers
to need updates if the upstream config API changed; the CI build and parity steps
exist specifically to catch that drift.