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