diff --git a/.github/agents/knowledge/architecture.md b/.github/agents/knowledge/architecture.md index 6248d42..a3ad6db 100644 --- a/.github/agents/knowledge/architecture.md +++ b/.github/agents/knowledge/architecture.md @@ -36,3 +36,24 @@ Template scopes: - `Files` — invoked once with all matched files (`{FILES}`) - `Project` — invoked once with no file args; skipped entirely if no matching files changed + +## Baseline Expansion + +Normal changed-file runs keep each check scoped to changed files. Before +execution, `src/main.rs` also computes a set of checks that need a full file +list to establish a new baseline. + +A check is expanded to all matching files when: + +- it was not active at the merge base, meaning its tool was newly added to + `mise.toml` +- its resolved tool version changed in `mise.toml` +- its registered `.linter_config(...)` file changed under `FLINT_CONFIG_DIR` +- `flint.toml` changed under `[settings]` +- `flint.toml` changed the check-specific section for a special check + +This is per-check. Unaffected checks still receive the normal changed-file list. +Explicit `--full` bypasses this selection because every check is already using +the all-files list. Config-change triggers use the raw git change list before +`settings.exclude` is applied, so excluded config paths still expand the affected +check. diff --git a/default.json b/default.json index 126c63f..47679ff 100644 --- a/default.json +++ b/default.json @@ -38,6 +38,8 @@ { "matchPackageNames": [ "actionlint", + "biome", + "cargo:yaml-lint", "cargo:xmloxide", "editorconfig-checker", "github:google/google-java-format", @@ -46,12 +48,10 @@ "golangci-lint", "hadolint", "lychee", - "npm:@biomejs/biome", - "npm:markdownlint-cli2", - "npm:prettier", "npm:renovate", "pipx:codespell", "pipx:ruff", + "rumdl", "shellcheck", "shfmt" ], diff --git a/docs/cli.md b/docs/cli.md index a447063..d5cf544 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -35,6 +35,31 @@ Every flag has an env var equivalent: `FLINT_FIX`, `FLINT_FULL`, `FLINT_FAST_ONL | Pre-push hook (CC / agentic) | `flint run --fix --fast-only` | 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. + +Some changed-file runs intentionally expand one or more affected checks to all +matching files. This establishes a baseline when lint coverage changes, while +leaving unrelated checks scoped to changed files. + +A check runs against all matching files when: + +- the check is newly active because its tool was added to `mise.toml` +- the check's tool version changed in `mise.toml` +- the check's flint-managed config file changed, such as `.shellcheckrc` or + `.yamllint.yml` in `FLINT_CONFIG_DIR` +- `flint.toml` changed under `[settings]` +- `flint.toml` changed the check-specific config for a special check, such as + `[checks.links]` or `[checks.renovate-deps]` + +`--full` is still the explicit whole-repo mode. The automatic baseline behavior +only applies in changed-file mode, and only to checks whose lint coverage may +have changed. Config-file triggers are detected from the raw git change list, so +they still apply when the config path itself is excluded from ordinary lint file +selection. + **`--short` output** — failed checks partitioned by fixability, fixable ones expressed as the exact command to run: diff --git a/src/files.rs b/src/files.rs index bf22980..0696029 100644 --- a/src/files.rs +++ b/src/files.rs @@ -12,6 +12,8 @@ const BUILTIN_EXCLUDES: &[&str] = COMMITTED_PATHS; #[derive(Clone)] pub struct FileList { pub files: Vec, + /// Changed paths from git before user excludes are applied. + pub changed_paths: Vec, /// The merge base ref, used by project-scoped checks (e.g. golangci-lint). pub merge_base: Option, /// True when the file list contains all project files (explicit --full or no merge base). @@ -29,21 +31,26 @@ pub fn changed( let exclude = build_exclude_set(cfg); if full { - return all_files(project_root, &exclude); + return all(project_root, cfg); } let merge_base = resolve_merge_base(project_root, cfg, from_ref)?; - let files = if let Some(ref base) = merge_base { + let (files, changed_paths) = if let Some(ref base) = merge_base { let to = to_ref.unwrap_or("HEAD"); - collect_changed_files(project_root, &exclude, base, to)? + let names = collect_changed_names(project_root, base, to)?; + ( + filter_names(project_root, &exclude, names.clone()), + names.into_iter().collect(), + ) } else { // No merge base (shallow clone etc.) — fall back to all files. - return all_files(project_root, &exclude); + return all(project_root, cfg); }; Ok(FileList { files, + changed_paths, merge_base, full: false, }) @@ -88,12 +95,11 @@ fn resolve_merge_base( Ok(None) } -fn collect_changed_files( +fn collect_changed_names( project_root: &Path, - exclude: &GlobSet, base: &str, to: &str, -) -> Result> { +) -> Result> { let range = format!("{base}...{to}"); let mut names: std::collections::BTreeSet = Default::default(); @@ -110,7 +116,12 @@ fn collect_changed_files( names.insert(line); } - Ok(filter_names(project_root, exclude, names)) + Ok(names) +} + +pub fn all(project_root: &Path, cfg: &Config) -> Result { + let exclude = build_exclude_set(cfg); + all_files(project_root, &exclude) } fn all_files(project_root: &Path, exclude: &GlobSet) -> Result { @@ -132,6 +143,7 @@ fn all_files(project_root: &Path, exclude: &GlobSet) -> Result { Ok(FileList { files: filter_names(project_root, exclude, names), + changed_paths: vec![], merge_base: None, full: true, }) diff --git a/src/linters/renovate_deps.rs b/src/linters/renovate_deps.rs index d9f5180..6042589 100644 --- a/src/linters/renovate_deps.rs +++ b/src/linters/renovate_deps.rs @@ -520,6 +520,7 @@ mod tests { fn file_list(paths: &[&str], full: bool) -> FileList { FileList { files: paths.iter().map(PathBuf::from).collect(), + changed_paths: paths.iter().map(|path| path.to_string()).collect(), merge_base: Some("base".to_string()), full, } diff --git a/src/main.rs b/src/main.rs index e849b42..e417cc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,8 @@ use anyhow::Result; use clap::{Args, Parser, Subcommand}; use registry::{CheckKind, FixBehavior, RunPolicy, Scope, SpecialKind}; use runner::{CheckResult, RunOptions}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; #[derive(Parser, Debug)] #[command(name = "flint", about = "flint — fast lint")] @@ -259,6 +260,20 @@ async fn run( } } + let baseline_names = + baseline_check_names(&active, &file_list, project_root, config_dir, &mise_tools); + let baseline_file_list = if baseline_names.is_empty() { + None + } else { + Some(files::all(project_root, &cfg)?) + }; + let run_ctx = RunContext { + active_checks: &active, + project_root, + cfg: &cfg, + config_dir, + }; + if args.fix { // Exits 0 if everything was already clean; 1 if anything was fixed (uncommitted) // or still needs review. @@ -274,19 +289,18 @@ async fn run( let mut post_fix_failed = vec![]; if !legacy_checks.is_empty() { - let check_results = runner::run( + let check_results = run_checks( &legacy_checks, - &active, &file_list, + baseline_file_list.as_ref(), + &baseline_names, RunOptions { fix: false, verbose: false, short: true, time: false, }, - project_root, - &cfg, - config_dir, + run_ctx, ) .await?; @@ -304,19 +318,18 @@ async fn run( .filter(|c| fixable_names.contains(&c.name)) .copied() .collect(); - let fix_results = runner::run( + let fix_results = run_checks( &to_fix, - &active, &file_list, + baseline_file_list.as_ref(), + &baseline_names, RunOptions { fix: true, verbose: false, short: true, time: false, }, - project_root, - &cfg, - config_dir, + run_ctx, ) .await?; for r in fix_results { @@ -340,19 +353,18 @@ async fn run( .filter(|c| verify_names.contains(&c.name)) .copied() .collect(); - let verify_results = runner::run( + let verify_results = run_checks( &to_verify_checks, - &active, &file_list, + baseline_file_list.as_ref(), + &baseline_names, RunOptions { fix: false, verbose: false, short: true, time: false, }, - project_root, - &cfg, - config_dir, + run_ctx, ) .await?; for r in verify_results { @@ -366,19 +378,18 @@ async fn run( } if !single_pass_fixable.is_empty() { - let fix_results = runner::run( + let fix_results = run_checks( &single_pass_fixable, - &active, &file_list, + baseline_file_list.as_ref(), + &baseline_names, RunOptions { fix: true, verbose: false, short: true, time: false, }, - project_root, - &cfg, - config_dir, + run_ctx, ) .await?; for r in fix_results { @@ -430,19 +441,18 @@ async fn run( return Ok(()); } - let results = runner::run( - &active, + let results = run_checks( &active, &file_list, + baseline_file_list.as_ref(), + &baseline_names, RunOptions { fix: false, verbose: args.verbose, short: args.short, time: args.time, }, - project_root, - &cfg, - config_dir, + run_ctx, ) .await?; @@ -485,6 +495,186 @@ async fn run( Ok(()) } +#[derive(Clone, Copy)] +struct RunContext<'a> { + active_checks: &'a [&'a registry::Check], + project_root: &'a Path, + cfg: &'a config::Config, + config_dir: &'a Path, +} + +async fn run_checks( + checks: &[®istry::Check], + file_list: &files::FileList, + baseline_file_list: Option<&files::FileList>, + baseline_names: &HashSet, + opts: RunOptions, + ctx: RunContext<'_>, +) -> Result> { + let (baseline, normal): (Vec<_>, Vec<_>) = checks + .iter() + .copied() + .partition(|c| baseline_names.contains(c.name)); + + let mut results = vec![]; + if !normal.is_empty() { + results.extend( + runner::run( + &normal, + ctx.active_checks, + file_list, + opts, + ctx.project_root, + ctx.cfg, + ctx.config_dir, + ) + .await?, + ); + } + if !baseline.is_empty() { + let files = baseline_file_list.unwrap_or(file_list); + results.extend( + runner::run( + &baseline, + ctx.active_checks, + files, + opts, + ctx.project_root, + ctx.cfg, + ctx.config_dir, + ) + .await?, + ); + } + results.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(results) +} + +fn baseline_check_names( + active: &[®istry::Check], + file_list: &files::FileList, + project_root: &Path, + config_dir: &Path, + current_tools: &HashMap, +) -> HashSet { + if file_list.full { + return HashSet::new(); + } + let Some(merge_base) = file_list.merge_base.as_deref() else { + return HashSet::new(); + }; + + let changed = changed_rel_paths(file_list, project_root); + let previous_tools = registry::read_mise_tools_at_ref(project_root, merge_base); + let flint_config = config_rel_path(project_root, config_dir, "flint.toml"); + let flint_config_changed = changed.contains(&flint_config); + let flint_toml = + flint_config_changed.then(|| flint_toml_change(project_root, config_dir, merge_base)); + + active + .iter() + .filter(|check| { + !registry::check_active(check, &previous_tools) + || registry::tool_version_changed(check, &previous_tools, current_tools) + || flint_toml.as_ref().is_some_and(|change| { + change.settings_changed + || (matches!(check.kind, CheckKind::Special(_)) + && change.check_changed(check.name)) + }) + || check.linter_config.is_some_and(|(file, _)| { + changed.contains(&config_rel_path(project_root, config_dir, file)) + }) + }) + .map(|check| check.name.to_string()) + .collect() +} + +struct FlintTomlChange { + current: toml::Value, + previous: toml::Value, + settings_changed: bool, +} + +impl FlintTomlChange { + fn check_changed(&self, name: &str) -> bool { + toml_section(&self.current, &["checks", name]) + != toml_section(&self.previous, &["checks", name]) + } +} + +fn flint_toml_change(project_root: &Path, config_dir: &Path, merge_base: &str) -> FlintTomlChange { + let rel = config_rel_path(project_root, config_dir, "flint.toml"); + let current_path = project_root.join(&rel); + let current = read_toml_file(¤t_path); + let previous = read_toml_at_ref(project_root, merge_base, &rel); + let settings_changed = + toml_section(¤t, &["settings"]) != toml_section(&previous, &["settings"]); + FlintTomlChange { + current, + previous, + settings_changed, + } +} + +fn read_toml_file(path: &Path) -> toml::Value { + std::fs::read_to_string(path) + .ok() + .and_then(|content| toml::from_str(&content).ok()) + .unwrap_or(toml::Value::Table(Default::default())) +} + +fn read_toml_at_ref(project_root: &Path, git_ref: &str, rel_path: &str) -> toml::Value { + let spec = format!("{git_ref}:{rel_path}"); + std::process::Command::new("git") + .args(["show", &spec]) + .current_dir(project_root) + .output() + .ok() + .filter(|out| out.status.success()) + .and_then(|out| String::from_utf8(out.stdout).ok()) + .and_then(|content| toml::from_str(&content).ok()) + .unwrap_or(toml::Value::Table(Default::default())) +} + +fn toml_section<'a>(value: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Value> { + let mut current = value; + for key in path { + current = current.get(*key)?; + } + Some(current) +} + +fn changed_rel_paths(file_list: &files::FileList, project_root: &Path) -> HashSet { + if !file_list.changed_paths.is_empty() { + return file_list.changed_paths.iter().cloned().collect(); + } + + file_list + .files + .iter() + .filter_map(|path| path.strip_prefix(project_root).ok()) + .map(normalize_path) + .collect() +} + +fn config_rel_path(project_root: &Path, config_dir: &Path, file: &str) -> String { + let path = if config_dir.is_absolute() { + config_dir.join(file) + } else { + project_root.join(config_dir).join(file) + }; + path.strip_prefix(project_root) + .map(normalize_path) + .unwrap_or_else(|_| normalize_path(&PathBuf::from(file))) +} + +fn normalize_path(path: &Path) -> String { + path.components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect::>() + .join("/") +} + fn print_linters_json(registry: &[registry::Check]) { let entries: Vec = registry.iter().map(linter_json).collect(); println!("{}", serde_json::to_string_pretty(&entries).unwrap()); diff --git a/src/registry/checks.rs b/src/registry/checks.rs index c3541fc..732518c 100644 --- a/src/registry/checks.rs +++ b/src/registry/checks.rs @@ -16,7 +16,7 @@ use crate::linters::renovate_deps::RENOVATE_CONFIG_PATTERNS; fn check_shellcheck() -> Check { Check::file( "shellcheck", - "shellcheck {FILE}", + "shellcheck -x -P SCRIPTDIR {FILE}", &["*.sh", "*.bash", "*.bats"], ) .linter_config(".shellcheckrc", "--rcfile") diff --git a/src/registry/mise.rs b/src/registry/mise.rs index c614c04..37e6e5c 100644 --- a/src/registry/mise.rs +++ b/src/registry/mise.rs @@ -18,7 +18,25 @@ pub fn read_mise_tools(project_root: &Path) -> HashMap { Ok(c) => c, Err(_) => return HashMap::new(), }; - let value: toml::Value = match toml::from_str(&content) { + read_mise_tools_from_str(&content) +} + +pub fn read_mise_tools_at_ref(project_root: &Path, git_ref: &str) -> HashMap { + let spec = format!("{git_ref}:mise.toml"); + let out = match std::process::Command::new("git") + .args(["show", &spec]) + .current_dir(project_root) + .output() + { + Ok(out) if out.status.success() => out, + _ => return HashMap::new(), + }; + let content = String::from_utf8_lossy(&out.stdout); + read_mise_tools_from_str(&content) +} + +fn read_mise_tools_from_str(content: &str) -> HashMap { + let value: toml::Value = match toml::from_str(content) { Ok(v) => v, Err(_) => return HashMap::new(), }; @@ -60,13 +78,7 @@ pub fn check_active(check: &Check, mise_tools: &HashMap) -> bool if check.activate_unconditionally { return true; } - let lookup_key = check.mise_tool_name.unwrap_or(check.bin_name); - // When mise_tool_name is set (e.g. "cargo:yaml-lint"), also accept - // the bare bin_name ("yaml-lint") so repos using either form work. - let declared = mise_tools - .get(lookup_key) - .or_else(|| check.mise_tool_name.and(mise_tools.get(check.bin_name))); - let Some(declared) = declared else { + let Some(declared) = declared_tool_version(check, mise_tools) else { return false; }; let Some(range_str) = check.version_range else { @@ -78,6 +90,32 @@ pub fn check_active(check: &Check, mise_tools: &HashMap) -> bool coerce_version(declared).is_some_and(|v| req.matches(&v)) } +pub fn tool_version_changed( + check: &Check, + previous_tools: &HashMap, + current_tools: &HashMap, +) -> bool { + let previous = declared_tool_version(check, previous_tools); + let current = declared_tool_version(check, current_tools); + previous.is_some() && current.is_some() && previous != current +} + +fn declared_tool_version<'a>( + check: &Check, + mise_tools: &'a HashMap, +) -> Option<&'a str> { + if check.activate_unconditionally { + return None; + } + let lookup_key = check.mise_tool_name.unwrap_or(check.bin_name); + // When mise_tool_name is set (e.g. "cargo:yaml-lint"), also accept + // the bare bin_name ("yaml-lint") so repos using either form work. + mise_tools + .get(lookup_key) + .or_else(|| check.mise_tool_name.and(mise_tools.get(check.bin_name))) + .map(String::as_str) +} + /// Parses a version string, padding with `.0` components if needed to satisfy /// semver's three-part requirement (e.g. `"20"` → `20.0.0`, `"3.12"` → `3.12.0`). fn coerce_version(s: &str) -> Option { diff --git a/src/registry/mod.rs b/src/registry/mod.rs index c1a7c1e..fdda651 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -5,7 +5,7 @@ mod resolve; mod types; pub use checks::builtin; -pub use mise::{check_active, read_mise_tools}; +pub use mise::{check_active, read_mise_tools, read_mise_tools_at_ref, tool_version_changed}; pub use obsolete::{OBSOLETE_KEYS, find_obsolete_key, find_unsupported_key}; pub use resolve::binary_on_path; pub use types::{Category, Check, CheckKind, FixBehavior, RunPolicy, Scope, SpecialKind}; diff --git a/src/runner.rs b/src/runner.rs index f028cff..cf75357 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -10,6 +10,7 @@ use crate::files::FileList; use crate::linters::{LinterOutput, license_header, lychee, renovate_deps}; use crate::registry::{Check, CheckKind, Scope, SpecialKind}; +#[derive(Clone, Copy)] pub struct RunOptions { pub fix: bool, pub verbose: bool, @@ -748,6 +749,7 @@ mod tests { .iter() .map(|s| PathBuf::from(format!("/repo/{s}"))) .collect(), + changed_paths: paths.iter().map(|path| path.to_string()).collect(), merge_base: Some("abc123".to_string()), full: false, } diff --git a/tests/cases/general/baseline-config-change/changes/config/.shellcheckrc b/tests/cases/general/baseline-config-change/changes/config/.shellcheckrc new file mode 100644 index 0000000..60232a7 --- /dev/null +++ b/tests/cases/general/baseline-config-change/changes/config/.shellcheckrc @@ -0,0 +1 @@ +# SC2086 is enabled diff --git a/tests/cases/general/baseline-config-change/files/bad.sh b/tests/cases/general/baseline-config-change/files/bad.sh new file mode 100644 index 0000000..706457d --- /dev/null +++ b/tests/cases/general/baseline-config-change/files/bad.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo $1 diff --git a/tests/cases/general/baseline-config-change/files/config/.shellcheckrc b/tests/cases/general/baseline-config-change/files/config/.shellcheckrc new file mode 100644 index 0000000..5de1df3 --- /dev/null +++ b/tests/cases/general/baseline-config-change/files/config/.shellcheckrc @@ -0,0 +1 @@ +disable=SC2086 diff --git a/tests/cases/general/baseline-config-change/files/mise.toml b/tests/cases/general/baseline-config-change/files/mise.toml new file mode 100644 index 0000000..d725983 --- /dev/null +++ b/tests/cases/general/baseline-config-change/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "latest" diff --git a/tests/cases/general/baseline-config-change/test.toml b/tests/cases/general/baseline-config-change/test.toml new file mode 100644 index 0000000..c967552 --- /dev/null +++ b/tests/cases/general/baseline-config-change/test.toml @@ -0,0 +1,22 @@ +[expected] +args = "run shellcheck" +exit = 1 +stderr = ''' +[shellcheck] + +In /bad.sh line 2: +echo $1 + ^-- SC2086 (info): Double quote to prevent globbing and word splitting. + +Did you mean: +echo "$1" + +For more information: + https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... + +flint: 1 check failed (shellcheck) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' + +[env] +FLINT_CONFIG_DIR = "config" diff --git a/tests/cases/general/baseline-excluded-config-change/changes/config/.shellcheckrc b/tests/cases/general/baseline-excluded-config-change/changes/config/.shellcheckrc new file mode 100644 index 0000000..60232a7 --- /dev/null +++ b/tests/cases/general/baseline-excluded-config-change/changes/config/.shellcheckrc @@ -0,0 +1 @@ +# SC2086 is enabled diff --git a/tests/cases/general/baseline-excluded-config-change/files/bad.sh b/tests/cases/general/baseline-excluded-config-change/files/bad.sh new file mode 100644 index 0000000..706457d --- /dev/null +++ b/tests/cases/general/baseline-excluded-config-change/files/bad.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo $1 diff --git a/tests/cases/general/baseline-excluded-config-change/files/config/.shellcheckrc b/tests/cases/general/baseline-excluded-config-change/files/config/.shellcheckrc new file mode 100644 index 0000000..5de1df3 --- /dev/null +++ b/tests/cases/general/baseline-excluded-config-change/files/config/.shellcheckrc @@ -0,0 +1 @@ +disable=SC2086 diff --git a/tests/cases/general/baseline-excluded-config-change/files/config/flint.toml b/tests/cases/general/baseline-excluded-config-change/files/config/flint.toml new file mode 100644 index 0000000..b7c8844 --- /dev/null +++ b/tests/cases/general/baseline-excluded-config-change/files/config/flint.toml @@ -0,0 +1,2 @@ +[settings] +exclude = ["config/**"] diff --git a/tests/cases/general/baseline-excluded-config-change/files/mise.toml b/tests/cases/general/baseline-excluded-config-change/files/mise.toml new file mode 100644 index 0000000..d725983 --- /dev/null +++ b/tests/cases/general/baseline-excluded-config-change/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "latest" diff --git a/tests/cases/general/baseline-excluded-config-change/test.toml b/tests/cases/general/baseline-excluded-config-change/test.toml new file mode 100644 index 0000000..c967552 --- /dev/null +++ b/tests/cases/general/baseline-excluded-config-change/test.toml @@ -0,0 +1,22 @@ +[expected] +args = "run shellcheck" +exit = 1 +stderr = ''' +[shellcheck] + +In /bad.sh line 2: +echo $1 + ^-- SC2086 (info): Double quote to prevent globbing and word splitting. + +Did you mean: +echo "$1" + +For more information: + https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... + +flint: 1 check failed (shellcheck) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' + +[env] +FLINT_CONFIG_DIR = "config" diff --git a/tests/cases/general/baseline-flint-settings-change/changes/config/flint.toml b/tests/cases/general/baseline-flint-settings-change/changes/config/flint.toml new file mode 100644 index 0000000..dd50385 --- /dev/null +++ b/tests/cases/general/baseline-flint-settings-change/changes/config/flint.toml @@ -0,0 +1,2 @@ +[settings] +exclude = [] diff --git a/tests/cases/general/baseline-flint-settings-change/files/bad.sh b/tests/cases/general/baseline-flint-settings-change/files/bad.sh new file mode 100644 index 0000000..706457d --- /dev/null +++ b/tests/cases/general/baseline-flint-settings-change/files/bad.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo $1 diff --git a/tests/cases/general/baseline-flint-settings-change/files/config/flint.toml b/tests/cases/general/baseline-flint-settings-change/files/config/flint.toml new file mode 100644 index 0000000..d2e648f --- /dev/null +++ b/tests/cases/general/baseline-flint-settings-change/files/config/flint.toml @@ -0,0 +1,2 @@ +[settings] +exclude = ["bad.sh"] diff --git a/tests/cases/general/baseline-flint-settings-change/files/mise.toml b/tests/cases/general/baseline-flint-settings-change/files/mise.toml new file mode 100644 index 0000000..d725983 --- /dev/null +++ b/tests/cases/general/baseline-flint-settings-change/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "latest" diff --git a/tests/cases/general/baseline-flint-settings-change/test.toml b/tests/cases/general/baseline-flint-settings-change/test.toml new file mode 100644 index 0000000..c967552 --- /dev/null +++ b/tests/cases/general/baseline-flint-settings-change/test.toml @@ -0,0 +1,22 @@ +[expected] +args = "run shellcheck" +exit = 1 +stderr = ''' +[shellcheck] + +In /bad.sh line 2: +echo $1 + ^-- SC2086 (info): Double quote to prevent globbing and word splitting. + +Did you mean: +echo "$1" + +For more information: + https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... + +flint: 1 check failed (shellcheck) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' + +[env] +FLINT_CONFIG_DIR = "config" diff --git a/tests/cases/general/baseline-new-linter/changes/mise.toml b/tests/cases/general/baseline-new-linter/changes/mise.toml new file mode 100644 index 0000000..d725983 --- /dev/null +++ b/tests/cases/general/baseline-new-linter/changes/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "latest" diff --git a/tests/cases/general/baseline-new-linter/files/bad.sh b/tests/cases/general/baseline-new-linter/files/bad.sh new file mode 100644 index 0000000..706457d --- /dev/null +++ b/tests/cases/general/baseline-new-linter/files/bad.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo $1 diff --git a/tests/cases/general/baseline-new-linter/files/mise.toml b/tests/cases/general/baseline-new-linter/files/mise.toml new file mode 100644 index 0000000..9487f20 --- /dev/null +++ b/tests/cases/general/baseline-new-linter/files/mise.toml @@ -0,0 +1 @@ +[tools] diff --git a/tests/cases/general/baseline-new-linter/test.toml b/tests/cases/general/baseline-new-linter/test.toml new file mode 100644 index 0000000..6ee2c8d --- /dev/null +++ b/tests/cases/general/baseline-new-linter/test.toml @@ -0,0 +1,19 @@ +[expected] +args = "run shellcheck" +exit = 1 +stderr = ''' +[shellcheck] + +In /bad.sh line 2: +echo $1 + ^-- SC2086 (info): Double quote to prevent globbing and word splitting. + +Did you mean: +echo "$1" + +For more information: + https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... + +flint: 1 check failed (shellcheck) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' diff --git a/tests/cases/general/baseline-tool-upgrade/changes/mise.toml b/tests/cases/general/baseline-tool-upgrade/changes/mise.toml new file mode 100644 index 0000000..4d3bcc4 --- /dev/null +++ b/tests/cases/general/baseline-tool-upgrade/changes/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "0.11.0" diff --git a/tests/cases/general/baseline-tool-upgrade/files/bad.sh b/tests/cases/general/baseline-tool-upgrade/files/bad.sh new file mode 100644 index 0000000..706457d --- /dev/null +++ b/tests/cases/general/baseline-tool-upgrade/files/bad.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo $1 diff --git a/tests/cases/general/baseline-tool-upgrade/files/mise.toml b/tests/cases/general/baseline-tool-upgrade/files/mise.toml new file mode 100644 index 0000000..e256d48 --- /dev/null +++ b/tests/cases/general/baseline-tool-upgrade/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "0.10.0" diff --git a/tests/cases/general/baseline-tool-upgrade/test.toml b/tests/cases/general/baseline-tool-upgrade/test.toml new file mode 100644 index 0000000..6ee2c8d --- /dev/null +++ b/tests/cases/general/baseline-tool-upgrade/test.toml @@ -0,0 +1,19 @@ +[expected] +args = "run shellcheck" +exit = 1 +stderr = ''' +[shellcheck] + +In /bad.sh line 2: +echo $1 + ^-- SC2086 (info): Double quote to prevent globbing and word splitting. + +Did you mean: +echo "$1" + +For more information: + https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... + +flint: 1 check failed (shellcheck) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' diff --git a/tests/cases/shellcheck/sourced-file/files/docker/logging.sh b/tests/cases/shellcheck/sourced-file/files/docker/logging.sh new file mode 100644 index 0000000..acf8619 --- /dev/null +++ b/tests/cases/shellcheck/sourced-file/files/docker/logging.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail + +function run_with_logging() { + name=$1 + shift + envvar=$1 + shift + if [[ ${envvar} == "true" || ${ENABLE_LOGS_ALL:-false} == "true" ]]; then + echo "Running ${name} logging=true" + exec "$@" + else + echo "Running ${name} logging=false" + exec "$@" >/dev/null 2>&1 + fi +} diff --git a/tests/cases/shellcheck/sourced-file/files/docker/run-pyroscope.sh b/tests/cases/shellcheck/sourced-file/files/docker/run-pyroscope.sh new file mode 100644 index 0000000..3cbbf4c --- /dev/null +++ b/tests/cases/shellcheck/sourced-file/files/docker/run-pyroscope.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +source ./logging.sh + +run_with_logging "Pyroscope ${PYROSCOPE_VERSION}" "${ENABLE_LOGS_PYROSCOPE:-false}" \ + ./pyroscope/pyroscope --config.file=./pyroscope-config.yaml diff --git a/tests/cases/shellcheck/sourced-file/files/mise.toml b/tests/cases/shellcheck/sourced-file/files/mise.toml new file mode 100644 index 0000000..d725983 --- /dev/null +++ b/tests/cases/shellcheck/sourced-file/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +shellcheck = "latest" diff --git a/tests/cases/shellcheck/sourced-file/test.toml b/tests/cases/shellcheck/sourced-file/test.toml new file mode 100644 index 0000000..0ce0ca5 --- /dev/null +++ b/tests/cases/shellcheck/sourced-file/test.toml @@ -0,0 +1,3 @@ +[expected] +args = "run --full shellcheck" +exit = 0