pedant_core/check_config/
module_boundary.rs1use std::collections::BTreeMap;
5
6use serde::Deserialize;
7use serde::de::IgnoredAny;
8use thiserror::Error;
9
10use super::gate::{GateRuleOverride, InvalidGateSeverity, RuleSetting, rule_override};
11
12#[derive(Debug, Deserialize)]
15#[serde(untagged)]
16pub(super) enum ModuleBoundaryValue {
17 Bool(bool),
19 Integer(i64),
21 String(String),
23 Other(IgnoredAny),
25}
26
27pub(super) type ModuleBoundaryTable = BTreeMap<Box<str>, ModuleBoundaryValue>;
29
30#[derive(Debug, Error)]
32pub(super) enum ModuleBoundaryConfigError {
33 #[error("unknown module-boundary field '{0}'")]
35 UnknownField(Box<str>),
36 #[error("module-boundary field '{field}' must be a boolean")]
38 NotBoolean {
39 field: Box<str>,
41 },
42 #[error("module-boundary field '{field}' must be an integer")]
44 NotInteger {
45 field: Box<str>,
47 },
48 #[error("module-boundary field '{field}' must fit in u32, got {value}")]
50 CountOutOfRange {
51 field: Box<str>,
53 value: i64,
55 },
56 #[error("module-boundary field '{field}' must be 0..=100, got {value}")]
58 PercentOutOfRange {
59 field: Box<str>,
61 value: i64,
63 },
64 #[error("module-boundary rule '{rule}' must be a boolean or a severity string")]
66 RuleNotToggleOrSeverity {
67 rule: Box<str>,
69 },
70 #[error(transparent)]
72 Severity(#[from] InvalidGateSeverity),
73}
74
75#[derive(Debug)]
82pub struct ModuleBoundaryConfig {
83 enabled: bool,
84 rules: BTreeMap<Box<str>, GateRuleOverride>,
85 max_nodes: u32,
86 max_references: u32,
87 max_edges: u32,
88 max_selected_edges: u32,
89 max_scc_members: u32,
90 misplaced_symbol_min_foreign_edges: u32,
91 misplaced_symbol_min_affinity_percent: u8,
92 low_module_cohesion_min_outgoing_edges: u32,
93 low_module_cohesion_min_percent: u8,
94}
95
96impl Default for ModuleBoundaryConfig {
97 fn default() -> Self {
98 Self {
99 enabled: true,
100 rules: BTreeMap::new(),
101 max_nodes: 250_000,
102 max_references: 1_000_000,
103 max_edges: 1_000_000,
104 max_selected_edges: 1_000_000,
105 max_scc_members: 8,
106 misplaced_symbol_min_foreign_edges: 3,
107 misplaced_symbol_min_affinity_percent: 60,
108 low_module_cohesion_min_outgoing_edges: 4,
109 low_module_cohesion_min_percent: 50,
110 }
111 }
112}
113
114impl ModuleBoundaryConfig {
115 pub fn enabled(&self) -> bool {
117 self.enabled
118 }
119
120 pub fn max_nodes(&self) -> u32 {
122 self.max_nodes
123 }
124
125 pub fn max_references(&self) -> u32 {
127 self.max_references
128 }
129
130 pub fn max_edges(&self) -> u32 {
132 self.max_edges
133 }
134
135 pub fn max_selected_edges(&self) -> u32 {
137 self.max_selected_edges
138 }
139
140 pub fn max_scc_members(&self) -> u32 {
142 self.max_scc_members
143 }
144
145 pub fn misplaced_symbol_min_foreign_edges(&self) -> u32 {
147 self.misplaced_symbol_min_foreign_edges
148 }
149
150 pub fn misplaced_symbol_min_affinity_percent(&self) -> u8 {
152 self.misplaced_symbol_min_affinity_percent
153 }
154
155 pub fn low_module_cohesion_min_outgoing_edges(&self) -> u32 {
157 self.low_module_cohesion_min_outgoing_edges
158 }
159
160 pub fn low_module_cohesion_min_percent(&self) -> u8 {
162 self.low_module_cohesion_min_percent
163 }
164
165 pub(crate) fn rule_overrides(&self) -> &BTreeMap<Box<str>, GateRuleOverride> {
167 &self.rules
168 }
169
170 pub(super) fn from_raw(raw: ModuleBoundaryTable) -> Result<Self, ModuleBoundaryConfigError> {
172 let mut config = Self::default();
173 for (key, value) in raw {
174 config.apply(key, value)?;
175 }
176 Ok(config)
177 }
178
179 fn apply(
181 &mut self,
182 key: Box<str>,
183 value: ModuleBoundaryValue,
184 ) -> Result<(), ModuleBoundaryConfigError> {
185 match &*key {
186 "enabled" => self.enabled = expect_bool(&key, value)?,
187 "max-nodes" => self.max_nodes = expect_count(&key, value)?,
188 "max-references" => self.max_references = expect_count(&key, value)?,
189 "max-edges" => self.max_edges = expect_count(&key, value)?,
190 "max-selected-edges" => self.max_selected_edges = expect_count(&key, value)?,
191 "max-scc-members" => self.max_scc_members = expect_count(&key, value)?,
192 "misplaced-symbol-min-foreign-edges" => {
193 self.misplaced_symbol_min_foreign_edges = expect_count(&key, value)?;
194 }
195 "misplaced-symbol-min-affinity-percent" => {
196 self.misplaced_symbol_min_affinity_percent = expect_percent(&key, value)?;
197 }
198 "low-module-cohesion-min-outgoing-edges" => {
199 self.low_module_cohesion_min_outgoing_edges = expect_count(&key, value)?;
200 }
201 "low-module-cohesion-min-percent" => {
202 self.low_module_cohesion_min_percent = expect_percent(&key, value)?;
203 }
204 _ => return self.apply_rule(key, value),
205 }
206 Ok(())
207 }
208
209 fn apply_rule(
210 &mut self,
211 rule: Box<str>,
212 value: ModuleBoundaryValue,
213 ) -> Result<(), ModuleBoundaryConfigError> {
214 if !crate::gate::is_module_boundary_rule(&rule) {
215 return Err(ModuleBoundaryConfigError::UnknownField(rule));
216 }
217 if let Some(setting) = rule_setting(&rule, value)? {
218 self.rules.insert(rule, setting);
219 }
220 Ok(())
221 }
222}
223
224fn rule_setting(
226 rule: &str,
227 value: ModuleBoundaryValue,
228) -> Result<Option<GateRuleOverride>, ModuleBoundaryConfigError> {
229 match value {
230 ModuleBoundaryValue::Bool(toggle) => Ok(rule_override(RuleSetting::Toggle(toggle))?),
231 ModuleBoundaryValue::String(name) => Ok(rule_override(RuleSetting::Named(&name))?),
232 _ => Err(ModuleBoundaryConfigError::RuleNotToggleOrSeverity {
233 rule: Box::from(rule),
234 }),
235 }
236}
237
238fn expect_bool(field: &str, value: ModuleBoundaryValue) -> Result<bool, ModuleBoundaryConfigError> {
239 match value {
240 ModuleBoundaryValue::Bool(toggle) => Ok(toggle),
241 _ => Err(ModuleBoundaryConfigError::NotBoolean {
242 field: Box::from(field),
243 }),
244 }
245}
246
247fn expect_integer(
248 field: &str,
249 value: ModuleBoundaryValue,
250) -> Result<i64, ModuleBoundaryConfigError> {
251 match value {
252 ModuleBoundaryValue::Integer(number) => Ok(number),
253 _ => Err(ModuleBoundaryConfigError::NotInteger {
254 field: Box::from(field),
255 }),
256 }
257}
258
259fn expect_count(field: &str, value: ModuleBoundaryValue) -> Result<u32, ModuleBoundaryConfigError> {
260 let raw = expect_integer(field, value)?;
261 u32::try_from(raw).map_err(|_| ModuleBoundaryConfigError::CountOutOfRange {
262 field: Box::from(field),
263 value: raw,
264 })
265}
266
267fn expect_percent(
268 field: &str,
269 value: ModuleBoundaryValue,
270) -> Result<u8, ModuleBoundaryConfigError> {
271 let raw = expect_integer(field, value)?;
272 match u8::try_from(raw) {
273 Ok(percent) if percent <= 100 => Ok(percent),
274 _ => Err(ModuleBoundaryConfigError::PercentOutOfRange {
275 field: Box::from(field),
276 value: raw,
277 }),
278 }
279}
280
281impl<'de> Deserialize<'de> for ModuleBoundaryConfig {
282 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283 where
284 D: serde::Deserializer<'de>,
285 {
286 use serde::de::Error;
287
288 let raw = ModuleBoundaryTable::deserialize(deserializer)?;
289 Self::from_raw(raw).map_err(D::Error::custom)
290 }
291}