Skip to content

Commit 19f2b25

Browse files
authored
feat: add flint v2 Rust binary (#139)
## Summary Introduces the `flint` Rust binary: a mise-native lint orchestrator that replaces the v1 bash task scripts. **How it works**: reads installed tools from `mise.toml`, maps them to a built-in check registry, runs checks against changed files (merge-base diff) in parallel. Special checks (lychee links, renovate-deps) are implemented in Rust. ## File breakdown 282 files changed — the count is misleading without context: | Category | Files | |---|---| | `tests/` — e2e test fixtures (70 test cases × ~3-4 files each) | 248 | | `src/` — Rust source | 13 | | Config, docs, CI, Cargo | 21 | The 23 linters covered by e2e tests: actionlint, biome, biome-format, cargo-clippy, cargo-fmt, codespell, dotnet-format, editorconfig-checker, gofmt, golangci-lint, google-java-format, hadolint, ktlint, license-header, lychee, markdownlint-cli2, prettier, renovate-deps, ruff, ruff-format, shellcheck, shfmt, plus general cases. ## What's not in this PR **Deferred features:** - `flint hook install` — installs a git pre-commit hook; replaces the per-repo `mise run setup:pre-commit-hook` task with something self-contained in the CLI - `check_task` / `fix_task` in `flint.toml` — allows a fast script (e.g. regex/Python) as the check with a slow canonical fixer (e.g. Gradle); motivating use case: javaagent's `StaticImportFormatter` - Biome config injection — `--config-path` takes a directory, not a file; needs a directory-injection variant in the registry API **Low-priority linters** (no consuming repo needs them yet): - `merge-conflict-markers` — pure-Rust special check - `dotenv-linter`, `go-mod-tidy`, `xmllint` **Post-merge:** - Bash task scripts (`tasks/lint/`) — retire once consumer PRs are merged - GitHub release / `github:grafana/flint` registration — cut after this PR merges; consumer repos switch from branch tracking to a pinned version ## Consumer migration status All 7 consumer PRs green — validated across Rust, Go, Java, Kotlin, Python, .NET, Shell, Dockerfile: - grafana/mox [#63](grafana/mox#63) - grafana/oats [#272](grafana/oats#272) - grafana/otel-checker [#267](grafana/otel-checker#267) - grafana/grafana-opentelemetry-java [#1251](grafana/grafana-opentelemetry-java#1251) - grafana/docker-otel-lgtm [#1243](grafana/docker-otel-lgtm#1243) - prometheus/client_java [#1988](prometheus/client_java#1988) - open-telemetry/opentelemetry-java-instrumentation [#17759](open-telemetry/opentelemetry-java-instrumentation#17759) ## Test plan - [x] CI passes - [x] `flint list` shows all registry entries with correct installed/missing status - [x] All 7 consumer PRs pass CI Release-As: 0.20.0 --------- Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent f0494b2 commit 19f2b25

File tree

295 files changed

+10701
-642
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

295 files changed

+10701
-642
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ max_line_length = 120
1010
[*.py]
1111
indent_size = 4
1212

13+
[*.rs]
14+
indent_size = 4
15+
max_line_length = off
16+
1317
[{CLAUDE.md,.editorconfig,super-linter.env,lychee.toml,renovate.json5,default.json,mise.toml}]
1418
max_line_length = 300

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.github/agents/knowledge/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Knowledge Index
2+
3+
Reusable repository guidance for agents working on flint v2.
4+
5+
Load only files relevant to the current scope.
6+
7+
## Topics
8+
9+
| File | Load when |
10+
| ----------------- | --------------------------------------------------------------------------------- |
11+
| `architecture.md` | Navigating the codebase; understanding module roles or check kinds |
12+
| `linters.md` | Adding, modifying, or debugging a linter; `registry.rs` changes; config injection |
13+
| `design.md` | Questioning why something works the way it does; avoiding known pitfalls |
14+
| `testing.md` | Writing or updating tests; adding fixture cases; regenerating snapshots |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Architecture
2+
3+
## Module Map
4+
5+
- **`src/registry.rs`**: Static linter registry. Defines
6+
`Check` (builder pattern) and `builtin()` which returns
7+
the full list of built-in checks. This is where new
8+
linters are added.
9+
- **`src/runner.rs`**: Executes checks against a file list.
10+
Handles parallel execution (check mode) and serial
11+
execution (fix mode, to avoid concurrent writes).
12+
- **`src/config.rs`**: Loads `flint.toml` from the project
13+
root. All fields have defaults — the file is optional.
14+
- **`src/files.rs`**: Git-aware file discovery. Returns
15+
changed files relative to the merge base, or all files
16+
with `--full`.
17+
- **`src/linters/`**: Custom logic for special checks that
18+
can't be expressed as a simple command template:
19+
- `lychee.rs`: Link checking orchestration
20+
- `renovate_deps.rs`: Renovate snapshot verification
21+
- **`src/main.rs`**: CLI parsing (clap), orchestration,
22+
output formatting.
23+
- **`tests/e2e.rs`**: End-to-end tests. Spin up a temp git
24+
repo, write files, run the flint binary, assert on
25+
stdout/stderr and exit code.
26+
27+
## Check Kinds
28+
29+
A `Check` is either a `Template` (a command string with
30+
`{FILE}`, `{FILES}`, or `{MERGE_BASE}` placeholders) or a
31+
`Special` (custom Rust logic in `src/linters/`).
32+
33+
Template scopes:
34+
35+
- `File` — invoked once per matched file (`{FILE}`)
36+
- `Files` — invoked once with all matched files (`{FILES}`)
37+
- `Project` — invoked once with no file args; skipped
38+
entirely if no matching files changed

.github/agents/knowledge/design.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Key Design Decisions
2+
3+
1. **Activation via `mise.toml`**: A check is active when
4+
its tool (or `mise_tool_name` override) is declared in
5+
the consuming repo's `mise.toml`. No PATH probing —
6+
mise guarantees declared tools are on PATH.
7+
8+
2. **`editorconfig-checker` deference**: `editorconfig-checker`
9+
(binary: `ec`) runs on all files but skips file types owned
10+
by active line-length-enforcing formatters (`cargo-fmt`,
11+
`ruff-format`, `biome-format`, `prettier`). Implemented
12+
via `.defer_to_formatters()` on the `editorconfig-checker`
13+
entry. This avoids its `max_line_length` check conflicting
14+
with formatter output.
15+
16+
3. **markdownlint + prettier on `*.md`**: Both checkers are
17+
active when their tools are installed. They cover
18+
different concerns (markdownlint: structural rules;
19+
prettier: formatting). To avoid MD013 (line length)
20+
conflicting with prettier's line wrapping, consuming
21+
repos must disable MD013 in `.markdownlint.json`:
22+
23+
```json
24+
{ "MD013": false }
25+
```
26+
27+
4. **Fix mode runs serially**: `runner.rs` runs checks in
28+
parallel in check mode, but serially in fix mode to
29+
avoid concurrent writes to the same file.
30+
31+
5. **Version ranges**: When a `bin_name` has any
32+
`version_range` entries, every entry for that binary
33+
must have one (enforced by a registry unit test). This
34+
prevents ambiguous activation when ranges don't cover
35+
all versions.
36+
37+
6. **Special checks**: `links` and `renovate-deps` have
38+
custom orchestration logic that doesn't fit the command
39+
template model. Their implementations live in
40+
`src/linters/`.
41+
42+
7. **Built-in file exclusions**: `src/files.rs` has a
43+
`BUILTIN_EXCLUDES` slice of paths that are always removed
44+
from the file list before any linter sees it. Currently
45+
contains `.github/renovate-tracked-deps.json` (a
46+
generated file that should never be linted by prettier,
47+
ec, etc.). Add entries here — not in user-facing `exclude`
48+
docs — when a file is managed by flint itself.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Adding a New Linter
2+
3+
Add an entry to `builtin()` in `src/registry.rs` using the
4+
builder pattern:
5+
6+
```rust
7+
// File scope — invoked per file
8+
Check::file("mytool", "mytool --check {FILE}", &["*.ext"])
9+
.fix("mytool --fix {FILE}"),
10+
11+
// Files scope — invoked once with all matched files (absolute paths)
12+
Check::files("mytool", "mytool {FILES}", &["*.ext"])
13+
.fix("mytool --fix {FILES}"),
14+
15+
// Files scope — invoked once with all matched files (relative to project root)
16+
// Use {RELFILES} when the tool requires paths relative to the project root
17+
// (e.g. dotnet format --include).
18+
Check::files("mytool", "mytool --include {RELFILES}", &["*.ext"])
19+
.fix("mytool --fix --include {RELFILES}"),
20+
21+
// Project scope — invoked once, skipped if no *.ext changed
22+
Check::project("mytool", "mytool run", &["*.ext"]),
23+
```
24+
25+
Available builder modifiers:
26+
27+
| Method | Purpose |
28+
| ---------------------------- | ----------------------------------------------------------------------------- |
29+
| `.fix(cmd)` | Enable `--fix` mode with this command |
30+
| `.bin(name)` | Override binary name (when check name ≠ binary) |
31+
| `.mise_tool(name)` | Look up availability under a different mise key (e.g. `rust` for `cargo-fmt`) |
32+
| `.version_req(range)` | Restrict to a semver range (e.g. `">=1.0.0"`) |
33+
| `.excludes(names)` | Skip files already owned by these active checks |
34+
| `.slow()` | Mark as slow — skipped by `--fast-only` |
35+
| `.linter_config(file, flag)` | Inject a config flag when `FLINT_CONFIG_DIR/<file>` exists (see below) |
36+
37+
## Config File Injection (`.linter_config`)
38+
39+
Use `.linter_config(filename, flag)` when the tool supports an explicit config
40+
file path via a CLI flag. At runtime, if `FLINT_CONFIG_DIR/<filename>` exists,
41+
flint injects `flag <abs-path>` right after the binary name in the command.
42+
If the file is absent the flag is silently omitted — native config discovery
43+
remains in effect.
44+
45+
```rust
46+
// Example: markdownlint accepts --config <path>
47+
Check::file("markdownlint", "markdownlint {FILE}", &["*.md"])
48+
.fix("markdownlint --fix {FILE}")
49+
.linter_config(".markdownlint.json", "--config"),
50+
// → markdownlint --config /repo/.github/config/.markdownlint.json <file>
51+
```
52+
53+
**When NOT to use it:**
54+
55+
- The tool has no explicit `--config`/`--rcfile`/equivalent flag (e.g. `shfmt`)
56+
- The flag accepts a **directory** rather than a file (e.g. biome's
57+
`--config-path <dir>`) — a different injection shape is needed. For biome,
58+
check for `biome.json` existence but pass `config_dir` itself as the arg:
59+
`biome --config-path <config_dir> check <file>`. This requires a variant of
60+
`.linter_config` that injects the directory rather than the full file path
61+
(not yet implemented)
62+
- The tool is project-scoped and its config must live at the project root to
63+
function (no explicit `--config` flag exists)
64+
65+
Look up the tool's `--help` or man page for the config flag name and expected
66+
argument type before adding `.linter_config`.
67+
68+
For checks that need custom logic (not a simple command template), add a module
69+
under `src/linters/` and use `CheckKind::Special`.
70+
71+
## Changed-files scoping
72+
73+
Most linters use `file` or `files` scope, so they naturally receive only changed
74+
files as arguments. `golangci-lint` uses `project` scope but scopes internally via
75+
`--new-from-rev={MERGE_BASE}`.
76+
77+
**`cargo-clippy` cannot scope to changed files.** Cargo has no git-aware flag
78+
equivalent to `--new-from-rev`. It still skips entirely when no `*.rs` files
79+
changed, but when it does run it checks the whole project. Workspace support
80+
(`-p <pkg> --no-deps` per changed package) would be a future improvement.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Testing
2+
3+
Run all tests with:
4+
5+
```bash
6+
cargo test
7+
```
8+
9+
## Unit Tests
10+
11+
In-module `#[cfg(test)]` blocks in `src/`. Notable:
12+
13+
- `src/registry.rs`: enforces version-range consistency
14+
- `src/runner.rs`: config injection, scope filtering
15+
- `src/linters/renovate_deps.rs`: log parsing, snapshot
16+
read/write, diff output
17+
18+
## Fixture-based E2E Tests
19+
20+
`tests/cases/` holds one directory per scenario. Each
21+
contains:
22+
23+
- `files/` — files copied verbatim into a temp git repo
24+
and staged before the run
25+
- `test.toml` — test spec:
26+
27+
```toml
28+
[expected]
29+
args = "--full shellcheck"
30+
exit = 1 # optional, default 0
31+
stderr = """
32+
...golden output...
33+
"""
34+
35+
[expected.files] # optional: assert files written by --fix
36+
".github/renovate-tracked-deps.json" = """
37+
{...}
38+
"""
39+
40+
[env] # optional extra env vars
41+
FOO = "bar"
42+
43+
[fake_bins] # optional fake binaries (Unix only)
44+
renovate = '''
45+
#!/bin/sh
46+
echo '...'
47+
'''
48+
```
49+
50+
The `cases` test in `tests/e2e.rs` runs all of them.
51+
Set `UPDATE_SNAPSHOTS=1` to regenerate `[expected].exit`/
52+
`stderr`/`stdout` in place. `[expected.files]` and `[fake_bins]`
53+
are always preserved by the snapshot writer.
54+
55+
Use fixture cases for any check — including ones that require
56+
fake external binaries (via `[fake_bins]`). The fixture runner
57+
writes each binary into a tempdir and prepends it to `PATH`.
58+
59+
When adding a new check, cover at least: clean pass, failure
60+
with correct diff/output, and fix mode if supported.

.github/config/flint.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[settings]
2+
exclude = ["CHANGELOG.md", "tests/cases/**"]
3+
4+
[checks.renovate-deps]
5+
exclude_managers = ["github-actions", "github-runners", "cargo"]

.github/renovate-tracked-deps.json

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,40 @@
44
"mise"
55
]
66
},
7-
"README.md": {
7+
".github/workflows/test.yml": {
88
"regex": [
9-
"grafana/flint"
9+
"mise"
1010
]
1111
},
1212
"mise.toml": {
1313
"mise": [
14+
"actionlint",
15+
"cargo:xmloxide",
16+
"dotnet",
17+
"editorconfig-checker",
18+
"github:google/google-java-format",
19+
"github:koalaman/shellcheck",
20+
"github:mvdan/sh",
21+
"github:pinterest/ktlint",
22+
"go",
23+
"golangci-lint",
24+
"hadolint",
1425
"lychee",
1526
"node",
16-
"npm:renovate"
17-
],
27+
"npm:@biomejs/biome",
28+
"npm:markdownlint-cli2",
29+
"npm:prettier",
30+
"npm:renovate",
31+
"pipx:codespell",
32+
"pipx:ruff",
33+
"rust"
34+
]
35+
},
36+
"src/init/generation.rs": {
1837
"regex": [
19-
"ghcr.io/super-linter/super-linter"
38+
"actions/checkout",
39+
"jdx/mise-action",
40+
"mise"
2041
]
2142
}
2243
}

.github/renovate.json5

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
dependencyDashboard: true,
66
platformCommit: "enabled",
77
automerge: true,
8-
ignorePaths: [],
8+
ignorePaths: ["tests/"],
99
ignorePresets: [":ignoreModulesAndTests"],
1010
ignoreUnstable: true,
1111
vulnerabilityAlerts: {
@@ -38,6 +38,33 @@
3838
depNameTemplate: "mise",
3939
matchStrings: ["jdx/mise-action.*\\n\\s*with:\\s*\\n\\s*version: [\"']?(?<currentValue>v[.\\d]+)[\"']?\\s*\\n\\s*sha256: [\"']?(?<currentDigest>\\w+)[\"']?"],
4040
},
41+
{
42+
customType: "regex",
43+
description: "Update GitHub Action SHA pins embedded in the generated workflow template (src/init/generation.rs)",
44+
managerFilePatterns: ["/^src/init/generation\\.rs$/"],
45+
matchStrings: ["uses: (?<depName>[^@\\\\]+)@(?<currentDigest>[a-f0-9]{40})\\s*#\\s*(?<currentValue>v[^\\s'\"\\\\]+)"],
46+
datasourceTemplate: "github-tags",
47+
},
48+
{
49+
customType: "regex",
50+
description: "Update mise per-platform sha256 hashes in test.yml matrix",
51+
managerFilePatterns: ["/.github/workflows/test.yml/"],
52+
datasourceTemplate: "github-release-attachments",
53+
packageNameTemplate: "jdx/mise",
54+
depNameTemplate: "mise",
55+
matchStrings: [
56+
"mise_version: [\"']?(?<currentValue>v[.\\d]+)[\"']?\\s*\\n\\s*mise_sha256: [\"']?(?<currentDigest>[a-f0-9]{64})[\"']?",
57+
],
58+
},
59+
{
60+
customType: "regex",
61+
description: "Update mise version embedded in the generated workflow template (src/init/generation.rs)",
62+
managerFilePatterns: ["/^src/init/generation\\.rs$/"],
63+
datasourceTemplate: "github-release-attachments",
64+
packageNameTemplate: "jdx/mise",
65+
depNameTemplate: "mise",
66+
matchStrings: ["jdx/mise-action.*\\n\\s*with:\\s*\\n\\s*version: [\"']?(?<currentValue>v[.\\d]+)[\"']?\\s*\\n\\s*sha256: [\"']?(?<currentDigest>\\w+)[\"']?"],
67+
},
4168
],
4269
packageRules: [
4370
{

0 commit comments

Comments
 (0)