use colored::*;
use rumdl_lib::config as rumdl_config;
use rumdl_lib::exit_codes::exit;
use rumdl_config::ConfigSource;
use rumdl_config::normalize_key;
use crate::ConfigSubcommand;
use crate::cli_utils::load_config_with_cli_error_handling;
use crate::formatter;
#[allow(clippy::too_many_arguments)]
pub fn handle_config(
subcmd: Option<ConfigSubcommand>,
defaults: bool,
no_defaults: bool,
output: Option<String>,
config_path: Option<&str>,
no_config: bool,
isolated: bool,
inline_overrides: &[toml::Table],
) {
if defaults && no_defaults {
eprintln!(
"{}: Cannot use both --defaults and --no-defaults flags together",
"Error".red().bold()
);
exit::tool_error();
}
if let Some(ConfigSubcommand::Get { key }) = subcmd {
handle_config_get(&key, config_path, no_config, inline_overrides);
} else if let Some(ConfigSubcommand::File) = subcmd {
handle_config_file(config_path, no_config, isolated);
} else {
handle_config_display(
defaults,
no_defaults,
output,
config_path,
no_config,
isolated,
inline_overrides,
);
}
}
const UNSET_DISPLAY: &str = "<unset>";
fn print_config_value(rule: &str, field: &str, value: &toml::Value, source: ConfigSource) {
println!(
"{}.{} = {} [from {}]",
rule,
field,
formatter::format_toml_value(value),
formatter::format_provenance(source)
);
}
fn print_unset_config_value(rule: &str, field: &str) {
println!("{rule}.{field} = {UNSET_DISPLAY} [no default]");
}
fn default_config_values_for_rule(rule_name: &str) -> toml::map::Map<String, toml::Value> {
rumdl_lib::rules::all_rules(&rumdl_config::Config::default())
.iter()
.find_map(|rule| match rule.default_config_section() {
Some((name, toml::Value::Table(table))) if normalize_key(&name) == rule_name => Some(table),
_ => None,
})
.unwrap_or_default()
}
fn handle_config_get(key: &str, config_path: Option<&str>, no_config: bool, inline_overrides: &[toml::Table]) {
let mut sourced = match rumdl_config::SourcedConfig::load_with_discovery(config_path, None, no_config) {
Ok(s) => s,
Err(e) => {
eprintln!("{}: {}", "Config error".red().bold(), e);
exit::tool_error();
}
};
crate::cli_config_override::apply_inline_overrides(&mut sourced, inline_overrides);
let final_config: rumdl_config::Config = sourced.clone().into_validated_unchecked().into();
if let Some((section_part, field_part)) = key.split_once('.') {
let normalized_field = normalize_key(field_part);
if section_part.eq_ignore_ascii_case("global") {
match rumdl_config::read_global_key(&final_config.global, &sourced.global, &normalized_field) {
Some(rumdl_config::GlobalKeyValue::Set(value, source)) => {
print_config_value("global", &normalized_field, &value, source);
}
Some(rumdl_config::GlobalKeyValue::Unset) => {
print_unset_config_value("global", &normalized_field);
}
None => {
eprintln!("Unknown global key: {field_part}");
exit::tool_error();
}
}
}
else {
let normalized_rule_name = rumdl_config::resolve_rule_name_alias(section_part)
.map(|s| s.to_string())
.unwrap_or_else(|| normalize_key(section_part));
let registry = rumdl_config::default_registry();
if !registry.rule_schemas.contains_key(&normalized_rule_name) {
eprintln!("Unknown config key: {normalized_rule_name}.{normalized_field}");
exit::tool_error();
}
if normalized_field == "severity" {
match sourced
.rules
.get(&normalized_rule_name)
.and_then(|sc| sc.severity.as_ref())
{
Some(sourced_severity) => print_config_value(
&normalized_rule_name,
&normalized_field,
&toml::Value::try_from(sourced_severity.value).expect("Severity serializes to a TOML string"),
sourced_severity.source,
),
None => print_unset_config_value(&normalized_rule_name, &normalized_field),
}
return;
}
let final_value: Option<&toml::Value> = final_config
.rules
.get(&normalized_rule_name)
.and_then(|rule_cfg| rule_cfg.values.get(&normalized_field));
if let Some(value) = final_value {
let provenance = sourced
.rules
.get(&normalized_rule_name)
.and_then(|sc| sc.values.get(&normalized_field))
.map_or(ConfigSource::Default, |sv| sv.source);
print_config_value(&normalized_rule_name, &normalized_field, value, provenance);
return;
}
let Some(canonical_key) = registry.canonical_config_key(&normalized_rule_name, &normalized_field) else {
if normalized_field == "enabled" {
print_unset_config_value(&normalized_rule_name, &normalized_field);
return;
}
eprintln!("Unknown config key: {normalized_rule_name}.{normalized_field}");
exit::tool_error();
};
match default_config_values_for_rule(&normalized_rule_name).get(canonical_key) {
Some(value) => {
print_config_value(&normalized_rule_name, &normalized_field, value, ConfigSource::Default)
}
None => print_unset_config_value(&normalized_rule_name, &normalized_field),
}
}
} else {
let normalized_rule_name = rumdl_config::resolve_rule_name_alias(key)
.map(|s| s.to_string())
.unwrap_or_else(|| normalize_key(key));
let registry = rumdl_config::default_registry();
if registry.rule_schemas.contains_key(&normalized_rule_name) {
let schema = ®istry.rule_schemas[&normalized_rule_name];
let defaults = default_config_values_for_rule(&normalized_rule_name);
let mut fields: Vec<&str> = schema.keys().map(String::as_str).collect();
fields.push("enabled");
fields.push("severity");
fields.sort_unstable();
fields.dedup();
for field in fields {
let normalized_field = normalize_key(field);
if normalized_field == "severity" {
match sourced
.rules
.get(&normalized_rule_name)
.and_then(|sc| sc.severity.as_ref())
{
Some(sourced_severity) => print_config_value(
&normalized_rule_name,
&normalized_field,
&toml::Value::try_from(sourced_severity.value)
.expect("Severity serializes to a TOML string"),
sourced_severity.source,
),
None => print_unset_config_value(&normalized_rule_name, &normalized_field),
}
continue;
}
let final_value = final_config
.rules
.get(&normalized_rule_name)
.and_then(|rule_cfg| rule_cfg.values.get(&normalized_field));
let (value, source) = if let Some(v) = final_value {
let provenance = sourced
.rules
.get(&normalized_rule_name)
.and_then(|sc| sc.values.get(&normalized_field))
.map_or(rumdl_config::ConfigSource::Default, |sv| sv.source);
(v, provenance)
} else if let Some(v) = defaults.get(field) {
(v, rumdl_config::ConfigSource::Default)
} else {
print_unset_config_value(&normalized_rule_name, &normalized_field);
continue;
};
print_config_value(&normalized_rule_name, &normalized_field, value, source);
}
} else {
eprintln!(
"Unknown key: {key}. Must be in the form global.key, MDxxx.key, or MDxxx (rule name for all keys)"
);
exit::tool_error();
}
}
}
fn handle_config_file(config_path: Option<&str>, no_config: bool, isolated: bool) {
let sourced = load_config_with_cli_error_handling(config_path, no_config || isolated);
for warning in &sourced.discovery_warnings {
eprintln!("\x1b[33m[config warning]\x1b[0m {warning}");
}
if sourced.loaded_files.is_empty() {
if no_config || isolated {
println!("No configuration file loaded (--no-config/--isolated specified)");
} else {
println!("No configuration file found (using defaults)");
}
} else {
for file_path in &sourced.loaded_files {
match std::fs::canonicalize(file_path) {
Ok(absolute_path) => {
println!("{}", absolute_path.display());
}
Err(_) => {
println!("{file_path}");
}
}
}
}
}
fn handle_config_display(
defaults: bool,
no_defaults: bool,
output: Option<String>,
config_path: Option<&str>,
no_config: bool,
isolated: bool,
inline_overrides: &[toml::Table],
) {
let registry = rumdl_config::default_registry();
let all_rules_reg = rumdl_lib::rules::all_rules(&rumdl_config::Config::default());
let mut sourced_reg = if defaults {
let mut default_sourced = rumdl_config::SourcedConfig::default();
for rule in &all_rules_reg {
if let Some((rule_name, toml::Value::Table(table))) = rule.default_config_section() {
let mut rule_config = rumdl_config::SourcedRuleConfig::default();
for (key, value) in table {
rule_config.values.insert(
key.clone(),
rumdl_config::SourcedValue::new(value.clone(), rumdl_config::ConfigSource::Default),
);
}
default_sourced.rules.insert(rule_name.to_uppercase(), rule_config);
}
}
default_sourced
} else {
load_config_with_cli_error_handling(config_path, no_config || isolated)
};
if !defaults {
crate::cli_config_override::apply_inline_overrides(&mut sourced_reg, inline_overrides);
}
for warning in &sourced_reg.discovery_warnings {
eprintln!("\x1b[33m[config warning]\x1b[0m {warning}");
}
let validation_warnings = rumdl_config::validate_config_sourced(&sourced_reg, registry);
if !validation_warnings.is_empty() {
for warn in &validation_warnings {
eprintln!("\x1b[33m[config warning]\x1b[0m {}", warn.message);
}
}
let final_sourced_to_print = sourced_reg;
match output.as_deref() {
Some("toml") => {
if defaults {
let mut default_config = rumdl_config::Config::default();
for rule in &all_rules_reg {
if let Some((rule_name, toml::Value::Table(table))) = rule.default_config_section() {
let rule_config = rumdl_config::RuleConfig {
severity: None,
values: table.into_iter().collect(),
};
default_config.rules.insert(rule_name.to_uppercase(), rule_config);
}
}
match toml::to_string_pretty(&default_config) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Failed to serialize config to TOML: {e}");
exit::tool_error();
}
}
} else if no_defaults {
let filtered_sourced = filter_sourced_config_to_non_defaults(&final_sourced_to_print);
let config_to_print: rumdl_config::Config = filtered_sourced.into_validated_unchecked().into();
match toml::to_string_pretty(&config_to_print) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Failed to serialize config to TOML: {e}");
exit::tool_error();
}
}
} else {
let config_to_print: rumdl_config::Config = final_sourced_to_print.into_validated_unchecked().into();
match toml::to_string_pretty(&config_to_print) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Failed to serialize config to TOML: {e}");
exit::tool_error();
}
}
}
}
Some("json") => {
if defaults {
let mut default_config = rumdl_config::Config::default();
for rule in &all_rules_reg {
if let Some((rule_name, toml::Value::Table(table))) = rule.default_config_section() {
let rule_config = rumdl_config::RuleConfig {
severity: None,
values: table.into_iter().collect(),
};
default_config.rules.insert(rule_name.to_uppercase(), rule_config);
}
}
match serde_json::to_string_pretty(&default_config) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Failed to serialize config to JSON: {e}");
exit::tool_error();
}
}
} else if no_defaults {
let filtered_sourced = filter_sourced_config_to_non_defaults(&final_sourced_to_print);
let config_to_print: rumdl_config::Config = filtered_sourced.into_validated_unchecked().into();
match serde_json::to_string_pretty(&config_to_print) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Failed to serialize config to JSON: {e}");
exit::tool_error();
}
}
} else {
let config_to_print: rumdl_config::Config = final_sourced_to_print.into_validated_unchecked().into();
match serde_json::to_string_pretty(&config_to_print) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("Failed to serialize config to JSON: {e}");
exit::tool_error();
}
}
}
}
_ => {
let editorconfig_enabled = final_sourced_to_print.global.editorconfig.value;
if no_defaults {
formatter::print_config_with_provenance_no_defaults(&final_sourced_to_print, &all_rules_reg);
} else {
formatter::print_config_with_provenance(&final_sourced_to_print, &all_rules_reg);
}
if editorconfig_enabled {
println!(
"\nNote: editorconfig = true. Settings that a `.editorconfig` supplies are resolved \
per file and are not shown above."
);
}
}
}
}
fn filter_sourced_config_to_non_defaults(
sourced: &rumdl_config::SourcedConfig<rumdl_config::ConfigLoaded>,
) -> rumdl_config::SourcedConfig<rumdl_config::ConfigLoaded> {
let mut filtered = rumdl_config::SourcedConfig::default();
if sourced.global.enable.source != rumdl_config::ConfigSource::Default {
filtered.global.enable = sourced.global.enable.clone();
}
if sourced.global.disable.source != rumdl_config::ConfigSource::Default {
filtered.global.disable = sourced.global.disable.clone();
}
if sourced.global.exclude.source != rumdl_config::ConfigSource::Default {
filtered.global.exclude = sourced.global.exclude.clone();
}
if sourced.global.include.source != rumdl_config::ConfigSource::Default {
filtered.global.include = sourced.global.include.clone();
}
if sourced.global.respect_gitignore.source != rumdl_config::ConfigSource::Default {
filtered.global.respect_gitignore = sourced.global.respect_gitignore.clone();
}
if sourced.global.line_length.source != rumdl_config::ConfigSource::Default {
filtered.global.line_length = sourced.global.line_length.clone();
}
if sourced.global.flavor.source != rumdl_config::ConfigSource::Default {
filtered.global.flavor = sourced.global.flavor.clone();
}
if sourced.global.force_exclude.source != rumdl_config::ConfigSource::Default {
filtered.global.force_exclude = sourced.global.force_exclude.clone();
}
if sourced.global.cache.source != rumdl_config::ConfigSource::Default {
filtered.global.cache = sourced.global.cache.clone();
}
if sourced.global.editorconfig.source != rumdl_config::ConfigSource::Default {
filtered.global.editorconfig = sourced.global.editorconfig.clone();
}
if sourced.global.fixable.source != rumdl_config::ConfigSource::Default {
filtered.global.fixable = sourced.global.fixable.clone();
}
if sourced.global.unfixable.source != rumdl_config::ConfigSource::Default {
filtered.global.unfixable = sourced.global.unfixable.clone();
}
if let Some(ref output_format) = sourced.global.output_format
&& output_format.source != rumdl_config::ConfigSource::Default
{
filtered.global.output_format = Some(output_format.clone());
}
if let Some(ref cache_dir) = sourced.global.cache_dir
&& cache_dir.source != rumdl_config::ConfigSource::Default
{
filtered.global.cache_dir = Some(cache_dir.clone());
}
if sourced.per_file_ignores.source != rumdl_config::ConfigSource::Default {
filtered.per_file_ignores = sourced.per_file_ignores.clone();
}
for (rule_name, rule_cfg) in &sourced.rules {
let mut filtered_rule = rumdl_config::SourcedRuleConfig::default();
for (key, sv) in &rule_cfg.values {
if sv.source != rumdl_config::ConfigSource::Default {
filtered_rule.values.insert(key.clone(), sv.clone());
}
}
if !filtered_rule.values.is_empty() {
filtered.rules.insert(rule_name.clone(), filtered_rule);
}
}
filtered.loaded_files = sourced.loaded_files.clone();
filtered.unknown_keys = sourced.unknown_keys.clone();
filtered.project_root = sourced.project_root.clone();
filtered
}