use std::collections::BTreeMap;
use rpm_spec::printer::PrinterConfig;
use rpm_spec_profile::ProfileEntry;
use serde::{Deserialize, Serialize};
use crate::diagnostic::Severity;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Config {
#[serde(default)]
pub lints: BTreeMap<String, Severity>,
#[serde(default)]
pub format: FormatConfig,
#[serde(default)]
pub shellcheck: ShellcheckConfig,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub profiles: BTreeMap<String, ProfileEntry>,
#[serde(skip)]
pub warnings_as_errors: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ShellcheckConfig {
pub binary: Option<String>,
pub disable: Vec<String>,
pub enable: Vec<String>,
pub shell: Option<String>,
pub timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
#[non_exhaustive]
pub struct FormatConfig {
pub preamble_align_column: u32,
pub conditional_indent: u32,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
preamble_align_column: 16,
conditional_indent: 0,
}
}
}
impl FormatConfig {
pub fn to_printer_config(&self) -> PrinterConfig {
let preamble_column = if self.preamble_align_column == 0 {
None
} else {
Some(self.preamble_align_column as usize)
};
PrinterConfig::new()
.with_indent(self.conditional_indent as usize)
.with_preamble_value_column(preamble_column)
}
}
impl Config {
pub fn from_toml_str(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
pub fn resolve_profile(
&self,
base_dir: &std::path::Path,
opts: rpm_spec_profile::ResolveOptions<'_>,
) -> Result<rpm_spec_profile::Profile, rpm_spec_profile::ResolveError> {
let section =
rpm_spec_profile::ProfileSection::new(self.profile.clone(), self.profiles.clone());
rpm_spec_profile::resolve_profile(§ion, base_dir, opts)
}
pub fn severity_for(&self, lint_name: &str, default: Severity) -> Severity {
let resolved = self.lints.get(lint_name).copied().unwrap_or(default);
if self.warnings_as_errors && resolved == Severity::Warn {
Severity::Deny
} else {
resolved
}
}
pub fn apply_overrides<S: AsRef<str>>(&mut self, lint_names: &[S], severity: Severity) {
for n in lint_names {
self.lints.insert(n.as_ref().to_owned(), severity);
}
}
pub fn apply_cli_overrides<S: AsRef<str>>(&mut self, allow: &[S], warn: &[S], deny: &[S]) {
let (allow_lints, allow_meta) = split_warnings_meta(allow);
let (warn_lints, warn_meta) = split_warnings_meta(warn);
let (deny_lints, deny_meta) = split_warnings_meta(deny);
self.apply_overrides(&allow_lints, Severity::Allow);
self.apply_overrides(&warn_lints, Severity::Warn);
self.apply_overrides(&deny_lints, Severity::Deny);
if allow_meta {
self.warnings_as_errors = false;
}
if warn_meta {
}
if deny_meta {
self.warnings_as_errors = true;
}
}
}
const META_WARNINGS: &str = "warnings";
fn split_warnings_meta<S: AsRef<str>>(list: &[S]) -> (Vec<String>, bool) {
let mut meta = false;
let mut lints = Vec::with_capacity(list.len());
for item in list {
if item.as_ref() == META_WARNINGS {
meta = true;
} else {
lints.push(item.as_ref().to_owned());
}
}
(lints, meta)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_toml_round_trip() {
let toml_str = r#"
[lints]
missing-changelog = "deny"
[format]
preamble-align-column = 20
"#;
let cfg = Config::from_toml_str(toml_str).unwrap();
assert_eq!(
cfg.severity_for("missing-changelog", Severity::Warn),
Severity::Deny
);
assert_eq!(cfg.format.preamble_align_column, 20);
}
#[test]
fn unknown_field_rejected() {
let toml_str = "unknown-key = 1\n";
assert!(Config::from_toml_str(toml_str).is_err());
}
#[test]
fn apply_overrides_replaces_severity() {
let mut cfg = Config::default();
cfg.lints.insert("foo".into(), Severity::Warn);
cfg.apply_overrides(&["foo", "bar"], Severity::Deny);
assert_eq!(cfg.severity_for("foo", Severity::Allow), Severity::Deny);
assert_eq!(cfg.severity_for("bar", Severity::Allow), Severity::Deny);
}
#[test]
fn to_printer_config_zero_means_single_space() {
let cfg = FormatConfig {
preamble_align_column: 0,
..FormatConfig::default()
};
assert!(cfg.to_printer_config().preamble_value_column.is_none());
}
#[test]
fn cli_overrides_priority_deny_over_allow() {
let mut cfg = Config::default();
cfg.apply_cli_overrides::<&str>(&["foo"], &[], &["foo"]);
assert_eq!(cfg.severity_for("foo", Severity::Warn), Severity::Deny);
}
#[test]
fn cli_overrides_priority_warn_over_allow() {
let mut cfg = Config::default();
cfg.apply_cli_overrides::<&str>(&["bar"], &["bar"], &[]);
assert_eq!(cfg.severity_for("bar", Severity::Deny), Severity::Warn);
}
#[test]
fn deny_warnings_meta_promotes_warn_to_deny() {
let mut cfg = Config::default();
cfg.apply_cli_overrides::<&str>(&[], &[], &["warnings"]);
assert_eq!(
cfg.severity_for("missing-changelog", Severity::Warn),
Severity::Deny
);
assert_eq!(
cfg.severity_for("opt-in-rule", Severity::Allow),
Severity::Allow
);
assert_eq!(cfg.severity_for("must-fix", Severity::Deny), Severity::Deny);
assert!(!cfg.lints.contains_key("warnings"));
}
#[test]
fn deny_warnings_respects_explicit_allow_per_lint() {
let mut cfg = Config::default();
cfg.apply_cli_overrides::<&str>(&["foo"], &[], &["warnings"]);
assert_eq!(cfg.severity_for("foo", Severity::Warn), Severity::Allow);
assert_eq!(cfg.severity_for("bar", Severity::Warn), Severity::Deny);
}
#[test]
fn allow_warnings_meta_clears_the_promotion() {
let mut cfg = Config::default();
cfg.apply_cli_overrides::<&str>(&[], &[], &["warnings"]);
cfg.apply_cli_overrides::<&str>(&["warnings"], &[], &[]);
assert!(!cfg.warnings_as_errors);
assert_eq!(
cfg.severity_for("missing-changelog", Severity::Warn),
Severity::Warn
);
}
#[test]
fn warn_warnings_meta_is_a_no_op() {
let mut cfg = Config::default();
cfg.apply_cli_overrides::<&str>(&[], &["warnings"], &[]);
assert!(!cfg.warnings_as_errors);
assert!(
cfg.lints.is_empty(),
"meta-name must not leak as a lint key"
);
}
#[test]
fn shellcheck_config_round_trip() {
let toml_str = r#"
[shellcheck]
binary = "/opt/sc"
disable = ["SC2086", "2155"]
"#;
let cfg = Config::from_toml_str(toml_str).unwrap();
assert_eq!(cfg.shellcheck.binary.as_deref(), Some("/opt/sc"));
assert_eq!(cfg.shellcheck.disable, vec!["SC2086", "2155"]);
}
}