use serde::Deserialize;
use std::path::{Path, PathBuf};
#[cfg(test)]
thread_local! {
static CONFIG_WARNINGS: std::cell::RefCell<Vec<String>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn emit_config_warning(msg: impl std::fmt::Display) {
let text = msg.to_string();
#[cfg(test)]
CONFIG_WARNINGS.with(|w| w.borrow_mut().push(text.clone()));
eprintln!("{text}");
}
#[cfg(test)]
pub(crate) fn take_config_warnings() -> Vec<String> {
CONFIG_WARNINGS.with(|w| std::mem::take(&mut *w.borrow_mut()))
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct WritePolicyConfig {
pub ensure_final_newline: Option<bool>,
pub normalize_eol: Option<String>,
pub trim_trailing_whitespace: Option<bool>,
pub collapse_blanks: Option<bool>,
pub respect_editorconfig: Option<bool>,
}
impl From<WritePolicyConfig> for crate::write::WritePolicyOverride {
fn from(c: WritePolicyConfig) -> Self {
Self {
ensure_final_newline: c.ensure_final_newline,
normalize_eol: c.normalize_eol,
trim_trailing_whitespace: c.trim_trailing_whitespace,
collapse_blanks: c.collapse_blanks,
respect_editorconfig: c.respect_editorconfig,
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProjectConfig {
pub write_policy: WritePolicyConfig,
pub exclude: Exclude,
pub output: Output,
pub tx: TxConfig,
pub defaults: Defaults,
pub format: FormatConfig,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Defaults {
pub apply: Option<bool>,
pub format: Option<String>,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct FormatConfig {
pub auto: Option<bool>,
pub command: Option<String>,
#[serde(default)]
pub by_extension: std::collections::HashMap<String, String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TxConfig {
pub strict: Option<bool>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Exclude {
#[serde(default)]
pub globs: Vec<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Output {
pub color: Option<String>,
}
pub fn find_and_load(start: &Path) -> Option<(ProjectConfig, PathBuf)> {
find_and_load_opts(start, false)
}
pub fn find_and_load_strict(start: &Path) -> anyhow::Result<Option<(ProjectConfig, PathBuf)>> {
let mut dir = start.to_path_buf();
loop {
let candidate = dir.join(".patchloom.toml");
if candidate.is_file() {
let content =
std::fs::read_to_string(&candidate).map_err(|e| crate::exit::ParseErrorError {
msg: format!("could not read {}: {e}", candidate.display()),
})?;
let config = toml_edit::de::from_str::<ProjectConfig>(&content).map_err(|e| {
crate::exit::ParseErrorError {
msg: format!("malformed {}: {e}", candidate.display()),
}
})?;
return Ok(Some((config, dir)));
}
if dir.join(".git").exists() {
return Ok(None);
}
if !dir.pop() {
return Ok(None);
}
}
}
pub fn find_and_load_opts(start: &Path, warn: bool) -> Option<(ProjectConfig, PathBuf)> {
match find_and_load_strict(start) {
Ok(v) => v,
Err(e) => {
if warn {
eprintln!("warning: {e}");
}
None
}
}
}
pub fn apply_config(global: &mut crate::cli::global::GlobalFlags, config: &ProjectConfig) {
let show_warn = !global.quiet && !global.json && !global.jsonl;
if !global.ensure_final_newline && config.write_policy.ensure_final_newline == Some(true) {
global.ensure_final_newline = true;
}
if global.normalize_eol.is_none() {
match config.write_policy.normalize_eol.as_deref() {
Some("lf") => global.normalize_eol = Some(crate::cli::global::EolMode::Lf),
Some("crlf") => global.normalize_eol = Some(crate::cli::global::EolMode::Crlf),
Some("cr") => global.normalize_eol = Some(crate::cli::global::EolMode::Cr),
Some("keep") => {} Some(invalid) if show_warn => {
emit_config_warning(invalid_normalize_eol_warning(invalid));
}
Some(_) | None => {}
}
}
if !global.trim_trailing_whitespace
&& config.write_policy.trim_trailing_whitespace == Some(true)
{
global.trim_trailing_whitespace = true;
}
if !global.collapse_blanks && config.write_policy.collapse_blanks == Some(true) {
global.collapse_blanks = true;
}
if !global.respect_editorconfig && config.write_policy.respect_editorconfig == Some(true) {
global.respect_editorconfig = true;
}
if config.defaults.apply == Some(true)
&& show_warn
&& global.write_command
&& !global.apply
&& !global.check
&& !global.diff
&& !global.confirm
{
emit_config_warning(ignored_defaults_apply_warning());
}
if global.format.is_none() {
if let Some(ref fmt) = config.defaults.format {
global.format = Some(fmt.clone());
} else if config.format.auto == Some(true)
&& let Some(ref cmd) = config.format.command
{
global.format = Some(cmd.clone());
}
}
if config.format.auto == Some(true)
&& (!config.format.by_extension.is_empty() || config.format.command.is_some())
{
global.format_config = Some(config.format.clone());
}
if !config.exclude.globs.is_empty() {
let mut merged = config.exclude.globs.clone();
merged.append(&mut global.exclude);
global.exclude = merged;
}
if !global.color_explicit
&& matches!(global.color, crate::cli::global::ColorMode::Auto)
&& let Some(ref color) = config.output.color
{
global.color = match color.as_str() {
"always" => crate::cli::global::ColorMode::Always,
"never" => crate::cli::global::ColorMode::Never,
"auto" => crate::cli::global::ColorMode::Auto,
invalid => {
if show_warn {
emit_config_warning(invalid_output_color_warning(invalid));
}
crate::cli::global::ColorMode::Auto
}
};
}
}
fn ignored_defaults_apply_warning() -> &'static str {
"warning: [defaults] apply = true is ignored; pass --apply to write"
}
fn invalid_normalize_eol_warning(invalid: &str) -> String {
format!(
"warning: invalid write_policy.normalize_eol value {invalid:?}; expected \"lf\", \"crlf\", or \"cr\""
)
}
fn invalid_output_color_warning(invalid: &str) -> String {
format!(
"warning: invalid output.color value {invalid:?}; expected \"always\" or \"never\". Using \"auto\"."
)
}
#[derive(Debug)]
pub struct CachedConfig {
config: ProjectConfig,
config_dir: Option<PathBuf>,
}
impl CachedConfig {
pub fn load(start: &Path) -> Self {
match find_and_load(start) {
Some((config, dir)) => Self {
config,
config_dir: Some(dir),
},
None => Self {
config: ProjectConfig::default(),
config_dir: None,
},
}
}
pub fn project_config(&self) -> &ProjectConfig {
&self.config
}
pub fn config_dir(&self) -> Option<&Path> {
self.config_dir.as_deref()
}
}
const _: () = {
fn _assert<T: Send + Sync>() {}
let _ = _assert::<CachedConfig>;
};
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn find_and_load_from_dir() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
r#"
[write_policy]
ensure_final_newline = true
normalize_eol = "lf"
collapse_blanks = true
[exclude]
globs = ["target/**"]
[output]
color = "always"
"#,
)
.unwrap();
let (config, found_dir) = find_and_load(dir.path()).unwrap();
assert_eq!(found_dir, dir.path());
assert_eq!(config.write_policy.ensure_final_newline, Some(true));
assert_eq!(config.write_policy.normalize_eol.as_deref(), Some("lf"));
assert_eq!(config.write_policy.collapse_blanks, Some(true));
assert_eq!(config.exclude.globs, vec!["target/**"]);
assert_eq!(config.output.color.as_deref(), Some("always"));
}
#[test]
fn find_and_load_walks_up() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"[write_policy]\nensure_final_newline = true\n",
)
.unwrap();
let sub = dir.path().join("sub/deep");
std::fs::create_dir_all(&sub).unwrap();
let (config, found_dir) = find_and_load(&sub).unwrap();
assert_eq!(found_dir, dir.path());
assert_eq!(config.write_policy.ensure_final_newline, Some(true));
}
#[test]
fn find_and_load_tx_strict_override() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".patchloom.toml"), "[tx]\nstrict = false\n").unwrap();
let (config, _) = find_and_load(dir.path()).unwrap();
assert_eq!(config.tx.strict, Some(false));
}
#[test]
fn find_and_load_returns_none_when_missing() {
let dir = TempDir::new().unwrap();
assert!(find_and_load(dir.path()).is_none());
}
#[test]
fn find_and_load_stops_at_git_boundary() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"[write_policy]\nensure_final_newline = true\n",
)
.unwrap();
let child = dir.path().join("child");
std::fs::create_dir_all(&child).unwrap();
std::fs::create_dir_all(child.join(".git")).unwrap();
assert!(find_and_load(&child).is_none());
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_sets_defaults() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
ensure_final_newline: Some(true),
normalize_eol: Some("lf".into()),
trim_trailing_whitespace: Some(true),
collapse_blanks: Some(true),
respect_editorconfig: Some(true),
},
exclude: Exclude {
globs: vec!["target/**".into()],
},
output: Output {
color: Some("always".into()),
},
tx: TxConfig::default(),
defaults: Defaults::default(),
format: FormatConfig::default(),
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(global.ensure_final_newline);
assert!(matches!(
global.normalize_eol,
Some(crate::cli::global::EolMode::Lf)
));
assert!(global.trim_trailing_whitespace);
assert!(global.collapse_blanks);
assert!(global.respect_editorconfig);
assert_eq!(global.exclude, vec!["target/**"]);
assert!(
global.glob.is_empty(),
"exclude globs must not leak into include globs"
);
assert!(matches!(
global.color,
crate::cli::global::ColorMode::Always
));
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_cli_flags_win() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
ensure_final_newline: Some(true),
..WritePolicyConfig::default()
},
exclude: Exclude {
globs: vec!["config_glob".into()],
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
ensure_final_newline: true,
glob: vec!["user_glob".into()],
color: crate::cli::global::ColorMode::Never,
..crate::cli::global::GlobalFlags::default()
};
apply_config(&mut global, &config);
assert!(global.ensure_final_newline);
assert_eq!(global.exclude, vec!["config_glob"]);
assert_eq!(global.glob, vec!["user_glob"]);
assert!(matches!(global.color, crate::cli::global::ColorMode::Never));
}
#[test]
fn find_and_load_rejects_unknown_keys() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"[write_policy]\nensur_final_newline = true\n",
)
.unwrap();
assert!(find_and_load(dir.path()).is_none());
}
#[test]
fn find_and_load_returns_none_on_malformed_toml() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"this is not valid { toml [",
)
.unwrap();
assert!(find_and_load(dir.path()).is_none());
}
#[test]
fn find_and_load_strict_errors_on_unknown_keys() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"[write_policy]\nensur_final_newline = true\n",
)
.unwrap();
let err = find_and_load_strict(dir.path()).expect_err("typo must be a hard error");
assert!(
crate::exit::is_parse_error(&err),
"malformed config must be parse_error: {err}"
);
assert!(
err.to_string().contains("ensur_final_newline")
|| err.to_string().contains("unknown")
|| err.to_string().contains("malformed"),
"error must name the problem: {err}"
);
}
#[test]
fn find_and_load_strict_errors_on_malformed_toml() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"this is not valid { toml [",
)
.unwrap();
let err =
find_and_load_strict(dir.path()).expect_err("malformed TOML must be a hard error");
assert!(
crate::exit::is_parse_error(&err),
"malformed config must be parse_error: {err}"
);
}
#[test]
fn find_and_load_strict_ok_when_missing() {
let dir = TempDir::new().unwrap();
assert!(find_and_load_strict(dir.path()).unwrap().is_none());
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_unknown_eol_value_ignored() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
normalize_eol: Some("CRLF".into()), ..WritePolicyConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(global.normalize_eol.is_none());
let warning = invalid_normalize_eol_warning("CRLF");
assert!(warning.contains("CRLF"));
assert!(warning.contains("\"lf\""));
assert!(warning.contains("\"crlf\""));
assert!(warning.contains("\"cr\""));
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_crlf_eol() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
normalize_eol: Some("crlf".into()),
..WritePolicyConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(matches!(
global.normalize_eol,
Some(crate::cli::global::EolMode::Crlf)
));
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_color_auto_no_warning() {
let config = ProjectConfig {
output: Output {
color: Some("auto".into()),
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(matches!(global.color, crate::cli::global::ColorMode::Auto));
}
#[test]
fn apply_config_unknown_color_value_stays_auto() {
let config = ProjectConfig {
output: Output {
color: Some("yes".into()), },
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(matches!(global.color, crate::cli::global::ColorMode::Auto));
let warning = invalid_output_color_warning("yes");
assert!(warning.contains("yes"));
assert!(warning.contains("always"));
assert!(warning.contains("never"));
}
#[test]
fn cached_config_load_with_file() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
"[write_policy]\nensure_final_newline = true\n",
)
.unwrap();
let cached = CachedConfig::load(dir.path());
assert_eq!(
cached.project_config().write_policy.ensure_final_newline,
Some(true)
);
assert_eq!(cached.config_dir(), Some(dir.path()));
}
#[test]
fn cached_config_load_defaults_when_missing() {
let dir = TempDir::new().unwrap();
let cached = CachedConfig::load(dir.path());
assert_eq!(
cached.project_config().write_policy.ensure_final_newline,
None
);
assert!(cached.config_dir().is_none());
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_respect_editorconfig_from_config() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
respect_editorconfig: Some(true),
..WritePolicyConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
assert!(!global.respect_editorconfig);
apply_config(&mut global, &config);
assert!(global.respect_editorconfig);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_ignores_defaults_apply_from_repo_config() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
assert!(!global.apply);
apply_config(&mut global, &config);
assert!(
!global.apply,
"repo [defaults] apply must not flip write mode; flags only"
);
let warning = ignored_defaults_apply_warning();
assert!(
warning.contains("[defaults] apply"),
"ignored-apply warning must name the config key: {warning}"
);
assert!(
warning.contains("--apply"),
"ignored-apply warning must point at --apply: {warning}"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_does_not_warn_on_read_only_for_defaults_apply() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
let _ = take_config_warnings();
apply_config(&mut global, &config);
assert!(!global.apply);
let warnings = take_config_warnings();
assert!(
warnings
.iter()
.all(|w| !w.contains("apply = true is ignored")),
"read-only must not warn about [defaults] apply: {warnings:?}"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_warns_on_write_preview_for_defaults_apply() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
write_command: true,
..crate::cli::global::GlobalFlags::default()
};
let _ = take_config_warnings();
apply_config(&mut global, &config);
assert!(!global.apply);
let warnings = take_config_warnings();
assert!(
warnings
.iter()
.any(|w| w.contains("apply = true is ignored")),
"write preview must still warn: {warnings:?}"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_does_not_warn_on_write_check_for_defaults_apply() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
write_command: true,
check: true,
..crate::cli::global::GlobalFlags::default()
};
let _ = take_config_warnings();
apply_config(&mut global, &config);
let warnings = take_config_warnings();
assert!(
warnings
.iter()
.all(|w| !w.contains("apply = true is ignored")),
"explicit --check must not warn: {warnings:?}"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_explicit_color_auto_overrides_config() {
let config = ProjectConfig {
output: Output {
color: Some("always".into()),
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
color: crate::cli::global::ColorMode::Auto,
color_explicit: true,
..crate::cli::global::GlobalFlags::default()
};
apply_config(&mut global, &config);
assert!(
matches!(global.color, crate::cli::global::ColorMode::Auto),
"explicit --color auto must not be overwritten by config: {:?}",
global.color
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_json_suppresses_defaults_apply_warning() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
json: true,
..crate::cli::global::GlobalFlags::default()
};
let _ = take_config_warnings();
apply_config(&mut global, &config);
assert!(!global.apply);
let warnings = take_config_warnings();
assert!(
warnings
.iter()
.all(|w| !w.contains("apply = true is ignored")),
"json output must suppress apply-ignore warning: {warnings:?}"
);
}
#[test]
fn apply_config_apply_default_respects_check_flag() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
check: true, ..crate::cli::global::GlobalFlags::default()
};
apply_config(&mut global, &config);
assert!(
!global.apply,
"defaults.apply should not override explicit --check"
);
}
#[test]
fn apply_config_apply_default_respects_diff_flag() {
let config = ProjectConfig {
defaults: Defaults {
apply: Some(true),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags {
diff: true, ..crate::cli::global::GlobalFlags::default()
};
apply_config(&mut global, &config);
assert!(
!global.apply,
"defaults.apply should not override explicit --diff"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_format_default_from_config() {
let config = ProjectConfig {
defaults: Defaults {
format: Some("cargo fmt --all".into()),
..Defaults::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
assert!(global.format.is_none());
apply_config(&mut global, &config);
assert_eq!(global.format.as_deref(), Some("cargo fmt --all"));
}
#[test]
fn find_and_load_with_defaults() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
r#"
[defaults]
apply = true
format = "cargo fmt"
[write_policy]
respect_editorconfig = true
"#,
)
.unwrap();
let (config, found_dir) = find_and_load(dir.path()).unwrap();
assert_eq!(found_dir, dir.path());
assert_eq!(config.defaults.apply, Some(true));
assert_eq!(config.defaults.format.as_deref(), Some("cargo fmt"));
assert_eq!(config.write_policy.respect_editorconfig, Some(true));
}
#[test]
fn find_and_load_format_config() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
r#"
[format]
auto = true
command = "treefmt"
[format.by_extension]
rs = "rustfmt"
go = "gofmt -w"
ts = "prettier --write"
"#,
)
.unwrap();
let (config, _) = find_and_load(dir.path()).unwrap();
assert_eq!(config.format.auto, Some(true));
assert_eq!(config.format.command.as_deref(), Some("treefmt"));
assert_eq!(config.format.by_extension.len(), 3);
assert_eq!(config.format.by_extension.get("rs").unwrap(), "rustfmt");
assert_eq!(config.format.by_extension.get("go").unwrap(), "gofmt -w");
assert_eq!(
config.format.by_extension.get("ts").unwrap(),
"prettier --write"
);
}
#[test]
fn find_and_load_format_auto_without_command() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(".patchloom.toml"),
r#"
[format]
auto = true
[format.by_extension]
rs = "rustfmt"
"#,
)
.unwrap();
let (config, _) = find_and_load(dir.path()).unwrap();
assert_eq!(config.format.auto, Some(true));
assert!(config.format.command.is_none());
assert_eq!(config.format.by_extension.get("rs").unwrap(), "rustfmt");
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_format_auto_command_sets_global_format() {
let config = ProjectConfig {
format: FormatConfig {
auto: Some(true),
command: Some("treefmt".into()),
..FormatConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
assert!(global.format.is_none());
apply_config(&mut global, &config);
assert_eq!(global.format.as_deref(), Some("treefmt"));
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_stores_format_config_for_by_extension() {
let mut by_ext = std::collections::HashMap::new();
by_ext.insert("rs".into(), "rustfmt".into());
by_ext.insert("py".into(), "black".into());
let config = ProjectConfig {
format: FormatConfig {
auto: Some(true),
command: None,
by_extension: by_ext,
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
assert!(global.format_config.is_none());
apply_config(&mut global, &config);
let fc = global
.format_config
.as_ref()
.expect("format_config should be set");
assert_eq!(fc.auto, Some(true));
assert_eq!(fc.by_extension.len(), 2);
assert_eq!(fc.by_extension.get("rs").unwrap(), "rustfmt");
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_skips_format_config_when_auto_false() {
let mut by_ext = std::collections::HashMap::new();
by_ext.insert("rs".into(), "rustfmt".into());
let config = ProjectConfig {
format: FormatConfig {
auto: None,
command: None,
by_extension: by_ext,
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(
global.format_config.is_none(),
"format_config should not be set when auto != true"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_keep_eol_is_noop() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
normalize_eol: Some("keep".into()),
..WritePolicyConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(
global.normalize_eol.is_none(),
"keep should not set normalize_eol"
);
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_cr_eol() {
let config = ProjectConfig {
write_policy: WritePolicyConfig {
normalize_eol: Some("cr".into()),
..WritePolicyConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert!(matches!(
global.normalize_eol,
Some(crate::cli::global::EolMode::Cr)
));
}
#[test]
#[cfg(feature = "cli")]
fn apply_config_defaults_format_wins_over_format_command() {
let config = ProjectConfig {
defaults: Defaults {
format: Some("cargo fmt --all".into()),
..Defaults::default()
},
format: FormatConfig {
auto: Some(true),
command: Some("treefmt".into()),
..FormatConfig::default()
},
..ProjectConfig::default()
};
let mut global = crate::cli::global::GlobalFlags::default();
apply_config(&mut global, &config);
assert_eq!(global.format.as_deref(), Some("cargo fmt --all"));
}
#[test]
fn write_policy_config_rejects_unknown_fields() {
let toml = r#"ensur_final_newline = true"#; let result = toml_edit::de::from_str::<WritePolicyConfig>(toml);
assert!(
result.is_err(),
"WritePolicyConfig should reject unknown fields to catch typos"
);
}
#[test]
fn write_policy_config_converts_to_override() {
let config = WritePolicyConfig {
ensure_final_newline: Some(true),
normalize_eol: Some("lf".into()),
trim_trailing_whitespace: Some(true),
collapse_blanks: None,
respect_editorconfig: Some(false),
};
let ov: crate::write::WritePolicyOverride = config.into();
assert_eq!(ov.ensure_final_newline, Some(true));
assert_eq!(ov.normalize_eol.as_deref(), Some("lf"));
assert_eq!(ov.trim_trailing_whitespace, Some(true));
assert!(ov.collapse_blanks.is_none());
assert_eq!(ov.respect_editorconfig, Some(false));
}
}