Skip to content
Molto

What style translates to

Every flag molto fmt and molto lint take, the exact command each one runs, and the clang-format and clang-tidy configuration Molto writes from format.json and linter.json.

Formatting and linting is the argument: Molto owns one vocabulary and renders it as the backend’s own. This page is the receipt — every flag, every key, and the exact bytes that end up in .bin/style/.

molto fmt

Writing is the default. --check and --diff are two ways of asking what it would do without doing it, and passing both is a usage error: two answers to one question.

Flag Short What it does
--check -c Report what would change and write nothing; exits 1 if any
--diff -d Print the unified diff to stdout and write nothing; exits 1 if any
--refresh-tools Ask pickup again which formatter this machine has
--refresh-analysis Format every file again instead of skipping what did not change
--jobs <n> -j Format at most n files at once; every core by default

There is no --profile and no --format. A profile decides which defines are in force, and a formatter never sees a define; the output shape is a diff or a count, and neither is negotiable.

molto lint

Flag Short What it does
--profile <name> -p debug (default), release, bench, custom
--format <fmt> -f rich, text, json. No default — see below
--refresh-toolchain Resolve the compiler again instead of reusing the cached one
--refresh-tools Ask pickup again which linter this machine has
--refresh-analysis Analyse every file again instead of replaying what did not change
--jobs <n> -j Analyse at most n files at once; every core by default

-j takes a count between 1 and 1024. Anything else — zero, a negative, a word, a number with a suffix — is refused with the value quoted back, and exits 4. A build told to take two workers and silently taking the whole machine is worse than one that stops.

The parser accepts --opt value, --opt=value, -o value and bare boolean flags, and --help is always there.

What each command reads

Command Directories Extensions
fmt src/, include/ .c .cpp .cc .h .hpp .hh
lint src/ .c .cpp .cc

fmt includes headers because a header is not a translation unit but it is code. lint does not analyse headers on their own — a header is analysed as part of every source that includes it, and the compiler pass records which ones it read so a change to any of them re-analyses the source.

Both then subtract what exclude matches. A directory that is not there is not a failure: a project with no src/ has nothing to do and exits 0.

Streams and exit codes

Command stdout stderr
molto fmt Whatever the formatter said N files formatted
molto fmt --check Whatever the formatter said N files would change
molto fmt --diff The unified diff N files would change
molto lint The diagnostics Checking (debug), 2 errors, 7 warnings
molto lint -f json The JSON document Nothing

So molto fmt --diff > patch yields a patch and nothing else, and molto lint --format json leaves stdout parseable — the header and the tally are suppressed rather than moved.

Code When
0 Nothing would change; no error-severity diagnostic
1 fmt --check/--diff found a file; lint produced an error; a tool would not run
2 format.json or linter.json is invalid; no Project.toml above here
3 fmt found no formatter on this machine; lint found no toolchain for [target]
4 Bad usage: --check with --diff, an unknown profile, an unknown --format, a bad -j

A warning is reported and still succeeds. Only fmt exits 3 for a missing tool — a missing linter is not an error, because the compiler pass is what molto lint promises with nothing installed.

--format has no default on purpose

Absent is not an implicit text. Absent means “whatever suits the stream”: rich at a terminal, text through a pipe. NO_COLOR drops the colour from rich but not the frames. Write the flag down when the output has to be the same wherever it goes.

What actually runs

molto fmt, once per file:

clang-format --style=file:.bin/style/clang-format.yaml -i               src/foo.c   # write
clang-format --style=file:.bin/style/clang-format.yaml --dry-run --Werror src/foo.c # --check
clang-format --style=file:.bin/style/clang-format.yaml                  src/foo.c   # --diff

--Werror is what makes --dry-run exit non-zero exactly when the file would change. -i exits zero either way, so a write reads the file back afterwards to know whether it changed at all. In diff mode the formatter’s stdout is the formatted file, so nothing else may be mixed into it and Molto computes the diff itself, with three lines of context.

molto lint, twice per source:

gcc -fsyntax-only -fdiagnostics-color=never src/foo.c \
    -MMD -MF .bin/lint/src/foo.c.d \
    -Wall -Wextra -Wpedantic  <std, defines, includes and flags of [target] and the profile> \
    -I<root>/src

clang-tidy --config-file=.bin/style/clang-tidy.yaml --quiet src/foo.c -- \
    <the same compile arguments> -I<root>/src

Three things are deliberate there. -O and -g are not passed: a syntax-only pass stops before optimisation, so -O2 would advertise -Wmaybe-uninitialized and friends without being able to deliver them. The preset’s warnings go first, because the last flag wins in gcc and clang, and a preset -Wall appended after your -Wno-unused would re-enable what you turned off. And the defines do go through, because #ifdef decides what even compiles: linting with the wrong profile analyses code the build never sees. Both passes run under the project’s [env].

format.json, key by key

Ten lines are written every run, whether or not the file exists:

# Generated by molto from format.json. Do not edit.
BasedOnStyle: LLVM
Standard: c++03
IndentWidth: 4
ColumnLimit: 100
UseTab: Never
BreakBeforeBraces: Attach
PointerAlignment: Right
SortIncludes: CaseSensitive
SpaceBeforeParens: Never
ReflowComments: true
Key clang-format option false / left true / right
indent_width IndentWidth 1–512 1–512
line_width ColumnLimit 1–512 1–512
use_tabs UseTab Never ForIndentation
pointer_alignment PointerAlignment Left Right
sort_includes SortIncludes Never CaseSensitive
space_before_paren SpaceBeforeParens Never ControlStatements
column_limit_comments ReflowComments false true
brace_style BreakBeforeBraces
attach Attach
break Stroustrup
linux Linux
allman Allman

ForIndentation rather than Always: the canonical key promises to indent with tabs, and Always would put them inside alignment too. ReflowComments is emitted as a boolean and not as the Always/Never enum that clang-format 20 introduced, because clang-format 19 refuses the whole file over the enum — and a rejected configuration leaves molto fmt reporting that it formatted nothing rather than that it could not.

Standard comes from the manifest

There is no key for the language. .h carries none in its extension and clang-format has no C mode at all — it parses everything as C++ and Standard only picks the dialect.

[target].cpp_std Standard
Absent — a C project c++03
c++03, gnu++03 c++03
c++11, gnu++11 c++11
c++14, gnu++14 c++14
c++17, gnu++17 c++17
c++20, gnu++20, c++2a, gnu++2a c++20
Anything else Latest

c++03 is the point of the exercise for C: it is the only dialect in which requires, concept and co_await are ordinary identifiers, which is what they are in C.

Everything Molto has no word for

BasedOnStyle: LLVM is the base, and the ten keys are layered on top of it. That is most of clang-format: LLVM decides your continuation indent, your alignment, your short-function rule. Seven keys is what RFC-0005 opened with, not the ceiling.

preset in format.json changes nothing today. It is read and validated — molto and none are accepted, kernel and gnu are refused by name — but the base is BasedOnStyle: LLVM either way. In linter.json it has teeth.

linter.json, rule by rule

# Generated by molto from linter.json. Do not edit.
Checks: '-*,clang-diagnostic-*,bugprone-*,readability-magic-numbers,-modernize-*'
WarningsAsErrors: 'bugprone-*'

The list is composed in one order, always:

  1. -* first. clang-tidy composes what it is given on top of its own default, which is clang-analyzer-*. Clearing it is what makes the generated file say exactly what linter.json says — and keeps a different clang-tidy, whose default is its own to change, from enabling a different set on the next machine.
  2. clang-diagnostic-*,bugprone-* when preset is molto, which is the default. none appends nothing.
  3. Each rule, in the order you wrote it. warn and error append the rule’s checks, skipping one the preset already enabled. error also appends them to WarningsAsErrors. off appends each positive check negated and drops the ones that were already negative — negating the list as a whole would only negate its first element.
Rule clang-tidy checks
bugprone bugprone-*
performance performance-*
portability portability-*
modernize modernize-*
readability readability-*
dataflow clang-analyzer-core.*, clang-analyzer-unix.* minus unix.Stream, clang-analyzer-valist.*, clang-analyzer-deadcode.*
security clang-analyzer-security.* minus security.insecureAPI.DeprecatedOrUnsafeBufferHandling
naming_snake_case readability-identifier-naming
readability_magic_numbers readability-magic-numbers
identifier_length readability-identifier-length
swappable_parameters bugprone-easily-swappable-parameters
spurious_wakeup bugprone-spuriously-wake-up-functions
unused clang-diagnostic-unused*
shadow clang-diagnostic-shadow
uninitialized clang-diagnostic-uninitialized
implicit_conversion clang-diagnostic-conversion
sign_compare clang-diagnostic-sign-compare

The analyzer separates its families with a dot, not the hyphen the rest of clang-tidy uses: clang-analyzer-security-* matches nothing at all. unix.Stream is subtracted because it reads while((n = fread(...)) > 0) as a read past the end of the file, and the Annex K check is subtracted because it asks for memcpy_s and snprintf_s, which are optional in C11 and absent from glibc — it would fire on every call to the C library and bury the rest.

The two named individually — swappable_parameters and spurious_wakeup — are bugprone checks that describe C badly. The first flags any two adjacent parameters of one type, which is how most C functions are written; the second cannot see a retry loop one function above the wait. A project should be able to refuse them without giving up the ninety-odd checks around them.

preset in linter.json also arms the compiler

It is the only key that reaches the compiler pass. molto adds -Wall -Wextra -Wpedantic to it; none leaves that pass with your [target].flags and nothing else. Setting "preset": "none" therefore turns off both halves at once, which is usually not what someone reaching for it wants.

The severity map does not silence the compiler

rules is translated into clang-tidy’s configuration, and nowhere else. "unused": "off" stops clang-tidy from repeating the warning; it does not stop gcc from raising -Wunused-variable under the preset’s -Wall. To silence the compiler, put -Wno-unused in [target].flags of Project.toml — where the build will see it too, which is the point.

Shared keys, and their limits

Key Shape Limit
backend "clang-format@22.1.8" 64 characters; name alone pins no version
preset "molto" or "none" kernel and gnu are refused by name
exclude List of globs 16 patterns, 128 characters each
rules Rule to off/warn/error 32 rules, names of 64 characters
style The eight keys above Widths between 1 and 512

A backend pin is looked for inside the version pickup reports, because that is a sentence (clang-format version 22.1.8) and not a number. Molto verifies the pin; it never installs — that is pickup’s half of the split.

exclude patterns are POSIX fnmatch without FNM_PATHNAME, matched against the path relative to the project root. A star crosses a slash, so vendor/* matches vendor/a/b.c. A pattern ending in /** is tried a second time with that suffix removed, so it matches the directory itself too.

The generated configuration is composed into a 4096-byte buffer. A linter.json that overflows it is an error — the translated check list is too long — not a file quietly cut in half.

Every error, and what it means

All of them exit 2, and all of them name what is wrong. Nothing in either file is ignored.

Message What happened
format.json: is not valid JSON A trailing comma, a missing brace
format.json: must hold an object The document is a list or a scalar
format.json: unknown key 'styl' A typo, at the top level or in style
format.json: unknown value for 'attached' Not one of the four brace styles, or the two alignments
format.json: expected a string for 'backend' A number or a list where text belongs
format.json: expected true or false for 'use_tabs' "yes" is not a boolean
format.json: expected a column count for 'line_width' Not a number, or outside 1–512
format.json: expected a list for 'exclude' A single string is still not a list
format.json: too many exclude patterns More than 16
linter.json: too many rules More than 32
linter.json: severity must be "off", "warn" or "error", not 'warning' Close, but not one of the three
linter.json: rule 'x' is not supported by clang-tidy (no equivalent check) Not a name from the rule table
format.json: preset is not implemented yet; use "molto" or "none": kernel kernel or gnu
format.json: backend 'uncrustify' is not supported by molto (only clang-format) A second backend is not written yet
format.json: backend is pinned to '…' but this machine has '…' The installed version differs

What --format json produces

[
  {
    "file": "src/foo.c",
    "line": 12,
    "column": 5,
    "severity": "warning",
    "message": "…",
    "rule": "unused_variable"
  }
]

severity is note, warning or error. Paths are relative to the project root. Every entry carries all six fields; an empty rule is "", never absent.

The caret lines are not in it. A compiler interleaves source snippets and In file included from headers, which carry no file and no line of their own. They are kept in the text form, where they are read next to the line they point at, and dropped from JSON, where nothing could act on them.

rule is Molto’s spelling: cut at the comma clang-tidy appends, with a -W or -Werror= prefix dropped and hyphens turned into underscores. -Wunused-variable reads as unused_variable and bugprone-assignment-in-if-condition as bugprone_assignment_in_if_condition. To act on one — NOLINT, -Wno-… — turn the underscores back into hyphens.

A pass that says more than 65536 bytes about one file is cut off there, and the cut is reported as a note on that file. It is never silent.

Where the state lives

Path What is in it
.bin/style/clang-format.yaml The translated format.json, rewritten every fmt
.bin/style/clang-tidy.yaml The translated linter.json, rewritten every lint
.bin/lint/<source>.d Which headers each source read, written by -MMD
.bin/wsdb The recorded results, and the formatter and linter pickup resolved

molto clean --all removes all of it. Neither command writes compile_commands.json — a build does — so an external clang-tidy reading that database is reading what molto build last left there.

What invalidates a recorded result

Command Fingerprint Watched files
fmt Formatter version + the generated clang-format.yaml The file itself
lint Compiler version + the whole compiler command line + linter version + the generated clang-tidy.yaml + the whole linter command line + [env] The source and every header its depfile named

The mode is not part of the fmt fingerprint: what is recorded is that a file is in its final form under this style, and that is the same fact --check asks about, --diff asks about and a write has just made true. One entry serves all three.

Nothing is recorded for a run that was killed by a signal, exited 127, or had its output cut off: a failure should be retried, not replayed. lint additionally refuses to record when the source changed while it was being analysed, which would otherwise pin stale diagnostics under a current signature and never invalidate them.

Environment

Variable Effect
MOLTO_CLANG_FORMAT Use this formatter; skips pickup and is not cached
MOLTO_CLANG_TIDY Use this linter; same
MOLTO_PICKUP Where pickup is, when it is not on the PATH
NO_COLOR Drops the colour from --format rich

An overridden tool reports no version, so the recorded results cannot notice that the binary changed underneath them. After swapping one, run with --refresh-analysis once.