Skip to content

Overview

The problem

Opinionated formatters (Prettier, Black, gofmt, rustfmt-with-defaults) made a trade: consistency in exchange for choice. That trade works well for many teams, but it has real costs:

  • Style is not arbitrary. Alignment, vertical whitespace, and line breaks carry meaning. A hand-aligned table of constants or a deliberately split boolean expression is communication; an opinionated formatter erases it.
  • One size fits one team. Organizations with established conventions (e.g. 4-space JS, tab-indented SCSS, trailing-comma-free Python) must either abandon them or abandon automated formatting.
  • Polyglot repositories get inconsistent. A Svelte + Python project runs 2–4 formatters, each with a different config format, option vocabulary, and ignore-comment syntax — and they disagree about shared concepts such as indentation and quote style.
  • New languages wait. Supporting a DSL (a template language, a config format, an in-house query language) in an existing formatter usually means writing a full printer in the formatter's host language.

The idea

rainbow-fmt separates what the code is (a syntax tree), how it should be laid out (declarative, user-overridable rules), and how to print it (a single, language-agnostic layout engine).

Every formatting decision is an option. Every option has a documented default, and every option can be set to preserve: keep whatever the author wrote. A team can start with "touch nothing but indentation" and tighten decisions one at a time.

Principles

  1. Choice over opinion. If two reasonable developers could disagree about it, it is an option. Defaults exist; mandates do not.
  2. preserve is a first-class value. Any decision can defer to the source text. This makes adoption incremental and non-destructive.
  3. One vocabulary across languages. indent, quotes, trailing_comma, max_width, blank_lines mean the same thing everywhere; languages add options only for concepts that are genuinely specific to them.
  4. Small languages, declarative overrides. A language is a grammar plus a set of small rule functions built from shared helpers; users override any rule from their configuration with a checked expression, never code (ADR 0005).
  5. Never change meaning. Formatting output is verified to be syntactically equivalent to the input (same tree, ignoring trivia) and idempotent (formatting twice yields the same result).
  6. Embedded languages are normal. HTML contains CSS and JS; Svelte contains HTML, TS, and SCSS; Python contains SQL strings and docstrings. Nesting is handled by the core, not re-implemented per language.
  7. Explainable. rainbow-fmt explain reports which rule and which option (from which config file) produced a given piece of output.

Non-goals

  • Linting or semantic transformations (renaming, import sorting beyond whitespace/ordering options, dead-code removal). Rainbow changes layout, not program meaning.
  • Winning benchmarks against single-language native formatters in the first releases. Performance must be good enough for editor-on-save and CI; it is a tuning target, not the design driver.
  • Being "zero-config". Presets make it low-config; zero-config is Prettier's job.

Comparison

Prettier / Black EditorConfig clang-format rainbow-fmt
Languages Fixed set per tool Any (whitespace only) C-family Any, via plugins
Configurability Minimal by design Indent / EOL / charset Very high Very high
Preserve author choices Rarely N/A Some options Every option
Embedded languages Partial No No Core feature
Adding a language Write a printer N/A Not supported Grammar + rule functions
Style inference from code No No No Planned (rainbow-fmt infer)

Glossary

  • CST — concrete syntax tree; the parse tree including every token.
  • Trivia — whitespace and comments; everything the formatter may move.
  • Doc / IR — the intermediate layout representation (text, line breaks, groups, indentation) that the printer turns into output.
  • Rule — a declarative mapping from a syntax-tree pattern to a Doc template, parameterized by options.
  • Language pack — a plugin providing a grammar, rules, option schema, and test fixtures for one language.
  • Injection — a region of one language embedded in another (e.g. <style> in HTML).