1#![warn(missing_docs)]
10#![cfg_attr(
11 test,
12 allow(
13 clippy::expect_used,
14 reason = "tests use expect to keep fixture setup concise"
15 )
16)]
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20use std::sync::atomic::AtomicBool;
21
22use fallow_config::EmailMode;
23use fallow_output::EffortEstimate;
24use serde::Serialize;
25
26mod analysis_context;
27mod audit_complexity;
28pub mod audit_keys;
32pub mod audit_output;
33pub mod audit_run;
34pub mod combined_output;
35pub mod compact_output;
38pub mod coverage;
39pub mod dead_code_codeclimate;
40pub mod dead_code_sarif;
41pub mod decision_surface;
42pub mod dependency_deltas;
43pub mod diff_risk;
44pub mod doctor;
45pub mod dupes_output;
46pub mod editor;
47pub mod explain;
48pub mod grouped_output;
49pub mod health_codeclimate;
50mod health_groups_markdown;
51pub mod json_output;
52pub mod list_output;
53mod list_runtime;
54pub mod markdown_output;
57pub mod mutation;
58mod next_steps;
59pub mod output_contracts;
60pub mod ownership;
61pub mod review_deltas;
62pub mod routing;
63pub mod runtime;
64mod runtime_json;
65mod runtime_output;
66pub mod sarif_output;
67pub mod schemas;
70pub mod security_output;
71pub mod similar_code;
73mod type_aware;
74pub use analysis_context::{ProgrammaticAnalysisContext, resolve_programmatic_analysis_context};
75pub use audit_output::{
76 AuditAttribution, AuditCodeClimateOutputInput, AuditJsonHeaderInput, AuditJsonOutputInput,
77 AuditSarifOutputInput, AuditSummary, AuditVerdict, attach_audit_diff_risk_attribution,
78 attach_audit_duplication_demotion_attribution, attach_audit_styling_attribution,
79 attach_audit_wire_attribution, build_audit_codeclimate, build_audit_codeclimate_issues,
80 build_audit_header_json, build_audit_header_map, build_audit_sarif,
81 build_diff_risk_codeclimate, build_review_brief_header, serialize_audit_json,
82};
83pub use combined_output::{
84 CombinedCheckJsonSection, CombinedJsonOutputInput, serialize_combined_dupes_json,
85 serialize_combined_health_json, serialize_combined_json,
86};
87pub use compact_output::{
88 build_compact_lines, build_diff_risk_compact_lines, build_duplication_compact_lines,
89 build_grouped_compact_lines, build_health_compact_lines,
90};
91pub use coverage::{
92 CoverageInputError, CoverageInputSource, CoverageInputs, resolve_coverage_inputs,
93};
94pub use dead_code_codeclimate::build_codeclimate;
95pub use dead_code_sarif::build_sarif;
96pub use doctor::{DoctorOptions, run_doctor, run_doctor_with_cache_dir};
97pub use dupes_output::{
98 AttributedCloneGroup, AttributedCloneGroupFinding, AttributedInstance, CloneDemotionReason,
99 CloneFamilyFinding, CloneGroupFinding, CombinedDupesSection, DupesReportPayload,
100 DuplicationGroup, DuplicationGrouping, build_duplication_codeclimate,
101};
102pub use editor::{
103 ChangedFilesError, EditorAnalysisOutput, EditorAnalysisResults, EditorAnalysisSession,
104 EditorCloneFamily, EditorCloneFingerprintSet, EditorCloneGroup, EditorCloneInstance,
105 EditorDeadCodeAnalysisOutput, EditorDuplicationReport, EditorDuplicationStats,
106 EditorInlineComplexityExceeded, EditorInlineComplexityFinding, EditorMirroredDirectory,
107 EditorProjectAnalysisOutput, EditorRefactoringKind, EditorRefactoringSuggestion,
108 EditorSessionParseCounts, collect_inline_complexity, editor_duplicates, editor_extract,
109 editor_results, editor_security, editor_suppress, filter_inline_complexity_by_change_scope,
110 filter_inline_complexity_by_changed_files, resolve_git_toplevel,
111 try_get_changed_files_with_toplevel,
112};
113pub use explain::{
114 AUDIT_RULES, CHECK_RULES, DUPES_RULES, FLAGS_RULES, HEALTH_RULES, RuleDef, RuleGuide,
115 SECURITY_RULES, all_rules, bare_rule_id, coverage_analyze_meta, coverage_setup_meta,
116 explain_issue_type, rule_by_id, rule_by_token, rule_command, rule_docs_url, rule_guide,
117 rule_severity_key, security_meta, serialize_explain_programmatic_json, unknown_explain_error,
118};
119pub use fallow_config::levenshtein::closest_match;
120pub use fallow_config::{AuditGate, AuditWorsened, HealthConfig, TypeAwareRequire};
121pub use fallow_engine::change_scope::{
124 ChangeScope, ChangeScopeOwner, ChangeScopeRequest, first_package_baselines,
125 package_baseline_statuses,
126};
127pub use fallow_engine::package_baselines::{PackageBaselineError, PackageChangeScope};
129pub use fallow_engine::warm_parse;
135pub use fallow_output::PackageBaselineStatus;
137pub use fallow_output::serialize_similar_code_json_output;
138pub use fallow_types::trace::{
139 CloneTrace, DependencyTrace, ExportReference, ExportTrace, FileTrace, ReExportChain,
140 TracedCloneGroup, TracedExport, TracedReExport,
141};
142pub use fallow_types::trace_usage::{
143 DependencyUsage, DependencyUsageQuery, SitePageRequest, UsageQueryError,
144};
145pub use grouped_output::{
146 ResultGroup, UNOWNED_GROUP_LABEL, build_duplication_grouping_with, group_analysis_results_with,
147 health_signal_header_part, largest_clone_group_owner_with,
148};
149pub use health_codeclimate::build_health_codeclimate;
150pub use health_groups_markdown::build_health_groups_markdown;
151pub use json_output::{
152 CheckJsonExtraOutputs, CheckJsonOutputInput, CheckJsonPayloadInput, DuplicationJsonOutputInput,
153 GroupedCheckJsonOutputInput, GroupedDuplicationJsonOutputInput, serialize_check_json,
154 serialize_check_json_payload, serialize_duplication_json, serialize_grouped_check_json,
155 serialize_grouped_duplication_json,
156};
157pub use list_output::{
158 ListJsonEnvelope, ListJsonOutputInput, build_list_json_output, serialize_list_json_output,
159};
160pub use list_runtime::{
161 BoundaryData, ListBoundariesOptions, ListBoundariesProgrammaticOutput, LogicalGroupInfo,
162 ProjectInfoOptions, ProjectInfoProgrammaticOutput, RuleInfo, ZoneInfo, boundary_data_to_output,
163 compute_boundary_data, run_list_boundaries, run_project_info,
164 serialize_list_boundaries_programmatic_json, serialize_project_info_programmatic_json,
165};
166pub use markdown_output::{
167 ComplexityListing, build_diff_risk_markdown, build_duplication_markdown,
168 build_grouped_markdown, build_health_markdown, build_markdown, build_walkthrough_markdown,
169};
170pub use mutation::{
171 MutationProgress, MutationProgressSink, MutationRunOptions, MutationRunScope,
172 apply_mutation_verdicts, validate_mutation_verdicts,
173};
174pub use output_contracts::{
175 AuditOutput, BoundariesListLogicalGroup, BoundariesListRule, BoundariesListZone,
176 BoundariesListing, CombinedOutput, FallowOutput, ImpactOutput, ListBoundariesOutput,
177 ListEntryPointOutput, ListOutput, ListPluginOutput, ReviewBriefWireOutput, SecurityGate,
178 SecurityOutput, SecurityOutputConfig, SecuritySummaryOutput, SimilarCodeCandidateSnapshot,
179 SimilarCodeOutput, TraceOutput, WorkspacesOutput,
180};
181pub use runtime::TYPE_AWARE_CONFIG_UNSUPPORTED;
182pub use runtime::{
183 AUDIT_CONFIG_BASELINE_UNSUPPORTED, ArchitectureOutput, ArchitectureProgrammaticOutput,
184 AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
185 BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
186 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
187 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
188 DuplicationProgrammaticOutput, EngineHealthRunner, FeatureFlagsOutput,
189 FeatureFlagsProgrammaticOutput, HealthJsonReportInput, HealthProgrammaticOutput,
190 ProgrammaticHealthAnalysis, ProgrammaticHealthNextStepFacts, ProgrammaticHealthRun,
191 ProgrammaticHealthRunner, TraceClassMemberOutput, TraceCloneBenchmarkResult, TraceCloneOutput,
192 TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
193 TraceErrorOutput, TraceErrorProgrammaticOutput, TraceExportOutput,
194 TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOutput,
195 TraceFileProgrammaticOutput, TraceImportPathOutput, TraceImportPathProgrammaticOutput,
196 benchmark_trace_clone_compact_json, benchmark_trace_graph_family_compact_json,
197 inspect_similar_code, load_health_config, parse_similar_code_candidate_snapshot, plan_mutation,
198 review_similar_code, run_architecture, run_audit, run_boundary_violations,
199 run_circular_dependencies, run_combined, run_complexity_with_runner, run_dead_code,
200 run_dead_code_with_baseline, run_decision_surface, run_duplication, run_feature_flags,
201 run_health, run_health_with_runner, run_mutation, run_mutation_with_progress, run_similar_code,
202 run_trace_clone, run_trace_dependency, run_trace_error, run_trace_export, run_trace_file,
203 run_trace_import_path, select_similar_code_candidate_snapshot, serialize_health_report_json,
204};
205pub use runtime_json::{
206 serialize_architecture_programmatic_json, serialize_audit_programmatic_json,
207 serialize_boundary_violations_programmatic_json,
208 serialize_circular_dependencies_programmatic_json, serialize_combined_programmatic_json,
209 serialize_dead_code_programmatic_json, serialize_decision_surface_programmatic_json,
210 serialize_duplication_programmatic_json, serialize_feature_flags_programmatic_json,
211 serialize_health_programmatic_json, serialize_trace_clone_programmatic_json,
212 serialize_trace_dependency_programmatic_json, serialize_trace_error_programmatic_json,
213 serialize_trace_export_programmatic_json, serialize_trace_file_programmatic_json,
214 serialize_trace_import_path_programmatic_json,
215};
216pub use sarif_output::{
217 annotate_sarif_results, build_duplication_sarif, build_grouped_duplication_sarif,
218 build_health_sarif,
219};
220pub use security_output::SecurityGateMode;
221pub use type_aware::{
222 SemanticCouplingOutcome, SemanticDeadCodeOutcome, SemanticInspectOutcome, TypeAwareError,
223 TypeAwareFileChanges, TypeAwareOutcome, TypeAwareSession, TypeAwareStatus,
224 discard_unverified_semantic_candidates, inspect_symbol as inspect_type_aware_symbol,
225 merge_type_aware_meta,
226 refine_configured_dead_code_results as refine_type_aware_results_with_config,
227 shutdown_type_aware_sidecars, status as type_aware_status,
228 symbol_impact as run_type_aware_symbol_impact, symbol_impact as type_aware_symbol_impact,
229 terminate_active_type_aware_sidecars, trace_symbol as run_type_aware_symbol_trace,
230 trace_symbol as trace_type_aware_symbol, type_coupling as analyze_type_coupling,
231};
232
233pub const COMMON_ANALYSIS_OPTION_FLAGS: &[&str] = &[
240 "root",
241 "config",
242 "no-cache",
243 "threads",
244 "changed-since",
245 "diff-file",
246 "production",
247 "workspace",
248 "changed-workspaces",
249 "explain",
250 "allow-remote-extends",
251];
252
253#[derive(Debug, Clone, Serialize)]
255pub struct ProgrammaticError {
256 pub message: String,
258 pub exit_code: u8,
260 pub code: Option<String>,
262 pub help: Option<String>,
264 pub context: Option<String>,
266}
267
268impl ProgrammaticError {
269 #[must_use]
272 pub fn new(message: impl Into<String>, exit_code: u8) -> Self {
273 Self {
274 message: message.into(),
275 exit_code,
276 code: None,
277 help: None,
278 context: None,
279 }
280 }
281
282 #[must_use]
284 pub fn with_help(mut self, help: impl Into<String>) -> Self {
285 self.help = Some(help.into());
286 self
287 }
288
289 #[must_use]
292 pub fn with_code(mut self, code: impl Into<String>) -> Self {
293 self.code = Some(code.into());
294 self
295 }
296
297 #[must_use]
300 pub fn with_context(mut self, context: impl Into<String>) -> Self {
301 self.context = Some(context.into());
302 self
303 }
304}
305
306impl std::fmt::Display for ProgrammaticError {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 write!(f, "{}", self.message)
309 }
310}
311
312impl std::error::Error for ProgrammaticError {}
313
314#[derive(Debug, Clone, Default)]
316pub struct AnalysisOptions {
317 pub root: Option<PathBuf>,
320 pub config_path: Option<PathBuf>,
322 pub allow_remote_extends: bool,
324 pub no_cache: bool,
326 pub threads: Option<usize>,
328 pub diff_file: Option<PathBuf>,
340 pub ambient_diff_file: Option<PathBuf>,
348 pub production: bool,
351 pub production_override: Option<bool>,
354 pub changed_since: Option<String>,
359 pub ambient_changed_since: Option<String>,
367 pub no_package_baselines: bool,
371 pub workspace: Option<Vec<String>>,
373 pub changed_workspaces: Option<String>,
375 pub explain: bool,
377 pub type_aware: TypeAwareOptions,
380 pub cancellation: Option<Arc<AtomicBool>>,
410}
411
412#[derive(Debug, Clone, Default, PartialEq, Eq)]
414pub struct TypeAwareOptions {
415 pub enabled: Option<bool>,
422 pub projects: Vec<PathBuf>,
425 pub require: fallow_config::TypeAwareRequire,
427}
428
429impl TypeAwareOptions {
430 #[must_use]
432 pub const fn requested(&self) -> bool {
433 matches!(self.enabled, Some(true))
434 }
435}
436
437#[derive(Debug, Clone, Default)]
442pub struct DeadCodeFilters {
443 pub unused_files: bool,
445 pub unused_exports: bool,
447 pub unused_deps: bool,
449 pub unused_types: bool,
451 pub private_type_leaks: bool,
453 pub deprecated_exports_in_use: bool,
455 pub unused_enum_members: bool,
457 pub unused_class_members: bool,
459 pub unused_store_members: bool,
461 pub unprovided_injects: bool,
463 pub unrendered_components: bool,
465 pub unused_component_props: bool,
467 pub absent_component_props: bool,
469 pub unused_component_emits: bool,
471 pub unused_component_inputs: bool,
473 pub unused_component_outputs: bool,
475 pub unused_svelte_events: bool,
477 pub unused_effect_layers: bool,
479 pub unused_tagged_errors: bool,
481 pub unproduced_catch_tags: bool,
483 pub unused_server_actions: bool,
485 pub unused_load_data_keys: bool,
487 pub unresolved_imports: bool,
489 pub unlisted_deps: bool,
491 pub duplicate_exports: bool,
493 pub circular_deps: bool,
495 pub re_export_cycles: bool,
497 pub package_cycles: bool,
499 pub boundary_violations: bool,
501 pub policy_violations: bool,
503 pub stale_suppressions: bool,
505 pub unused_catalog_entries: bool,
507 pub empty_catalog_groups: bool,
509 pub unresolved_catalog_references: bool,
511 pub unused_dependency_overrides: bool,
513 pub misconfigured_dependency_overrides: bool,
515}
516
517impl DeadCodeFilters {
518 pub(crate) fn reports_dependency_findings(&self) -> bool {
522 !self.any_active() || self.unused_deps || self.unlisted_deps
523 }
524
525 fn any_active(&self) -> bool {
526 self.unused_files
527 || self.unused_exports
528 || self.unused_deps
529 || self.unused_types
530 || self.private_type_leaks
531 || self.deprecated_exports_in_use
532 || self.unused_enum_members
533 || self.unused_class_members
534 || self.unused_store_members
535 || self.unprovided_injects
536 || self.unrendered_components
537 || self.unused_component_props
538 || self.absent_component_props
539 || self.unused_component_emits
540 || self.unused_component_inputs
541 || self.unused_component_outputs
542 || self.unused_svelte_events
543 || self.unused_effect_layers
544 || self.unused_tagged_errors
545 || self.unproduced_catch_tags
546 || self.unused_server_actions
547 || self.unused_load_data_keys
548 || self.unresolved_imports
549 || self.unlisted_deps
550 || self.duplicate_exports
551 || self.circular_deps
552 || self.re_export_cycles
553 || self.package_cycles
554 || self.boundary_violations
555 || self.policy_violations
556 || self.stale_suppressions
557 || self.unused_catalog_entries
558 || self.empty_catalog_groups
559 || self.unresolved_catalog_references
560 || self.unused_dependency_overrides
561 || self.misconfigured_dependency_overrides
562 }
563
564 pub fn enable_registry_selector(&mut self, selector: &str) -> bool {
570 let Some(flag) = fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS
571 .iter()
572 .find_map(|&(name, flag)| (name == selector).then_some(flag))
573 else {
574 return false;
575 };
576 self.enable_cli_filter_flag(flag);
577 true
578 }
579
580 fn enable_cli_filter_flag(&mut self, flag: &str) {
581 match flag {
582 "--unused-files" => self.unused_files = true,
583 "--unused-exports" => self.unused_exports = true,
584 "--unused-types" => self.unused_types = true,
585 "--private-type-leaks" => self.private_type_leaks = true,
586 "--deprecated-exports-in-use" => self.deprecated_exports_in_use = true,
587 "--unused-deps" => self.unused_deps = true,
588 "--unused-enum-members" => self.unused_enum_members = true,
589 "--unused-class-members" => self.unused_class_members = true,
590 "--unused-store-members" => self.unused_store_members = true,
591 "--unprovided-injects" => self.unprovided_injects = true,
592 "--unrendered-components" => self.unrendered_components = true,
593 "--unused-component-props" => self.unused_component_props = true,
594 "--absent-component-props" => self.absent_component_props = true,
595 "--unused-component-emits" => self.unused_component_emits = true,
596 "--unused-component-inputs" => self.unused_component_inputs = true,
597 "--unused-component-outputs" => self.unused_component_outputs = true,
598 "--unused-svelte-events" => self.unused_svelte_events = true,
599 "--unused-effect-layers" => self.unused_effect_layers = true,
600 "--unused-tagged-errors" => self.unused_tagged_errors = true,
601 "--unproduced-catch-tags" => self.unproduced_catch_tags = true,
602 "--unused-server-actions" => self.unused_server_actions = true,
603 "--unused-load-data-keys" => self.unused_load_data_keys = true,
604 "--unresolved-imports" => self.unresolved_imports = true,
605 "--unlisted-deps" => self.unlisted_deps = true,
606 "--duplicate-exports" => self.duplicate_exports = true,
607 "--circular-deps" => self.circular_deps = true,
608 "--re-export-cycles" => self.re_export_cycles = true,
609 "--package-cycles" => self.package_cycles = true,
610 "--boundary-violations" => self.boundary_violations = true,
611 "--policy-violations" => self.policy_violations = true,
612 "--stale-suppressions" => self.stale_suppressions = true,
613 "--unused-catalog-entries" => self.unused_catalog_entries = true,
614 "--empty-catalog-groups" => self.empty_catalog_groups = true,
615 "--unresolved-catalog-references" => self.unresolved_catalog_references = true,
616 "--unused-dependency-overrides" => self.unused_dependency_overrides = true,
617 "--misconfigured-dependency-overrides" => {
618 self.misconfigured_dependency_overrides = true;
619 }
620 _ => unreachable!("registry emitted unsupported dead-code filter flag: {flag}"),
621 }
622 }
623}
624
625#[derive(Debug, Clone, Default)]
627pub struct DeadCodeOptions {
628 pub analysis: AnalysisOptions,
630 pub filters: DeadCodeFilters,
632 pub files: Vec<PathBuf>,
634 pub include_entry_exports: bool,
636 pub show_cascade: bool,
638 pub finding_ids: Vec<String>,
643}
644
645#[derive(Debug, Clone, Default)]
651pub struct ArchitectureOptions {
652 pub analysis: AnalysisOptions,
654 pub files: Vec<PathBuf>,
656 pub cycles: bool,
659 pub boundaries: bool,
661 pub policy: bool,
663}
664
665impl ArchitectureOptions {
666 #[must_use]
668 pub fn dead_code_filters(&self) -> DeadCodeFilters {
669 let all = !(self.cycles || self.boundaries || self.policy);
670 DeadCodeFilters {
671 circular_deps: self.cycles || all,
672 re_export_cycles: self.cycles || all,
673 package_cycles: self.cycles || all,
674 boundary_violations: self.boundaries || all,
675 policy_violations: self.policy || all,
676 ..DeadCodeFilters::default()
677 }
678 }
679
680 #[must_use]
682 pub fn dead_code_options(&self) -> DeadCodeOptions {
683 DeadCodeOptions {
684 analysis: self.analysis.clone(),
685 filters: self.dead_code_filters(),
686 files: self.files.clone(),
687 include_entry_exports: false,
688 show_cascade: false,
689 finding_ids: Vec::new(),
690 }
691 }
692}
693
694#[derive(Debug, Clone, Default)]
696pub struct AuditOptions {
697 pub analysis: AnalysisOptions,
699 pub base: Option<String>,
702 pub production: bool,
704 pub production_dead_code: Option<bool>,
706 pub production_health: Option<bool>,
708 pub production_dupes: Option<bool>,
711 pub dead_code: Option<bool>,
715 pub health: Option<bool>,
718 pub duplication: Option<bool>,
721 pub css: Option<bool>,
724 pub css_deep: Option<bool>,
727 pub gate: Option<fallow_config::AuditGate>,
730 pub worsened: Option<fallow_config::AuditWorsened>,
733 pub max_crap: Option<f64>,
735 pub coverage: Option<PathBuf>,
737 pub coverage_root: Option<PathBuf>,
739 pub include_entry_exports: bool,
741 pub show_cascade: bool,
743 pub runtime_coverage: Option<PathBuf>,
745 pub min_invocations_hot: u64,
747}
748
749#[derive(Debug, Clone)]
751pub struct CombinedOptions {
752 pub analysis: AnalysisOptions,
754 pub dead_code: bool,
756 pub duplication: bool,
758 pub health: bool,
760 pub include_entry_exports: bool,
762 pub show_cascade: bool,
764 pub duplication_options: DuplicationOptions,
766 pub health_options: ComplexityOptions,
768}
769
770impl Default for CombinedOptions {
771 fn default() -> Self {
772 Self {
773 analysis: AnalysisOptions::default(),
774 dead_code: true,
775 duplication: true,
776 health: true,
777 include_entry_exports: false,
778 show_cascade: false,
779 duplication_options: DuplicationOptions::default(),
780 health_options: ComplexityOptions::default(),
781 }
782 }
783}
784
785#[derive(Debug, Clone, Default)]
787pub struct DecisionSurfaceOptions {
788 pub analysis: AnalysisOptions,
790 pub base: Option<String>,
793 pub max_decisions: Option<usize>,
795}
796
797#[derive(Debug, Clone, Default)]
799pub struct FeatureFlagsOptions {
800 pub analysis: AnalysisOptions,
802 pub top: Option<usize>,
805 pub retirement: Option<FeatureFlagsRetirementOptions>,
807}
808
809#[derive(Debug, Clone, Default)]
812pub struct FeatureFlagsRetirementOptions {
813 pub flag_age: fallow_types::flag_retirement::FlagAgeMode,
815 pub flag_state: Option<std::path::PathBuf>,
817 pub reasons: Vec<fallow_types::flag_retirement::RetirementReason>,
819 pub sort: fallow_engine::flag_retirement::RetirementSort,
821 pub min_age_days: Option<u64>,
823 pub max_flag_age: Option<u64>,
825}
826
827#[derive(Debug, Clone, Copy, Default)]
829pub enum DuplicationMode {
830 Strict,
833 #[default]
835 Mild,
836 Weak,
838 Semantic,
841}
842
843#[derive(Debug, Clone, Default)]
845pub struct DuplicationOptions {
846 pub analysis: AnalysisOptions,
848 pub mode: Option<DuplicationMode>,
850 pub near: Option<bool>,
853 pub min_tokens: Option<usize>,
855 pub min_lines: Option<usize>,
857 pub min_occurrences: Option<usize>,
860 pub threshold: Option<f64>,
863 pub skip_local: Option<bool>,
866 pub ignore_symlinks: Option<bool>,
869 pub cross_language: Option<bool>,
871 pub ignore_imports: Option<bool>,
874 pub top: Option<usize>,
876 pub include_fragments: Option<bool>,
880}
881
882#[derive(Debug, Clone, Default)]
884pub struct SimilarCodeOptions {
885 pub analysis: AnalysisOptions,
887 pub threshold: Option<f64>,
889 pub min_lines: Option<usize>,
892 pub top: Option<usize>,
894 pub files: Vec<PathBuf>,
898 #[doc(hidden)]
903 pub adapter_provider_path: Option<PathBuf>,
904}
905
906#[derive(Debug, Clone)]
908pub struct SimilarCodeInspectOptions {
909 pub analysis: AnalysisOptions,
912 pub snapshot: fallow_output::SimilarCodeCandidateSnapshot,
914}
915
916#[derive(Debug, Clone, Default)]
918pub struct TraceExportOptions {
919 pub analysis: AnalysisOptions,
921 pub file: String,
923 pub export_name: String,
925}
926
927#[derive(Debug, Clone, Default)]
929pub struct TraceFileOptions {
930 pub analysis: AnalysisOptions,
932 pub file: String,
934}
935
936#[derive(Debug, Clone, Default)]
938pub struct TraceImportPathOptions {
939 pub analysis: AnalysisOptions,
941 pub from: String,
943 pub to: String,
945 pub eager_only: bool,
948}
949
950#[derive(Debug, Clone, Default)]
952pub struct TraceErrorOptions {
953 pub analysis: AnalysisOptions,
955 pub trace: String,
957 pub source: String,
959}
960
961#[derive(Debug, Clone, Default)]
963pub struct TraceDependencyOptions {
964 pub analysis: AnalysisOptions,
966 pub package_name: String,
968 pub usage: Option<DependencyUsageQuery>,
971}
972
973#[derive(Debug, Clone, PartialEq, Eq)]
975pub enum TraceCloneTarget {
976 Location {
978 file: String,
980 line: usize,
982 },
983 Fingerprint(String),
985}
986
987#[derive(Debug, Clone)]
989pub struct TraceCloneOptions {
990 pub duplication: DuplicationOptions,
992 pub target: TraceCloneTarget,
994}
995
996#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
998pub enum ComplexitySort {
999 #[default]
1001 Cyclomatic,
1002 Cognitive,
1004 Lines,
1006 Severity,
1008}
1009
1010#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1012pub enum OwnershipEmailMode {
1013 Raw,
1015 #[default]
1017 Handle,
1018 Anonymized,
1020 Hash,
1022}
1023
1024#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1026pub enum TargetEffort {
1027 Low,
1029 Medium,
1031 High,
1033}
1034
1035#[derive(Debug, Clone, Default)]
1037pub struct ComplexityOptions {
1038 pub analysis: AnalysisOptions,
1040 pub max_cyclomatic: Option<u16>,
1042 pub max_cognitive: Option<u16>,
1044 pub max_crap: Option<f64>,
1046 pub top: Option<usize>,
1048 pub sort: ComplexitySort,
1050 pub complexity_breakdown: bool,
1052 pub complexity: bool,
1054 pub file_scores: bool,
1056 pub coverage_gaps: bool,
1058 pub hotspots: bool,
1060 pub ownership: bool,
1062 pub ownership_emails: Option<OwnershipEmailMode>,
1064 pub targets: bool,
1066 pub css: bool,
1068 pub css_deep: bool,
1070 pub effort: Option<TargetEffort>,
1073 pub score: bool,
1075 pub since: Option<String>,
1077 pub min_commits: Option<u32>,
1079 pub coverage: Option<PathBuf>,
1081 pub coverage_root: Option<PathBuf>,
1083 pub coverage_relocated: bool,
1088}
1089
1090#[derive(Debug, Clone, Copy, Default, PartialEq)]
1092pub struct ComplexityThresholdOverrides {
1093 pub max_cyclomatic: Option<u16>,
1095 pub max_cognitive: Option<u16>,
1097 pub max_crap: Option<f64>,
1099}
1100
1101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1103pub struct ComplexityCoverageInputs<'a> {
1104 pub coverage: Option<&'a Path>,
1106 pub coverage_root: Option<&'a Path>,
1108 pub coverage_relocated: bool,
1111}
1112
1113#[derive(Debug, Clone)]
1115pub struct HealthSectionOptions {
1116 pub output: fallow_types::output_format::OutputFormat,
1118 pub complexity: bool,
1120 pub file_scores: bool,
1122 pub coverage_gaps: bool,
1124 pub hotspots: bool,
1126 pub targets: bool,
1128 pub css: bool,
1130 pub score: bool,
1132 pub score_gate: bool,
1134 pub snapshot_requested: bool,
1136 pub trend: bool,
1138}
1139
1140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1142pub struct DerivedHealthSections {
1143 pub any_section: bool,
1145 pub complexity: bool,
1147 pub file_scores: bool,
1149 pub coverage_gaps: bool,
1151 pub hotspots: bool,
1153 pub targets: bool,
1155 pub css: bool,
1157 pub score: bool,
1159 pub force_full: bool,
1161 pub score_only_output: bool,
1163}
1164
1165#[derive(Debug, Clone)]
1167pub struct ComplexitySectionOptions {
1168 pub complexity: bool,
1170 pub file_scores: bool,
1172 pub coverage_gaps: bool,
1174 pub hotspots: bool,
1176 pub ownership: bool,
1178 pub targets: bool,
1180 pub css: bool,
1182 pub score: bool,
1184}
1185
1186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1188pub struct DerivedComplexityOptions {
1189 pub any_section: bool,
1191 pub complexity: bool,
1193 pub file_scores: bool,
1195 pub coverage_gaps: bool,
1197 pub hotspots: bool,
1199 pub ownership: bool,
1201 pub targets: bool,
1203 pub force_full: bool,
1205 pub score_only_output: bool,
1207 pub score: bool,
1209}
1210
1211#[derive(Debug, Clone, PartialEq)]
1213pub struct ComplexityRunOptions<'a> {
1214 pub thresholds: ComplexityThresholdOverrides,
1216 pub top: Option<usize>,
1218 pub sort: ComplexitySort,
1220 pub complexity_breakdown: bool,
1222 pub sections: DerivedComplexityOptions,
1224 pub ownership_emails: Option<OwnershipEmailMode>,
1226 pub effort: Option<TargetEffort>,
1228 pub css: bool,
1230 pub css_deep: bool,
1232 pub since: Option<&'a str>,
1234 pub min_commits: Option<u32>,
1236 pub coverage_inputs: ComplexityCoverageInputs<'a>,
1238}
1239
1240#[must_use]
1242pub fn derive_health_sections(options: &HealthSectionOptions) -> DerivedHealthSections {
1243 let score = options.score
1244 || options.score_gate
1245 || options.trend
1246 || matches!(
1247 options.output,
1248 fallow_types::output_format::OutputFormat::Badge
1249 );
1250 let any_section = options.complexity
1251 || options.file_scores
1252 || options.coverage_gaps
1253 || options.hotspots
1254 || options.targets
1255 || score;
1256 let effective_score = if any_section { score } else { true } || options.snapshot_requested;
1257 let force_full = options.snapshot_requested || effective_score;
1258
1259 DerivedHealthSections {
1260 any_section,
1261 complexity: if any_section {
1262 options.complexity
1263 } else {
1264 true
1265 },
1266 file_scores: if any_section {
1267 options.file_scores
1268 } else {
1269 true
1270 } || force_full,
1271 coverage_gaps: if any_section {
1272 options.coverage_gaps
1273 } else {
1274 false
1275 },
1276 hotspots: if any_section { options.hotspots } else { true }
1277 || options.snapshot_requested
1278 || options.trend,
1279 targets: if any_section { options.targets } else { true },
1280 css: options.css,
1281 score: effective_score,
1282 force_full,
1283 score_only_output: is_health_score_only_output(options, score),
1284 }
1285}
1286
1287#[must_use]
1289pub fn derive_complexity_sections(options: &ComplexitySectionOptions) -> DerivedComplexityOptions {
1290 let requested_hotspots = options.hotspots || options.ownership;
1291 let sections = derive_health_sections(&HealthSectionOptions {
1292 output: fallow_types::output_format::OutputFormat::Human,
1293 complexity: options.complexity,
1294 file_scores: options.file_scores,
1295 coverage_gaps: options.coverage_gaps,
1296 hotspots: requested_hotspots,
1297 targets: options.targets,
1298 css: options.css,
1299 score: options.score,
1300 score_gate: false,
1301 snapshot_requested: false,
1302 trend: false,
1303 });
1304
1305 DerivedComplexityOptions {
1306 any_section: sections.any_section,
1307 complexity: sections.complexity,
1308 file_scores: sections.file_scores,
1309 coverage_gaps: sections.coverage_gaps,
1310 hotspots: sections.hotspots,
1311 ownership: options.ownership && sections.hotspots,
1312 targets: sections.targets,
1313 force_full: sections.force_full,
1314 score_only_output: sections.score_only_output,
1315 score: sections.score,
1316 }
1317}
1318
1319#[must_use]
1321pub fn derive_complexity_options(options: &ComplexityOptions) -> DerivedComplexityOptions {
1322 derive_complexity_sections(&complexity_section_options(options))
1323}
1324
1325#[must_use]
1327pub fn derive_complexity_run_options(options: &ComplexityOptions) -> ComplexityRunOptions<'_> {
1328 ComplexityRunOptions {
1329 thresholds: ComplexityThresholdOverrides {
1330 max_cyclomatic: options.max_cyclomatic,
1331 max_cognitive: options.max_cognitive,
1332 max_crap: options.max_crap,
1333 },
1334 top: options.top,
1335 sort: options.sort,
1336 complexity_breakdown: options.complexity_breakdown,
1337 sections: derive_complexity_options(options),
1338 ownership_emails: options.ownership_emails,
1339 effort: options.effort,
1340 css: options.css,
1341 css_deep: options.css_deep,
1342 since: options.since.as_deref(),
1343 min_commits: options.min_commits,
1344 coverage_inputs: ComplexityCoverageInputs {
1345 coverage: options.coverage.as_deref(),
1346 coverage_root: options.coverage_root.as_deref(),
1347 coverage_relocated: options.coverage_relocated,
1348 },
1349 }
1350}
1351
1352pub fn validate_complexity_options(options: &ComplexityOptions) -> Result<(), ProgrammaticError> {
1375 if let Some(path) = &options.coverage {
1376 let resolved = fallow_engine::health::scoring::resolve_relative_to_root(
1377 path,
1378 options.analysis.root.as_deref(),
1379 );
1380 if !resolved.exists() {
1381 return Err(ProgrammaticError::new(
1382 format!("coverage path does not exist: {}", resolved.display()),
1383 2,
1384 )
1385 .with_code("FALLOW_INVALID_COVERAGE_PATH")
1386 .with_context("health.coverage"));
1387 }
1388 }
1389 if let Err(message) =
1390 fallow_engine::health::validate_coverage_root_absolute(options.coverage_root.as_deref())
1391 {
1392 return Err(ProgrammaticError::new(message, 2)
1393 .with_code("FALLOW_INVALID_COVERAGE_ROOT")
1394 .with_context("health.coverage_root"));
1395 }
1396
1397 Ok(())
1398}
1399
1400fn complexity_section_options(options: &ComplexityOptions) -> ComplexitySectionOptions {
1401 let ownership = options.ownership || options.ownership_emails.is_some();
1402 let requested_targets = options.targets || options.effort.is_some();
1403 ComplexitySectionOptions {
1404 complexity: options.complexity,
1405 file_scores: options.file_scores,
1406 coverage_gaps: options.coverage_gaps,
1407 hotspots: options.hotspots,
1408 ownership,
1409 targets: requested_targets,
1410 css: options.css,
1411 score: options.score,
1412 }
1413}
1414
1415fn is_health_score_only_output(options: &HealthSectionOptions, score: bool) -> bool {
1416 score
1417 && !options.complexity
1418 && !options.file_scores
1419 && !options.coverage_gaps
1420 && !options.hotspots
1421 && !options.targets
1422 && !options.trend
1423}
1424
1425const fn thresholds_to_engine(
1426 thresholds: ComplexityThresholdOverrides,
1427) -> fallow_engine::health::HealthThresholdOverrides {
1428 fallow_engine::health::HealthThresholdOverrides {
1429 max_cyclomatic: thresholds.max_cyclomatic,
1430 max_cognitive: thresholds.max_cognitive,
1431 max_crap: thresholds.max_crap,
1432 }
1433}
1434
1435const fn complexity_sort_to_engine(sort: ComplexitySort) -> fallow_engine::health::HealthSort {
1436 match sort {
1437 ComplexitySort::Severity => fallow_engine::health::HealthSort::Severity,
1438 ComplexitySort::Cyclomatic => fallow_engine::health::HealthSort::Cyclomatic,
1439 ComplexitySort::Cognitive => fallow_engine::health::HealthSort::Cognitive,
1440 ComplexitySort::Lines => fallow_engine::health::HealthSort::Lines,
1441 }
1442}
1443
1444const fn coverage_inputs_to_engine(
1445 coverage_inputs: ComplexityCoverageInputs<'_>,
1446) -> fallow_engine::health::HealthCoverageInputs<'_> {
1447 fallow_engine::health::HealthCoverageInputs {
1448 coverage: coverage_inputs.coverage,
1449 coverage_root: coverage_inputs.coverage_root,
1450 coverage_relocated: coverage_inputs.coverage_relocated,
1451 }
1452}
1453
1454const fn ownership_email_mode_to_config(mode: OwnershipEmailMode) -> EmailMode {
1455 match mode {
1456 OwnershipEmailMode::Raw => EmailMode::Raw,
1457 OwnershipEmailMode::Handle => EmailMode::Handle,
1458 OwnershipEmailMode::Anonymized => EmailMode::Anonymized,
1459 OwnershipEmailMode::Hash => EmailMode::Hash,
1460 }
1461}
1462
1463const fn target_effort_to_output(effort: TargetEffort) -> EffortEstimate {
1464 match effort {
1465 TargetEffort::Low => EffortEstimate::Low,
1466 TargetEffort::Medium => EffortEstimate::Medium,
1467 TargetEffort::High => EffortEstimate::High,
1468 }
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use super::*;
1474
1475 #[test]
1476 fn duplication_defaults_match_cli_contract() {
1477 let options = DuplicationOptions::default();
1478 assert!(options.mode.is_none());
1479 assert!(options.min_tokens.is_none());
1480 assert!(options.min_lines.is_none());
1481 assert!(options.min_occurrences.is_none());
1482 }
1483
1484 #[test]
1485 fn programmatic_error_builder_keeps_optional_fields() {
1486 let error = ProgrammaticError::new("boom", 2)
1487 .with_code("FALLOW_TEST")
1488 .with_help("Try again")
1489 .with_context("analysis.root");
1490
1491 assert_eq!(error.message, "boom");
1492 assert_eq!(error.exit_code, 2);
1493 assert_eq!(error.code.as_deref(), Some("FALLOW_TEST"));
1494 assert_eq!(error.help.as_deref(), Some("Try again"));
1495 assert_eq!(error.context.as_deref(), Some("analysis.root"));
1496 }
1497
1498 #[test]
1499 fn dead_code_filters_accept_shared_registry_selectors() {
1500 for (selector, _) in fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS.iter() {
1501 let mut filters = DeadCodeFilters::default();
1502 assert!(
1503 filters.enable_registry_selector(selector),
1504 "{selector} should be accepted"
1505 );
1506 }
1507
1508 let mut filters = DeadCodeFilters::default();
1509 assert!(filters.enable_registry_selector("unused-files"));
1510 assert!(filters.unused_files);
1511 assert!(filters.enable_registry_selector("boundary-violations"));
1512 assert!(filters.boundary_violations);
1513 assert!(!filters.enable_registry_selector("not-a-real-selector"));
1514 }
1515
1516 #[test]
1517 fn default_complexity_options_match_programmatic_health_defaults() {
1518 let derived = derive_complexity_options(&ComplexityOptions::default());
1519
1520 assert!(!derived.any_section);
1521 assert!(derived.complexity);
1522 assert!(derived.file_scores);
1523 assert!(!derived.coverage_gaps);
1524 assert!(derived.hotspots);
1525 assert!(!derived.ownership);
1526 assert!(derived.targets);
1527 assert!(derived.force_full);
1528 assert!(!derived.score_only_output);
1529 assert!(derived.score);
1530 }
1531
1532 #[test]
1533 fn score_only_complexity_options_request_score_only_output() {
1534 let derived = derive_complexity_options(&ComplexityOptions {
1535 score: true,
1536 ..ComplexityOptions::default()
1537 });
1538
1539 assert!(derived.any_section);
1540 assert!(!derived.complexity);
1541 assert!(derived.file_scores);
1542 assert!(!derived.hotspots);
1543 assert!(!derived.targets);
1544 assert!(derived.force_full);
1545 assert!(derived.score_only_output);
1546 assert!(derived.score);
1547 }
1548
1549 #[test]
1550 fn ownership_implies_hotspots_when_requested() {
1551 let derived = derive_complexity_options(&ComplexityOptions {
1552 ownership: true,
1553 ..ComplexityOptions::default()
1554 });
1555
1556 assert!(derived.any_section);
1557 assert!(derived.hotspots);
1558 assert!(derived.ownership);
1559 assert!(!derived.targets);
1560 }
1561
1562 #[test]
1563 fn complexity_run_options_normalize_public_api_options() {
1564 let options = ComplexityOptions {
1565 max_cyclomatic: Some(42),
1566 max_cognitive: Some(21),
1567 max_crap: Some(18.5),
1568 top: Some(7),
1569 sort: ComplexitySort::Severity,
1570 complexity_breakdown: true,
1571 ownership_emails: Some(OwnershipEmailMode::Hash),
1572 effort: Some(TargetEffort::High),
1573 coverage: Some(PathBuf::from("coverage/coverage-final.json")),
1574 coverage_root: Some(PathBuf::from("/ci/workspace")),
1575 since: Some("30d".to_string()),
1576 min_commits: Some(4),
1577 ..ComplexityOptions::default()
1578 };
1579
1580 let run = derive_complexity_run_options(&options);
1581
1582 assert_eq!(run.thresholds.max_cyclomatic, Some(42));
1583 assert_eq!(run.thresholds.max_cognitive, Some(21));
1584 assert_eq!(run.thresholds.max_crap, Some(18.5));
1585 assert_eq!(run.top, Some(7));
1586 assert!(matches!(run.sort, ComplexitySort::Severity));
1587 assert!(run.complexity_breakdown);
1588 assert!(run.sections.hotspots);
1589 assert!(run.sections.ownership);
1590 assert!(run.sections.targets);
1591 assert!(matches!(
1592 run.ownership_emails,
1593 Some(OwnershipEmailMode::Hash)
1594 ));
1595 assert!(matches!(run.effort, Some(TargetEffort::High)));
1596 assert_eq!(run.since, Some("30d"));
1597 assert_eq!(run.min_commits, Some(4));
1598 assert_eq!(run.coverage_inputs.coverage, options.coverage.as_deref());
1599 assert_eq!(
1600 run.coverage_inputs.coverage_root,
1601 options.coverage_root.as_deref()
1602 );
1603 }
1604
1605 #[test]
1606 fn complexity_options_validation_accepts_existing_coverage_path_and_absolute_root() {
1607 let dir = tempfile::tempdir().expect("tempdir");
1608 let coverage = dir.path().join("coverage-final.json");
1609 std::fs::write(&coverage, "{}").expect("coverage fixture");
1610
1611 let result = validate_complexity_options(&ComplexityOptions {
1612 coverage: Some(coverage),
1613 coverage_root: Some(PathBuf::from("/ci/workspace")),
1614 ..ComplexityOptions::default()
1615 });
1616
1617 assert!(result.is_ok());
1618 }
1619
1620 #[test]
1621 fn complexity_options_validation_keeps_missing_coverage_error_contract() {
1622 let err = validate_complexity_options(&ComplexityOptions {
1623 coverage: Some(PathBuf::from("/missing/coverage-final.json")),
1624 ..ComplexityOptions::default()
1625 })
1626 .expect_err("missing coverage path should fail");
1627
1628 assert_eq!(err.exit_code, 2);
1629 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1630 assert_eq!(err.context.as_deref(), Some("health.coverage"));
1631 }
1632
1633 #[test]
1634 fn complexity_options_validation_keeps_relative_coverage_root_error_contract() {
1635 let err = validate_complexity_options(&ComplexityOptions {
1636 coverage_root: Some(PathBuf::from("coverage")),
1637 ..ComplexityOptions::default()
1638 })
1639 .expect_err("relative coverage root should fail");
1640
1641 assert_eq!(err.exit_code, 2);
1642 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_ROOT"));
1643 assert_eq!(err.context.as_deref(), Some("health.coverage_root"));
1644 }
1645
1646 #[test]
1650 fn complexity_options_validation_resolves_relative_coverage_against_root() {
1651 let dir = tempfile::tempdir().expect("tempdir");
1652 std::fs::create_dir_all(dir.path().join("artifacts")).expect("artifacts dir");
1653 std::fs::write(dir.path().join("artifacts/coverage-final.json"), "{}")
1654 .expect("coverage fixture");
1655 let relative = PathBuf::from("artifacts/coverage-final.json");
1656 assert!(
1657 !relative.exists(),
1658 "the fixture must not also exist under the test cwd"
1659 );
1660
1661 let result = validate_complexity_options(&ComplexityOptions {
1662 analysis: AnalysisOptions {
1663 root: Some(dir.path().to_path_buf()),
1664 ..AnalysisOptions::default()
1665 },
1666 coverage: Some(relative.clone()),
1667 ..ComplexityOptions::default()
1668 });
1669 assert!(result.is_ok(), "{result:?}");
1670
1671 let err = validate_complexity_options(&ComplexityOptions {
1672 analysis: AnalysisOptions {
1673 root: Some(dir.path().join("elsewhere")),
1674 ..AnalysisOptions::default()
1675 },
1676 coverage: Some(relative),
1677 ..ComplexityOptions::default()
1678 })
1679 .expect_err("the path does not exist under the other root");
1680 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1681 assert!(
1682 err.message.contains("elsewhere"),
1683 "the message names the resolved path: {}",
1684 err.message
1685 );
1686 }
1687
1688 #[test]
1689 fn default_health_sections_match_full_health_output() {
1690 let derived = derive_health_sections(&HealthSectionOptions {
1691 output: fallow_types::output_format::OutputFormat::Human,
1692 complexity: false,
1693 file_scores: false,
1694 coverage_gaps: false,
1695 hotspots: false,
1696 targets: false,
1697 css: false,
1698 score: false,
1699 score_gate: false,
1700 snapshot_requested: false,
1701 trend: false,
1702 });
1703
1704 assert!(!derived.any_section);
1705 assert!(derived.complexity);
1706 assert!(derived.file_scores);
1707 assert!(!derived.coverage_gaps);
1708 assert!(derived.hotspots);
1709 assert!(derived.targets);
1710 assert!(derived.score);
1711 assert!(derived.force_full);
1712 assert!(!derived.score_only_output);
1713 }
1714
1715 #[test]
1716 fn health_score_gate_requests_score_only_output() {
1717 let derived = derive_health_sections(&HealthSectionOptions {
1718 output: fallow_types::output_format::OutputFormat::Human,
1719 complexity: false,
1720 file_scores: false,
1721 coverage_gaps: false,
1722 hotspots: false,
1723 targets: false,
1724 css: false,
1725 score: false,
1726 score_gate: true,
1727 snapshot_requested: false,
1728 trend: false,
1729 });
1730
1731 assert!(derived.any_section);
1732 assert!(!derived.complexity);
1733 assert!(derived.file_scores);
1734 assert!(!derived.hotspots);
1735 assert!(!derived.targets);
1736 assert!(derived.score);
1737 assert!(derived.force_full);
1738 assert!(derived.score_only_output);
1739 }
1740
1741 #[test]
1742 fn health_snapshot_keeps_full_hidden_inputs_without_section_request() {
1743 let derived = derive_health_sections(&HealthSectionOptions {
1744 output: fallow_types::output_format::OutputFormat::Human,
1745 complexity: false,
1746 file_scores: false,
1747 coverage_gaps: false,
1748 hotspots: false,
1749 targets: false,
1750 css: true,
1751 score: false,
1752 score_gate: false,
1753 snapshot_requested: true,
1754 trend: false,
1755 });
1756
1757 assert!(!derived.any_section);
1758 assert!(derived.css);
1759 assert!(derived.file_scores);
1760 assert!(derived.hotspots);
1761 assert!(derived.score);
1762 assert!(derived.force_full);
1763 }
1764}