Skip to content

Fixing

A rule that can mechanically repair its violation declares a fix: block. alint check only reports; alint fix applies the repairs. Evaluation runs in parallel across files, but the fix pass is a single sequential loop, one rule at a time, because each fixer writes to the tree that the next rule will read.

alint evaluates files in parallel, then applies fixes one rule at a time Three files are evaluated in parallel and produce fixable violations. Those violations feed a single sequential lane where fixes are applied one rule at a time. evaluate parallel, all files at once README.md Cargo.toml main.rs apply sequential, one rule at a time fix 1 fix 2 fix 3 26 ops, two tiers Safe: applied by alint fixtrim, create, prepend, insert_header, set_value ... Unsafe: need --unsafe-fixesremove, replace, indent_style, insert_line, sync_from ...

A rule is auto-fixable only if it declares a fix: block; otherwise its violation is report-only and you repair it by hand. alint check --format human marks the fixable ones, and the summary counts them. alint fix applies them, alint fix --dry-run prints what it would change without writing, alint fix --only <rule-id> applies just one rule's fixers (the command the agent surface emits), and alint fix --changed restricts the pass to the diff (cross-file and existence rules still see the whole tree). A violation with no fixer still fails the gate; fixing is an accelerant, not an escape hatch.

The fix pass runs rule by rule in sequence (evaluate the rule, then apply its fixers) rather than in parallel like check, because a fixer mutates files on disk and a later rule must see the result, not race it.

A fix: block names exactly one op. Twenty-six ship, grouped by what they touch:

  • Text hygiene (7) rewrite bytes in place: file_trim_trailing_whitespace, file_append_final_newline, file_normalize_line_endings, file_strip_bom, file_strip_bidi, file_strip_zero_width, file_collapse_blank_lines.
  • Boilerplate (4) add declared content: file_create (a whole file), file_prepend / file_append (at start / end), and insert_header (after a leading shebang or XML declaration, so it does not push either off line 1).
  • Path, metadata, and VCS (7): file_remove, file_rename, dir_create, relocate (move a stray file such as a nested lockfile back to the repo root), chmod, git_untrack (git rm --cached), and command (run a user-supplied fix tool such as eslint --fix).
  • Located value edits (3) splice one span of a structured file: replace (a regex match), set_value (write a *_path_equals value), and remove_value (drop a *_path_absent node).
  • Cross-file (2): sync_from (mirror a drifted file from its canonical source) and create_and_register (create a workspace member and add it to a manifest list such as workspace.members).
  • Ordering (3): sort (keep a marked block or a whole markerless file ordered under its comparator), insert_line (splice a missing require:d line at its sorted position), and indent_style (reindent tabs to spaces).

Rules lists which kind ships which op and the options each takes.

Every op has a tier. Safe fixes are behavior-preserving -- normalizing whitespace, adding a missing header, creating a file from a template -- so alint fix applies them by default. Unsafe fixes can change what a file means or delete content -- removing a file, rewriting a matched span, reindenting an indent-significant file, inserting a line at a computed position -- so a bare alint fix holds them back as suggestions and applies them only with --unsafe-fixes. A per-rule applicability: overrides the default either way: safe opts an Unsafe op in, suggestion (or never) holds a Safe one back. alint check --format human marks a Safe fix as auto-fixable and an Unsafe one as a suggestion. And a content-injecting op (one that writes ruleset-authored bytes, such as file_prepend or insert_header) pulled in through an untrusted extends: URL is demoted to a suggestion whatever its tier, so a third-party ruleset can never silently write into your files.

The five content-providing ops -- file_create, file_prepend, file_append, insert_header, and create_and_register -- take either an inline content: string or a content_from: <path> that reads the bytes from a file (exactly one of the two must be set). This is how boilerplate that is awkward to inline stays under version control: a real Apache-2 LICENSE is ~10 KB, and pasting it into YAML is fragile (escape rules, indentation drift, stray code-search hits), so stash the canonical bytes under .alint/templates/ and point content_from: at them. The path resolves against the lint root (and, like every fix op that writes a path, stays confined to that root unless allow_out_of_root is set) and is read at fix-apply time, so the template need not exist when alint check runs, only when alint fix writes the target; a missing source is reported as Skipped, never a half-written file. In a monorepo with nested_configs: true, a sub-config's content_from: still resolves against the workspace root, so one root .alint/templates/ supplies every package.

These ops are careful about repeat runs: file_prepend, file_append, and insert_header are idempotent (a no-op when the content is already present); file_prepend and insert_header preserve a leading byte-order mark; and file_create skips a target that already exists.

fix_size_limit (default 1 MiB) bounds the file a fixer rewrites in place: if that target is over the cap it is reported Skipped rather than rewritten. It covers the whole-file ops that read a target to splice or rewrite its content -- the seven text-hygiene ops, file_prepend / file_append / insert_header, sort / insert_line / indent_style, and sync_from. The pure path and metadata ops read no target content and fall outside it (file_remove, file_rename, relocate, dir_create, chmod, git_untrack); file_create reads its content_from: template, but that read is not itself bounded by fix_size_limit.

Trim trailing whitespace across Markdown, and create a missing LICENSE from a template:

version: 1
rules:
- id: md-trim
kind: no_trailing_whitespace
paths: "**/*.md"
level: info
fix:
file_trim_trailing_whitespace: {}
- id: license-present
kind: file_exists
paths: [LICENSE]
root_only: true
level: error
fix:
file_create:
content_from: ".alint/templates/LICENSE-MIT.txt"

alint fix prints a block per rule and a final count:

error [license-present]:
✓ created LICENSE
info [md-trim]:
✓ trimmed trailing whitespace in docs/guide.md
2 applied, 0 skipped, 0 unfixable.

Each applied line names the file in its summary; skipped and unfixable lines carry it as a prefix. A file over fix_size_limit, or a content_from: source that is missing, shows on its rule as (skipped: <reason>) and lands in the skipped count instead.

A finding that check does not tag as fixable even though its rule declares a fix (a UTF-16 BOM, a bidi control in a binary-looking file, an ambiguous reindent) is listed as unfixable without running the fixer, and lands in the unfixable count.

  • Configuration documents fix_size_limit and the per-rule fix: field.
  • Rules lists which kinds ship a fixer and the options each op takes.
  • Changed mode covers alint fix --changed.