use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct TyposConfig {
#[serde(default)]
pub extend_words: BTreeMap<String, String>,
#[serde(default)]
pub extend_identifiers: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct UncommentConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub remove_todos: bool,
#[serde(default)]
pub remove_fixme: bool,
#[serde(default)]
pub remove_docs: bool,
#[serde(default = "default_true")]
pub use_default_ignores: bool,
#[serde(default)]
pub preserve_patterns: Vec<String>,
}
fn default_true() -> bool {
true
}
impl Default for UncommentConfig {
fn default() -> Self {
Self {
enabled: true,
remove_todos: false,
remove_fixme: false,
remove_docs: false,
use_default_ignores: true,
preserve_patterns: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct PolyConfig {
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub file_safety_exclude: Vec<String>,
#[serde(default)]
pub typos: TyposConfig,
#[serde(default)]
pub per_file_ignores: BTreeMap<String, Vec<String>>,
#[serde(default)]
pub pyrefly_sub_configs: BTreeMap<String, Vec<String>>,
#[serde(default)]
pub uncomment: Option<UncommentConfig>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn poly_config_deserializes_empty() {
let cfg: PolyConfig = toml::from_str("").unwrap();
assert!(cfg.exclude.is_empty());
assert!(cfg.file_safety_exclude.is_empty());
assert!(cfg.typos.extend_words.is_empty());
assert!(cfg.typos.extend_identifiers.is_empty());
assert!(cfg.per_file_ignores.is_empty());
assert!(
cfg.uncomment.is_none(),
"absent [uncomment] must deserialize to None (no table emitted)"
);
}
#[test]
fn poly_config_deserializes_full() {
let toml_str = r#"
exclude = ["vendor/**", "third-party/**"]
file-safety-exclude = ["crates/*/src/lib.rs", "**/inner_attrs.rs"]
[typos.extend-words]
flate = "flate"
arange = "arange"
[typos.extend-identifiers]
PyMuPDF = "PyMuPDF"
[per-file-ignores]
"**/legacy.py" = ["ANN", "D103"]
"**/compat.py" = ["UP035"]
[pyrefly-sub-configs]
"**/app.py" = ["bad-argument-type"]
"**/schema.py" = ["missing-import", "bad-return"]
[uncomment]
enabled = true
remove-todos = true
remove-fixme = true
remove-docs = false
use-default-ignores = false
preserve-patterns = ["SAFETY:", "allow("]
"#;
let cfg: PolyConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.exclude, vec!["vendor/**", "third-party/**"]);
assert_eq!(
cfg.file_safety_exclude,
vec!["crates/*/src/lib.rs", "**/inner_attrs.rs"]
);
let uncomment = cfg.uncomment.as_ref().expect("uncomment section present");
assert!(uncomment.enabled);
assert!(uncomment.remove_todos);
assert!(uncomment.remove_fixme);
assert!(!uncomment.remove_docs);
assert!(!uncomment.use_default_ignores);
assert_eq!(uncomment.preserve_patterns, vec!["SAFETY:", "allow("]);
assert_eq!(cfg.typos.extend_words.len(), 2);
assert_eq!(cfg.typos.extend_words["flate"], "flate");
assert_eq!(cfg.typos.extend_words["arange"], "arange");
assert_eq!(cfg.typos.extend_identifiers.len(), 1);
assert_eq!(cfg.typos.extend_identifiers["PyMuPDF"], "PyMuPDF");
assert_eq!(cfg.per_file_ignores.len(), 2);
assert_eq!(cfg.per_file_ignores["**/legacy.py"], vec!["ANN", "D103"]);
assert_eq!(cfg.per_file_ignores["**/compat.py"], vec!["UP035"]);
assert_eq!(cfg.pyrefly_sub_configs.len(), 2);
assert_eq!(cfg.pyrefly_sub_configs["**/app.py"], vec!["bad-argument-type"]);
assert_eq!(
cfg.pyrefly_sub_configs["**/schema.py"],
vec!["missing-import", "bad-return"]
);
}
#[test]
fn poly_config_rejects_unknown_fields() {
let err = toml::from_str::<PolyConfig>("unknown_field = true");
assert!(err.is_err(), "deny_unknown_fields must reject unrecognised keys");
}
#[test]
fn poly_config_uncomment_empty_table_uses_poly_defaults() {
let cfg: PolyConfig = toml::from_str("[uncomment]\n").unwrap();
let uncomment = cfg.uncomment.as_ref().expect("empty [uncomment] table -> Some");
assert!(uncomment.enabled, "enabled defaults to true");
assert!(!uncomment.remove_todos, "remove_todos defaults to false");
assert!(!uncomment.remove_fixme, "remove_fixme defaults to false");
assert!(!uncomment.remove_docs, "remove_docs defaults to false");
assert!(uncomment.use_default_ignores, "use_default_ignores defaults to true");
assert!(
uncomment.preserve_patterns.is_empty(),
"preserve_patterns defaults empty"
);
}
#[test]
fn poly_config_file_safety_exclude_defaults_empty() {
let cfg: PolyConfig = toml::from_str("exclude = [\"a/**\"]").unwrap();
assert!(
cfg.file_safety_exclude.is_empty(),
"file-safety-exclude defaults empty when unset"
);
}
#[test]
fn uncomment_config_rejects_unknown_fields() {
let err = toml::from_str::<UncommentConfig>("unknown_field = true");
assert!(err.is_err(), "deny_unknown_fields must reject unrecognised keys");
}
#[test]
fn uncomment_config_default_matches_poly_defaults() {
let cfg = UncommentConfig::default();
assert!(cfg.enabled);
assert!(!cfg.remove_todos);
assert!(!cfg.remove_fixme);
assert!(!cfg.remove_docs);
assert!(cfg.use_default_ignores);
assert!(cfg.preserve_patterns.is_empty());
}
#[test]
fn typos_config_deserializes_empty() {
let cfg: TyposConfig = toml::from_str("").unwrap();
assert!(cfg.extend_words.is_empty());
assert!(cfg.extend_identifiers.is_empty());
}
#[test]
fn typos_config_rejects_unknown_fields() {
let err = toml::from_str::<TyposConfig>("unknown_field = true");
assert!(err.is_err(), "deny_unknown_fields must reject unrecognised keys");
}
}