Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/agents/knowledge/linters.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ Available builder modifiers:
| `.mise_tool(name)` | Look up availability under a different mise key (e.g. `rust` for `cargo-fmt`) |
| `.version_req(range)` | Restrict to a semver range (e.g. `">=1.0.0"`) |
| `.excludes(names)` | Skip files already owned by these active checks |
| `.slow()` | Mark as comprehensive-only and skipped by `--fast-only` |
| `.adaptive()` | Mark as comprehensive-only and relevance-gated in `--fast-only` |
| `.slow()` | Mark as comprehensive-only in `flint init` |
| `.adaptive_relevance(fn)` | Skip on local default runs unless the hook reports changed files relevant |
| `.linter_config(file, flag)` | Inject a config flag when `FLINT_CONFIG_DIR/<file>` exists (see below) |

## Config File Injection (`.linter_config`)
Expand Down
3 changes: 1 addition & 2 deletions .github/renovate-tracked-deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"datasource": "github-tags"
},
"google-java-format": {
"packageName": "google-java-format",
"datasource": "github-releases"
"packageName": "google-java-format"
},
"hadolint": {
"packageName": "hadolint/hadolint",
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ json5 = "1.0"
similar = "3"
toml_edit = "0.25"
dunce = "1.0.5"
tempfile = "3"

[dev-dependencies]
tempfile = "3"
regex = "1"
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ 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
- **Local + CI aligned** — one binary, one config model, local defaults tuned
for day-to-day work and broader coverage in CI
- **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,
Expand Down Expand Up @@ -88,6 +89,21 @@ description = "Auto-fix lint issues"
run = "flint run --fix"
```

### Day-to-day use

Run lints on your changes:

```bash
mise run lint # check
mise run lint:fix # auto-fix what's fixable
```

> [!NOTE]
> In rare cases (currently only `renovate-deps`) a failure may show up
> only in CI. That is a deliberate performance optimization — see
> [adaptive runs](#adaptive-runs). When it happens, flint prints the
> command to reproduce locally (usually `--full` or the linter name).

### CI setup

```yaml
Expand Down Expand Up @@ -207,6 +223,24 @@ Click a name in the table below for details. See the

<!-- registry-table-end -->

### Adaptive runs

Some linters are expensive enough that running them on every local
`flint run` would slow the inner loop. For those, `flint run` skips the
linter when none of the changed files could plausibly affect its result.
CI is unaffected — it always runs the full set.

Affected linters:

| Linter | Skipped locally when… |
| ------------------------------------------------------------------- | --------------------------------------------------------------- |
| [`renovate-deps`](docs/linters/renovate-deps.md#when-does-this-run) | No change to Renovate config, the snapshot, or any tracked file |

To force a local run of a skipped linter:

- `flint run --full` — runs every active linter
- `flint run <linter>` — runs just that one

## Versioning

This project uses [Semantic Versioning](https://semver.org/).
Expand Down
14 changes: 7 additions & 7 deletions docs/alternatives.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ the main [why/principles page](why.md).
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 |
| Tool / approach | Speed | Setup effort | Cross-platform | Cross-language | Autofix support | Delta / diff-aware | Predictable and updatable linter versions | Local + CI aligned |
| ------------------------- | --------------------- | ----------------------------- | --------------- | -------------- | ---------------------- | ------------------ | ----------------------------------------- | ------------------------- |
| flint | high | low | yes | yes | yes, where supported | yes | yes | yes |
| pre-commit | medium | medium | yes | yes | mixed | mixed | mixed | mixed |
Expand All @@ -19,7 +19,7 @@ Ratings are relative and intentionally coarse. The sections below explain the
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.
versions, and how closely local and CI behavior stay aligned.

## Flint

Expand All @@ -41,7 +41,7 @@ linter or formatter should govern each domain.
| 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. |
| Local + CI aligned | yes | The same binary, config model, and pinned tools are used in both environments, with local defaults tuned for changed-file feedback and CI activating the full linter set. |

## pre-commit

Expand All @@ -66,7 +66,7 @@ lives in hook wiring rather than in a single built-in policy.
| 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. |
| Local + CI aligned | mixed | Teams often use pre-commit locally but a different command or environment in CI. |

## Husky

Expand All @@ -86,7 +86,7 @@ with no install step and no language runtime dependency.
| 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. |
| Local + CI aligned | mixed | Husky is usually local-hook infrastructure, while CI often uses separate scripts or commands. |

## Spotless and formatter plugins

Expand All @@ -112,7 +112,7 @@ clean.
| 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. |
| Local + CI aligned | 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

Expand All @@ -134,4 +134,4 @@ explicit style ownership instead of a broad kitchen-sink layer.
| 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. |
| Local + CI aligned | mixed | CI often uses the canonical containerized flow, while local usage may be slower, less common, or configured differently. |
13 changes: 7 additions & 6 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,29 @@ it do not need to re-learn the interface.
| -------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `--fix` | Fix what's fixable, report `clean` / `fixed` / `partial` / `review` outcomes; exit non-zero if anything needs action |
| `--full` | Lint all files instead of only changed files |
| `--fast-only` | Skip checks tagged as slow in the registry. Overridden by explicit linter names. |
| `--short` | Compact summary output, no per-check noise |
| `--verbose` | Show all linter output, not just failures |
| `--new-from-rev REV` | Diff base (default: merge base with base branch) |
| `--to-ref REF` | Diff head (default: HEAD) |

Every flag has an env var equivalent: `FLINT_FIX`, `FLINT_FULL`, `FLINT_FAST_ONLY`,
Every flag has an env var equivalent: `FLINT_FIX`, `FLINT_FULL`,
`FLINT_VERBOSE`, `FLINT_SHORT`, `FLINT_NEW_FROM_REV`, `FLINT_TO_REF`.

## Intended use by context

| Context | Command | Why |
| ---------------------------- | -------------------------------------- | ----------------------------------------------------------------- |
| Interactive development | `flint run` or `flint run --fast-only` | Full output so you can read the details |
| Interactive development | `flint run` | Full output so you can read the details |
| Human wanting a summary | `flint run --short` | Compact output, no per-check noise |
| Pre-push hook (CC / agentic) | `flint run --fix --fast-only` | Fixes what it can silently, surfaces only what needs human review |
| Pre-push hook (CC / agentic) | `flint run --fix` | Fixes what it can silently, surfaces only what needs human review |
| CI | `flint run` | Full output for humans reading CI logs |

## Changed-file and baseline runs

By default, `flint run` checks only files changed relative to the merge base.
Use `--full` to check every matching file explicitly.
By default, local `flint run` checks linters triggered by changes relative to
the merge base. In CI, `flint run` activates the full linter set while still
keeping diff-aware scoping where each linter supports it. Use `--full` to
check every matching file explicitly.

Some changed-file runs intentionally expand one or more affected checks to all
matching files. This establishes a baseline when lint coverage changes, while
Expand Down
10 changes: 1 addition & 9 deletions docs/linters.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ check_all_local = true
| Binary | `renovate` |
| Scope | [native](#scope-native) |
| Patterns | `renovate.json renovate.json5 .github/renovate.json .github/renovate.json5 .renovaterc .renovaterc.json .renovaterc.json5` |
| Run policy | adaptive — runs in `--fast-only` only when relevant |
| Run policy | adaptive — see [when does this run?](linters/renovate-deps.md#when-does-this-run) |

Verifies `renovate-tracked-deps.json` next to the active Renovate
config is up to date by running Renovate locally and comparing its
Expand Down Expand Up @@ -382,14 +382,6 @@ whole project when it does run. `golangci-lint` is the exception — it uses
Implemented in-process rather than via a command template. These checks may run
without file arguments or use custom orchestration logic.

Checks use one of three run policies:

- `fast` — always runs, including in `--fast-only`
- `slow` — skipped by `--fast-only`
- `adaptive` — runs in `--fast-only` only when the changed files are relevant

Use `--fast-only` for local/pre-push feedback and the full set in CI.

**`editorconfig-checker` defers to formatters**: `editorconfig-checker` runs on
all files, but automatically skips file types owned by an active formatter. If
none of those formatters are installed, `editorconfig-checker` checks those
Expand Down
32 changes: 26 additions & 6 deletions docs/linters/renovate-deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@
The second check is there to catch configuration mistakes before they show up as
separate Renovate PRs or README drift.

## When does this run?

CI always runs `renovate-deps`. Locally `flint run` only runs it when the
changed files plausibly affect the snapshot. `--full` or naming the
linter explicitly bypass the skip.

| Change | Local | CI |
| --------------------------------------------- | ----- | --- |
| Renovate config edited | ✅ | ✅ |
| `renovate-tracked-deps.json` snapshot edited | ✅ | ✅ |
| File already tracked in the snapshot edited | ✅ | ✅ |
| New tool/action added that is not yet tracked | ❌ | ✅ |
| Unrelated change (docs, source, etc.) | ❌ | ✅ |

The "new tool not yet tracked" case is the typical reason a CI failure
won't reproduce locally without `--full`.

## What it catches

Goal: `mise.toml` and `README.md` both refer to actionlint, so you want
Expand Down Expand Up @@ -100,13 +117,16 @@ If the snapshot is stale:
flint run --fix renovate-deps
```

If you want to force a fresh metadata rebuild instead of reusing any existing
committed metadata for the same dependency names, for example after changing Renovate
grouping config or while debugging suspicious `meta` entries:
Verification (plain `flint run`) uses Renovate's cheap `--dry-run=extract`
plus the committed snapshot's metadata. `--fix` regenerates via
`--dry-run=lookup` so meta is authoritative.

```bash
FLINT_RENOVATE_DEPS_REFRESH_META=1 flint run --fix renovate-deps
```
The linter requires every dep referenced by a `packageRule` to have
`packageName`; deps matched via `matchPackageNames` additionally require
`datasource` so Renovate's `(packageName, datasource)` grouping is
deterministic. `matchDepNames` rules don't require datasource — bare-key
mise tools like `biome` don't always surface one even in lookup-mode
output, and Renovate matches them by name regardless.

If rule coverage is inconsistent:

Expand Down
10 changes: 6 additions & 4 deletions docs/why.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ This is the primary goal; everything else serves it.
- 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`
- Local runs skip slower checks by default unless you use `--full` or name the
linter explicitly

## Local same as CI
## Local and CI stay aligned

One binary, one config model, identical behavior. There is no "native mode
subset" distinction. If it passes locally, it passes in CI.
One binary, one config model, and the same pinned tools in both environments.
Local runs default to the change-triggered subset for day-to-day speed, while
CI activates the full linter set.

## Predictable and updatable linter versions

Expand Down
2 changes: 0 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ pub struct LycheeConfig {
pub struct RenovateDepsConfig {
// Env var: FLINT_RENOVATE_DEPS_EXCLUDE_MANAGERS (JSON array, e.g. '["npm"]')
pub exclude_managers: Vec<String>,
// Env var: FLINT_RENOVATE_DEPS_REFRESH_META
pub refresh_meta: bool,
}

#[derive(Debug, Deserialize, Clone)]
Expand Down
Loading
Loading