1use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashSet;
6
7use fallow_config::{ResolvedConfig, RulesConfig, Severity};
8use fallow_types::discover::StableFileKey;
9
10pub use crate::results::{
11 AnalysisResults, DeadCodeAnalysis, DeadCodeAnalysisArtifacts, DeadCodeAnalysisOutput,
12 DeadCodeAnalysisWithHashes, derive_security_severity, enable_security_rules,
13 security_catalogue_title, security_finding_id, security_rule_id,
14};
15
16use crate::{
17 EngineResult, session::analyze_dead_code_with_parse_result_from_config, source::ModuleInfo,
18};
19
20pub(crate) fn analyze_with_parse_result(
26 config: &ResolvedConfig,
27 modules: &[ModuleInfo],
28) -> EngineResult<DeadCodeAnalysisArtifacts> {
29 analyze_dead_code_with_parse_result_from_config(config, modules)
30}
31
32pub fn filter_to_workspaces(results: &mut AnalysisResults, ws_roots: &[PathBuf]) {
37 let any_under = |path: &Path| ws_roots.iter().any(|root| path.starts_with(root));
38 let pkg_jsons = ws_roots
39 .iter()
40 .map(|root| root.join("package.json"))
41 .collect::<Vec<_>>();
42 let in_pkg_jsons = |path: &Path| pkg_jsons.iter().any(|pkg| path == pkg);
43
44 filter_workspace_source_findings(results, &any_under);
45 filter_workspace_dependency_findings(results, &any_under, &in_pkg_jsons);
46 filter_workspace_graph_findings(results, &any_under);
47 filter_workspace_policy_findings(results, &any_under);
48}
49
50#[expect(
52 clippy::implicit_hasher,
53 reason = "fallow standardizes on FxHashSet across the workspace"
54)]
55pub fn filter_by_changed_files(results: &mut AnalysisResults, changed_files: &FxHashSet<PathBuf>) {
56 crate::changed_files::filter_results_by_changed_files(results, changed_files);
57}
58
59pub fn filter_configured_ignored_findings(results: &mut AnalysisResults, config: &ResolvedConfig) {
65 if config.ignore_findings.is_empty() {
66 return;
67 }
68
69 results.remove_ignored_dead_code_findings(|path| {
70 let key = if path.is_absolute() {
71 let Ok(relative) = path.strip_prefix(&config.root) else {
72 return false;
73 };
74 StableFileKey::from_relative(relative)
75 } else {
76 StableFileKey::from_relative(path)
77 };
78 config.ignore_findings.is_ignored(key.as_str())
79 });
80}
81
82fn filter_workspace_source_findings(
83 results: &mut AnalysisResults,
84 any_under: &dyn Fn(&Path) -> bool,
85) {
86 results
87 .unused_files
88 .retain(|finding| any_under(&finding.file.path));
89 results
90 .unused_exports
91 .retain(|finding| any_under(&finding.export.path));
92 results
93 .unused_types
94 .retain(|finding| any_under(&finding.export.path));
95 results
96 .private_type_leaks
97 .retain(|finding| any_under(&finding.leak.path));
98 results
99 .unused_enum_members
100 .retain(|finding| any_under(&finding.member.path));
101 results
102 .unused_class_members
103 .retain(|finding| any_under(&finding.member.path));
104 results
105 .unused_store_members
106 .retain(|finding| any_under(&finding.member.path));
107 results
108 .unprovided_injects
109 .retain(|finding| any_under(&finding.inject.path));
110 results
111 .unrendered_components
112 .retain(|finding| any_under(&finding.component.path));
113 results
114 .unused_component_props
115 .retain(|finding| any_under(&finding.prop.path));
116 results
117 .unused_component_emits
118 .retain(|finding| any_under(&finding.emit.path));
119 results
120 .unused_component_inputs
121 .retain(|finding| any_under(&finding.input.path));
122 results
123 .unused_component_outputs
124 .retain(|finding| any_under(&finding.output.path));
125 results
126 .unused_svelte_events
127 .retain(|finding| any_under(&finding.event.path));
128 results
129 .unused_server_actions
130 .retain(|finding| any_under(&finding.action.path));
131 results
132 .unused_load_data_keys
133 .retain(|finding| any_under(&finding.key.path));
134 results
135 .unresolved_imports
136 .retain(|finding| any_under(&finding.import.path));
137}
138
139fn filter_workspace_dependency_findings(
140 results: &mut AnalysisResults,
141 any_under: &dyn Fn(&Path) -> bool,
142 in_pkg_jsons: &dyn Fn(&Path) -> bool,
143) {
144 results
145 .unused_dependencies
146 .retain(|finding| in_pkg_jsons(&finding.dep.path));
147 results
148 .unused_dev_dependencies
149 .retain(|finding| in_pkg_jsons(&finding.dep.path));
150 results
151 .unused_optional_dependencies
152 .retain(|finding| in_pkg_jsons(&finding.dep.path));
153 results
154 .type_only_dependencies
155 .retain(|finding| in_pkg_jsons(&finding.dep.path));
156 results
157 .test_only_dependencies
158 .retain(|finding| in_pkg_jsons(&finding.dep.path));
159 results
160 .dev_dependencies_in_production
161 .retain(|finding| in_pkg_jsons(&finding.dep.path));
162
163 results.unlisted_dependencies.retain(|finding| {
164 finding
165 .dep
166 .imported_from
167 .iter()
168 .any(|source| any_under(&source.path))
169 });
170 results.unused_dependency_overrides.clear();
171 results.misconfigured_dependency_overrides.clear();
172}
173
174fn filter_workspace_graph_findings(
175 results: &mut AnalysisResults,
176 any_under: &dyn Fn(&Path) -> bool,
177) {
178 for duplicate in &mut results.duplicate_exports {
179 duplicate
180 .export
181 .locations
182 .retain(|location| any_under(&location.path));
183 }
184 results
185 .duplicate_exports
186 .retain(|duplicate| duplicate.export.locations.len() >= 2);
187
188 results
189 .circular_dependencies
190 .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
191
192 results
193 .re_export_cycles
194 .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
195}
196
197fn filter_workspace_policy_findings(
198 results: &mut AnalysisResults,
199 any_under: &dyn Fn(&Path) -> bool,
200) {
201 results
202 .boundary_violations
203 .retain(|finding| any_under(&finding.violation.from_path));
204 results
205 .boundary_coverage_violations
206 .retain(|finding| any_under(&finding.violation.path));
207 results
208 .boundary_call_violations
209 .retain(|finding| any_under(&finding.violation.path));
210 results
211 .policy_violations
212 .retain(|finding| any_under(&finding.violation.path));
213
214 results
215 .stale_suppressions
216 .retain(|finding| any_under(&finding.path));
217
218 results
219 .security_findings
220 .retain(|finding| any_under(&finding.path));
221 results
222 .security_unresolved_callee_diagnostics
223 .retain(|finding| any_under(&finding.path));
224
225 results.unused_catalog_entries.clear();
226 results.empty_catalog_groups.clear();
227 results
228 .unresolved_catalog_references
229 .retain(|finding| any_under(&finding.reference.path));
230
231 results
232 .invalid_client_exports
233 .retain(|finding| any_under(&finding.export.path));
234
235 results
236 .mixed_client_server_barrels
237 .retain(|finding| any_under(&finding.barrel.path));
238
239 results
240 .misplaced_directives
241 .retain(|finding| any_under(&finding.directive_site.path));
242
243 results
244 .route_collisions
245 .retain(|finding| any_under(&finding.collision.path));
246
247 results
248 .dynamic_segment_name_conflicts
249 .retain(|finding| any_under(&finding.conflict.path));
250}
251
252pub fn apply_rule_severities(results: &mut AnalysisResults, config: &ResolvedConfig) {
268 let rules = &config.rules;
269 let has_overrides = !config.overrides.is_empty();
270
271 if has_overrides {
272 apply_file_override_rules(results, config);
273 apply_boundary_override_rules(results, config);
274 } else {
275 apply_base_file_rules(results, rules);
276 }
277
278 apply_base_collection_rules(results, rules);
279}
280
281fn apply_base_collection_rules(results: &mut AnalysisResults, rules: &RulesConfig) {
282 if rules.unused_dependencies == Severity::Off {
283 results.unused_dependencies.clear();
284 }
285 if rules.unused_dev_dependencies == Severity::Off {
286 results.unused_dev_dependencies.clear();
287 }
288 if rules.unused_optional_dependencies == Severity::Off {
289 results.unused_optional_dependencies.clear();
290 }
291 if rules.unlisted_dependencies == Severity::Off {
292 results.unlisted_dependencies.clear();
293 }
294 if rules.duplicate_exports == Severity::Off {
295 results.duplicate_exports.clear();
296 }
297 if rules.type_only_dependencies == Severity::Off {
298 results.type_only_dependencies.clear();
299 }
300 if rules.test_only_dependencies == Severity::Off {
301 results.test_only_dependencies.clear();
302 }
303 if rules.dev_dependencies_in_production == Severity::Off {
304 results.dev_dependencies_in_production.clear();
305 }
306 if rules.circular_dependencies == Severity::Off {
307 results.circular_dependencies.clear();
308 }
309 if rules.re_export_cycle == Severity::Off {
310 results.re_export_cycles.clear();
311 }
312 if rules.boundary_violation == Severity::Off {
313 results.boundary_violations.clear();
314 results.boundary_coverage_violations.clear();
315 results.boundary_call_violations.clear();
316 }
317 if rules.policy_violation == Severity::Off {
318 results.policy_violations.clear();
319 }
320 if rules.unused_catalog_entries == Severity::Off {
321 results.unused_catalog_entries.clear();
322 }
323 if rules.empty_catalog_groups == Severity::Off {
324 results.empty_catalog_groups.clear();
325 }
326 if rules.unresolved_catalog_references == Severity::Off {
327 results.unresolved_catalog_references.clear();
328 }
329 if rules.unused_dependency_overrides == Severity::Off {
330 results.unused_dependency_overrides.clear();
331 }
332 if rules.misconfigured_dependency_overrides == Severity::Off {
333 results.misconfigured_dependency_overrides.clear();
334 }
335}
336
337fn apply_file_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
338 apply_dead_code_override_rules(results, config);
339 apply_catalog_override_rules(results, config);
340 apply_framework_override_rules(results, config);
341 apply_circular_override_rules(results, config);
342}
343
344fn apply_dead_code_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
345 apply_core_dead_code_override_rules(results, config);
346 apply_component_dead_code_override_rules(results, config);
347}
348
349fn apply_core_dead_code_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
351 results
352 .unused_files
353 .retain(|f| config.resolve_rules_for_path(&f.file.path).unused_files != Severity::Off);
354 results
355 .unused_exports
356 .retain(|e| config.resolve_rules_for_path(&e.export.path).unused_exports != Severity::Off);
357 results
358 .unused_types
359 .retain(|e| config.resolve_rules_for_path(&e.export.path).unused_types != Severity::Off);
360 results.private_type_leaks.retain(|e| {
361 config
362 .resolve_rules_for_path(&e.leak.path)
363 .private_type_leaks
364 != Severity::Off
365 });
366 results.unused_enum_members.retain(|m| {
367 config
368 .resolve_rules_for_path(&m.member.path)
369 .unused_enum_members
370 != Severity::Off
371 });
372 results.unused_class_members.retain(|m| {
373 config
374 .resolve_rules_for_path(&m.member.path)
375 .unused_class_members
376 != Severity::Off
377 });
378 results.unused_store_members.retain(|m| {
379 config
380 .resolve_rules_for_path(&m.member.path)
381 .unused_store_members
382 != Severity::Off
383 });
384 results.unprovided_injects.retain(|f| {
385 config
386 .resolve_rules_for_path(&f.inject.path)
387 .unprovided_injects
388 != Severity::Off
389 });
390 results.unresolved_imports.retain(|i| {
391 config
392 .resolve_rules_for_path(&i.import.path)
393 .unresolved_imports
394 != Severity::Off
395 });
396}
397
398fn apply_component_dead_code_override_rules(
400 results: &mut AnalysisResults,
401 config: &ResolvedConfig,
402) {
403 results.unrendered_components.retain(|c| {
404 config
405 .resolve_rules_for_path(&c.component.path)
406 .unrendered_components
407 != Severity::Off
408 });
409 results.unused_component_props.retain(|p| {
410 config
411 .resolve_rules_for_path(&p.prop.path)
412 .unused_component_props
413 != Severity::Off
414 });
415 results.unused_component_emits.retain(|e| {
416 config
417 .resolve_rules_for_path(&e.emit.path)
418 .unused_component_emits
419 != Severity::Off
420 });
421 results.unused_component_inputs.retain(|i| {
422 config
423 .resolve_rules_for_path(&i.input.path)
424 .unused_component_inputs
425 != Severity::Off
426 });
427 results.unused_component_outputs.retain(|o| {
428 config
429 .resolve_rules_for_path(&o.output.path)
430 .unused_component_outputs
431 != Severity::Off
432 });
433 results.unused_svelte_events.retain(|e| {
434 config
435 .resolve_rules_for_path(&e.event.path)
436 .unused_svelte_events
437 != Severity::Off
438 });
439 results.unused_server_actions.retain(|a| {
440 config
441 .resolve_rules_for_path(&a.action.path)
442 .unused_server_actions
443 != Severity::Off
444 });
445 results.unused_load_data_keys.retain(|k| {
446 config
447 .resolve_rules_for_path(&k.key.path)
448 .unused_load_data_keys
449 != Severity::Off
450 });
451}
452
453fn apply_catalog_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
454 results.stale_suppressions.retain(|s| {
455 let rules = config.resolve_rules_for_path(&s.path);
456 if s.missing_reason {
457 rules.require_suppression_reason != Severity::Off
458 } else {
459 rules.stale_suppressions != Severity::Off
460 }
461 });
462 results.unresolved_catalog_references.retain(|r| {
463 config
464 .resolve_rules_for_path(&r.reference.path)
465 .unresolved_catalog_references
466 != Severity::Off
467 });
468 results.empty_catalog_groups.retain(|g| {
469 config
470 .resolve_rules_for_path(&g.group.path)
471 .empty_catalog_groups
472 != Severity::Off
473 });
474 results.unused_dependency_overrides.retain(|o| {
475 config
476 .resolve_rules_for_path(&o.entry.path)
477 .unused_dependency_overrides
478 != Severity::Off
479 });
480 results.misconfigured_dependency_overrides.retain(|o| {
481 config
482 .resolve_rules_for_path(&o.entry.path)
483 .misconfigured_dependency_overrides
484 != Severity::Off
485 });
486}
487
488fn apply_framework_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
489 results.invalid_client_exports.retain(|e| {
490 config
491 .resolve_rules_for_path(&e.export.path)
492 .invalid_client_export
493 != Severity::Off
494 });
495 results.mixed_client_server_barrels.retain(|b| {
496 config
497 .resolve_rules_for_path(&b.barrel.path)
498 .mixed_client_server_barrel
499 != Severity::Off
500 });
501 results.misplaced_directives.retain(|d| {
502 config
503 .resolve_rules_for_path(&d.directive_site.path)
504 .misplaced_directive
505 != Severity::Off
506 });
507 results.route_collisions.retain(|c| {
508 config
509 .resolve_rules_for_path(&c.collision.path)
510 .route_collision
511 != Severity::Off
512 });
513 results.dynamic_segment_name_conflicts.retain(|c| {
514 config
515 .resolve_rules_for_path(&c.conflict.path)
516 .dynamic_segment_name_conflict
517 != Severity::Off
518 });
519}
520
521fn apply_circular_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
522 results.circular_dependencies.retain(|c| {
523 c.cycle
524 .files
525 .iter()
526 .any(|path| config.resolve_rules_for_path(path).circular_dependencies != Severity::Off)
527 });
528}
529
530fn apply_base_file_rules(results: &mut AnalysisResults, rules: &RulesConfig) {
531 clear_base_core_dead_code(results, rules);
532 clear_base_component_dead_code(results, rules);
533 clear_base_suppression_and_framework(results, rules);
534}
535
536fn clear_base_core_dead_code(results: &mut AnalysisResults, rules: &RulesConfig) {
538 if rules.unused_files == Severity::Off {
539 results.unused_files.clear();
540 }
541 if rules.unused_exports == Severity::Off {
542 results.unused_exports.clear();
543 }
544 if rules.unused_types == Severity::Off {
545 results.unused_types.clear();
546 }
547 if rules.private_type_leaks == Severity::Off {
548 results.private_type_leaks.clear();
549 }
550 if rules.unused_enum_members == Severity::Off {
551 results.unused_enum_members.clear();
552 }
553 if rules.unused_class_members == Severity::Off {
554 results.unused_class_members.clear();
555 }
556 if rules.unused_store_members == Severity::Off {
557 results.unused_store_members.clear();
558 }
559 if rules.unprovided_injects == Severity::Off {
560 results.unprovided_injects.clear();
561 }
562 if rules.unresolved_imports == Severity::Off {
563 results.unresolved_imports.clear();
564 }
565}
566
567fn clear_base_component_dead_code(results: &mut AnalysisResults, rules: &RulesConfig) {
569 if rules.unrendered_components == Severity::Off {
570 results.unrendered_components.clear();
571 }
572 if rules.unused_component_props == Severity::Off {
573 results.unused_component_props.clear();
574 }
575 if rules.unused_component_emits == Severity::Off {
576 results.unused_component_emits.clear();
577 }
578 if rules.unused_component_inputs == Severity::Off {
579 results.unused_component_inputs.clear();
580 }
581 if rules.unused_component_outputs == Severity::Off {
582 results.unused_component_outputs.clear();
583 }
584 if rules.unused_svelte_events == Severity::Off {
585 results.unused_svelte_events.clear();
586 }
587 if rules.unused_server_actions == Severity::Off {
588 results.unused_server_actions.clear();
589 }
590 if rules.unused_load_data_keys == Severity::Off {
591 results.unused_load_data_keys.clear();
592 }
593}
594
595fn clear_base_suppression_and_framework(results: &mut AnalysisResults, rules: &RulesConfig) {
598 results.stale_suppressions.retain(|s| {
599 if s.missing_reason {
600 rules.require_suppression_reason != Severity::Off
601 } else {
602 rules.stale_suppressions != Severity::Off
603 }
604 });
605 if rules.invalid_client_export == Severity::Off {
606 results.invalid_client_exports.clear();
607 }
608 if rules.mixed_client_server_barrel == Severity::Off {
609 results.mixed_client_server_barrels.clear();
610 }
611 if rules.misplaced_directive == Severity::Off {
612 results.misplaced_directives.clear();
613 }
614 if rules.route_collision == Severity::Off {
615 results.route_collisions.clear();
616 }
617 if rules.dynamic_segment_name_conflict == Severity::Off {
618 results.dynamic_segment_name_conflicts.clear();
619 }
620}
621
622fn apply_boundary_override_rules(results: &mut AnalysisResults, config: &ResolvedConfig) {
623 results.boundary_violations.retain(|v| {
624 config
625 .resolve_rules_for_path(&v.violation.from_path)
626 .boundary_violation
627 != Severity::Off
628 });
629 results.boundary_coverage_violations.retain(|v| {
630 config
631 .resolve_rules_for_path(&v.violation.path)
632 .boundary_violation
633 != Severity::Off
634 });
635 results.boundary_call_violations.retain(|v| {
636 config
637 .resolve_rules_for_path(&v.violation.path)
638 .boundary_violation
639 != Severity::Off
640 });
641 results.policy_violations.retain(|v| {
642 config
643 .resolve_rules_for_path(&v.violation.path)
644 .policy_violation
645 != Severity::Off
646 });
647}
648
649#[cfg(test)]
650mod tests {
651 use std::path::PathBuf;
652
653 use super::*;
654 use fallow_types::output_dead_code::{
655 BoundaryViolationFinding, CircularDependencyFinding, PrivateTypeLeakFinding,
656 UnusedExportFinding, UnusedFileFinding,
657 };
658 use fallow_types::results::{
659 BoundaryViolation, CircularDependency, PrivateTypeLeak, UnusedExport, UnusedFile,
660 };
661
662 #[test]
663 fn workspace_filter_keeps_findings_under_workspace_root() {
664 let root = PathBuf::from("/repo/packages/app");
665 let mut results = AnalysisResults::default();
666 results
667 .unused_files
668 .push(UnusedFileFinding::with_actions(UnusedFile {
669 path: root.join("src/unused.ts"),
670 }));
671 results
672 .unused_files
673 .push(UnusedFileFinding::with_actions(UnusedFile {
674 path: PathBuf::from("/repo/packages/docs/src/unused.ts"),
675 }));
676
677 filter_to_workspaces(&mut results, std::slice::from_ref(&root));
678
679 assert_eq!(results.unused_files.len(), 1);
680 assert_eq!(
681 results.unused_files[0].file.path,
682 root.join("src/unused.ts")
683 );
684 }
685
686 #[test]
687 fn configured_filter_removes_findings_added_after_engine_analysis() {
688 let project = tempfile::tempdir().expect("project");
689 let config = serde_json::from_str::<fallow_config::FallowConfig>(
690 r#"{"ignoreFindings":["src/hidden.ts"]}"#,
691 )
692 .expect("config parses")
693 .resolve(
694 project.path().to_path_buf(),
695 fallow_config::OutputFormat::Human,
696 1,
697 true,
698 true,
699 None,
700 );
701 let mut results = AnalysisResults::default();
702 results
703 .private_type_leaks
704 .push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
705 path: project.path().join("src/hidden.ts"),
706 export_name: "publicApi".to_string(),
707 type_name: "PrivateShape".to_string(),
708 line: 1,
709 col: 0,
710 span_start: 0,
711 semantic: None,
712 }));
713 results
714 .boundary_violations
715 .push(BoundaryViolationFinding::with_actions(BoundaryViolation {
716 from_path: project.path().join("src/hidden.ts"),
717 to_path: project.path().join("src/data.ts"),
718 from_zone: "ui".to_string(),
719 to_zone: "data".to_string(),
720 import_specifier: "./data".to_string(),
721 line: 1,
722 col: 0,
723 }));
724
725 filter_configured_ignored_findings(&mut results, &config);
726
727 assert!(results.private_type_leaks.is_empty());
728 assert_eq!(results.boundary_violations.len(), 1);
729 }
730
731 fn config_with_override(
732 pattern: &str,
733 configure: impl FnOnce(&mut fallow_config::PartialRulesConfig),
734 ) -> ResolvedConfig {
735 let mut partial = fallow_config::PartialRulesConfig::default();
736 configure(&mut partial);
737 fallow_config::FallowConfig {
738 rules: RulesConfig {
739 private_type_leaks: Severity::Warn,
740 ..RulesConfig::default()
741 },
742 overrides: vec![fallow_config::ConfigOverride {
743 files: vec![pattern.to_string()],
744 rules: partial,
745 }],
746 ..fallow_config::FallowConfig::default()
747 }
748 .resolve(
749 PathBuf::from("/project"),
750 fallow_config::OutputFormat::Human,
751 1,
752 true,
753 true,
754 None,
755 )
756 }
757
758 fn unused_export(path: &str) -> UnusedExportFinding {
759 UnusedExportFinding::with_actions(UnusedExport {
760 path: PathBuf::from(path),
761 export_name: "Unused".to_string(),
762 is_type_only: false,
763 line: 1,
764 col: 0,
765 span_start: 0,
766 is_re_export: false,
767 })
768 }
769
770 fn private_type_leak(path: &str) -> PrivateTypeLeakFinding {
771 PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
772 path: PathBuf::from(path),
773 export_name: "Unused".to_string(),
774 type_name: "Props".to_string(),
775 line: 1,
776 col: 0,
777 span_start: 0,
778 semantic: None,
779 })
780 }
781
782 fn overridden_fixture() -> AnalysisResults {
783 let mut results = AnalysisResults::default();
784 results
785 .unused_exports
786 .push(unused_export("/project/src/ui/kit.ts"));
787 results
788 .unused_exports
789 .push(unused_export("/project/src/lib/util.ts"));
790 results
791 .private_type_leaks
792 .push(private_type_leak("/project/src/ui/kit.ts"));
793 results
794 .private_type_leaks
795 .push(private_type_leak("/project/src/lib/util.ts"));
796 results
797 }
798
799 #[test]
800 fn rule_severities_drop_findings_only_on_overridden_paths() {
801 let config = config_with_override("src/ui/**", |rules| {
802 rules.unused_exports = Some(Severity::Off);
803 rules.private_type_leaks = Some(Severity::Off);
804 });
805 let mut results = overridden_fixture();
806
807 apply_rule_severities(&mut results, &config);
808
809 assert_eq!(
810 results
811 .unused_exports
812 .iter()
813 .map(|finding| finding.export.path.clone())
814 .collect::<Vec<_>>(),
815 vec![PathBuf::from("/project/src/lib/util.ts")]
816 );
817 assert_eq!(
818 results
819 .private_type_leaks
820 .iter()
821 .map(|finding| finding.leak.path.clone())
822 .collect::<Vec<_>>(),
823 vec![PathBuf::from("/project/src/lib/util.ts")]
824 );
825 }
826
827 #[test]
828 fn rule_severities_are_idempotent() {
829 let config = config_with_override("src/ui/**", |rules| {
833 rules.unused_exports = Some(Severity::Off);
834 rules.private_type_leaks = Some(Severity::Off);
835 });
836
837 let mut once = overridden_fixture();
838 apply_rule_severities(&mut once, &config);
839 let mut twice = overridden_fixture();
840 apply_rule_severities(&mut twice, &config);
841 apply_rule_severities(&mut twice, &config);
842
843 assert_eq!(
844 once.unused_exports
845 .iter()
846 .map(|finding| finding.export.path.clone())
847 .collect::<Vec<_>>(),
848 twice
849 .unused_exports
850 .iter()
851 .map(|finding| finding.export.path.clone())
852 .collect::<Vec<_>>()
853 );
854 assert_eq!(
855 once.private_type_leaks
856 .iter()
857 .map(|finding| finding.leak.path.clone())
858 .collect::<Vec<_>>(),
859 twice
860 .private_type_leaks
861 .iter()
862 .map(|finding| finding.leak.path.clone())
863 .collect::<Vec<_>>()
864 );
865 }
866
867 #[test]
868 fn rule_severities_keep_a_cycle_when_any_member_file_stays_enabled() {
869 let config = config_with_override("src/ui/**", |rules| {
870 rules.circular_dependencies = Some(Severity::Off);
871 });
872 let mut results = AnalysisResults::default();
873 results
874 .circular_dependencies
875 .push(CircularDependencyFinding::with_actions(
876 CircularDependency {
877 files: vec![
878 PathBuf::from("/project/src/ui/a.ts"),
879 PathBuf::from("/project/src/lib/b.ts"),
880 ],
881 length: 2,
882 line: 1,
883 col: 0,
884 edges: Vec::new(),
885 is_cross_package: false,
886 },
887 ));
888 results
889 .circular_dependencies
890 .push(CircularDependencyFinding::with_actions(
891 CircularDependency {
892 files: vec![
893 PathBuf::from("/project/src/ui/c.ts"),
894 PathBuf::from("/project/src/ui/d.ts"),
895 ],
896 length: 2,
897 line: 1,
898 col: 0,
899 edges: Vec::new(),
900 is_cross_package: false,
901 },
902 ));
903
904 apply_rule_severities(&mut results, &config);
905
906 assert_eq!(results.circular_dependencies.len(), 1);
907 assert_eq!(
908 results.circular_dependencies[0].cycle.files[0],
909 PathBuf::from("/project/src/ui/a.ts")
910 );
911 }
912
913 #[test]
914 fn rule_severities_clear_base_rules_without_overrides() {
915 let config = fallow_config::FallowConfig {
916 rules: RulesConfig {
917 unused_exports: Severity::Off,
918 private_type_leaks: Severity::Warn,
919 ..RulesConfig::default()
920 },
921 ..fallow_config::FallowConfig::default()
922 }
923 .resolve(
924 PathBuf::from("/project"),
925 fallow_config::OutputFormat::Human,
926 1,
927 true,
928 true,
929 None,
930 );
931 let mut results = overridden_fixture();
932
933 apply_rule_severities(&mut results, &config);
934
935 assert!(results.unused_exports.is_empty());
936 assert_eq!(results.private_type_leaks.len(), 2);
937 }
938}