diff --git a/README.md b/README.md index 595c91e9..2428d030 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,27 @@

-Linter runner built for speed and consistency: +Linter runner built for speed, consistency, and low setup friction: - **Fast** — native execution (no Docker), parallel, diff-aware (changed files only), opt-in (undeclared tools don't run), small binary cached by mise - **Local == CI** — one binary, one config, identical behavior +- **Sensible defaults** — `flint init` scaffolds a working setup quickly, and most + repos can stick with the generated defaults +- **Opinionated config** — Flint chooses canonical config filenames per linter, + while still letting you keep them in a directory such as `.github/config` - **AI-friendly** — fix silently, surface only what needs review +- **Separated ownership** — dedicated linters and formatters own their file + types to avoid overlapping rules and editor-config conflicts +- **Predictable and updatable linter versions** — lint behavior stays stable + until the repo intentionally updates pinned linter versions, for example via + Renovate updates to `mise.toml` - **Cross-platform** — Linux, macOS, Windows - **Autofix** — `--fix` fixes what's fixable; reports what still needs review -Read the [background and principles](docs/why.md). +Read the [background and principles](docs/why.md) and +[alternatives/comparisons](docs/alternatives.md). > [!TIP] > **Legacy v1** (bash task scripts): see [README-V1.md](README-V1.md). @@ -146,11 +156,12 @@ FLINT_CONFIG_DIR = ".github/config" When set, `flint.toml` is loaded from that directory, and each linter that supports an explicit config path via a CLI flag will have it injected -automatically when the corresponding file exists there (see the "Config file" -column in the table below). +automatically when the corresponding canonical Flint-managed file exists there +(see the "Config file" column in the table below). Files that are absent are silently skipped. Some tools still rely on project-root discovery semantics, and some alternate upstream config locations are rejected to -avoid config drift. +avoid config drift. In practice, Flint is opinionated about which config filename +each linter should use, but flexible about the directory those files live in. > [!NOTE] > `editorconfig-checker`'s config file (`.editorconfig-checker.json`) controls diff --git a/docs/alternatives.md b/docs/alternatives.md new file mode 100644 index 00000000..859e5b13 --- /dev/null +++ b/docs/alternatives.md @@ -0,0 +1,137 @@ +# Alternatives / Comparisons + +This page captures the _"why not X?"_ comparisons that would otherwise clutter +the main [why/principles page](why.md). + +## Overview + +Ratings are relative and intentionally coarse. The sections below explain the +"why" behind each row in more detail. + +| Tool / approach | Speed | Setup effort | Cross-platform | Cross-language | Autofix support | Delta / diff-aware | Predictable and updatable linter versions | Local == CI | +| ------------------------- | --------------------- | ----------------------------- | --------------- | -------------- | ---------------------- | ------------------ | ----------------------------------------- | ------------------------- | +| flint | high | low | yes | yes | yes, where supported | yes | yes | yes | +| pre-commit | medium | medium | yes | yes | mixed | mixed | mixed | mixed | +| Husky | medium for Node repos | medium to high outside Node | yes | hook-dependent | hook-dependent | hook-dependent | hook-dependent | mixed | +| Spotless / build plugins | medium | medium in matching ecosystems | ecosystem-bound | low to medium | yes, formatter-focused | usually no | usually yes in that ecosystem | usually yes in that build | +| MegaLinter / super-linter | low to medium | medium | yes | yes | mixed | limited / mixed | mixed | mixed | + +Use these sections as relative comparisons against Flint on a few recurring +dimensions: speed, setup effort, cross-platform support, cross-language scope, +autofix support, delta/diff awareness, predictable and updatable linter +versions, and how closely local behavior matches CI. + +## Flint + +Flint is the reference point for the comparisons on this page: a native lint +runner that discovers active tools from the repo, scopes most checks to changed +files, and keeps the local and CI path aligned. + +It is also intentionally opinionated about ownership boundaries. Checks stay +separate instead of being fused into one meta-tool, and overlapping file types +have a clear default owner so repos are not forced to keep deciding which +linter or formatter should govern each domain. + +| Dimension | Rating | Why | +| ----------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Speed | high | Flint runs native tools directly, avoids container startup, and scopes most checks to changed files by default. | +| Setup effort | low | `flint init` scaffolds the baseline setup, and most repos only need to choose which tools to enable rather than repeatedly deciding how to compose overlapping tools. | +| Cross-platform | yes | Flint supports Linux, macOS, and Windows. | +| Cross-language | yes | It orchestrates multiple language-specific tools behind one runner. | +| Autofix support | yes, where supported | `flint run --fix` uses each tool's fixer when one exists and reports what still needs review. | +| Delta / diff-aware | yes | Changed-file execution is the default model, with baseline expansion only when coverage changes require it. | +| Predictable and updatable linter versions | yes | Linter versions are pinned by the repo, so behavior stays stable until the repo intentionally updates to a newer version, for example through Renovate updates to `mise.toml`. | +| Local == CI | yes | The same binary, config model, and pinned tools are used in both environments. | + +## pre-commit + +pre-commit adds a parallel tool management system on top of mise. Consuming +repos already declare their tools in `mise.toml`, so pre-commit means +maintaining a second inventory in `.pre-commit-config.yaml`, with its own +versioning and install lifecycle. + +For repos that are already mise-first, that is extra setup and drift surface +without much benefit. + +It can also push ownership decisions back onto each repo. Teams still need to +decide which hooks to compose for overlapping domains, and that composition +lives in hook wiring rather than in a single built-in policy. + +| Dimension | Rating | Why | +| ----------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Speed | medium | Hook startup is usually acceptable, but the extra hook layer and environment management add overhead compared with running native tools directly. | +| Setup effort | medium | You need to add and maintain `.pre-commit-config.yaml` and its hook definitions in addition to the repo's normal tool setup, including repo-level decisions about how overlapping hooks should compose. | +| Cross-platform | yes | pre-commit itself is cross-platform. | +| Cross-language | yes | It supports many languages through its hook ecosystem. | +| Autofix support | mixed | Some hooks fix in place, some only report, and behavior depends on the chosen hooks. | +| Delta / diff-aware | mixed | Hook-based runs are often scoped to staged files, but broader CI parity and baseline behavior depend on how each hook is configured. | +| Predictable and updatable linter versions | mixed | Hook revisions can be pinned, but version management lives in separate hook configuration instead of flowing through Renovate updates to `mise.toml`. | +| Local == CI | mixed | Teams often use pre-commit locally but a different command or environment in CI. | + +## Husky + +Husky manages git hooks for Node.js projects and requires `npm install` to +activate. Repos that are not Node-first still need a `package.json` and a dev +dependency just to run hooks. + +`flint hook install` writes a single shell script directly to `.git/hooks/` +with no install step and no language runtime dependency. + +| Dimension | Rating | Why | +| ----------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Speed | medium in Node repos, lower outside them | The hook runner itself is lightweight, but it still adds a Node-oriented wrapper around the real lint commands, and that wrapper is a worse fit outside Node-first repos. | +| Setup effort | low in Node repos, high outside them | It fits naturally in Node projects, but non-Node repos need extra package-management setup just to get hooks. | +| Cross-platform | yes | Husky works across platforms through Git hooks and Node tooling. | +| Cross-language | hook-dependent | Husky can launch anything, but it does not provide language coverage by itself. | +| Autofix support | hook-dependent | Whether fixes are available depends entirely on the commands wired into the hooks. | +| Delta / diff-aware | hook-dependent | It can run on changed or staged files, but only if the hook commands are written that way. | +| Predictable and updatable linter versions | hook-dependent | Husky only runs whatever commands the repo wires into hooks, so version stability depends on those underlying tools and how the repo manages them. | +| Local == CI | mixed | Husky is usually local-hook infrastructure, while CI often uses separate scripts or commands. | + +## Spotless and formatter plugins + +Spotless runs `google-java-format` as a Maven build phase, which means format +failures block compilation and test runs. flint keeps formatting as a separate +lint step, scoped to changed files, which is a better fit for fast feedback. + +That separation matters beyond speed: formatting remains one explicit check +instead of being entangled with compile or test phases, so repos can reason +about ownership and failures more cleanly. + +To migrate: remove `spotless-maven-plugin` from `pom.xml` (and any +`spotless.skip` properties), add `"github:google/google-java-format"` to +`[tools]` in `mise.toml`, and run `flint run --fix` once to confirm the repo is +clean. + +| Dimension | Rating | Why | +| ----------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Speed | medium | Build-plugin integration is convenient inside the matching ecosystem, but it is not as cheap as directly invoking a small lint runner on changed files. | +| Setup effort | medium in matching ecosystems | Setup is reasonable when the repo already uses Maven, Gradle, or a similar build tool, but much less general outside that stack. | +| Cross-platform | ecosystem-bound | It follows the portability of the underlying build tool rather than acting as a general cross-platform lint runner. | +| Cross-language | low to medium | It is strong within specific ecosystems, but not a general multi-language lint orchestration layer. | +| Autofix support | yes, formatter-focused | Formatter plugins are usually good at in-place fixes. | +| Delta / diff-aware | usually no | They commonly run at project or module scope rather than being natively optimized around changed-file diffs. | +| Predictable and updatable linter versions | usually yes in that ecosystem | Build plugins and formatter versions are often pinned through the build system, but the model is tied to that ecosystem rather than being a general lint-runner property. | +| Local == CI | usually yes in that build | Reusing the same build plugin in local and CI is straightforward when the repo already standardizes on that build system. | + +## MegaLinter and super-linter + +Container-based linters such as super-linter and MegaLinter ship their own tool +versions, independent of what the repo pins in `mise.toml`. That breaks the +"declare once, use everywhere" model. Container startup also adds latency to +every run. + +They also tend to bundle many checks behind one larger wrapper. That can be +convenient, but it is a worse fit when repos want cleanly separated checks and +explicit style ownership instead of a broad kitchen-sink layer. + +| Dimension | Rating | Why | +| ----------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| Speed | low to medium | Container startup and larger orchestration layers add noticeable latency compared with native direct execution. | +| Setup effort | medium | Centralized meta-linter setup can be convenient, but it introduces its own config model and container-oriented workflow. | +| Cross-platform | yes | Container-based runners generally work across platforms where the container runtime is available. | +| Cross-language | yes | Broad language coverage is one of the main strengths of these tools. | +| Autofix support | mixed | Some integrated tools can fix in place, but support varies across the bundled linter set and may be awkward in container workflows. | +| Delta / diff-aware | limited / mixed | Some support changed-file or PR-oriented modes, but the model is usually broader and less native than a runner built around git diffs. | +| Predictable and updatable linter versions | mixed | The wrapper itself is versioned predictably, but the bundled linter set and containerized execution model can still make upgrades feel more indirect. | +| Local == CI | mixed | CI often uses the canonical containerized flow, while local usage may be slower, less common, or configured differently. | diff --git a/docs/linters.md b/docs/linters.md index 264f0e4c..00da01ef 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -3,9 +3,13 @@ Every supported check, its config file (when applicable), and its scope. The [summary table lives in the README](../README.md#built-in-linter-registry). +Flint is intentionally opinionated about config shape: when a linter supports an +explicit config flag, Flint manages one canonical filename for it. Repos can +still choose the config directory via `FLINT_CONFIG_DIR` where supported. + > [!NOTE] > Biome is the exception to `FLINT_CONFIG_DIR`: its real CLI does not work -> reliably with a nested managed config, so flint treats root `biome.jsonc` as +> reliably with a nested managed config, so Flint treats root `biome.jsonc` as > the canonical Biome config. Flint is opinionated here: use JSONC, not > `biome.json`. @@ -328,23 +332,22 @@ support, so treat this check as TOML 1.0-oriented for now. ## Scopes -### Scope: `file` +### Scope: file Invoked once per matched file. -### Scope: `files` +### Scope: files -Invoked once with all matched files as args; only changed files are - passed +Invoked once with all matched files as args; only changed files are passed. -### Scope: `project` +### Scope: project Invoked once with no file args; for checks with patterns set (e.g. `cargo-clippy`), skipped entirely if no matching files changed, but runs on the whole project when it does run. `golangci-lint` is the exception — it uses - `--new-from-rev` to scope analysis to changed code even within the project run. +`--new-from-rev` to scope analysis to changed code even within the project run. -### Scope: `special` +### Scope: special Implemented in-process rather than via a command template. These checks may run without file arguments or use custom orchestration logic. @@ -364,6 +367,6 @@ files itself. **Flint writes shared `.editorconfig` carve-outs for known formatter-owned line length**: today that means `rumdl` for `*.md`, `rustfmt` for `*.rs`, and -`google-java-format` for `*.java`. Those sections use `max_line_length = off` so editors and -`editorconfig-checker` share the same intent instead of relying on -checker-specific JSON excludes. +`google-java-format` for `*.java`. Those sections use +`max_line_length = off` so editors and `editorconfig-checker` share the same +intent instead of relying on checker-specific JSON excludes. diff --git a/docs/why.md b/docs/why.md index dc8b4edf..dc3e8019 100644 --- a/docs/why.md +++ b/docs/why.md @@ -1,75 +1,86 @@ -# Why / Principles +# Why Flint -## Why +Flint exists to make repository linting fast, predictable, and easy to keep +consistent between local development, hooks, CI, and agentic workflows. -The bash task scripts (v1) have two problems: +It uses the tools the repo has chosen to install, runs only the checks that +are actually opted in, and keeps behavior aligned across every place the repo +is linted. -**Local ≠ CI**: `--native` runs a subset of linters; CI runs full super-linter -in Docker. Different tools, different behavior. Passing locally does not mean -passing in CI. +For comparisons with other lint runners and hook managers, see +[Alternatives / Comparisons](alternatives.md). -**Bash has limits**: the registry pattern was already at the edge of what bash -does cleanly. Adding built-in checks (links, renovate) would make it worse. +## Fast -### Why not pre-commit? +This is the primary goal; everything else serves it. -pre-commit adds a parallel tool management system on top of mise. Consuming repos -already declare their tools in `mise.toml` — pre-commit would require maintaining -a second inventory of the same tools in `.pre-commit-config.yaml`, with its own -versioning and install lifecycle. That's friction without benefit for repos that -are already mise-first. +- Native execution only: no Docker startup overhead +- Parallel runs in check mode +- Small binary, cached by mise +- Diff-aware by default: changed files only unless `--full` is requested +- Opt-in activation: undeclared tools are skipped entirely +- Slow checks can be skipped via `--fast-only` -### Why not Husky? +## Local same as CI -Husky manages git hooks for Node.js projects and requires `npm install` to activate. -Repos that aren't Node-first still need a `package.json` and a dev dependency just to -run hooks. `flint hook install` writes a single shell script directly to `.git/hooks/` -with no install step and no language runtime dependency. +One binary, one config model, identical behavior. There is no "native mode +subset" distinction. If it passes locally, it passes in CI. -### Why not Spotless (or other Maven formatter plugins)? +## Predictable and updatable linter versions -Spotless runs `google-java-format` as a Maven build phase, which means format -failures block compilation and test runs — that's the wrong place for a style -check. flint's `google-java-format` check runs as a separate lint step, only on -changed files, and is fast. +Flint runs pinned linter versions chosen by the repo, so lint behavior does not +suddenly change just because an upstream release landed. When a repo wants a +new `lychee`, `ruff`, or `shellcheck`, it updates that version explicitly and +reviews the result as a normal change. In practice that also works well with +tools like dependabot and Renovate, because the pinned versions live in +`mise.toml`. -To migrate: remove `spotless-maven-plugin` from your `pom.xml` (and any -`spotless.skip` properties), add `"github:google/google-java-format"` to -`[tools]` in `mise.toml`, and run `flint run --fix` once to confirm the repo is -clean. +## Easy setup, sane defaults -### Why not MegaLinter / super-linter? +`flint init` bootstraps a repo quickly, the active checks come from +`mise.toml`, and most repos do not need much custom configuration beyond +choosing tools. -Container-based linters (super-linter, MegaLinter) ship their own tool versions, -independent of what the repo pins in `mise.toml`. This breaks the "declare once, -use everywhere" promise of mise. Container startup also adds latency to every run. +## Opinionated where it matters -## Principles +Flint prefers one canonical config shape per linter to avoid discovery drift, +while still letting repos choose a config directory with `FLINT_CONFIG_DIR` +when the tool supports explicit config injection. -1. **Fast** — the primary goal; everything else serves it: - - Native execution only (no Docker); linters run in parallel (Rust binary, short startup) - - Small binary, cached by mise — fast install, near-zero overhead between runs - - Diff-aware: only changed files are linted by default; `--full` to check everything - - Opt-in via `mise.toml`: undeclared tools are skipped entirely - - Checks can be tagged slow in the registry and skipped via `--fast-only` +## Separated ownership -2. **Local same as CI** — one binary, one config, identical behavior. - No "native mode subset" distinction. If it passes locally, it passes in CI. +Linters and formatters are distinct checks, and overlapping file types have a +clear style owner. `editorconfig-checker` defers where formatter ownership +should win, which avoids contradictory output. -3. **AI-friendly** — `--fix` fixes what's fixable silently, prints output - only for issues needing review, and exits with a structured summary: +Examples: - ```text - [shellcheck] - ... - flint: fixed: cargo-fmt — commit before pushing | review: shellcheck - ``` +- Markdown style is owned by `rumdl`, not split between multiple Markdown tools +- JS/TS/JSON formatting is owned by Biome, with root `biome.jsonc` as the + canonical config +- `editorconfig-checker` defers to active formatters for file types where the + formatter should be authoritative - Only unfixable issues surface for review — no reasoning step required. +## AI-friendly -4. **Cross-platform** — runs on Linux, macOS, and Windows. The built-in - registry accounts for platform differences (e.g. binary names, path quoting). +`--fix` fixes what's fixable silently, prints output only for issues needing +review, and exits with a structured summary: -5. **Autofix where possible** — `--fix` checks first, fixes what's fixable, - reports what needs review. Fix mode runs serially to avoid concurrent writes. - Pass specific linter names to limit which fixers run (`flint run --fix rumdl shfmt`). +```text +[shellcheck] +... +flint: fixed: cargo-fmt — commit before pushing | review: shellcheck +``` + +Only unfixable issues surface for review; no reasoning step is required. + +## Cross-platform + +Flint runs on Linux, macOS, and Windows. The built-in registry accounts for +platform differences such as binary names and path quoting. + +## Autofix where possible + +`--fix` checks first, fixes what's fixable, and reports what needs review. Fix +mode runs serially to avoid concurrent writes. Pass specific linter names to +limit which fixers run, for example `flint run --fix rumdl shfmt`.