1mod boundaries;
2mod duplicates_config;
3mod format;
4mod health;
5mod parsing;
6mod resolution;
7mod rules;
8
9pub use boundaries::{
10 BoundaryConfig, BoundaryPreset, BoundaryRule, BoundaryZone, ResolvedBoundaryConfig,
11 ResolvedBoundaryRule, ResolvedZone,
12};
13pub use duplicates_config::{
14 DetectionMode, DuplicatesConfig, NormalizationConfig, ResolvedNormalization,
15};
16pub use format::OutputFormat;
17pub use health::HealthConfig;
18pub use resolution::{ConfigOverride, IgnoreExportRule, ResolvedConfig, ResolvedOverride};
19pub use rules::{PartialRulesConfig, RulesConfig, Severity};
20
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23
24use crate::external_plugin::ExternalPluginDef;
25use crate::workspace::WorkspaceConfig;
26
27#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
48#[serde(deny_unknown_fields, rename_all = "camelCase")]
49pub struct FallowConfig {
50 #[serde(rename = "$schema", default, skip_serializing)]
52 #[schemars(skip)]
53 pub schema: Option<String>,
54
55 #[serde(default, skip_serializing)]
60 pub extends: Vec<String>,
61
62 #[serde(default)]
64 pub entry: Vec<String>,
65
66 #[serde(default)]
68 pub ignore_patterns: Vec<String>,
69
70 #[serde(default)]
72 pub framework: Vec<ExternalPluginDef>,
73
74 #[serde(default)]
76 pub workspaces: Option<WorkspaceConfig>,
77
78 #[serde(default)]
80 pub ignore_dependencies: Vec<String>,
81
82 #[serde(default)]
84 pub ignore_exports: Vec<IgnoreExportRule>,
85
86 #[serde(default)]
88 pub duplicates: DuplicatesConfig,
89
90 #[serde(default)]
92 pub health: HealthConfig,
93
94 #[serde(default)]
96 pub rules: RulesConfig,
97
98 #[serde(default)]
100 pub boundaries: BoundaryConfig,
101
102 #[serde(default)]
104 pub production: bool,
105
106 #[serde(default)]
114 pub plugins: Vec<String>,
115
116 #[serde(default)]
118 pub overrides: Vec<ConfigOverride>,
119
120 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub regression: Option<RegressionConfig>,
125}
126
127#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
134#[serde(rename_all = "camelCase")]
135pub struct RegressionConfig {
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub baseline: Option<RegressionBaseline>,
139}
140
141#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
143#[serde(rename_all = "camelCase")]
144pub struct RegressionBaseline {
145 #[serde(default)]
146 pub total_issues: usize,
147 #[serde(default)]
148 pub unused_files: usize,
149 #[serde(default)]
150 pub unused_exports: usize,
151 #[serde(default)]
152 pub unused_types: usize,
153 #[serde(default)]
154 pub unused_dependencies: usize,
155 #[serde(default)]
156 pub unused_dev_dependencies: usize,
157 #[serde(default)]
158 pub unused_optional_dependencies: usize,
159 #[serde(default)]
160 pub unused_enum_members: usize,
161 #[serde(default)]
162 pub unused_class_members: usize,
163 #[serde(default)]
164 pub unresolved_imports: usize,
165 #[serde(default)]
166 pub unlisted_dependencies: usize,
167 #[serde(default)]
168 pub duplicate_exports: usize,
169 #[serde(default)]
170 pub circular_dependencies: usize,
171 #[serde(default)]
172 pub type_only_dependencies: usize,
173 #[serde(default)]
174 pub test_only_dependencies: usize,
175 #[serde(default)]
176 pub boundary_violations: usize,
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
186 fn default_config_has_empty_collections() {
187 let config = FallowConfig::default();
188 assert!(config.schema.is_none());
189 assert!(config.extends.is_empty());
190 assert!(config.entry.is_empty());
191 assert!(config.ignore_patterns.is_empty());
192 assert!(config.framework.is_empty());
193 assert!(config.workspaces.is_none());
194 assert!(config.ignore_dependencies.is_empty());
195 assert!(config.ignore_exports.is_empty());
196 assert!(config.plugins.is_empty());
197 assert!(config.overrides.is_empty());
198 assert!(!config.production);
199 }
200
201 #[test]
202 fn default_config_rules_are_error() {
203 let config = FallowConfig::default();
204 assert_eq!(config.rules.unused_files, Severity::Error);
205 assert_eq!(config.rules.unused_exports, Severity::Error);
206 assert_eq!(config.rules.unused_dependencies, Severity::Error);
207 }
208
209 #[test]
210 fn default_config_duplicates_enabled() {
211 let config = FallowConfig::default();
212 assert!(config.duplicates.enabled);
213 assert_eq!(config.duplicates.min_tokens, 50);
214 assert_eq!(config.duplicates.min_lines, 5);
215 }
216
217 #[test]
218 fn default_config_health_thresholds() {
219 let config = FallowConfig::default();
220 assert_eq!(config.health.max_cyclomatic, 20);
221 assert_eq!(config.health.max_cognitive, 15);
222 }
223
224 #[test]
227 fn deserialize_empty_json_object() {
228 let config: FallowConfig = serde_json::from_str("{}").unwrap();
229 assert!(config.entry.is_empty());
230 assert!(!config.production);
231 }
232
233 #[test]
234 fn deserialize_json_with_all_top_level_fields() {
235 let json = r#"{
236 "$schema": "https://fallow.dev/schema.json",
237 "entry": ["src/main.ts"],
238 "ignorePatterns": ["generated/**"],
239 "ignoreDependencies": ["postcss"],
240 "production": true,
241 "plugins": ["custom-plugin.toml"],
242 "rules": {"unused-files": "warn"},
243 "duplicates": {"enabled": false},
244 "health": {"maxCyclomatic": 30}
245 }"#;
246 let config: FallowConfig = serde_json::from_str(json).unwrap();
247 assert_eq!(
248 config.schema.as_deref(),
249 Some("https://fallow.dev/schema.json")
250 );
251 assert_eq!(config.entry, vec!["src/main.ts"]);
252 assert_eq!(config.ignore_patterns, vec!["generated/**"]);
253 assert_eq!(config.ignore_dependencies, vec!["postcss"]);
254 assert!(config.production);
255 assert_eq!(config.plugins, vec!["custom-plugin.toml"]);
256 assert_eq!(config.rules.unused_files, Severity::Warn);
257 assert!(!config.duplicates.enabled);
258 assert_eq!(config.health.max_cyclomatic, 30);
259 }
260
261 #[test]
262 fn deserialize_json_deny_unknown_fields() {
263 let json = r#"{"unknownField": true}"#;
264 let result: Result<FallowConfig, _> = serde_json::from_str(json);
265 assert!(result.is_err(), "unknown fields should be rejected");
266 }
267
268 #[test]
269 fn deserialize_json_production_mode_default_false() {
270 let config: FallowConfig = serde_json::from_str("{}").unwrap();
271 assert!(!config.production);
272 }
273
274 #[test]
275 fn deserialize_json_production_mode_true() {
276 let config: FallowConfig = serde_json::from_str(r#"{"production": true}"#).unwrap();
277 assert!(config.production);
278 }
279
280 #[test]
283 fn deserialize_toml_minimal() {
284 let toml_str = r#"
285entry = ["src/index.ts"]
286production = true
287"#;
288 let config: FallowConfig = toml::from_str(toml_str).unwrap();
289 assert_eq!(config.entry, vec!["src/index.ts"]);
290 assert!(config.production);
291 }
292
293 #[test]
294 fn deserialize_toml_with_inline_framework() {
295 let toml_str = r#"
296[[framework]]
297name = "my-framework"
298enablers = ["my-framework-pkg"]
299entryPoints = ["src/routes/**/*.tsx"]
300"#;
301 let config: FallowConfig = toml::from_str(toml_str).unwrap();
302 assert_eq!(config.framework.len(), 1);
303 assert_eq!(config.framework[0].name, "my-framework");
304 assert_eq!(config.framework[0].enablers, vec!["my-framework-pkg"]);
305 assert_eq!(
306 config.framework[0].entry_points,
307 vec!["src/routes/**/*.tsx"]
308 );
309 }
310
311 #[test]
312 fn deserialize_toml_with_workspace_config() {
313 let toml_str = r#"
314[workspaces]
315patterns = ["packages/*", "apps/*"]
316"#;
317 let config: FallowConfig = toml::from_str(toml_str).unwrap();
318 assert!(config.workspaces.is_some());
319 let ws = config.workspaces.unwrap();
320 assert_eq!(ws.patterns, vec!["packages/*", "apps/*"]);
321 }
322
323 #[test]
324 fn deserialize_toml_with_ignore_exports() {
325 let toml_str = r#"
326[[ignoreExports]]
327file = "src/types/**/*.ts"
328exports = ["*"]
329"#;
330 let config: FallowConfig = toml::from_str(toml_str).unwrap();
331 assert_eq!(config.ignore_exports.len(), 1);
332 assert_eq!(config.ignore_exports[0].file, "src/types/**/*.ts");
333 assert_eq!(config.ignore_exports[0].exports, vec!["*"]);
334 }
335
336 #[test]
337 fn deserialize_toml_deny_unknown_fields() {
338 let toml_str = r"bogus_field = true";
339 let result: Result<FallowConfig, _> = toml::from_str(toml_str);
340 assert!(result.is_err(), "unknown fields should be rejected");
341 }
342
343 #[test]
346 fn json_serialize_roundtrip() {
347 let config = FallowConfig {
348 entry: vec!["src/main.ts".to_string()],
349 production: true,
350 ..FallowConfig::default()
351 };
352 let json = serde_json::to_string(&config).unwrap();
353 let restored: FallowConfig = serde_json::from_str(&json).unwrap();
354 assert_eq!(restored.entry, vec!["src/main.ts"]);
355 assert!(restored.production);
356 }
357
358 #[test]
359 fn schema_field_not_serialized() {
360 let config = FallowConfig {
361 schema: Some("https://example.com/schema.json".to_string()),
362 ..FallowConfig::default()
363 };
364 let json = serde_json::to_string(&config).unwrap();
365 assert!(
367 !json.contains("$schema"),
368 "schema field should be skipped in serialization"
369 );
370 }
371
372 #[test]
373 fn extends_field_not_serialized() {
374 let config = FallowConfig {
375 extends: vec!["base.json".to_string()],
376 ..FallowConfig::default()
377 };
378 let json = serde_json::to_string(&config).unwrap();
379 assert!(
380 !json.contains("extends"),
381 "extends field should be skipped in serialization"
382 );
383 }
384}