Skip to content
Molto

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