use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
const BUILTIN_CLIPPY_DEFAULTS: &[(&str, &str)] = &[
("dbg_macro", "deny"),
("print_stderr", "deny"),
("print_stdout", "deny"),
];
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CargoLintsConfig {
#[serde(default)]
#[schemars(with = "BTreeMap<String, serde_json::Value>")]
pub rust: BTreeMap<String, toml::Value>,
#[serde(default)]
#[schemars(with = "BTreeMap<String, serde_json::Value>")]
pub clippy: BTreeMap<String, toml::Value>,
}
impl CargoLintsConfig {
pub fn is_empty(&self) -> bool {
self.rust.is_empty() && self.clippy.is_empty()
}
fn effective_clippy(&self) -> BTreeMap<String, toml::Value> {
let mut merged: BTreeMap<String, toml::Value> = BUILTIN_CLIPPY_DEFAULTS
.iter()
.map(|(key, value)| ((*key).to_string(), toml::Value::String((*value).to_string())))
.collect();
merged.extend(self.clippy.iter().map(|(key, value)| (key.clone(), value.clone())));
merged
}
pub fn render(&self) -> String {
render_lint_tables(&self.rust, &self.effective_clippy())
}
pub fn extra_rust_lines(&self, exclude: &[&str]) -> Vec<String> {
self.rust
.iter()
.filter(|(key, _)| !exclude.contains(&key.as_str()))
.map(|(key, value)| format!("{key} = {value}"))
.collect()
}
pub fn clippy_block(&self) -> String {
render_table("[lints.clippy]", &self.effective_clippy())
}
}
fn render_lint_tables(rust: &BTreeMap<String, toml::Value>, clippy: &BTreeMap<String, toml::Value>) -> String {
let mut sections = Vec::new();
if !rust.is_empty() {
sections.push(render_table("[lints.rust]", rust));
}
if !clippy.is_empty() {
sections.push(render_table("[lints.clippy]", clippy));
}
sections.join("\n\n")
}
fn render_table(header: &str, table: &BTreeMap<String, toml::Value>) -> String {
let lines: Vec<String> = table.iter().map(|(key, value)| format!("{key} = {value}")).collect();
format!("{header}\n{}", lines.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_have_no_configured_entries() {
let cfg = CargoLintsConfig::default();
assert!(cfg.is_empty());
}
#[test]
fn render_emits_the_builtin_clippy_block_when_nothing_is_configured() {
let cfg = CargoLintsConfig::default();
assert_eq!(
cfg.render(),
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"deny\""
);
}
#[test]
fn deserializes_string_and_table_values() {
let toml_src = r#"
[rust]
unused_must_use = "deny"
[clippy]
print_stdout = "deny"
print_stderr = "deny"
dbg_macro = "deny"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
assert!(!cfg.is_empty());
assert_eq!(cfg.rust.len(), 1);
assert_eq!(cfg.clippy.len(), 3);
assert_eq!(cfg.rust["unused_must_use"].as_str(), Some("deny"));
}
#[test]
fn render_emits_both_tables_sorted_and_no_trailing_newline() {
let toml_src = r#"
[rust]
unused_must_use = "deny"
[clippy]
print_stdout = "deny"
dbg_macro = "deny"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
let rendered = cfg.render();
assert_eq!(
rendered,
"[lints.rust]\nunused_must_use = \"deny\"\n\n[lints.clippy]\ndbg_macro = \"deny\"\n\
print_stderr = \"deny\"\nprint_stdout = \"deny\""
);
}
#[test]
fn render_omits_absent_rust_table_and_merges_builtin_clippy_defaults() {
let toml_src = r#"
[clippy]
print_stdout = "deny"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
assert_eq!(
cfg.render(),
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"deny\""
);
}
#[test]
fn render_keeps_a_non_builtin_configured_clippy_key_alongside_the_builtin_defaults() {
let toml_src = r#"
[clippy]
unwrap_used = "warn"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
assert_eq!(
cfg.render(),
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"deny\"\n\
unwrap_used = \"warn\""
);
}
#[test]
fn render_lets_a_configured_value_override_the_builtin_default_for_the_same_key() {
let toml_src = r#"
[clippy]
print_stdout = "warn"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
assert_eq!(
cfg.render(),
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"warn\""
);
}
#[test]
fn render_accepts_table_valued_entries() {
let toml_src = r#"
[rust.unexpected_cfgs]
level = "warn"
check-cfg = ["cfg(docsrs)"]
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
let rendered = cfg.render();
assert!(rendered.starts_with("[lints.rust]\nunexpected_cfgs = "));
assert!(rendered.contains("level = \"warn\""));
assert!(rendered.contains("check-cfg = [\"cfg(docsrs)\"]"));
toml::from_str::<toml::Value>(&format!("[a]\n{rendered}\n")).expect("rendered entry must itself be valid TOML");
}
#[test]
fn extra_rust_lines_excludes_the_builtin_key_and_sorts_the_rest() {
let toml_src = r#"
[rust]
unexpected_cfgs = "warn"
unused_must_use = "deny"
non_snake_case = "warn"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
let lines = cfg.extra_rust_lines(&["unexpected_cfgs"]);
assert_eq!(
lines,
vec![
"non_snake_case = \"warn\"".to_string(),
"unused_must_use = \"deny\"".to_string(),
],
"the excluded key must be dropped and the rest sorted by key"
);
}
#[test]
fn extra_rust_lines_is_empty_when_only_the_excluded_key_is_set() {
let toml_src = r#"
[rust]
unexpected_cfgs = "warn"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
assert!(cfg.extra_rust_lines(&["unexpected_cfgs"]).is_empty());
}
#[test]
fn clippy_block_renders_the_clippy_table_alone_merged_with_builtin_defaults() {
let toml_src = r#"
[rust]
unused_must_use = "deny"
[clippy]
print_stdout = "deny"
"#;
let cfg: CargoLintsConfig = toml::from_str(toml_src).expect("deserializes");
assert_eq!(
cfg.clippy_block(),
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"deny\""
);
}
#[test]
fn clippy_block_renders_the_builtin_defaults_when_clippy_table_unset() {
let cfg = CargoLintsConfig::default();
assert_eq!(
cfg.clippy_block(),
"[lints.clippy]\ndbg_macro = \"deny\"\nprint_stderr = \"deny\"\nprint_stdout = \"deny\""
);
}
}