use std::fs;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use dirs::config_dir;
use serde::Deserialize;
use toml::from_str;
use super::diagnostics_config::DiagnosticsConfig;
use crate::constants::HELP_URL_BASE;
pub(crate) const APP_NAME: &str = "cargo-mend";
pub(crate) const GLOBAL_CONFIG_FILE: &str = "config.toml";
#[derive(Debug, Default, Deserialize)]
struct GlobalConfigFile {
#[serde(default, rename = "diagnostics")]
diagnostics_config: DiagnosticsConfig,
}
pub(crate) fn global_config_path() -> Option<PathBuf> {
config_dir().map(|d| d.join(APP_NAME).join(GLOBAL_CONFIG_FILE))
}
pub(crate) fn load_global_diagnostics() -> DiagnosticsConfig {
let Some(path) = global_config_path() else {
return DiagnosticsConfig::default();
};
if !path.exists() {
let _ = create_default_global_config(&path);
let Ok(contents) = fs::read_to_string(&path) else {
return DiagnosticsConfig::default();
};
return from_str::<GlobalConfigFile>(&contents).map_or_else(
|_| DiagnosticsConfig::default(),
|file| file.diagnostics_config,
);
}
let Ok(contents) = fs::read_to_string(&path) else {
return DiagnosticsConfig::default();
};
from_str::<GlobalConfigFile>(&contents).map_or_else(
|_| DiagnosticsConfig::default(),
|file| file.diagnostics_config,
)
}
fn default_global_config_toml() -> String {
format!(
r"# cargo-mend global configuration
# See {HELP_URL_BASE}#diagnostics for details on each rule.
# Per-project overrides go in mend.toml at your project or workspace root.
[diagnostics]
forbidden_pub_crate = true
forbidden_pub_in_crate = true
review_pub_mod = true
suspicious_pub = true
unused_pub = true
prefer_module_import = true
inline_path_qualified_type = true
shorten_local_crate_import = true
replace_deep_super_import = true
wildcard_parent_pub_use = true
internal_parent_pub_use_facade = true
narrow_to_pub_crate = true
field_visibility_wider_than_type = true
imports_at_top = true
"
)
}
fn create_default_global_config(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create config directory {}", parent.display()))?;
}
fs::write(path, default_global_config_toml())
.with_context(|| format!("failed to write default config to {}", path.display()))?;
Ok(())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
reason = "tests should panic on unexpected values"
)]
mod tests {
use toml::from_str;
use super::GlobalConfigFile;
use super::default_global_config_toml;
use crate::config::DiagnosticCode;
use crate::config::DiagnosticStatus;
#[test]
fn default_global_config_toml_parses_correctly() {
let result: Result<GlobalConfigFile, _> = from_str(&default_global_config_toml());
assert!(result.is_ok(), "default_global_config_toml() should parse");
let global_config_file = result.unwrap();
for (code, enabled) in global_config_file.diagnostics_config.entries() {
assert!(
matches!(enabled, DiagnosticStatus::Enabled),
"default config should have {} enabled",
code.as_str()
);
}
}
#[test]
fn partial_toml_uses_defaults_for_missing_fields() {
let toml_str = r"
[diagnostics]
prefer_module_import = false
";
let result: Result<GlobalConfigFile, _> = from_str(toml_str);
assert!(result.is_ok(), "partial toml should parse");
let global_config_file = result.unwrap();
assert!(matches!(
global_config_file
.diagnostics_config
.is_enabled(DiagnosticCode::PreferModuleImport),
DiagnosticStatus::Disabled
));
assert!(matches!(
global_config_file
.diagnostics_config
.is_enabled(DiagnosticCode::ForbiddenPubCrate),
DiagnosticStatus::Enabled
));
assert!(matches!(
global_config_file
.diagnostics_config
.is_enabled(DiagnosticCode::SuspiciousPub),
DiagnosticStatus::Enabled
));
}
}