Formatting and linting
molto fmt and molto lint, the two JSON files that configure them, and why your repository never grows a .clang-format.
Molto owns the configuration; the tools do the work.
Molto does not format or analyse code and never will. What it owns is the vocabulary: you write
indent_width and brace_style, and Molto renders that as the backend’s own configuration into
.bin/style/ right before running it. Your repository never grows a .clang-format, and
switching backends later is a one-line change rather than a rewrite.
Getting the tools
Molto installs nothing. It asks pickup:
$ pickup tools
KIND NAME VERSION SOURCE
formatter clang-format clang-format version 22.1.8 pickup
linter clang-tidy LLVM version 22.1.8 pickup
It takes the path pickup reports and runs it — it does not search your PATH.
MOLTO_CLANG_FORMAT and MOLTO_CLANG_TIDY bypass resolution entirely and are not cached.
--refresh-tools asks pickup again instead of reusing the recorded answer.
No linter is not an error. molto lint still runs the compiler’s own diagnostics, which is
most of the value and needs nothing installed.
molto fmt
molto fmt # rewrite src/ and include/ in place
molto fmt --check # report what would change, write nothing; exits 1 if any
molto fmt --diff # print the unified diff, write nothing
--check is the CI form. --diff is the same question with the answer shown as a patch. Passing
both is a usage error — two answers to one question. Headers are formatted too.
molto lint
molto lint # compiler diagnostics, then the linter
molto lint --format json # machine-readable, for CI
molto lint --profile release # analyse with the release profile's defines
Two passes over every source under src/: the compiler declared in [target], syntax-only,
writing no object files; then the linter, if there is one, configured from linter.json and
handed the same compile flags the build would use.
--profile matters more than it looks. The profile decides which defines are in force, and an
#ifdef decides what even compiles. Linting with the wrong profile analyses code the build never
sees.
--format has no default. Absent is not the same as an explicit --format text: absent means
“whatever suits the stream”, which is rich at a terminal — framed blocks, a caret under the
character the tool named, colour — and text through a pipe. Ask for one by name when the output
has to be stable wherever it goes; CI wants text or json written down, not inferred.
[
{
"file": "src/foo.c",
"line": 12,
"column": 5,
"severity": "warning",
"message": "…",
"rule": "…"
}
]
Exit codes: 0 when no error-severity diagnostic was produced, 1 when one was, 2 for invalid
configuration, 4 for bad usage. A warning is reported and still succeeds.
Both commands are incremental
A file that has not changed is not analysed again: the diagnostics are recorded in the workspace database and replayed, and what you see is identical either way — same diagnostics, same order, same exit code.
A file is re-analysed when its content changes, when a header it includes changes, when the
command would differ, when linter.json changes, or when the linter’s version does.
--refresh-analysis ignores the recorded results and runs everything.
molto fmt records that a file is in its final form under the current style, so formatting a
project leaves fmt --check with nothing to do. Headers are not involved there: a formatter reads
only the file it is given.
Neither command writes compile_commands.json — a build does. An external clang-tidy reading
that database is reading what molto build last left there, not this run.
format.json
Optional; absent means the defaults below.
{
"backend": "clang-format@22.1.8",
"preset": "molto",
"exclude": ["src/vendor/**"],
"style": {
"indent_width": 4,
"line_width": 100,
"brace_style": "attach",
"pointer_alignment": "right",
"sort_includes": true
}
}
| Key | Type | Default | Meaning |
|---|---|---|---|
indent_width |
int | 4 |
Columns per indentation level |
use_tabs |
bool | false |
Indent with tabs instead of spaces |
line_width |
int | 100 |
Maximum column before wrapping |
brace_style |
string | attach |
attach, break, linux, allman |
pointer_alignment |
string | right |
left for int* p, right for int *p |
sort_includes |
bool | true |
Sort #include blocks |
space_before_paren |
bool | false |
Space between a keyword and ( |
column_limit_comments |
bool | true |
Wrap comments at line_width |
The formatter is told what language it reads
There is no key for the language: Molto takes it from [target].cpp_std.
A .h carries no language in its extension, so clang-format left to itself assumes the newest
C++. A C struct with a field named requires is then read as a C++20 requires-clause, and the
declaration is torn across three lines. Molto therefore emits Standard:
[target].cpp_std |
Standard |
|---|---|
| Absent — a C project | c++03, the only dialect where requires and concept are ordinary identifiers |
c++11, c++17, c++20, gnu++20 |
That standard |
| Anything else | Latest |
A C++ project that never declares cpp_std is formatted as C, and its C++20 headers suffer
the mirror image of the same bug. Declare it.
linter.json
Optional. A severity map in the style of ESLint: each key names a rule or a family, each value is
off, warn or error.
{
"backend": "clang-tidy@22.1.8",
"preset": "molto",
"exclude": ["src/generated/**"],
"rules": {
"bugprone": "error",
"readability_magic_numbers": "warn",
"modernize": "off"
}
}
The rule names are Molto’s, not the backend’s:
| Rule | Covers |
|---|---|
bugprone |
Patterns that are usually a bug |
performance, portability, modernize, readability |
The families of the same name |
dataflow, security |
The path-sensitive analyzer, by half |
naming_snake_case |
Identifier naming |
readability_magic_numbers, identifier_length |
Individual readability checks |
swappable_parameters, spurious_wakeup |
Two bugprone checks, named so one can be refused without dropping the family |
unused, shadow, uninitialized, implicit_conversion, sign_compare |
Compiler diagnostics, by name |
A rule Molto cannot express for the selected backend is an error naming the rule and the backend, never something quietly dropped.
The path-sensitive analyzer
Deliberately out of the default preset, and asked for in two halves:
"dataflow": "warn"walks paths rather than syntax, and finds what that buys: a null dereference, a leak, a use of an uninitialized field, a dead store."security": "warn"adds the security family, minus the check demanding the C11 Annex K functions that glibc does not ship — that one fires on every call to the C library and buries everything else.
Expect false positives from either, and expect both to be slow. They are the reason
--refresh-analysis exists as a flag separate from --refresh-tools.
Shared keys
preset — molto (default) or none. The molto preset asks the compiler for
-Wall -Wextra -Wpedantic and the linter for clang-diagnostic-* and bugprone-*. kernel and
gnu are named in the RFC but not implemented; declaring one is an error, not a silent
substitution.
exclude — glob patterns matched against the path relative to the project root. A star
crosses a slash, so vendor/* matches vendor/a/b.c; a trailing /** also matches the directory
itself.
backend — pins a name and optionally a version. Molto verifies the pin against what
pickup reports; it does not install. An unmet pin is an error naming both versions, because two
releases of one formatter produce different output for the same file — the exact noise a formatter
exists to remove.
Style configuration fails closed
An unknown key, an unknown value, a rule with no translation, a list that overflows, a pin that is
not met: each is an error naming what is wrong. Nothing in format.json or linter.json is
ignored.
That is deliberate. A key that silently does nothing is worse than one that is refused — you would
keep the line in your configuration for years believing it applied. This does not extend to
Project.toml, which drops unknown keys without warning.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
this machine has no formatter |
Pickup reports none | pickup install clang-format |
unknown key 'styl' |
A typo | Check it against the tables above |
rule 'x' is not supported by clang-tidy |
Non-canonical rule name | Use one from the rule table |
backend is pinned to '…' but this machine has '…' |
Installed version differs | Install the pin, or change it |
preset is not implemented yet |
kernel or gnu |
Use molto or none |
| Lint reports nothing from the linter | No linter installed | Check pickup tools; the compiler pass still ran |
| Lint reports what the build does not | Wrong profile | molto lint --profile release |