diff --git a/docs/linters.md b/docs/linters.md index 455cedbd..ca96af84 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -120,8 +120,8 @@ This verifies and fixes Flint-managed setup: - keep lint-managed tool entries under the `# Linters` header - keep runtime, SDK, and unknown tool entries above that header -With `--fix`, rewrites Flint-managed config in place and advances -`settings.setup_migration_version` when a migration applies. +With `--fix`, rewrites Flint-managed config in place and applies any +currently actionable setup migration. ## `gofmt` diff --git a/src/config.rs b/src/config.rs index 0d641904..5d2b4a41 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,7 +21,6 @@ pub struct Config { pub struct Settings { pub base_branch: String, pub exclude: Vec, - pub setup_migration_version: u32, } impl Default for Settings { @@ -29,7 +28,6 @@ impl Default for Settings { Self { base_branch: "main".to_string(), exclude: vec![], - setup_migration_version: crate::setup::V2_BASELINE_SETUP_VERSION, } } } @@ -123,21 +121,3 @@ pub fn load(config_dir: &Path) -> Result { .extract()?; Ok(cfg) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn missing_setup_migration_version_defaults_to_v2_baseline() { - let tmp = tempfile::TempDir::new().unwrap(); - std::fs::write(tmp.path().join("flint.toml"), "[settings]\n").unwrap(); - - let cfg = load(tmp.path()).unwrap(); - - assert_eq!( - cfg.settings.setup_migration_version, - crate::setup::V2_BASELINE_SETUP_VERSION - ); - } -} diff --git a/src/init/config_files.rs b/src/init/config_files.rs index f9d80598..6eae3f61 100644 --- a/src/init/config_files.rs +++ b/src/init/config_files.rs @@ -7,11 +7,7 @@ use crate::registry::EditorconfigDirectiveStyle; /// Writes a skeleton `flint.toml` in `config_dir`. Creates the directory if needed. /// Returns `true` if the file was written, `false` if it already existed. -pub(super) fn generate_flint_toml( - config_dir: &Path, - base_branch: &str, - setup_migration_version: u32, -) -> Result { +pub(super) fn generate_flint_toml(config_dir: &Path, base_branch: &str) -> Result { let toml_path = config_dir.join("flint.toml"); if toml_path.exists() { return Ok(false); @@ -21,51 +17,12 @@ pub(super) fn generate_flint_toml( if base_branch != "main" { content.push_str(&format!("base_branch = \"{base_branch}\"\n")); } - content.push_str(&format!( - "setup_migration_version = {setup_migration_version}\n" - )); content.push_str("# exclude = [\"CHANGELOG\\\\.md\"]\n"); std::fs::write(&toml_path, &content)?; println!(" wrote {}", toml_path.display()); Ok(true) } -pub(crate) fn write_setup_migration_version( - config_dir: &Path, - base_branch: &str, - version: u32, -) -> Result { - let toml_path = config_dir.join("flint.toml"); - if !toml_path.exists() { - return generate_flint_toml(config_dir, base_branch, version); - } - - let content = std::fs::read_to_string(&toml_path) - .with_context(|| format!("failed to read {}", toml_path.display()))?; - let mut doc: toml_edit::DocumentMut = content.parse().context("failed to parse flint.toml")?; - if doc.get("settings").is_none() { - doc["settings"] = toml_edit::table(); - } - let Some(settings) = doc.get_mut("settings").and_then(|item| item.as_table_mut()) else { - anyhow::bail!("[settings] is not a table in {}", toml_path.display()); - }; - let current = settings - .get("setup_migration_version") - .and_then(|item| item.as_value()) - .and_then(|value| value.as_integer()) - .and_then(|value| u32::try_from(value).ok()); - if current == Some(version) { - return Ok(false); - } - settings.insert( - "setup_migration_version", - toml_edit::value(i64::from(version)), - ); - std::fs::write(&toml_path, doc.to_string()) - .with_context(|| format!("failed to write {}", toml_path.display()))?; - Ok(true) -} - /// Removes stale v1/super-linter-era files that flint v2 no longer uses. /// Returns the list of removed paths relative to `project_root`. pub(super) fn remove_legacy_lint_files( @@ -90,19 +47,6 @@ pub(super) fn remove_legacy_lint_files( Ok(removed) } -pub(super) fn existing_legacy_lint_files(project_root: &Path, config_dir: &Path) -> Vec { - legacy_lint_files(project_root, config_dir) - .into_iter() - .filter(|path| path.exists()) - .map(|path| { - path.strip_prefix(project_root) - .unwrap_or(&path) - .display() - .to_string() - }) - .collect() -} - fn legacy_lint_files(project_root: &Path, config_dir: &Path) -> Vec { vec![ project_root.join(".prettierignore"), @@ -135,16 +79,6 @@ pub(super) fn remove_stale_markdownlint_line_length_directives( Ok(changed_files) } -pub(super) fn stale_markdownlint_line_length_directive_files( - project_root: &Path, -) -> Result> { - stale_transformed_files( - project_root, - &[&["*.md"]], - strip_stale_markdownlint_md013_directives, - ) -} - fn tracked_files_for_patterns(project_root: &Path, patterns: &[&[&str]]) -> Result> { let mut tracked_files = std::collections::BTreeSet::new(); for group in patterns { @@ -197,45 +131,6 @@ pub(super) fn remove_stale_editorconfig_checker_directives( Ok(changed_files) } -pub(super) fn stale_editorconfig_checker_directive_files( - project_root: &Path, - delegated_sections: &[(&[&str], EditorconfigDirectiveStyle)], -) -> Result> { - let mut changed_files = vec![]; - for (patterns, directive_style) in delegated_sections { - changed_files.extend(stale_transformed_files( - project_root, - &[*patterns], - |content| strip_stale_editorconfig_checker_directives(content, *directive_style), - )?); - } - changed_files.sort(); - changed_files.dedup(); - Ok(changed_files) -} - -fn stale_transformed_files( - project_root: &Path, - patterns: &[&[&str]], - transform: F, -) -> Result> -where - F: Fn(&str) -> String, -{ - let tracked_files = tracked_files_for_patterns(project_root, patterns)?; - let mut changed_files = vec![]; - for rel in tracked_files { - let path = project_root.join(rel.as_str()); - let Ok(content) = std::fs::read_to_string(&path) else { - continue; - }; - if transform(&content) != content { - changed_files.push(rel); - } - } - Ok(changed_files) -} - fn strip_stale_markdownlint_md013_directives(content: &str) -> String { let mut kept = Vec::with_capacity(content.lines().count()); let had_trailing_newline = content.ends_with('\n'); diff --git a/src/init/migrations.rs b/src/init/migrations.rs index 11f0323e..c2c02d14 100644 --- a/src/init/migrations.rs +++ b/src/init/migrations.rs @@ -5,9 +5,8 @@ use std::path::Path; use crate::registry::{Check, EditorconfigDirectiveStyle, EditorconfigLineLengthPolicy, builtin}; use super::config_files::{ - existing_legacy_lint_files, remove_legacy_lint_files, - remove_stale_editorconfig_checker_directives, remove_stale_markdownlint_line_length_directives, - stale_editorconfig_checker_directive_files, stale_markdownlint_line_length_directive_files, + remove_legacy_lint_files, remove_stale_editorconfig_checker_directives, + remove_stale_markdownlint_line_length_directives, }; use super::detection::parse_tool_keys; use super::generation; @@ -25,7 +24,6 @@ pub(super) struct RepoMigrationSummary { struct MigrationInputs { tool_keys: HashSet, - delegated_sections: Vec<(&'static [&'static str], EditorconfigDirectiveStyle)>, mise_content: String, } @@ -62,26 +60,35 @@ impl RepoMigrationSummary { } pub(crate) fn apply_setup_migrations(project_root: &Path, config_dir: &Path) -> Result { - let mise_path = project_root.join("mise.toml"); - let current_content = std::fs::read_to_string(&mise_path).unwrap_or_default(); - let current_tool_keys = parse_tool_keys(¤t_content); - let delegated_sections = active_editorconfig_cleanup_sections(¤t_tool_keys); - let migration_summary = apply_repo_migrations(project_root, config_dir, &delegated_sections)?; - Ok(!migration_summary.is_noop()) -} - -pub(crate) fn detect_setup_migrations( - project_root: &Path, - config_dir: &Path, - setup_migration_version: u32, -) -> Result { - let migration_summary = - detect_setup_migrations_after(project_root, config_dir, setup_migration_version)?; + let inputs = migration_inputs(project_root)?; + let obsolete_keys = crate::registry::obsolete_keys(); + let unsupported_keys = crate::registry::unsupported_keys(); + let delegated_sections = if legacy_markdownlint_stack_active(&inputs.tool_keys) { + active_editorconfig_cleanup_sections(&inputs.tool_keys) + } else { + vec![] + }; + let migration_summary = apply_repo_migrations_with_keys( + project_root, + config_dir, + &delegated_sections, + &obsolete_keys, + &unsupported_keys, + legacy_markdownlint_stack_active(&inputs.tool_keys), + )?; Ok(!migration_summary.is_noop()) } -pub(crate) fn detect_setup_drift(project_root: &Path, config_dir: &Path) -> Result { - let migration_summary = detect_repo_migrations(project_root, config_dir)?; +pub(crate) fn detect_setup_migrations(project_root: &Path) -> Result { + let inputs = migration_inputs(project_root)?; + let obsolete_keys = crate::registry::obsolete_keys(); + let unsupported_keys = crate::registry::unsupported_keys(); + let migration_summary = detect_setup_migrations_with_keys( + &obsolete_keys, + &unsupported_keys, + &inputs.tool_keys, + &inputs.mise_content, + ); Ok(!migration_summary.is_noop()) } @@ -174,43 +181,7 @@ pub(super) fn apply_repo_migrations( delegated_sections, &obsolete_keys, &unsupported_keys, - ) -} - -pub(crate) fn detect_repo_migrations( - project_root: &Path, - config_dir: &Path, -) -> Result { - let inputs = migration_inputs(project_root)?; - let obsolete_keys = crate::registry::obsolete_keys(); - let unsupported_keys = crate::registry::unsupported_keys(); - detect_repo_migrations_with_keys( - project_root, - config_dir, - &inputs.delegated_sections, - &obsolete_keys, - &unsupported_keys, - &inputs.tool_keys, - &inputs.mise_content, - ) -} - -pub(crate) fn detect_setup_migrations_after( - project_root: &Path, - config_dir: &Path, - setup_migration_version: u32, -) -> Result { - let inputs = migration_inputs(project_root)?; - let obsolete_keys = crate::registry::obsolete_keys_after(setup_migration_version); - let unsupported_keys = crate::setup::unsupported_keys_after(setup_migration_version); - detect_repo_migrations_with_keys( - project_root, - config_dir, - &inputs.delegated_sections, - &obsolete_keys, - &unsupported_keys, - &inputs.tool_keys, - &inputs.mise_content, + true, ) } @@ -218,23 +189,18 @@ fn migration_inputs(project_root: &Path) -> Result { let mise_path = project_root.join("mise.toml"); let current_content = std::fs::read_to_string(&mise_path).unwrap_or_default(); let current_tool_keys = parse_tool_keys(¤t_content); - let delegated_sections = active_editorconfig_cleanup_sections(¤t_tool_keys); Ok(MigrationInputs { tool_keys: current_tool_keys, - delegated_sections, mise_content: current_content, }) } -fn detect_repo_migrations_with_keys( - project_root: &Path, - config_dir: &Path, - delegated_sections: &[(&'static [&'static str], EditorconfigDirectiveStyle)], +fn detect_setup_migrations_with_keys( obsolete_keys: &[(&'static str, &'static str)], unsupported_keys: &[(&'static str, &'static str)], tool_keys: &HashSet, mise_content: &str, -) -> Result { +) -> RepoMigrationSummary { let replaced_obsolete = obsolete_keys .iter() .filter(|(old_key, _)| tool_keys.contains(*old_key)) @@ -246,26 +212,14 @@ fn detect_repo_migrations_with_keys( .map(|(old_key, _)| (*old_key).to_string()) .collect(); let node_added = needs_node_for_npm(mise_content); - let legacy_files_removed = existing_legacy_lint_files(project_root, config_dir); - let stale_md013_comments_removed = if delegated_patterns_include(delegated_sections, "*.md") { - stale_markdownlint_line_length_directive_files(project_root)? - } else { - vec![] - }; - let stale_editorconfig_checker_comments_removed = if delegated_sections.is_empty() { - vec![] - } else { - stale_editorconfig_checker_directive_files(project_root, delegated_sections)? - }; - - Ok(RepoMigrationSummary { + RepoMigrationSummary { replaced_obsolete, removed_unsupported, node_added, - legacy_files_removed, - stale_md013_comments_removed, - stale_editorconfig_checker_comments_removed, - }) + legacy_files_removed: vec![], + stale_md013_comments_removed: vec![], + stale_editorconfig_checker_comments_removed: vec![], + } } fn apply_repo_migrations_with_keys( @@ -274,6 +228,7 @@ fn apply_repo_migrations_with_keys( delegated_sections: &[(&'static [&'static str], EditorconfigDirectiveStyle)], obsolete_keys: &[(&'static str, &'static str)], unsupported_keys: &[(&'static str, &'static str)], + include_repo_cleanup: bool, ) -> Result { let replaced_obsolete = generation::replace_obsolete_keys(project_root, obsolete_keys)?; let removed_unsupported = remove_tool_keys( @@ -284,17 +239,23 @@ fn apply_repo_migrations_with_keys( .collect::>(), )?; let node_added = ensure_node_for_npm(project_root)?; - let legacy_files_removed = remove_legacy_lint_files(project_root, config_dir)?; - let stale_md013_comments_removed = if delegated_patterns_include(delegated_sections, "*.md") { - remove_stale_markdownlint_line_length_directives(project_root)? + let legacy_files_removed = if include_repo_cleanup { + remove_legacy_lint_files(project_root, config_dir)? } else { vec![] }; - let stale_editorconfig_checker_comments_removed = if delegated_sections.is_empty() { - vec![] - } else { - remove_stale_editorconfig_checker_directives(project_root, delegated_sections)? - }; + let stale_md013_comments_removed = + if include_repo_cleanup && delegated_patterns_include(delegated_sections, "*.md") { + remove_stale_markdownlint_line_length_directives(project_root)? + } else { + vec![] + }; + let stale_editorconfig_checker_comments_removed = + if include_repo_cleanup && !delegated_sections.is_empty() { + remove_stale_editorconfig_checker_directives(project_root, delegated_sections)? + } else { + vec![] + }; Ok(RepoMigrationSummary { replaced_obsolete, @@ -306,6 +267,18 @@ fn apply_repo_migrations_with_keys( }) } +fn legacy_markdownlint_stack_active(tool_keys: &HashSet) -> bool { + const MARKDOWNLINT_STACK_KEYS: &[&str] = &[ + "npm:markdownlint-cli", + "npm:markdownlint-cli2", + "npm:prettier", + ]; + + MARKDOWNLINT_STACK_KEYS + .iter() + .any(|key| tool_keys.contains(*key)) +} + fn delegated_patterns_include( delegated_sections: &[(&'static [&'static str], EditorconfigDirectiveStyle)], needle: &str, diff --git a/src/init/mod.rs b/src/init/mod.rs index 8abf7791..8e5bf2a1 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -15,8 +15,6 @@ mod scaffold; mod ui; mod v1; -pub(crate) use config_files::write_setup_migration_version; - use config_files::{ disable_editorconfig_line_length_for_patterns, generate_editorconfig, generate_flint_toml, }; @@ -32,7 +30,7 @@ use migrations::{ apply_repo_migrations, selected_editorconfig_cleanup_sections, selected_editorconfig_line_length_sections, }; -pub(crate) use migrations::{apply_setup_migrations, detect_setup_drift, detect_setup_migrations}; +pub(crate) use migrations::{apply_setup_migrations, detect_setup_migrations}; use scaffold::{apply_env_and_tasks, generate_lint_workflow, maybe_install_hook}; use ui::{interactive_select_linters, select_categories_arrow}; @@ -373,11 +371,7 @@ Add and stage your source files before running init so the detection is accurate let base_branch = detect_base_branch(project_root); let config_dir_path = project_root.join(&config_dir_rel); - let toml_generated = generate_flint_toml( - &config_dir_path, - &base_branch, - crate::setup::LATEST_SUPPORTED_SETUP_VERSION, - )?; + let toml_generated = generate_flint_toml(&config_dir_path, &base_branch)?; let needs_rust_components = selected_checks .iter() .any(|check| check.workflow_setup == Some(WorkflowSetup::RustComponents)); diff --git a/src/init/tests.rs b/src/init/tests.rs index e0fc5ce1..9af4c66f 100644 --- a/src/init/tests.rs +++ b/src/init/tests.rs @@ -726,8 +726,7 @@ fn generate_biome_config_migrates_legacy_supported_json_name() { fn generate_flint_toml_writes_skeleton() { let tmp = tempfile::TempDir::new().unwrap(); let dir = tmp.path().join("config"); - let written = - generate_flint_toml(&dir, "main", crate::setup::V2_BASELINE_SETUP_VERSION).unwrap(); + let written = generate_flint_toml(&dir, "main").unwrap(); assert!(written); let content = std::fs::read_to_string(dir.join("flint.toml")).unwrap(); assert!(content.contains("[settings]")); @@ -738,12 +737,7 @@ fn generate_flint_toml_writes_skeleton() { #[test] fn generate_flint_toml_non_main_branch() { let tmp = tempfile::TempDir::new().unwrap(); - let written = generate_flint_toml( - tmp.path(), - "master", - crate::setup::V2_BASELINE_SETUP_VERSION, - ) - .unwrap(); + let written = generate_flint_toml(tmp.path(), "master").unwrap(); assert!(written); let content = std::fs::read_to_string(tmp.path().join("flint.toml")).unwrap(); assert!(content.contains("base_branch = \"master\"")); @@ -753,8 +747,7 @@ fn generate_flint_toml_non_main_branch() { fn generate_flint_toml_skips_existing() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write(tmp.path().join("flint.toml"), "existing content").unwrap(); - let written = - generate_flint_toml(tmp.path(), "main", crate::setup::V2_BASELINE_SETUP_VERSION).unwrap(); + let written = generate_flint_toml(tmp.path(), "main").unwrap(); assert!(!written); let content = std::fs::read_to_string(tmp.path().join("flint.toml")).unwrap(); assert_eq!(content, "existing content"); diff --git a/src/linters/flint_setup.rs b/src/linters/flint_setup.rs index c7f92893..fc1688e1 100644 --- a/src/linters/flint_setup.rs +++ b/src/linters/flint_setup.rs @@ -1,12 +1,12 @@ use std::path::Path; use std::path::PathBuf; +use crate::init::generation::needs_node_for_npm; use crate::init::generation::{normalize_tools_section, tools_section_needs_normalization}; -use crate::init::write_setup_migration_version; use crate::linters::LinterOutput; use crate::registry::{ CheckTypeDef, NativeCheckDef, NativePrepareContext, NativeRunContext, NativeRunFuture, - PreparedNativeCheck, + PreparedNativeCheck, SetupOutcome, }; pub(crate) static CHECK_TYPE: CheckTypeDef = CheckTypeDef::native( @@ -18,7 +18,6 @@ pub(crate) static CHECK_TYPE: CheckTypeDef = CheckTypeDef::native( struct PreparedFlintSetup { name: String, config_dir: PathBuf, - setup_migration_version: u32, tracked_files: Vec, } @@ -26,7 +25,6 @@ fn prepare(ctx: NativePrepareContext<'_>) -> Option Some(Box::new(PreparedFlintSetup { name: ctx.name.to_string(), config_dir: ctx.config_dir.to_path_buf(), - setup_migration_version: ctx.cfg.settings.setup_migration_version, tracked_files: vec![ ctx.project_root.join("mise.toml"), ctx.config_dir.join("flint.toml"), @@ -45,79 +43,47 @@ impl PreparedNativeCheck for PreparedFlintSetup { fn run(self: Box, ctx: NativeRunContext) -> NativeRunFuture { Box::pin(async move { - crate::linters::flint_setup::run( - ctx.fix, - &ctx.project_root, - &self.config_dir, - self.setup_migration_version, - ) - .await + crate::linters::flint_setup::run(ctx.fix, &ctx.project_root, &self.config_dir).await }) } } -pub async fn run( - fix: bool, - project_root: &Path, - config_dir: &Path, - setup_migration_version: u32, -) -> LinterOutput { +pub async fn run(fix: bool, project_root: &Path, config_dir: &Path) -> LinterOutput { let path = project_root.join("mise.toml"); - let flint_toml = config_dir.join("flint.toml"); let mut errors = vec![]; - let mut versioned_migrations_pending = false; - let mut setup_drift_reported = false; - - if flint_toml.exists() && setup_migration_version > crate::setup::LATEST_SUPPORTED_SETUP_VERSION - { - errors.push(format!( - "flint.toml setup_migration_version is {setup_migration_version}, but this flint only supports {}.", - crate::setup::LATEST_SUPPORTED_SETUP_VERSION - )); - } else if setup_migration_version < crate::setup::LATEST_SUPPORTED_SETUP_VERSION { - match crate::init::detect_setup_migrations( - project_root, - config_dir, - setup_migration_version, - ) { - Ok(true) => { - versioned_migrations_pending = true; - setup_drift_reported = true; - errors.push(format!( - "Flint setup migrations after version {setup_migration_version} apply to this repo." - )); - } - Ok(false) => {} - Err(e) => return LinterOutput::err(format!("flint: flint-setup: {e}\n")), - } - } - - if !setup_drift_reported { - match crate::init::detect_setup_drift(project_root, config_dir) { - Ok(true) => errors.push("Flint setup drift applies to this repo.".to_string()), - Ok(false) => {} - Err(e) => return LinterOutput::err(format!("flint: flint-setup: {e}\n")), - } - } + let mut setup_outcome = SetupOutcome::Clean; + let mise_content = std::fs::read_to_string(&path).unwrap_or_default(); let mise_tools = crate::registry::read_mise_tools(project_root); if let Some((old, new)) = crate::registry::find_obsolete_key(&mise_tools) { + setup_outcome = setup_outcome.at_least(SetupOutcome::Blocking); errors.push(format!( "obsolete tool key in mise.toml: {old:?} (replaced by {new:?})." )); } if let Some((old, hint)) = crate::registry::find_unsupported_key(&mise_tools) { + setup_outcome = setup_outcome.at_least(SetupOutcome::Blocking); errors.push(format!( "unsupported legacy lint tool in mise.toml: {old:?}. Migration required: {hint}." )); } + if needs_node_for_npm(&mise_content) { + setup_outcome = setup_outcome.at_least(SetupOutcome::Blocking); + errors.push("mise.toml is missing `node` for npm: backend tools.".to_string()); + } match tools_section_needs_normalization(&path) { Ok(true) => { + setup_outcome = setup_outcome.at_least(SetupOutcome::NonBlocking); errors.push("mise.toml [tools] entries are not in Flint's canonical order.".to_string()) } Ok(false) => {} - Err(e) => return LinterOutput::err(format!("flint: flint-setup: {e}\n")), + Err(e) => { + return LinterOutput::setup_err( + SetupOutcome::Fatal, + format!("flint: flint-setup: {e}\n"), + ); + } } if errors.is_empty() { @@ -125,53 +91,61 @@ pub async fn run( ok: true, stdout: Vec::new(), stderr: Vec::new(), + setup_outcome: Some(SetupOutcome::Clean), }; } if !fix { - return LinterOutput::err(format!( - "ERROR: {}\nRun `flint run --fix flint-setup` to apply Flint setup migrations.\n", - errors.join("\nERROR: ") - )); + return LinterOutput { + ok: false, + stdout: Vec::new(), + stderr: format!( + "ERROR: {}\nRun `flint run --fix flint-setup` to apply Flint setup migrations.\n", + errors.join("\nERROR: ") + ) + .into_bytes(), + setup_outcome: Some(setup_outcome), + }; } - if flint_toml.exists() && setup_migration_version > crate::setup::LATEST_SUPPORTED_SETUP_VERSION - { - return LinterOutput::err(format!( - "ERROR: {}\nUpgrade flint before changing this repo setup.\n", - errors.join("\nERROR: ") - )); + let setup_migrations_pending = match crate::init::detect_setup_migrations(project_root) { + Ok(pending) => pending, + Err(e) => { + return LinterOutput::setup_err( + SetupOutcome::Fatal, + format!("flint: flint-setup: {e}\n"), + ); + } + }; + if setup_migrations_pending { + setup_outcome = setup_outcome.at_least(SetupOutcome::Blocking); } - let migrations_applied = match crate::init::apply_setup_migrations(project_root, config_dir) { + let _migrations_applied = match crate::init::apply_setup_migrations(project_root, config_dir) { Ok(applied) => applied, - Err(e) => return LinterOutput::err(format!("flint: flint-setup: {e}\n")), + Err(e) => { + return LinterOutput::setup_err( + SetupOutcome::Fatal, + format!("flint: flint-setup: {e}\n"), + ); + } }; if let Err(e) = normalize_tools_section(&path) { - return LinterOutput::err(format!("flint: flint-setup: {e}\n")); - } - if migrations_applied - && versioned_migrations_pending - && flint_toml.exists() - && let Err(e) = write_setup_migration_version( - config_dir, - "main", - crate::setup::LATEST_SUPPORTED_SETUP_VERSION, - ) - { - return LinterOutput::err(format!("flint: flint-setup: {e}\n")); + return LinterOutput::setup_err(SetupOutcome::Fatal, format!("flint: flint-setup: {e}\n")); } LinterOutput { ok: true, stdout: Vec::new(), stderr: Vec::new(), + setup_outcome: Some(setup_outcome), } } #[cfg(test)] mod tests { use super::*; + use std::process::Command; #[tokio::test] async fn check_mode_reports_drift() { @@ -182,13 +156,7 @@ mod tests { ) .unwrap(); - let out = run( - false, - tmp.path(), - tmp.path(), - crate::setup::LATEST_SUPPORTED_SETUP_VERSION, - ) - .await; + let out = run(false, tmp.path(), tmp.path()).await; let content = std::fs::read_to_string(tmp.path().join("mise.toml")).unwrap(); assert!(!out.ok); @@ -209,13 +177,7 @@ mod tests { ) .unwrap(); - let out = run( - true, - tmp.path(), - tmp.path(), - crate::setup::LATEST_SUPPORTED_SETUP_VERSION, - ) - .await; + let out = run(true, tmp.path(), tmp.path()).await; let content = std::fs::read_to_string(tmp.path().join("mise.toml")).unwrap(); assert!(out.ok); @@ -225,76 +187,68 @@ mod tests { } #[tokio::test] - async fn missing_setup_migration_version_without_drift_passes() { + async fn existing_flint_toml_without_setup_drift_passes() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write(tmp.path().join("mise.toml"), "[tools]\n").unwrap(); std::fs::write(tmp.path().join("flint.toml"), "[settings]\n").unwrap(); - let out = run( - false, - tmp.path(), - tmp.path(), - crate::setup::V2_BASELINE_SETUP_VERSION, - ) - .await; + let out = run(false, tmp.path(), tmp.path()).await; assert!(out.ok); } #[tokio::test] - async fn current_setup_migration_version_still_reports_actionable_drift() { + async fn setup_check_does_not_report_broad_repo_cleanup_drift() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write( tmp.path().join("mise.toml"), - "[tools]\n\"npm:renovate\" = \"latest\"\n", + "[tools]\nnode = \"24\"\n\n# Linters\n\"npm:renovate\" = \"latest\"\n", ) .unwrap(); std::fs::write( - tmp.path().join("flint.toml"), - "[settings]\nsetup_migration_version = 2\n", + tmp.path().join("README.md"), + "\n# Title\n", ) .unwrap(); - let out = run( - false, - tmp.path(), - tmp.path(), - crate::setup::LATEST_SUPPORTED_SETUP_VERSION, - ) - .await; + let out = run(false, tmp.path(), tmp.path()).await; - assert!(!out.ok); - assert!( - String::from_utf8(out.stderr) - .unwrap() - .contains("Flint setup drift applies to this repo") - ); + assert!(out.ok, "{}", String::from_utf8_lossy(&out.stderr)); } #[tokio::test] - async fn fix_mode_writes_current_setup_migration_version_when_migration_applies() { + async fn fix_mode_applies_markdownlint_stack_cleanup() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write( tmp.path().join("mise.toml"), - "[tools]\n\"npm:markdownlint-cli2\" = \"0.18.1\"\n", - ) - .unwrap(); - std::fs::write( - tmp.path().join("flint.toml"), - "[settings]\nsetup_migration_version = 1\n", + "[tools]\nnode = \"24\"\n\n# Linters\nrumdl = \"0.1.78\"\n\"npm:markdownlint-cli2\" = \"0.18.1\"\n", ) .unwrap(); + let readme = "\n# Title\n"; + std::fs::write(tmp.path().join("README.md"), readme).unwrap(); + assert!( + Command::new("git") + .args(["init", "-q"]) + .current_dir(tmp.path()) + .status() + .unwrap() + .success() + ); + assert!( + Command::new("git") + .args(["add", "README.md", "mise.toml"]) + .current_dir(tmp.path()) + .status() + .unwrap() + .success() + ); - let out = run( - true, - tmp.path(), - tmp.path(), - crate::setup::V2_BASELINE_SETUP_VERSION, - ) - .await; - let content = std::fs::read_to_string(tmp.path().join("flint.toml")).unwrap(); + let out = run(true, tmp.path(), tmp.path()).await; + let content = std::fs::read_to_string(tmp.path().join("mise.toml")).unwrap(); + let readme_after = std::fs::read_to_string(tmp.path().join("README.md")).unwrap(); assert!(out.ok); - assert!(content.contains("setup_migration_version = 2")); + assert!(!content.contains("npm:markdownlint-cli2")); + assert_eq!(readme_after, "# Title\n"); } } diff --git a/src/linters/license_header.rs b/src/linters/license_header.rs index e5bfef43..47994f62 100644 --- a/src/linters/license_header.rs +++ b/src/linters/license_header.rs @@ -88,6 +88,7 @@ pub async fn run( ok: all_ok, stdout: vec![], stderr, + setup_outcome: None, } } diff --git a/src/linters/lychee.rs b/src/linters/lychee.rs index c707edf5..c0f8a445 100644 --- a/src/linters/lychee.rs +++ b/src/linters/lychee.rs @@ -82,6 +82,7 @@ pub async fn run( ok: false, stdout: Vec::new(), stderr: stderr.into_bytes(), + setup_outcome: None, }; } } @@ -104,6 +105,7 @@ pub async fn run( ok: false, stdout: Vec::new(), stderr: format!("flint: links: failed to collect files: {e}\n").into_bytes(), + setup_outcome: None, }; } }; @@ -199,6 +201,7 @@ pub async fn run( ok: all_ok, stdout: combined_stdout, stderr: combined_stderr, + setup_outcome: None, } } @@ -347,12 +350,14 @@ async fn run_lychee_cmd( ok: out.status.success(), stdout, stderr: out.stderr, + setup_outcome: None, } } Err(e) => LinterOutput { ok: false, stdout, stderr: format!("flint: links: failed to spawn lychee: {e}\n").into_bytes(), + setup_outcome: None, }, } } @@ -783,7 +788,6 @@ mod tests { &Settings { base_branch: "main".to_string(), exclude: vec!["tests/cases/**".to_string()], - setup_migration_version: crate::setup::V2_BASELINE_SETUP_VERSION, }, ) .unwrap(); diff --git a/src/linters/renovate_deps.rs b/src/linters/renovate_deps.rs index ca95e58f..e1bf235c 100644 --- a/src/linters/renovate_deps.rs +++ b/src/linters/renovate_deps.rs @@ -396,6 +396,7 @@ async fn run_inner( ok: true, stdout: format!("{COMMITTED_FILE} has been created.\n").into_bytes(), stderr: vec![], + setup_outcome: None, }); } return Ok(LinterOutput::err(format!( @@ -410,6 +411,7 @@ async fn run_inner( ok: true, stdout: format!("{COMMITTED_FILE} is up to date.\n").into_bytes(), stderr: vec![], + setup_outcome: None, }); } @@ -423,6 +425,7 @@ async fn run_inner( ok: true, stdout, stderr: vec![], + setup_outcome: None, }); } @@ -433,6 +436,7 @@ async fn run_inner( "ERROR: {COMMITTED_FILE} is out of date.\nRun `flint run --fix renovate-deps` to update.\n" ) .into_bytes(), + setup_outcome: None, }) } diff --git a/src/main.rs b/src/main.rs index 6c0771ab..3953046c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -254,6 +254,8 @@ async fn run( out }; + let mut setup_check_result = None; + let mut setup_fix_outcome = None; let setup_check = active.iter().copied().find(|check| is_flint_setup(check)); if let Some(check) = setup_check { let setup_results = run_checks( @@ -280,11 +282,22 @@ async fn run( .next() .expect("flint-setup preflight produced a result"); if args.fix { - finish_fix_outcomes( - vec![classify_single_pass_fix(setup_result)], - args.allow_fixed, - ); - } else if !setup_result.ok { + let stop_after_setup = setup_result_blocks_fix(&setup_result); + let setup_outcome = classify_single_pass_fix(setup_result); + if stop_after_setup + || matches!( + setup_outcome, + FixOutcome::Partial(_) | FixOutcome::Review(_) + ) + { + finish_fix_outcomes(vec![setup_outcome], args.allow_fixed); + return Ok(()); + } else { + setup_fix_outcome = Some(setup_outcome); + } + } else if setup_result.ok { + // Clean setup never affects later lint execution. + } else if setup_result_blocks_check(&setup_result) { let failed = [setup_result.name.as_str()]; if args.short { eprintln!("flint: 1 check failed — flint run --fix {}", failed[0]); @@ -295,6 +308,8 @@ async fn run( ); } std::process::exit(1); + } else { + setup_check_result = Some(setup_result); } } let active: Vec<®istry::Check> = active @@ -303,6 +318,12 @@ async fn run( .collect(); if active.is_empty() { + if let Some(outcome) = setup_fix_outcome { + finish_fix_outcomes(vec![outcome], args.allow_fixed); + } + if let Some(setup_result) = setup_check_result { + finish_check_results(vec![setup_result], &active, args.short); + } return Ok(()); } @@ -359,7 +380,7 @@ async fn run( .copied() .partition(|c| supports_single_pass_fix(c)); - let mut outcomes = vec![]; + let mut outcomes = setup_fix_outcome.into_iter().collect::>(); if !legacy_checks.is_empty() { let check_results = run_checks( @@ -474,7 +495,7 @@ async fn run( return Ok(()); } - let results = run_checks( + let mut results = run_checks( &active, &file_list, baseline_file_list.as_ref(), @@ -489,41 +510,10 @@ async fn run( ) .await?; - let failed: Vec<&str> = results - .iter() - .filter(|r| !r.ok) - .map(|r| r.name.as_str()) - .collect(); - - if !failed.is_empty() { - let n = failed.len(); - let noun = if n == 1 { "check" } else { "checks" }; - if args.short { - // Partition by fixability. Emit the exact command for fixable checks - // so AI callers can act without a reasoning step. - let (fixable, reviewable): (Vec<&str>, Vec<&str>) = failed - .iter() - .copied() - .partition(|name| is_fixable(name, &active)); - let mut segments = vec![]; - if !fixable.is_empty() { - segments.push(format!("flint run --fix {}", fixable.join(" "))); - } - if !reviewable.is_empty() { - segments.push(format!("review: {}", reviewable.join(", "))); - } - eprintln!("flint: {n} {noun} failed — {}", segments.join(" | ")); - } else { - eprintln!( - "\nflint: {n} {noun} failed ({names})", - names = failed.join(", ") - ); - eprintln!( - "💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify." - ); - } - std::process::exit(1); + if let Some(setup_result) = setup_check_result { + results.push(setup_result); } + finish_check_results(results, &active, args.short); Ok(()) } @@ -627,6 +617,47 @@ fn finish_fix_outcomes(outcomes: Vec, allow_fixed: bool) { } } +fn finish_check_results(results: Vec, active: &[®istry::Check], short: bool) { + let mut failed: Vec<&str> = results + .iter() + .filter(|r| !r.ok) + .map(|r| r.name.as_str()) + .collect(); + failed.sort(); + + if failed.is_empty() { + return; + } + + let n = failed.len(); + let noun = if n == 1 { "check" } else { "checks" }; + if short { + // Partition by fixability. Emit the exact command for fixable checks + // so AI callers can act without a reasoning step. + let (fixable, reviewable): (Vec<&str>, Vec<&str>) = failed + .iter() + .copied() + .partition(|name| is_fixable(name, active)); + let mut segments = vec![]; + if !fixable.is_empty() { + segments.push(format!("flint run --fix {}", fixable.join(" "))); + } + if !reviewable.is_empty() { + segments.push(format!("review: {}", reviewable.join(", "))); + } + eprintln!("flint: {n} {noun} failed — {}", segments.join(" | ")); + } else { + eprintln!( + "\nflint: {n} {noun} failed ({names})", + names = failed.join(", ") + ); + eprintln!( + "💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify." + ); + } + std::process::exit(1); +} + fn classify_single_pass_fix(result: CheckResult) -> FixOutcome { if result.ok { if result.changed { @@ -641,6 +672,26 @@ fn classify_single_pass_fix(result: CheckResult) -> FixOutcome { } } +fn setup_result_kind(result: &CheckResult) -> registry::SetupOutcome { + result + .setup_outcome + .unwrap_or(registry::SetupOutcome::Fatal) +} + +fn setup_result_blocks_check(result: &CheckResult) -> bool { + matches!( + setup_result_kind(result), + registry::SetupOutcome::Blocking | registry::SetupOutcome::Fatal + ) +} + +fn setup_result_blocks_fix(result: &CheckResult) -> bool { + matches!( + setup_result_kind(result), + registry::SetupOutcome::Blocking | registry::SetupOutcome::Fatal + ) +} + fn is_flint_setup(check: ®istry::Check) -> bool { check.kind.is_setup() } @@ -925,7 +976,7 @@ fn run_policy_label(run_policy: RunPolicy) -> &'static str { } fn is_fixable(name: &str, active: &[®istry::Check]) -> bool { - active.iter().any(|c| c.name == name && c.has_fix()) + name == "flint-setup" || active.iter().any(|c| c.name == name && c.has_fix()) } fn supports_single_pass_fix(check: ®istry::Check) -> bool { diff --git a/src/registry/checks.rs b/src/registry/checks.rs index c8a0104b..ac437ae1 100644 --- a/src/registry/checks.rs +++ b/src/registry/checks.rs @@ -3,8 +3,6 @@ use crate::linters::{ biome, flint_setup, license_header, lychee, renovate_deps, renovate_deps::RENOVATE_CONFIG_PATTERNS, rumdl, rustfmt, taplo, yamllint, }; -use crate::setup::{V1_BOOTSTRAP_SETUP_VERSION, V2_BASELINE_SETUP_VERSION}; - const TOOL_RUMDL: &[&str] = &["tool", "rumdl"]; const TOOL_CODESPELL: &[&str] = &["tool", "codespell"]; const TOOL_RUFF: &[&str] = &["tool", "ruff"]; @@ -93,7 +91,7 @@ fn check_shellcheck() -> Check { .linter_config(".shellcheckrc", "--rcfile") .baseline_config(ConfigFile::config_dir(".shellcheckrc")) .unsupported_configs(SHELLCHECK_UNSUPPORTED_CONFIGS) - .migrate_tool_keys_after(V2_BASELINE_SETUP_VERSION, &["shellcheck"]) + .migrate_tool_keys(&["shellcheck"]) .desc("Lint shell scripts for common mistakes") .style() } @@ -102,7 +100,7 @@ fn check_shfmt() -> Check { Check::file("shfmt", "shfmt -d {FILE}", &["*.sh", "*.bash"]) .fix("shfmt -w {FILE}") .formatter() - .migrate_tool_keys_after(V1_BOOTSTRAP_SETUP_VERSION, &["github:mvdan/sh"]) + .migrate_tool_keys(&["github:mvdan/sh"]) .desc("Format shell scripts") .style() } @@ -134,10 +132,7 @@ fn check_yaml_lint() -> Check { .formatter() .desc("Lint YAML files for style and consistency") .mise_tool("aqua:owenlamont/ryl") - .migrate_tool_keys_after( - V2_BASELINE_SETUP_VERSION, - &["cargo:yaml-lint", "github:owenlamont/ryl"], - ) + .migrate_tool_keys(&["cargo:yaml-lint", "github:owenlamont/ryl"]) } fn check_taplo() -> Check { @@ -154,7 +149,7 @@ fn check_taplo() -> Check { .stderr_filter_prefixes(&[" INFO taplo:"]) .nonverbose_failure_output(taplo::normalize_nonverbose_failure_output) .formatter() - .migrate_tool_keys_after(V2_BASELINE_SETUP_VERSION, &["github:tamasfe/taplo"]) + .migrate_tool_keys(&["github:tamasfe/taplo"]) .desc("Format TOML files") .docs( "Formats TOML files with [Taplo](https://taplo.tamasfe.dev/).\n\ @@ -198,7 +193,7 @@ fn check_hadolint() -> Check { fn check_xmllint() -> Check { Check::files("xmllint", "xmllint --noout {FILES}", &["*.xml"]) .mise_tool("github:jonwiggins/xmloxide") - .migrate_tool_keys_after(V2_BASELINE_SETUP_VERSION, &["cargo:xmloxide"]) + .migrate_tool_keys(&["cargo:xmloxide"]) .desc("Validate XML files are well-formed") } @@ -245,10 +240,7 @@ fn check_ruff() -> Check { .linter_config("ruff.toml", "--config") .baseline_config(RUFF_BASELINE_CONFIG) .unsupported_configs(RUFF_UNSUPPORTED_CONFIGS) - .migrate_tool_keys_after( - V2_BASELINE_SETUP_VERSION, - &["pipx:ruff", "github:astral-sh/ruff"], - ) + .migrate_tool_keys(&["pipx:ruff", "github:astral-sh/ruff"]) .desc("Lint Python code") .lang() } @@ -276,7 +268,7 @@ fn check_biome() -> Check { .baseline_config(BIOME_BASELINE_CONFIG) .unsupported_configs(BIOME_UNSUPPORTED_CONFIGS) .check_type(&biome::CHECK_TYPE) - .migrate_tool_keys_after(V2_BASELINE_SETUP_VERSION, &["npm:@biomejs/biome"]) + .migrate_tool_keys(&["npm:@biomejs/biome"]) .desc("Lint JS/TS/JSON files") .lang() } @@ -359,10 +351,7 @@ fn check_google_java_format() -> Check { "Java line length is handled by google-java-format", Some(EditorconfigDirectiveStyle::Slash), ) - .migrate_tool_keys_after( - V1_BOOTSTRAP_SETUP_VERSION, - &["ubi:google/google-java-format"], - ) + .migrate_tool_keys(&["ubi:google/google-java-format"]) .desc("Format Java code") .lang() } @@ -380,8 +369,8 @@ fn check_ktlint() -> Check { ) .windows_java_jar() .formatter() - .migrate_tool_keys_after(V1_BOOTSTRAP_SETUP_VERSION, &["ubi:pinterest/ktlint"]) - .migrate_tool_keys_after(V2_BASELINE_SETUP_VERSION, &["github:pinterest/ktlint"]) + .migrate_tool_keys(&["ubi:pinterest/ktlint"]) + .migrate_tool_keys(&["github:pinterest/ktlint"]) .desc("Lint and format Kotlin code") .lang() } @@ -485,8 +474,8 @@ fn check_flint_setup() -> Check { - keep lint-managed tool entries under the `# Linters` header\n\ - keep runtime, SDK, and unknown tool entries above that header\n\ \n\ - With `--fix`, rewrites Flint-managed config in place and advances\n\ - `settings.setup_migration_version` when a migration applies.", + With `--fix`, rewrites Flint-managed config in place and applies any\n\ + currently actionable setup migration.", ) } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index ccd7aa2f..3cd10675 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -9,18 +9,14 @@ pub use mise::{ check_active, flint_version_changed, read_mise_tools, read_mise_tools_at_ref, tool_version_changed, }; -#[cfg(test)] -pub(crate) use obsolete::latest_registry_tool_migration_target_version; -pub use obsolete::{ - find_obsolete_key, find_unsupported_key, obsolete_keys, obsolete_keys_after, unsupported_keys, -}; +pub use obsolete::{find_obsolete_key, find_unsupported_key, obsolete_keys, unsupported_keys}; pub use resolve::binary_on_path; pub use types::{ AdaptiveRelevanceContext, Category, Check, CheckKind, CheckTypeDef, ConfigBase, ConfigFile, ConfigMatch, EditorconfigDirectiveStyle, EditorconfigLineLengthPolicy, FixBehavior, InitHookContext, LinterConfig, LinterOutput, MissingComponentHint, NativeCheck, NativeCheckDef, NativePrepareContext, NativeRunContext, NativeRunFuture, NonverboseFailureOutputHook, - PreparedNativeCheck, RunPolicy, Scope, StatusContext, WorkflowSetup, + PreparedNativeCheck, RunPolicy, Scope, SetupOutcome, StatusContext, WorkflowSetup, }; /// Returns the explicit set of flint-managed tool keys that belong under the diff --git a/src/registry/obsolete.rs b/src/registry/obsolete.rs index 35325e3e..a017fb67 100644 --- a/src/registry/obsolete.rs +++ b/src/registry/obsolete.rs @@ -10,12 +10,6 @@ pub fn unsupported_keys() -> Vec<(&'static str, &'static str)> { crate::setup::unsupported_keys() } -pub fn obsolete_keys_after(version: u32) -> Vec<(&'static str, &'static str)> { - let mut keys = crate::setup::obsolete_keys_after(version); - keys.extend(registry_tool_key_migrations_after(version)); - keys -} - pub fn find_obsolete_key( mise_tools: &HashMap, ) -> Option<(&'static str, &'static str)> { @@ -30,15 +24,6 @@ pub fn find_unsupported_key( crate::setup::find_unsupported_key(mise_tools) } -#[cfg(test)] -pub(crate) fn latest_registry_tool_migration_target_version() -> Option { - crate::registry::builtin() - .into_iter() - .flat_map(|check| check.tool_key_migrations.into_iter()) - .map(|migration| migration.after_setup_migration_version + 1) - .max() -} - fn registry_tool_key_migrations() -> Vec<(&'static str, &'static str)> { crate::registry::builtin() .into_iter() @@ -55,23 +40,6 @@ fn registry_tool_key_migrations() -> Vec<(&'static str, &'static str)> { .collect() } -fn registry_tool_key_migrations_after(version: u32) -> Vec<(&'static str, &'static str)> { - crate::registry::builtin() - .into_iter() - .filter_map(|check| { - let new_key = check.install_key()?; - Some( - check - .tool_key_migrations - .into_iter() - .filter(move |migration| version <= migration.after_setup_migration_version) - .map(move |migration| (migration.old_key, new_key)), - ) - }) - .flatten() - .collect() -} - fn obsolete_key_present(mise_tools: &HashMap, old: &str) -> bool { if old == "shellcheck" && mise_tools.contains_key("github:koalaman/shellcheck") { return false; diff --git a/src/registry/tests.rs b/src/registry/tests.rs index 9d976965..81d1df58 100644 --- a/src/registry/tests.rs +++ b/src/registry/tests.rs @@ -64,8 +64,8 @@ fn shellcheck_alias_does_not_make_github_backend_obsolete() { } #[test] -fn check_owned_tool_migrations_apply_after_v2_baseline() { - let obsolete = obsolete_keys_after(crate::setup::V2_BASELINE_SETUP_VERSION); +fn check_owned_tool_migrations_are_always_actionable() { + let obsolete = obsolete_keys(); assert!(obsolete.contains(&("cargo:yaml-lint", "aqua:owenlamont/ryl"))); assert!(obsolete.contains(&("github:owenlamont/ryl", "aqua:owenlamont/ryl"))); @@ -73,7 +73,6 @@ fn check_owned_tool_migrations_apply_after_v2_baseline() { assert!(obsolete.contains(&("github:astral-sh/ruff", "ruff"))); assert!(obsolete.contains(&("shellcheck", "github:koalaman/shellcheck"))); assert!(obsolete.contains(&("cargo:xmloxide", "github:jonwiggins/xmloxide"))); - assert!(obsolete_keys_after(crate::setup::LATEST_SUPPORTED_SETUP_VERSION).is_empty()); } #[test] diff --git a/src/registry/types.rs b/src/registry/types.rs index f9c57d32..04830b9a 100644 --- a/src/registry/types.rs +++ b/src/registry/types.rs @@ -182,7 +182,6 @@ pub enum EditorconfigLineLengthPolicy { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ToolKeyMigration { - pub after_setup_migration_version: u32, pub old_key: &'static str, } @@ -201,6 +200,7 @@ pub struct LinterOutput { pub ok: bool, pub stdout: Vec, pub stderr: Vec, + pub setup_outcome: Option, } impl LinterOutput { @@ -209,6 +209,35 @@ impl LinterOutput { ok: false, stdout: vec![], stderr: stderr.into(), + setup_outcome: None, + } + } + + pub fn setup_err(setup_outcome: SetupOutcome, stderr: impl Into>) -> Self { + Self { + ok: false, + stdout: vec![], + stderr: stderr.into(), + setup_outcome: Some(setup_outcome), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SetupOutcome { + Clean, + NonBlocking, + Blocking, + Fatal, +} + +impl SetupOutcome { + pub fn at_least(self, other: Self) -> Self { + match (self, other) { + (Self::Fatal, _) | (_, Self::Fatal) => Self::Fatal, + (Self::Blocking, _) | (_, Self::Blocking) => Self::Blocking, + (Self::NonBlocking, _) | (_, Self::NonBlocking) => Self::NonBlocking, + (Self::Clean, Self::Clean) => Self::Clean, } } } @@ -923,19 +952,11 @@ impl Check { self } - /// Old mise tool keys that should migrate to this check's current install - /// key when the repo setup migration version is at or before - /// `after_setup_migration_version`. - pub fn migrate_tool_keys_after( - mut self, - after_setup_migration_version: u32, - old_keys: &'static [&'static str], - ) -> Self { + /// Old mise tool keys that should always migrate to this check's current + /// install key when encountered in `mise.toml`. + pub fn migrate_tool_keys(mut self, old_keys: &'static [&'static str]) -> Self { self.tool_key_migrations - .extend(old_keys.iter().map(|old_key| ToolKeyMigration { - after_setup_migration_version, - old_key, - })); + .extend(old_keys.iter().map(|old_key| ToolKeyMigration { old_key })); self } } diff --git a/src/runner.rs b/src/runner.rs index fed7c925..5548d738 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -27,6 +27,7 @@ pub struct CheckResult { pub changed: bool, pub stdout: Vec, pub stderr: Vec, + pub setup_outcome: Option, pub duration: Duration, } @@ -130,6 +131,7 @@ impl PreparedCheck { changed, stdout: out.stdout, stderr: out.stderr, + setup_outcome: out.setup_outcome, duration: start.elapsed(), } } @@ -570,6 +572,7 @@ async fn run_invocations( ok: all_ok, stdout: combined_stdout, stderr: combined_stderr, + setup_outcome: None, } } diff --git a/src/setup.rs b/src/setup.rs index ba3904b0..9cf39592 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -1,19 +1,5 @@ use std::collections::HashMap; -// Name only durable setup boundaries. Routine migration targets can stay numeric -// in SETUP_MIGRATIONS unless they become a baseline that call sites need to -// reference directly. LATEST_SUPPORTED_SETUP_VERSION is intentionally the only -// moving constant. -pub const V1_BOOTSTRAP_SETUP_VERSION: u32 = 0; -pub const V2_BASELINE_SETUP_VERSION: u32 = 1; -pub const LATEST_SUPPORTED_SETUP_VERSION: u32 = 2; - -pub struct SetupMigration { - pub target_version: u32, - pub obsolete_keys: &'static [(&'static str, &'static str)], - pub unsupported_keys: &'static [(&'static str, &'static str)], -} - const UNSUPPORTED_KEYS_TO_SETUP_VERSION_2: &[(&str, &str)] = &[ ( "npm:markdownlint-cli", @@ -29,128 +15,47 @@ const UNSUPPORTED_KEYS_TO_SETUP_VERSION_2: &[(&str, &str)] = &[ ), ]; -pub const SETUP_MIGRATIONS: &[SetupMigration] = &[ - SetupMigration { - target_version: V2_BASELINE_SETUP_VERSION, - obsolete_keys: &[], - unsupported_keys: &[], - }, - SetupMigration { - target_version: LATEST_SUPPORTED_SETUP_VERSION, - obsolete_keys: &[], - unsupported_keys: UNSUPPORTED_KEYS_TO_SETUP_VERSION_2, - }, -]; - pub fn find_unsupported_key( mise_tools: &HashMap, ) -> Option<(&'static str, &'static str)> { - SETUP_MIGRATIONS + UNSUPPORTED_KEYS_TO_SETUP_VERSION_2 .iter() - .flat_map(|migration| migration.unsupported_keys.iter()) .find(|(old, _)| mise_tools.contains_key(*old)) .copied() } pub fn obsolete_keys() -> Vec<(&'static str, &'static str)> { - SETUP_MIGRATIONS - .iter() - .flat_map(|migration| migration.obsolete_keys.iter().copied()) - .collect() + vec![] } pub fn unsupported_keys() -> Vec<(&'static str, &'static str)> { - SETUP_MIGRATIONS - .iter() - .flat_map(|migration| migration.unsupported_keys.iter().copied()) - .collect() -} - -pub fn obsolete_keys_after(version: u32) -> Vec<(&'static str, &'static str)> { - SETUP_MIGRATIONS - .iter() - .filter(|migration| migration.target_version > version) - .flat_map(|migration| migration.obsolete_keys.iter().copied()) - .collect() -} - -pub fn unsupported_keys_after(version: u32) -> Vec<(&'static str, &'static str)> { - SETUP_MIGRATIONS - .iter() - .filter(|migration| migration.target_version > version) - .flat_map(|migration| migration.unsupported_keys.iter().copied()) - .collect() + UNSUPPORTED_KEYS_TO_SETUP_VERSION_2.to_vec() } #[cfg(test)] mod tests { use super::*; - #[test] - fn latest_supported_setup_migration_version_matches_latest_migration_target() { - let latest_setup_migration = SETUP_MIGRATIONS - .iter() - .map(|migration| migration.target_version) - .max() - .unwrap_or(0); - let latest_registry_migration = - crate::registry::latest_registry_tool_migration_target_version().unwrap_or(0); - let latest = latest_setup_migration.max(latest_registry_migration); - assert_eq!( - LATEST_SUPPORTED_SETUP_VERSION, latest, - "LATEST_SUPPORTED_SETUP_VERSION must match the latest setup migration target version" - ); - } - - #[test] - fn setup_migration_versions_are_strictly_increasing() { - let mut previous = V1_BOOTSTRAP_SETUP_VERSION; - for migration in SETUP_MIGRATIONS { - assert!( - migration.target_version > previous, - "setup migration versions must be strictly increasing" - ); - previous = migration.target_version; - } - } - #[test] fn setup_migration_keys_are_unique() { - let mut obsolete_seen = std::collections::HashSet::new(); let mut unsupported_seen = std::collections::HashSet::new(); - for migration in SETUP_MIGRATIONS { - for (old, _) in migration.obsolete_keys { - assert!( - obsolete_seen.insert(*old), - "duplicate obsolete setup migration key: {old}" - ); - } - for (old, _) in migration.unsupported_keys { - assert!( - unsupported_seen.insert(*old), - "duplicate unsupported setup migration key: {old}" - ); - } + for (old, _) in UNSUPPORTED_KEYS_TO_SETUP_VERSION_2 { + assert!( + unsupported_seen.insert(*old), + "duplicate unsupported setup migration key: {old}" + ); } } #[test] - fn v2_baseline_tombstones_are_explicit() { - let obsolete = obsolete_keys_after(V2_BASELINE_SETUP_VERSION); - let unsupported = unsupported_keys_after(V2_BASELINE_SETUP_VERSION); - - assert!( - obsolete.is_empty(), - "live tool-key migrations should live in the registry" - ); + fn unsupported_tombstones_are_explicit() { + let unsupported = unsupported_keys(); assert!( unsupported .iter() .any(|(old, _)| *old == "npm:markdownlint-cli2") ); assert!(unsupported.iter().any(|(old, _)| *old == "npm:prettier")); - assert!(obsolete_keys_after(LATEST_SUPPORTED_SETUP_VERSION).is_empty()); - assert!(unsupported_keys_after(LATEST_SUPPORTED_SETUP_VERSION).is_empty()); } } diff --git a/tests/cases/general/flint-setup-obsolete-key/files/flint.toml b/tests/cases/general/flint-setup-obsolete-key/files/flint.toml index d600bea4..dd6b520a 100644 --- a/tests/cases/general/flint-setup-obsolete-key/files/flint.toml +++ b/tests/cases/general/flint-setup-obsolete-key/files/flint.toml @@ -1,2 +1 @@ [settings] -setup_migration_version = 1 diff --git a/tests/cases/general/flint-setup-obsolete-key/test.toml b/tests/cases/general/flint-setup-obsolete-key/test.toml index 59529efb..26ebc7dd 100644 --- a/tests/cases/general/flint-setup-obsolete-key/test.toml +++ b/tests/cases/general/flint-setup-obsolete-key/test.toml @@ -14,5 +14,4 @@ ruff = "0.15.0" ''' "flint.toml" = ''' [settings] -setup_migration_version = 2 ''' diff --git a/tests/cases/general/flint-setup-unaffected-old-version/files/flint.toml b/tests/cases/general/flint-setup-unaffected-old-version/files/flint.toml index d600bea4..dd6b520a 100644 --- a/tests/cases/general/flint-setup-unaffected-old-version/files/flint.toml +++ b/tests/cases/general/flint-setup-unaffected-old-version/files/flint.toml @@ -1,2 +1 @@ [settings] -setup_migration_version = 1 diff --git a/tests/cases/general/init-rust/test.toml b/tests/cases/general/init-rust/test.toml index 1ba31718..df60a11c 100644 --- a/tests/cases/general/init-rust/test.toml +++ b/tests/cases/general/init-rust/test.toml @@ -54,7 +54,6 @@ rules: ''' ".github/config/flint.toml" = ''' [settings] -setup_migration_version = 2 # exclude = ["CHANGELOG\\.md"] ''' ".github/config/rustfmt.toml" = ''' diff --git a/tests/cases/general/setup-order-continues-check/files/mise.toml b/tests/cases/general/setup-order-continues-check/files/mise.toml new file mode 100644 index 00000000..a47659e6 --- /dev/null +++ b/tests/cases/general/setup-order-continues-check/files/mise.toml @@ -0,0 +1,3 @@ +[tools] +shfmt = "latest" +node = "20" diff --git a/tests/cases/general/setup-order-continues-check/files/script.sh b/tests/cases/general/setup-order-continues-check/files/script.sh new file mode 100644 index 00000000..97a8c2ca --- /dev/null +++ b/tests/cases/general/setup-order-continues-check/files/script.sh @@ -0,0 +1,4 @@ +#!/bin/sh +if true; then +echo "hello" +fi diff --git a/tests/cases/general/setup-order-continues-check/test.toml b/tests/cases/general/setup-order-continues-check/test.toml new file mode 100644 index 00000000..62dbfa1d --- /dev/null +++ b/tests/cases/general/setup-order-continues-check/test.toml @@ -0,0 +1,56 @@ +[expected] +args = "run --full" +exit = 1 +stderr = ''' +[flint-setup] +ERROR: mise.toml [tools] entries are not in Flint's canonical order. +Run `flint run --fix flint-setup` to apply Flint setup migrations. +[shfmt] +diff "/script.sh.orig" "/script.sh" +--- "/script.sh.orig" ++++ "/script.sh" +@@ -1,4 +1,4 @@ + #!/bin/sh + if true; then +-echo "hello" ++ echo "hello" + fi + +flint: 2 checks failed (flint-setup, shfmt) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' + +[fake_bins] +shfmt = ''' +#!/bin/sh +set -eu + +case "${1:-}" in + -d) + cat <"$2" <<'EOF' +#!/bin/sh +if true; then + echo "hello" +fi +EOF + ;; + *) + echo "unexpected shfmt invocation: $*" >&2 + exit 1 + ;; +esac +''' diff --git a/tests/cases/general/setup-order-continues-fix/files/mise.toml b/tests/cases/general/setup-order-continues-fix/files/mise.toml new file mode 100644 index 00000000..a47659e6 --- /dev/null +++ b/tests/cases/general/setup-order-continues-fix/files/mise.toml @@ -0,0 +1,3 @@ +[tools] +shfmt = "latest" +node = "20" diff --git a/tests/cases/general/setup-order-continues-fix/files/script.sh b/tests/cases/general/setup-order-continues-fix/files/script.sh new file mode 100644 index 00000000..97a8c2ca --- /dev/null +++ b/tests/cases/general/setup-order-continues-fix/files/script.sh @@ -0,0 +1,4 @@ +#!/bin/sh +if true; then +echo "hello" +fi diff --git a/tests/cases/general/setup-order-continues-fix/test.toml b/tests/cases/general/setup-order-continues-fix/test.toml new file mode 100644 index 00000000..1e66d811 --- /dev/null +++ b/tests/cases/general/setup-order-continues-fix/test.toml @@ -0,0 +1,38 @@ +[expected] +args = "run --full --fix" +exit = 1 +stdout = " normalized [tools] in /mise.toml\n" +stderr = "flint: fixed: flint-setup, shfmt — commit before pushing\n" + +[expected.files] +"mise.toml" = ''' +[tools] +node = "20" + +# Linters +shfmt = "latest" +''' +"script.sh" = ''' +#!/bin/sh +if true; then + echo "hello" +fi +''' + +[fake_bins] +shfmt = ''' +#!/bin/sh +set -eu + +if [ "${1:-}" != "-w" ]; then + echo "unexpected shfmt invocation: $*" >&2 + exit 1 +fi + +cat >"$2" <<'EOF' +#!/bin/sh +if true; then + echo "hello" +fi +EOF +'''