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