Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
.mise.super-linter-*.toml
/target
.cache/
27 changes: 19 additions & 8 deletions docs/linters.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Every supported check, its config file (when applicable), and its scope. The
config injection for `biome` and `biome-format` is not yet implemented.

<!-- editorconfig-checker-disable -->
<!-- markdownlint-disable MD013 -->
<!-- linter-details-start -->
<!-- Generated. Run `UPDATE_README=1 cargo test readme_linter_table_in_sync` to regenerate. -->

Expand Down Expand Up @@ -207,6 +208,7 @@ check_all_local = true
| Binary | `renovate` |
| Scope | [special](#scopes) |
| 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 |

Verifies `.github/renovate-tracked-deps.json` is up to date by running Renovate locally and comparing its output against the committed snapshot. Requires `renovate` in `[tools]`.

Expand Down Expand Up @@ -273,25 +275,34 @@ exclude_managers = ["github-actions", "github-runners"]
| Patterns | `*.xml` |

<!-- linter-details-end -->
<!-- markdownlint-enable MD013 -->
<!-- editorconfig-checker-enable -->

## Scopes

- `file` — invoked once per matched file
- `files` — invoked once with all matched files as args; only changed files are passed
- `files` — invoked once with all matched files as args; only changed files are
passed
- `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
(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.

Checks tagged slow in the registry are skipped by `--fast-only`. Use
`--fast-only` for local/pre-push feedback and the full set in CI. (No
builtin is currently marked slow, but the mechanism is preserved.)
Checks use one of three run policies:

**`editorconfig-checker` defers to formatters**: `editorconfig-checker` runs on all files, but
- `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 line-length-enforcing
formatter. When `cargo-fmt`, `ruff-format`, `biome-format`, or `prettier`
are active, their file types are excluded from `editorconfig-checker` — those formatters
are active, their file types are excluded from `editorconfig-checker` — those
formatters
already enforce line length and would conflict with `editorconfig-checker`'s
`max_line_length` editorconfig check. If none of those formatters are
installed, `editorconfig-checker` checks those files itself.
12 changes: 5 additions & 7 deletions src/init/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ use crossterm::{
terminal::{self, ClearType},
};

use crate::registry::Category;

use super::{CategoryItem, LinterGroup};

fn run_arrow_selector<T>(
Expand Down Expand Up @@ -218,15 +216,15 @@ fn print_linter_table(
"[ ]"
};
let cursor_mark = if flat_idx == cursor { ">" } else { " " };
let speed = if check.category == Category::Slow {
"slow"
} else {
"fast"
let speed = match check.run_policy {
crate::registry::RunPolicy::Fast => "fast",
crate::registry::RunPolicy::Slow => "slow",
crate::registry::RunPolicy::Adaptive => "adaptive",
};
let patterns = check.patterns.join(" ");
write!(
stdout,
" {} {} {:<name_w$} {:<bin_w$} {:<4} {:<30} {}\r\n",
" {} {} {:<name_w$} {:<bin_w$} {:<8} {:<30} {}\r\n",
cursor_mark,
sel_mark,
check.name,
Expand Down
149 changes: 149 additions & 0 deletions src/linters/renovate_deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
use std::process::Stdio;

use crate::config::RenovateDepsConfig;
use crate::files::FileList;
use crate::linters::LinterOutput;

const COMMITTED_FILE: &str = "renovate-tracked-deps.json";
Expand All @@ -29,6 +30,57 @@ pub async fn run(cfg: &RenovateDepsConfig, fix: bool, project_root: &Path) -> Li
}
}

pub(crate) fn is_relevant(file_list: &FileList, project_root: &Path) -> bool {
if file_list.full {
return true;
}

let changed: HashSet<String> = file_list
.files
.iter()
.filter_map(|path| {
path.strip_prefix(project_root)
.ok()
.map(|rel| rel.to_string_lossy().into_owned())
})
.collect();

if changed.is_empty() {
return false;
}

if changed
.iter()
.any(|path| RENOVATE_CONFIG_PATTERNS.contains(&path.as_str()))
{
return true;
}

let committed_path = COMMITTED_PATHS
.iter()
.map(|path| project_root.join(path))
.find(|path| path.exists());

let Some(committed_path) = committed_path else {
return false;
};

let committed_rel = display_path(project_root, &committed_path);
if changed.contains(&committed_rel) {
return true;
}

let committed: DepMap = match std::fs::read_to_string(&committed_path)
.ok()
.and_then(|contents| serde_json::from_str(&contents).ok())
{
Some(committed) => committed,
None => return true,
};

committed.keys().any(|path| changed.contains(path))
}

async fn run_inner(
cfg: &RenovateDepsConfig,
fix: bool,
Expand Down Expand Up @@ -464,4 +516,101 @@ mod tests {
PathBuf::from(".github/renovate-tracked-deps.json")
);
}

fn file_list(paths: &[&str], full: bool) -> FileList {
FileList {
files: paths.iter().map(PathBuf::from).collect(),
merge_base: Some("base".to_string()),
full,
}
}

#[test]
fn relevant_when_full_mode() {
let dir = tempfile::tempdir().unwrap();
assert!(is_relevant(&file_list(&[], true), dir.path()));
}

#[test]
fn relevant_when_renovate_config_changed() {
let dir = tempfile::tempdir().unwrap();
assert!(is_relevant(
&file_list(
&[dir.path().join(".github/renovate.json5").to_str().unwrap()],
false
),
dir.path()
));
}

#[test]
fn relevant_when_snapshot_changed() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".github")).unwrap();
std::fs::write(
dir.path().join(".github/renovate-tracked-deps.json"),
"{}\n",
)
.unwrap();

assert!(is_relevant(
&file_list(
&[dir
.path()
.join(".github/renovate-tracked-deps.json")
.to_str()
.unwrap()],
false
),
dir.path()
));
}

#[test]
fn relevant_when_tracked_manifest_changed() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".github")).unwrap();
write_snapshot(
&dir.path().join(".github/renovate-tracked-deps.json"),
&dep_map(&[("package.json", &[("npm", &["express"])])]),
)
.unwrap();

assert!(is_relevant(
&file_list(&[dir.path().join("package.json").to_str().unwrap()], false),
dir.path()
));
}

#[test]
fn not_relevant_for_untracked_change() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".github")).unwrap();
write_snapshot(
&dir.path().join(".github/renovate-tracked-deps.json"),
&dep_map(&[("package.json", &[("npm", &["express"])])]),
)
.unwrap();

assert!(!is_relevant(
&file_list(&[dir.path().join("README.md").to_str().unwrap()], false),
dir.path()
));
}

#[test]
fn relevant_when_snapshot_is_unparseable() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".github")).unwrap();
std::fs::write(
dir.path().join(".github/renovate-tracked-deps.json"),
"{not json}\n",
)
.unwrap();

assert!(is_relevant(
&file_list(&[dir.path().join("README.md").to_str().unwrap()], false),
dir.path()
));
}
}
63 changes: 41 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod runner;

use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use registry::{CheckKind, Scope};
use registry::{CheckKind, RunPolicy, Scope, SpecialKind};
use runner::{CheckResult, RunOptions};
use std::collections::HashMap;

Expand Down Expand Up @@ -195,9 +195,17 @@ async fn run(
registry.iter().collect()
};

let file_list = files::changed(
project_root,
&cfg,
args.full,
args.new_from_rev.as_deref(),
args.to_ref.as_deref(),
)?;

// Discover which checks are declared in the consuming repo's mise.toml, and apply
// --fast-only filter (skipped when linters are named explicitly).
// mise guarantees declared tools are on PATH, so no PATH check needed.
// --fast-only policy (skipped when linters are named explicitly, relevance-gated for
// adaptive checks). mise guarantees declared tools are on PATH, so no PATH check needed.
let mise_tools = registry::read_mise_tools(project_root);
if let Some((old, new)) = registry::find_obsolete_key(&mise_tools) {
eprintln!("flint: obsolete tool key in mise.toml: {old:?} (replaced by {new:?})");
Expand All @@ -208,7 +216,21 @@ async fn run(
let mut out = vec![];
for c in checks {
if registry::check_active(c, &mise_tools) {
if explicit || !args.fast_only || c.category != registry::Category::Slow {
let include = if explicit || !args.fast_only {
true
} else {
match c.run_policy {
RunPolicy::Fast => true,
RunPolicy::Slow => false,
RunPolicy::Adaptive => match &c.kind {
CheckKind::Special(SpecialKind::RenovateDeps) => {
linters::renovate_deps::is_relevant(&file_list, project_root)
}
_ => true,
},
}
};
if include {
out.push(c);
}
} else if explicit {
Expand All @@ -231,14 +253,6 @@ async fn run(
}
}

let file_list = files::changed(
project_root,
&cfg,
args.full,
args.new_from_rev.as_deref(),
args.to_ref.as_deref(),
)?;

if args.fix {
// Pre-check, fix what's fixable, report outcome.
// Exits 0 if everything was already clean; 1 if anything was fixed (uncommitted)
Expand Down Expand Up @@ -446,12 +460,21 @@ pub fn linter_json(check: &registry::Check) -> serde_json::Value {
"binary": if check.uses_binary() { check.bin_name } else { "(built-in)" },
"patterns": patterns,
"fix": check.has_fix(),
"slow": check.category == registry::Category::Slow,
"run_policy": run_policy_label(check.run_policy),
"slow": check.run_policy == RunPolicy::Slow,
"scope": scope,
"config_file": config_file,
})
}

fn run_policy_label(run_policy: RunPolicy) -> &'static str {
match run_policy {
RunPolicy::Fast => "fast",
RunPolicy::Slow => "slow",
RunPolicy::Adaptive => "adaptive",
}
}

fn is_fixable(name: &str, active: &[&registry::Check]) -> bool {
active.iter().any(|c| c.name == name && c.has_fix())
}
Expand Down Expand Up @@ -482,7 +505,7 @@ fn print_linters(
.max(11);

println!(
"{:<name_w$} {:<bin_w$} {:<13} {:<4} {:<3} {:<desc_w$} PATTERNS",
"{:<name_w$} {:<bin_w$} {:<13} {:<8} {:<3} {:<desc_w$} PATTERNS",
"NAME",
"BINARY",
"STATUS",
Expand All @@ -493,7 +516,7 @@ fn print_linters(
bin_w = bin_w,
desc_w = desc_w,
);
println!("{}", "-".repeat(name_w + bin_w + desc_w + 42));
println!("{}", "-".repeat(name_w + bin_w + desc_w + 46));

for check in registry {
let status = if registry::check_active(check, mise_tools) {
Expand All @@ -511,16 +534,12 @@ fn print_linters(
} else {
"missing"
};
let speed = if check.category == registry::Category::Slow {
"slow"
} else {
"fast"
};
let speed = run_policy_label(check.run_policy);
let fix = if check.has_fix() { "yes" } else { "no" };
let patterns_str = check.patterns.join(" ");
if patterns_str.is_empty() {
println!(
"{:<name_w$} {:<bin_w$} {:<13} {:<4} {:<3} {:<desc_w$}",
"{:<name_w$} {:<bin_w$} {:<13} {:<8} {:<3} {:<desc_w$}",
check.name,
check.bin_name,
status,
Expand All @@ -533,7 +552,7 @@ fn print_linters(
);
} else {
println!(
"{:<name_w$} {:<bin_w$} {:<13} {:<4} {:<3} {:<desc_w$} {}",
"{:<name_w$} {:<bin_w$} {:<13} {:<8} {:<3} {:<desc_w$} {}",
check.name,
check.bin_name,
status,
Expand Down
Loading
Loading