Skip to main content

harper_core/linting/lint_group/
flat_config.rs

1use std::collections::BTreeMap;
2use std::hash::{Hash, Hasher};
3use std::mem;
4use std::sync::OnceLock;
5
6use hashbrown::HashMap;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::LintGroup;
10use crate::Dialect;
11use crate::spell::MutableDictionary;
12
13fn ser_ordered<S>(map: &HashMap<String, Option<bool>>, ser: S) -> Result<S::Ok, S::Error>
14where
15    S: Serializer,
16{
17    let ordered: BTreeMap<_, _> = map.iter().map(|(k, v)| (k.clone(), *v)).collect();
18    ordered.serialize(ser)
19}
20
21fn de_hashbrown<'de, D>(de: D) -> Result<HashMap<String, Option<bool>>, D::Error>
22where
23    D: Deserializer<'de>,
24{
25    let ordered: BTreeMap<String, Option<bool>> = BTreeMap::deserialize(de)?;
26    Ok(ordered.into_iter().collect())
27}
28
29/// The rule-level configuration for a [`LintGroup`].
30/// Previously named `LintGroupConfig`.
31/// Each child linter can be enabled, disabled, or set to a curated value.
32/// So named because it represents the structure of a [`LintGroup`] exactly: it's flat.
33#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
34#[serde(transparent)]
35pub struct FlatConfig {
36    /// We do this shenanigans with the [`BTreeMap`] to keep the serialized format consistent.
37    #[serde(serialize_with = "ser_ordered", deserialize_with = "de_hashbrown")]
38    inner: HashMap<String, Option<bool>>,
39}
40
41impl FlatConfig {
42    fn curated() -> Self {
43        static CURATED: OnceLock<FlatConfig> = OnceLock::new();
44
45        CURATED
46            .get_or_init(|| {
47                // The Dictionary and Dialect do not matter, we're just after the config.
48                let group =
49                    LintGroup::new_curated(MutableDictionary::new().into(), Dialect::American);
50                group.config
51            })
52            .clone()
53    }
54
55    /// Check if a rule exists in the configuration.
56    pub fn has_rule(&self, key: impl AsRef<str>) -> bool {
57        self.inner.contains_key(key.as_ref())
58    }
59
60    pub fn set_rule_enabled(&mut self, key: impl ToString, val: bool) {
61        self.inner.insert(key.to_string(), Some(val));
62    }
63
64    /// Remove any configuration attached to a rule.
65    /// This allows it to assume its default (curated) state.
66    pub fn unset_rule_enabled(&mut self, key: impl AsRef<str>) {
67        self.inner.remove(key.as_ref());
68    }
69
70    pub fn set_rule_enabled_if_unset(&mut self, key: impl AsRef<str>, val: bool) {
71        if !self.inner.contains_key(key.as_ref()) {
72            self.set_rule_enabled(key.as_ref().to_string(), val);
73        }
74    }
75
76    pub fn is_rule_enabled(&self, key: &str) -> bool {
77        self.inner.get(key).cloned().flatten().unwrap_or(false)
78    }
79
80    /// Clear all config options.
81    /// This will reset them all to disable them.
82    pub fn clear(&mut self) {
83        for val in self.inner.values_mut() {
84            *val = None
85        }
86    }
87
88    /// Merge the contents of another [`FlatConfig`] into this one.
89    ///
90    /// Conflicting keys will be overridden by the value in the other group.
91    pub fn merge_from(&mut self, other: FlatConfig) {
92        for (key, val) in other.inner {
93            if val.is_none() {
94                continue;
95            }
96
97            self.inner.insert(key.to_string(), val);
98        }
99    }
100
101    /// Fill the group with the values for the curated lint group.
102    pub fn fill_with_curated(&mut self) {
103        let mut temp = Self::new_curated();
104        mem::swap(self, &mut temp);
105        self.merge_from(temp);
106    }
107
108    pub fn new_curated() -> Self {
109        Self::curated()
110    }
111}
112
113impl Hash for FlatConfig {
114    fn hash<H: Hasher>(&self, hasher: &mut H) {
115        for (key, value) in &self.inner {
116            hasher.write(key.as_bytes());
117            if let Some(value) = value {
118                hasher.write_u8(1);
119                hasher.write_u8(*value as u8);
120            } else {
121                // Do it twice so we fill the same number of bytes as the other branch.
122                hasher.write_u8(0);
123                hasher.write_u8(0);
124            }
125        }
126    }
127}