Skip to content

Configuration model

Configuration is the product. This document sketches its shape; exact option names are provisional and will be settled by tests-first API work.

File formats

TOML is the primary format; YAML and package.json are supported with the same schema and keys. See ADR 0001 for the rationale.

File Format
rainbow.toml, .rainbow.toml TOML
rainbow.yaml, rainbow.yml, .rainbow.yaml, .rainbow.yml YAML (1.2 core schema)
pyproject.toml — [tool.rainbow] table TOML
package.json — "rainbow" key JSON

More than one rainbow configuration in the same directory is an error; rainbow-fmt never picks one silently.

Finding the configuration

For each formatted file, rainbow-fmt looks in the file's directory, then in each parent directory up to the filesystem root, and uses the nearest configuration. A pyproject.toml without [tool.rainbow] or a package.json without "rainbow" does not count. Configurations in directories further up are not merged in; combine files explicitly with extends.

extends

# rainbow.toml
extends = ["../team/style.yaml", "local.toml"]   # or a single path

Paths are relative to the file that names them and may be in any supported format (a pyproject.toml or package.json contributes its rainbow section). Extended files may extend others; a cycle is an error. Later files override earlier ones, and the extending file overrides them all:

  • tables are merged key by key;
  • [[override]] and [[rule]] entries are concatenated, base entries first (so the extending file's rules win);
  • every other value, arrays included, is replaced.

Files

[files]
exclude = ["tests/fixtures/**", "*.generated.json"]  # skipped when a directory is walked
ignore_unknown = true   # no warning for explicitly named files of unknown type
editorconfig = false    # do not read .editorconfig files (default: true)
verify = false          # skip verifying formatted output (default: true; see cli.md)
cache = false           # neither skip nor record formatted files (default: true)
jobs = 1                # worker processes; 0 (default) is one per CPU (working directory's configuration)

See cli.md.

Values, positions and errors

Every format loads into the same data: tables, arrays, strings, integers, floats and booleans. null (YAML, JSON) and dates (TOML, YAML) are rejected, and so are duplicate keys. Every value keeps its position, so errors name the file, line and column (in characters):

rainbow.yaml:2:16: core.line_breaks: expected one of "preserve", "off", "fit"; got boolean false; if you meant the string "off", quote it
rainbow.toml:4:1: rule[0] (json/pair): unknown name 'nope'

TOML is read with the standard library's tomllib; YAML with ruamel.yaml (1.2 core schema); package.json with a small built-in parser that tracks positions (ADR 0001, amendment 1).

Example

# rainbow.toml
preset = "rainbow:balanced"     # starting point; optional

[core]
max_width      = 100
indent_style   = "space"        # "space" | "tab" | "preserve"
indent_size    = 4
line_ending    = "lf"           # "lf" | "crlf" | "preserve"
final_newline  = true

[shared]                        # cross-language vocabulary
quotes         = "single"       # "single" | "double" | "preserve" | "fewest-escapes"
trailing_comma = "multiline"    # "never" | "always" | "multiline" | "preserve"
blank_lines    = { max = 2, between_top_level = "preserve" }
align          = { assignments = true, object_values = false }

[language.python]
trailing_comma    = "never"     # overrides [shared] for Python only
bracket_wrap      = "fit"       # "magic_trailing_comma" | "fit" | "preserve"
top_level_blank_lines = 2

[language.javascript]
semicolons        = "as-needed" # "always" | "as-needed" | "preserve"
arrow_parens      = "preserve"
brace_style       = "1tbs"      # "1tbs" | "allman" | "stroustrup" | "preserve"

[language.svelte]
indent_script_and_style = false

[[override]]                    # path-glob overrides, applied in order
files = ["legacy/**/*.js"]
core.indent_size = 2
language.javascript.semicolons = "preserve"

[[rule]]                        # user-level rule override (ADR 0005)
language = "css"
select   = "declaration"
template = "[field('property'), ': ', field('value'), ';']"

The same configuration (abridged) in YAML:

# rainbow.yaml
preset: rainbow:balanced

core:
  max_width: 100
  indent_style: space
  indent_size: 4

shared:
  quotes: single
  trailing_comma: multiline
  blank_lines: { max: 2, between_top_level: preserve }

language:
  python:
    bracket_wrap: fit
  javascript:
    semicolons: as-needed

override:
  - files: ["legacy/**/*.js"]
    core: { indent_size: 2 }
    language: { javascript: { semicolons: preserve } }

Note: YAML is read with the 1.2 core schema, so unquoted off, no, and on are strings, not booleans. A boolean supplied where an enum is expected is rejected with a message suggesting quotes.

Option schema

Each option is declared once, with a name, a type (integer with a minimum, a choice of strings, or a boolean), a default and a one-sentence description (rainbow_fmt.options). Core, shared and [files] options are declared centrally; each language pack declares its own (extending.md). options.md is the complete reference, generated from the declarations. Planned: node scope for inline directives, and before/after examples in the reference.

The options most often set:

Key Values Default
core.max_width integer ≥ 1 80
core.indent_style "space", "tab" "space"
core.indent_size integer ≥ 0 4
core.tab_width integer ≥ 1 4
core.line_ending "lf", "crlf", "preserve" (the input's first line ending) "lf"
core.max_blank_lines integer ≥ 0: blank lines kept between members, at most (none are added) 1
shared.trailing_comma "never", "multiline" "never" ("multiline" for Python, JavaScript and TypeScript)
language.json.trailing_comma "never" (JSON has no trailing commas) "never"
language.json.object_wrap "preserve", "fit", "always" "preserve"
language.json.align_values boolean false
language.css.selector_list "one_per_line", "fit" "one_per_line"
language.css.rule_wrap "always", "fit", "preserve" "always"
language.css.last_semicolon "always", "never", "preserve" "always"
language.python.bracket_wrap "magic_trailing_comma", "fit", "preserve" "magic_trailing_comma"
language.python.binary_operator_break "before", "after" "before"
language.python.definition_blank_lines "enforce", "cap", "preserve" "enforce"
language.python.top_level_blank_lines integer ≥ 0 2
language.python.nested_blank_lines integer ≥ 0 1
language.python.inline_comment_spaces integer ≥ 1 2
language.python.statement_blank_lines "preserve", "separate" (blank lines around loops and after if statements in functions) "preserve"
language.python.docstrings "preserve", "aligned" (closing quotes on their own line, continuation lines under the first letter) "preserve"
language.python.bracket_hug boolean: a list of dicts as [{ … }, { … }] false
language.javascript.return_semicolons "as_statements", "unless_last" (a return followed by another statement ends with ;) "as_statements"
language.javascript.bracket_hug boolean: an array of objects as [{ … }, { … }] false
language.html.whitespace_sensitivity "css", "strict", "ignore" (html.md) "css"
language.html.bracket_same_line boolean false
language.html.void_elements "no_slash" (<br>), "slash" (<br />), "preserve" "no_slash"
language.svelte.whitespace_sensitivity, language.svelte.bracket_same_line as for HTML (svelte.md)
language.yaml.sequence_indent "indent", "none" (yaml.md) "indent"
language.sql.keyword_case "preserve", "upper", "lower" (sql.md) "preserve"
language.sql.clause_alignment "left", "right" (sql.md) "left"
language.markdown.fenced_code "format", "preserve" (markdown.md) "format"
language.markdown.table_width integer ≥ 1 (markdown.md) 180
files.exclude list of patterns (as in [[override]] files, relative to the configuration file): files and directories a directory walk skips; a preset's patterns add to the project's; a file named on the command line is always formatted []
files.ignore_unknown boolean false
files.editorconfig boolean true
files.verify boolean true
files.cache boolean true
files.jobs integer ≥ 0 (0: one per CPU) 0

Any core or shared option may also be set in a [language.X] section, for that language only. Language options may appear only in their language's section.

A language may narrow a core or shared option, allowing fewer values. The option then belongs to the language: values in [core] or [shared] (and --set core.…/shared.…) do not apply to it, and a disallowed value in [language.X] is an error. JSON narrows trailing_comma to "never", because trailing commas are not valid JSON; [shared] trailing_comma = "multiline" applies to other languages only.

A language may also change only the default of a core or shared option. Values in [core] and [shared] still apply to it. Python's default for trailing_comma is "multiline" (PEP 8, Black); [shared] trailing_comma = "never" applies to Python too. rainbow-fmt options shows such a default as language.python.trailing_comma.

Everything is validated when a file is formatted: unknown keys, wrong types and malformed [[override]] sections are errors that name the file, line and column. Sections for languages rainbow-fmt does not know (for example [language.html] today) are ignored, so a configuration can be written before a language pack exists.

Resolution order (lowest → highest precedence)

  1. Built-in defaults from the schema
  2. Presets — named bundles: rainbow:balanced, rainbow:minimal, rainbow:black, rainbow:prettier and rainbow:styleguide (team presets published as packages are planned)
  3. .editorconfig (mapped to core options)
  4. Project config: any file listed under File formats — nearest directory wins; extends combines files (a target may be in a different format)
  5. [[override]] sections whose files patterns match, in order (later sections win)
  6. Inline directives in the source (rainbow: set, for one member)
  7. CLI flags (--set language.python.bracket_wrap=fit)

A higher level always wins, whatever section it is written in. Within a level, [language.X] beats [shared] beats [core] for the same concept. For example, an override's core.indent_size beats the project's [language.json] indent_size.

Presets

preset = "rainbow:balanced"                       # or a list; later presets win
Preset Sets
rainbow:balanced core.max_width = 100, core.indent_size = 2, core.line_ending = "lf", shared.trailing_comma = "never", language.json.object_wrap = "fit"
rainbow:minimal core.max_width = 120, core.line_ending = "preserve", language.json.object_wrap = "preserve"
rainbow:black Black's layout where an option reaches it: 88 columns, 4 spaces, trailing_comma = "multiline", the magic trailing comma, blank lines enforced; strings, parentheses and docstrings stay as written, unlike Black
rainbow:prettier Prettier's defaults where an option reaches them: 80 columns, 2 spaces, semicolons, bracket_spacing, object_wrap = "preserve", void_elements = "slash"; quotes and parentheses stay as written, and a line Prettier would break inside a tag stays long
rainbow:styleguide This repository's STYLEGUIDE.md (how it was derived): 4 spaces, 99 columns (120 for HTML and Svelte), trailing_comma = "multiline", blank lines between the thoughts of a function, aligned docstrings, bracket_hug, semicolons only after a return that is not last, void_elements = "no_slash"; Django migrations directories excluded

Presets are configuration files shipped with rainbow-fmt (rainbow_fmt/presets/); their contents grow with the option set. No preset applies unless the configuration names one (ADR 0006). preset is an ordinary top-level setting, so it passes through extends; a preset cannot itself name presets, and only the built-in rainbow: names are accepted.

.editorconfig

For each file, .editorconfig files are read from the file's directory upwards, stopping after one with root = true; sections whose glob matches the file apply, later sections and nearer files winning (EditorConfig semantics, including **, {a,b} and {1..3} in globs).

EditorConfig property Option
indent_style core.indent_style
indent_size core.indent_size (tab is ignored)
tab_width core.tab_width
max_line_length core.max_width (off is ignored)
end_of_line core.line_ending (cr is ignored)

Invalid values are ignored, as the EditorConfig specification requires, and unset removes an earlier value. .editorconfig is below the project configuration: a value in rainbow.toml wins. Turn it off with [files] editorconfig = false.

[[override]]

[[override]]
files = ["legacy/**/*.json", "*.jsonc"]
core.indent_size = 2
language.json.object_wrap = "fit"

files is required. Patterns are matched against the file's path relative to the directory of the configuration file that contains the override: * matches within one directory, ? one character, [abc] one of a set ([!abc] none of it), and ** any number of directories. A pattern without / matches the file name in any directory. An override may contain core, shared and language sections.

--set

rainbow-fmt format --set core.max_width=100 --set shared.trailing_comma=multiline . sets options for every file, above all configuration files. The value is a TOML value; a bare word is a string. [files] settings cannot be set this way (use -u for ignore_unknown, --no-verify for verify, --no-cache for cache, --jobs for jobs).

Seeing the result

rainbow-fmt options FILE prints the options that apply to FILE and where each value came from (cli.md).

Inline directives

Comments in the source can switch formatting off, skip one member, or change options for one member. They use each language's comment syntax (#, //, /* … */):

# rainbow: off
MATRIX = [
    1, 0, 0,
    0, 1, 0,
    0, 0, 1,
]
# rainbow: on

x = compute( )  # rainbow: skip-line

# rainbow: set trailing_comma=never
def f(a, b, c): ...
Directive Where Effect
rainbow: off … rainbow: on own lines everything between is printed as written; without on, to the end of the block, object or list
rainbow: skip-next own line the next member is printed as written
rainbow: skip-line after code the member it follows is printed as written (for a Python if, def …: its header)
rainbow: set KEY=VALUE … own line the next member is formatted with these option values

A member is a Python, JavaScript or TypeScript statement, a class member, an interface or type literal member, or an item of a bracketed list, a JSON object or array member (or the document's value), a CSS rule, at-rule or declaration. KEY is an option name without its section (trailing_comma, bracket_wrap); VALUE is written as for --set. Options the printer applies to the whole file (max_width, indent_style, indent_size, tab_width, line_ending) cannot be set by a directive. Python and JavaScript statements printed as written move as a whole to their block's indentation; their relative indentation, multi-line strings, template literals and JSX are kept.

Other tools' comments are honoured too: Black's # fmt: off, # fmt: on and # fmt: skip (like skip-line) in Python, and Prettier's prettier-ignore (like skip-next) in JSON, CSS, JavaScript and TypeScript. Where they cannot apply, they are ordinary comments.

A rainbow: comment that is not a valid directive (# rainbow: of; a comment of several words whose first is no directive, such as # rainbow: a formatter, is prose and left alone), is on the wrong kind of line, has nothing to act on (skip-next at the end of a block, on without off) or sits where no member is (inside an expression) is an error: the file is left unchanged and the error names its line and column:

error: app.py:12:5: unknown directive 'rainbow: of', not formatted

preserve

preserve means: for this decision, reproduce what the source did. It is implemented per option by consulting the trivia and tokens recorded in the CST. Examples:

  • quotes = "preserve" keeps each string's original quote character.
  • trailing_comma = "preserve" keeps a trailing comma if and only if one was present.
  • line_breaks = "preserve" treats every source newline inside a construct as a hard break, and never joins lines.
  • blank_lines.* = "preserve" keeps the original count (still capped by blank_lines.max, if set).

Provenance and explain

Every resolved value records its origin: the default, a preset, an .editorconfig line and section, a configuration position (and the override[N] it belongs to), or --set. rainbow-fmt options FILE shows them today; explain (planned) will show them per source position:

$ rainbow-fmt explain src/app.py:42
line 42, col 5  string literal
  quotes = "double"
    set by  rainbow.toml:14  [language.python]
    overrides [shared] quotes = "single" (rainbow.toml:9)
  rule    python/string  (rainbow_fmt.languages.python.format:88)