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::thread_pool::{WORKER_STACK_SIZE, register_thread_stack};
136pub use fallow_engine::warm_parse;
142pub use fallow_output::PackageBaselineStatus;
144pub use fallow_output::serialize_similar_code_json_output;
145pub use fallow_types::trace::{
146 CloneTrace, DependencyTrace, ExportReference, ExportTrace, FileTrace, ReExportChain,
147 TracedCloneGroup, TracedExport, TracedReExport,
148};
149pub use fallow_types::trace_usage::{
150 DependencyUsage, DependencyUsageQuery, SitePageRequest, UsageQueryError,
151};
152pub use grouped_output::{
153 ResultGroup, UNOWNED_GROUP_LABEL, build_duplication_grouping_with, group_analysis_results_with,
154 health_signal_header_part, largest_clone_group_owner_with,
155};
156pub use health_codeclimate::build_health_codeclimate;
157pub use health_groups_markdown::build_health_groups_markdown;
158pub use json_output::{
159 CheckJsonExtraOutputs, CheckJsonOutputInput, CheckJsonPayloadInput, DuplicationJsonOutputInput,
160 GroupedCheckJsonOutputInput, GroupedDuplicationJsonOutputInput, serialize_check_json,
161 serialize_check_json_payload, serialize_duplication_json, serialize_grouped_check_json,
162 serialize_grouped_duplication_json,
163};
164pub use list_output::{
165 ListJsonEnvelope, ListJsonOutputInput, build_list_json_output, serialize_list_json_output,
166};
167pub use list_runtime::{
168 BoundaryData, ListBoundariesOptions, ListBoundariesProgrammaticOutput, LogicalGroupInfo,
169 ProjectInfoOptions, ProjectInfoProgrammaticOutput, RuleInfo, ZoneInfo, boundary_data_to_output,
170 compute_boundary_data, run_list_boundaries, run_project_info,
171 serialize_list_boundaries_programmatic_json, serialize_project_info_programmatic_json,
172};
173pub use markdown_output::{
174 ComplexityListing, build_diff_risk_markdown, build_duplication_markdown,
175 build_grouped_markdown, build_health_markdown, build_markdown, build_walkthrough_markdown,
176};
177pub use mutation::{
178 MutationProgress, MutationProgressSink, MutationRunOptions, MutationRunScope,
179 apply_mutation_verdicts, validate_mutation_verdicts,
180};
181pub use output_contracts::{
182 AuditOutput, BoundariesListLogicalGroup, BoundariesListRule, BoundariesListZone,
183 BoundariesListing, CombinedOutput, FallowOutput, ImpactOutput, ListBoundariesOutput,
184 ListEntryPointOutput, ListOutput, ListPluginOutput, ReviewBriefWireOutput, SecurityGate,
185 SecurityOutput, SecurityOutputConfig, SecuritySummaryOutput, SimilarCodeCandidateSnapshot,
186 SimilarCodeOutput, TraceOutput, WorkspacesOutput,
187};
188pub use runtime::TYPE_AWARE_CONFIG_UNSUPPORTED;
189pub use runtime::{
190 AUDIT_CONFIG_BASELINE_UNSUPPORTED, ArchitectureOutput, ArchitectureProgrammaticOutput,
191 AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
192 BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
193 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
194 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
195 DuplicationProgrammaticOutput, EngineHealthRunner, FeatureFlagsOutput,
196 FeatureFlagsProgrammaticOutput, HealthJsonReportInput, HealthProgrammaticOutput,
197 ProgrammaticHealthAnalysis, ProgrammaticHealthNextStepFacts, ProgrammaticHealthRun,
198 ProgrammaticHealthRunner, TraceClassMemberOutput, TraceCloneBenchmarkResult, TraceCloneOutput,
199 TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
200 TraceErrorOutput, TraceErrorProgrammaticOutput, TraceExportOutput,
201 TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOutput,
202 TraceFileProgrammaticOutput, TraceImportPathOutput, TraceImportPathProgrammaticOutput,
203 benchmark_trace_clone_compact_json, benchmark_trace_graph_family_compact_json,
204 inspect_similar_code, load_health_config, parse_similar_code_candidate_snapshot, plan_mutation,
205 review_similar_code, run_architecture, run_audit, run_boundary_violations,
206 run_circular_dependencies, run_combined, run_complexity_with_runner, run_dead_code,
207 run_dead_code_with_baseline, run_decision_surface, run_duplication, run_feature_flags,
208 run_health, run_health_with_runner, run_mutation, run_mutation_with_progress, run_similar_code,
209 run_trace_clone, run_trace_dependency, run_trace_error, run_trace_export, run_trace_file,
210 run_trace_import_path, select_similar_code_candidate_snapshot, serialize_health_report_json,
211};
212pub use runtime_json::{
213 serialize_architecture_programmatic_json, serialize_audit_programmatic_json,
214 serialize_boundary_violations_programmatic_json,
215 serialize_circular_dependencies_programmatic_json, serialize_combined_programmatic_json,
216 serialize_dead_code_programmatic_json, serialize_decision_surface_programmatic_json,
217 serialize_duplication_programmatic_json, serialize_feature_flags_programmatic_json,
218 serialize_health_programmatic_json, serialize_trace_clone_programmatic_json,
219 serialize_trace_dependency_programmatic_json, serialize_trace_error_programmatic_json,
220 serialize_trace_export_programmatic_json, serialize_trace_file_programmatic_json,
221 serialize_trace_import_path_programmatic_json,
222};
223pub use sarif_output::{
224 annotate_sarif_results, build_duplication_sarif, build_grouped_duplication_sarif,
225 build_health_sarif,
226};
227pub use security_output::SecurityGateMode;
228pub use type_aware::{
229 SemanticCouplingOutcome, SemanticDeadCodeOutcome, SemanticInspectOutcome, TypeAwareError,
230 TypeAwareFileChanges, TypeAwareOutcome, TypeAwareSession, TypeAwareStatus,
231 discard_unverified_semantic_candidates, inspect_symbol as inspect_type_aware_symbol,
232 merge_type_aware_meta,
233 refine_configured_dead_code_results as refine_type_aware_results_with_config,
234 shutdown_type_aware_sidecars, status as type_aware_status,
235 symbol_impact as run_type_aware_symbol_impact, symbol_impact as type_aware_symbol_impact,
236 terminate_active_type_aware_sidecars, trace_symbol as run_type_aware_symbol_trace,
237 trace_symbol as trace_type_aware_symbol, type_coupling as analyze_type_coupling,
238};
239
240pub const COMMON_ANALYSIS_OPTION_FLAGS: &[&str] = &[
247 "root",
248 "config",
249 "no-cache",
250 "threads",
251 "changed-since",
252 "diff-file",
253 "production",
254 "workspace",
255 "changed-workspaces",
256 "explain",
257 "allow-remote-extends",
258];
259
260#[derive(Debug, Clone, Serialize)]
262pub struct ProgrammaticError {
263 pub message: String,
265 pub exit_code: u8,
267 pub code: Option<String>,
269 pub help: Option<String>,
271 pub context: Option<String>,
273}
274
275impl ProgrammaticError {
276 #[must_use]
279 pub fn new(message: impl Into<String>, exit_code: u8) -> Self {
280 Self {
281 message: message.into(),
282 exit_code,
283 code: None,
284 help: None,
285 context: None,
286 }
287 }
288
289 #[must_use]
291 pub fn with_help(mut self, help: impl Into<String>) -> Self {
292 self.help = Some(help.into());
293 self
294 }
295
296 #[must_use]
299 pub fn with_code(mut self, code: impl Into<String>) -> Self {
300 self.code = Some(code.into());
301 self
302 }
303
304 #[must_use]
307 pub fn with_context(mut self, context: impl Into<String>) -> Self {
308 self.context = Some(context.into());
309 self
310 }
311}
312
313impl std::fmt::Display for ProgrammaticError {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 write!(f, "{}", self.message)
316 }
317}
318
319impl std::error::Error for ProgrammaticError {}
320
321#[derive(Debug, Clone, Default)]
323pub struct AnalysisOptions {
324 pub root: Option<PathBuf>,
327 pub config_path: Option<PathBuf>,
329 pub allow_remote_extends: bool,
331 pub no_cache: bool,
333 pub threads: Option<usize>,
335 pub diff_file: Option<PathBuf>,
347 pub ambient_diff_file: Option<PathBuf>,
355 pub production: bool,
358 pub production_override: Option<bool>,
361 pub changed_since: Option<String>,
366 pub ambient_changed_since: Option<String>,
374 pub no_package_baselines: bool,
378 pub workspace: Option<Vec<String>>,
380 pub changed_workspaces: Option<String>,
382 pub explain: bool,
384 pub type_aware: TypeAwareOptions,
387 pub cancellation: Option<Arc<AtomicBool>>,
417}
418
419#[derive(Debug, Clone, Default, PartialEq, Eq)]
421pub struct TypeAwareOptions {
422 pub enabled: Option<bool>,
429 pub projects: Vec<PathBuf>,
432 pub require: fallow_config::TypeAwareRequire,
434}
435
436impl TypeAwareOptions {
437 #[must_use]
439 pub const fn requested(&self) -> bool {
440 matches!(self.enabled, Some(true))
441 }
442}
443
444#[derive(Debug, Clone, Default)]
449pub struct DeadCodeFilters {
450 pub unused_files: bool,
452 pub unused_exports: bool,
454 pub unused_deps: bool,
456 pub unused_types: bool,
458 pub private_type_leaks: bool,
460 pub deprecated_exports_in_use: bool,
462 pub unused_enum_members: bool,
464 pub unused_class_members: bool,
466 pub unused_store_members: bool,
468 pub unprovided_injects: bool,
470 pub unrendered_components: bool,
472 pub unused_component_props: bool,
474 pub absent_component_props: bool,
476 pub unused_component_emits: bool,
478 pub unused_component_inputs: bool,
480 pub unused_component_outputs: bool,
482 pub unused_svelte_events: bool,
484 pub unused_effect_layers: bool,
486 pub unused_tagged_errors: bool,
488 pub unproduced_catch_tags: bool,
490 pub unused_server_actions: bool,
492 pub unused_load_data_keys: bool,
494 pub unresolved_imports: bool,
496 pub unlisted_deps: bool,
498 pub duplicate_exports: bool,
500 pub circular_deps: bool,
502 pub re_export_cycles: bool,
504 pub package_cycles: bool,
506 pub boundary_violations: bool,
508 pub policy_violations: bool,
510 pub stale_suppressions: bool,
512 pub unused_catalog_entries: bool,
514 pub empty_catalog_groups: bool,
516 pub unresolved_catalog_references: bool,
518 pub unused_dependency_overrides: bool,
520 pub misconfigured_dependency_overrides: bool,
522}
523
524impl DeadCodeFilters {
525 pub(crate) fn reports_dependency_findings(&self) -> bool {
529 !self.any_active() || self.unused_deps || self.unlisted_deps
530 }
531
532 fn any_active(&self) -> bool {
533 self.unused_files
534 || self.unused_exports
535 || self.unused_deps
536 || self.unused_types
537 || self.private_type_leaks
538 || self.deprecated_exports_in_use
539 || self.unused_enum_members
540 || self.unused_class_members
541 || self.unused_store_members
542 || self.unprovided_injects
543 || self.unrendered_components
544 || self.unused_component_props
545 || self.absent_component_props
546 || self.unused_component_emits
547 || self.unused_component_inputs
548 || self.unused_component_outputs
549 || self.unused_svelte_events
550 || self.unused_effect_layers
551 || self.unused_tagged_errors
552 || self.unproduced_catch_tags
553 || self.unused_server_actions
554 || self.unused_load_data_keys
555 || self.unresolved_imports
556 || self.unlisted_deps
557 || self.duplicate_exports
558 || self.circular_deps
559 || self.re_export_cycles
560 || self.package_cycles
561 || self.boundary_violations
562 || self.policy_violations
563 || self.stale_suppressions
564 || self.unused_catalog_entries
565 || self.empty_catalog_groups
566 || self.unresolved_catalog_references
567 || self.unused_dependency_overrides
568 || self.misconfigured_dependency_overrides
569 }
570
571 pub fn enable_registry_selector(&mut self, selector: &str) -> bool {
577 let Some(flag) = fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS
578 .iter()
579 .find_map(|&(name, flag)| (name == selector).then_some(flag))
580 else {
581 return false;
582 };
583 self.enable_cli_filter_flag(flag);
584 true
585 }
586
587 fn enable_cli_filter_flag(&mut self, flag: &str) {
588 match flag {
589 "--unused-files" => self.unused_files = true,
590 "--unused-exports" => self.unused_exports = true,
591 "--unused-types" => self.unused_types = true,
592 "--private-type-leaks" => self.private_type_leaks = true,
593 "--deprecated-exports-in-use" => self.deprecated_exports_in_use = true,
594 "--unused-deps" => self.unused_deps = true,
595 "--unused-enum-members" => self.unused_enum_members = true,
596 "--unused-class-members" => self.unused_class_members = true,
597 "--unused-store-members" => self.unused_store_members = true,
598 "--unprovided-injects" => self.unprovided_injects = true,
599 "--unrendered-components" => self.unrendered_components = true,
600 "--unused-component-props" => self.unused_component_props = true,
601 "--absent-component-props" => self.absent_component_props = true,
602 "--unused-component-emits" => self.unused_component_emits = true,
603 "--unused-component-inputs" => self.unused_component_inputs = true,
604 "--unused-component-outputs" => self.unused_component_outputs = true,
605 "--unused-svelte-events" => self.unused_svelte_events = true,
606 "--unused-effect-layers" => self.unused_effect_layers = true,
607 "--unused-tagged-errors" => self.unused_tagged_errors = true,
608 "--unproduced-catch-tags" => self.unproduced_catch_tags = true,
609 "--unused-server-actions" => self.unused_server_actions = true,
610 "--unused-load-data-keys" => self.unused_load_data_keys = true,
611 "--unresolved-imports" => self.unresolved_imports = true,
612 "--unlisted-deps" => self.unlisted_deps = true,
613 "--duplicate-exports" => self.duplicate_exports = true,
614 "--circular-deps" => self.circular_deps = true,
615 "--re-export-cycles" => self.re_export_cycles = true,
616 "--package-cycles" => self.package_cycles = true,
617 "--boundary-violations" => self.boundary_violations = true,
618 "--policy-violations" => self.policy_violations = true,
619 "--stale-suppressions" => self.stale_suppressions = true,
620 "--unused-catalog-entries" => self.unused_catalog_entries = true,
621 "--empty-catalog-groups" => self.empty_catalog_groups = true,
622 "--unresolved-catalog-references" => self.unresolved_catalog_references = true,
623 "--unused-dependency-overrides" => self.unused_dependency_overrides = true,
624 "--misconfigured-dependency-overrides" => {
625 self.misconfigured_dependency_overrides = true;
626 }
627 _ => unreachable!("registry emitted unsupported dead-code filter flag: {flag}"),
628 }
629 }
630}
631
632#[derive(Debug, Clone, Default)]
634pub struct DeadCodeOptions {
635 pub analysis: AnalysisOptions,
637 pub filters: DeadCodeFilters,
639 pub files: Vec<PathBuf>,
641 pub include_entry_exports: bool,
643 pub show_cascade: bool,
645 pub finding_ids: Vec<String>,
650}
651
652#[derive(Debug, Clone, Default)]
658pub struct ArchitectureOptions {
659 pub analysis: AnalysisOptions,
661 pub files: Vec<PathBuf>,
663 pub cycles: bool,
666 pub boundaries: bool,
668 pub policy: bool,
670}
671
672impl ArchitectureOptions {
673 #[must_use]
675 pub fn dead_code_filters(&self) -> DeadCodeFilters {
676 let all = !(self.cycles || self.boundaries || self.policy);
677 DeadCodeFilters {
678 circular_deps: self.cycles || all,
679 re_export_cycles: self.cycles || all,
680 package_cycles: self.cycles || all,
681 boundary_violations: self.boundaries || all,
682 policy_violations: self.policy || all,
683 ..DeadCodeFilters::default()
684 }
685 }
686
687 #[must_use]
689 pub fn dead_code_options(&self) -> DeadCodeOptions {
690 DeadCodeOptions {
691 analysis: self.analysis.clone(),
692 filters: self.dead_code_filters(),
693 files: self.files.clone(),
694 include_entry_exports: false,
695 show_cascade: false,
696 finding_ids: Vec::new(),
697 }
698 }
699}
700
701#[derive(Debug, Clone, Default)]
703pub struct AuditOptions {
704 pub analysis: AnalysisOptions,
706 pub base: Option<String>,
709 pub production: bool,
711 pub production_dead_code: Option<bool>,
713 pub production_health: Option<bool>,
715 pub production_dupes: Option<bool>,
718 pub dead_code: Option<bool>,
722 pub health: Option<bool>,
725 pub duplication: Option<bool>,
728 pub css: Option<bool>,
731 pub css_deep: Option<bool>,
734 pub gate: Option<fallow_config::AuditGate>,
737 pub worsened: Option<fallow_config::AuditWorsened>,
740 pub max_crap: Option<f64>,
742 pub coverage: Option<PathBuf>,
744 pub coverage_root: Option<PathBuf>,
746 pub include_entry_exports: bool,
748 pub show_cascade: bool,
750 pub runtime_coverage: Option<PathBuf>,
752 pub min_invocations_hot: u64,
754}
755
756#[derive(Debug, Clone)]
758pub struct CombinedOptions {
759 pub analysis: AnalysisOptions,
761 pub dead_code: bool,
763 pub duplication: bool,
765 pub health: bool,
767 pub include_entry_exports: bool,
769 pub show_cascade: bool,
771 pub duplication_options: DuplicationOptions,
773 pub health_options: ComplexityOptions,
775}
776
777impl Default for CombinedOptions {
778 fn default() -> Self {
779 Self {
780 analysis: AnalysisOptions::default(),
781 dead_code: true,
782 duplication: true,
783 health: true,
784 include_entry_exports: false,
785 show_cascade: false,
786 duplication_options: DuplicationOptions::default(),
787 health_options: ComplexityOptions::default(),
788 }
789 }
790}
791
792#[derive(Debug, Clone, Default)]
794pub struct DecisionSurfaceOptions {
795 pub analysis: AnalysisOptions,
797 pub base: Option<String>,
800 pub max_decisions: Option<usize>,
802}
803
804#[derive(Debug, Clone, Default)]
806pub struct FeatureFlagsOptions {
807 pub analysis: AnalysisOptions,
809 pub top: Option<usize>,
812 pub retirement: Option<FeatureFlagsRetirementOptions>,
814}
815
816#[derive(Debug, Clone, Default)]
819pub struct FeatureFlagsRetirementOptions {
820 pub flag_age: fallow_types::flag_retirement::FlagAgeMode,
822 pub flag_state: Option<std::path::PathBuf>,
824 pub reasons: Vec<fallow_types::flag_retirement::RetirementReason>,
826 pub sort: fallow_engine::flag_retirement::RetirementSort,
828 pub min_age_days: Option<u64>,
830 pub max_flag_age: Option<u64>,
832}
833
834#[derive(Debug, Clone, Copy, Default)]
836pub enum DuplicationMode {
837 Strict,
840 #[default]
842 Mild,
843 Weak,
845 Semantic,
848}
849
850#[derive(Debug, Clone, Default)]
852pub struct DuplicationOptions {
853 pub analysis: AnalysisOptions,
855 pub mode: Option<DuplicationMode>,
857 pub near: Option<bool>,
860 pub min_tokens: Option<usize>,
862 pub min_lines: Option<usize>,
864 pub min_occurrences: Option<usize>,
867 pub threshold: Option<f64>,
870 pub skip_local: Option<bool>,
873 pub ignore_symlinks: Option<bool>,
876 pub cross_language: Option<bool>,
878 pub ignore_imports: Option<bool>,
881 pub top: Option<usize>,
883 pub include_fragments: Option<bool>,
887}
888
889#[derive(Debug, Clone, Default)]
891pub struct SimilarCodeOptions {
892 pub analysis: AnalysisOptions,
894 pub threshold: Option<f64>,
896 pub min_lines: Option<usize>,
899 pub top: Option<usize>,
901 pub files: Vec<PathBuf>,
905 #[doc(hidden)]
910 pub adapter_provider_path: Option<PathBuf>,
911}
912
913#[derive(Debug, Clone)]
915pub struct SimilarCodeInspectOptions {
916 pub analysis: AnalysisOptions,
919 pub snapshot: fallow_output::SimilarCodeCandidateSnapshot,
921}
922
923#[derive(Debug, Clone, Default)]
925pub struct TraceExportOptions {
926 pub analysis: AnalysisOptions,
928 pub file: String,
930 pub export_name: String,
932}
933
934#[derive(Debug, Clone, Default)]
936pub struct TraceFileOptions {
937 pub analysis: AnalysisOptions,
939 pub file: String,
941}
942
943#[derive(Debug, Clone, Default)]
945pub struct TraceImportPathOptions {
946 pub analysis: AnalysisOptions,
948 pub from: String,
950 pub to: String,
952 pub eager_only: bool,
955}
956
957#[derive(Debug, Clone, Default)]
959pub struct TraceErrorOptions {
960 pub analysis: AnalysisOptions,
962 pub trace: String,
964 pub source: String,
966}
967
968#[derive(Debug, Clone, Default)]
970pub struct TraceDependencyOptions {
971 pub analysis: AnalysisOptions,
973 pub package_name: String,
975 pub usage: Option<DependencyUsageQuery>,
978}
979
980#[derive(Debug, Clone, PartialEq, Eq)]
982pub enum TraceCloneTarget {
983 Location {
985 file: String,
987 line: usize,
989 },
990 Fingerprint(String),
992}
993
994#[derive(Debug, Clone)]
996pub struct TraceCloneOptions {
997 pub duplication: DuplicationOptions,
999 pub target: TraceCloneTarget,
1001}
1002
1003#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1005pub enum ComplexitySort {
1006 #[default]
1008 Cyclomatic,
1009 Cognitive,
1011 Lines,
1013 Severity,
1015}
1016
1017#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1019pub enum OwnershipEmailMode {
1020 Raw,
1022 #[default]
1024 Handle,
1025 Anonymized,
1027 Hash,
1029}
1030
1031#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1033pub enum TargetEffort {
1034 Low,
1036 Medium,
1038 High,
1040}
1041
1042#[derive(Debug, Clone, Default)]
1044pub struct ComplexityOptions {
1045 pub analysis: AnalysisOptions,
1047 pub max_cyclomatic: Option<u16>,
1049 pub max_cognitive: Option<u16>,
1051 pub max_crap: Option<f64>,
1053 pub top: Option<usize>,
1055 pub sort: ComplexitySort,
1057 pub complexity_breakdown: bool,
1059 pub complexity: bool,
1061 pub file_scores: bool,
1063 pub coverage_gaps: bool,
1065 pub hotspots: bool,
1067 pub ownership: bool,
1069 pub ownership_emails: Option<OwnershipEmailMode>,
1071 pub targets: bool,
1073 pub css: bool,
1075 pub css_deep: bool,
1077 pub effort: Option<TargetEffort>,
1080 pub score: bool,
1082 pub since: Option<String>,
1084 pub min_commits: Option<u32>,
1086 pub coverage: Option<PathBuf>,
1088 pub coverage_root: Option<PathBuf>,
1090 pub coverage_relocated: bool,
1095}
1096
1097#[derive(Debug, Clone, Copy, Default, PartialEq)]
1099pub struct ComplexityThresholdOverrides {
1100 pub max_cyclomatic: Option<u16>,
1102 pub max_cognitive: Option<u16>,
1104 pub max_crap: Option<f64>,
1106}
1107
1108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1110pub struct ComplexityCoverageInputs<'a> {
1111 pub coverage: Option<&'a Path>,
1113 pub coverage_root: Option<&'a Path>,
1115 pub coverage_relocated: bool,
1118}
1119
1120#[derive(Debug, Clone)]
1122pub struct HealthSectionOptions {
1123 pub output: fallow_types::output_format::OutputFormat,
1125 pub complexity: bool,
1127 pub file_scores: bool,
1129 pub coverage_gaps: bool,
1131 pub hotspots: bool,
1133 pub targets: bool,
1135 pub css: bool,
1137 pub score: bool,
1139 pub score_gate: bool,
1141 pub snapshot_requested: bool,
1143 pub trend: bool,
1145}
1146
1147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1149pub struct DerivedHealthSections {
1150 pub any_section: bool,
1152 pub complexity: bool,
1154 pub file_scores: bool,
1156 pub coverage_gaps: bool,
1158 pub hotspots: bool,
1160 pub targets: bool,
1162 pub css: bool,
1164 pub score: bool,
1166 pub force_full: bool,
1168 pub score_only_output: bool,
1170}
1171
1172#[derive(Debug, Clone)]
1174pub struct ComplexitySectionOptions {
1175 pub complexity: bool,
1177 pub file_scores: bool,
1179 pub coverage_gaps: bool,
1181 pub hotspots: bool,
1183 pub ownership: bool,
1185 pub targets: bool,
1187 pub css: bool,
1189 pub score: bool,
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1195pub struct DerivedComplexityOptions {
1196 pub any_section: bool,
1198 pub complexity: bool,
1200 pub file_scores: bool,
1202 pub coverage_gaps: bool,
1204 pub hotspots: bool,
1206 pub ownership: bool,
1208 pub targets: bool,
1210 pub force_full: bool,
1212 pub score_only_output: bool,
1214 pub score: bool,
1216}
1217
1218#[derive(Debug, Clone, PartialEq)]
1220pub struct ComplexityRunOptions<'a> {
1221 pub thresholds: ComplexityThresholdOverrides,
1223 pub top: Option<usize>,
1225 pub sort: ComplexitySort,
1227 pub complexity_breakdown: bool,
1229 pub sections: DerivedComplexityOptions,
1231 pub ownership_emails: Option<OwnershipEmailMode>,
1233 pub effort: Option<TargetEffort>,
1235 pub css: bool,
1237 pub css_deep: bool,
1239 pub since: Option<&'a str>,
1241 pub min_commits: Option<u32>,
1243 pub coverage_inputs: ComplexityCoverageInputs<'a>,
1245}
1246
1247pub fn configure_global_analysis_pool(threads: usize) -> bool {
1256 fallow_engine::thread_pool::worker_pool_builder(threads)
1257 .build_global()
1258 .is_ok()
1259}
1260
1261#[must_use]
1263pub fn derive_health_sections(options: &HealthSectionOptions) -> DerivedHealthSections {
1264 let score = options.score
1265 || options.score_gate
1266 || options.trend
1267 || matches!(
1268 options.output,
1269 fallow_types::output_format::OutputFormat::Badge
1270 );
1271 let any_section = options.complexity
1272 || options.file_scores
1273 || options.coverage_gaps
1274 || options.hotspots
1275 || options.targets
1276 || score;
1277 let effective_score = if any_section { score } else { true } || options.snapshot_requested;
1278 let force_full = options.snapshot_requested || effective_score;
1279
1280 DerivedHealthSections {
1281 any_section,
1282 complexity: if any_section {
1283 options.complexity
1284 } else {
1285 true
1286 },
1287 file_scores: if any_section {
1288 options.file_scores
1289 } else {
1290 true
1291 } || force_full,
1292 coverage_gaps: if any_section {
1293 options.coverage_gaps
1294 } else {
1295 false
1296 },
1297 hotspots: if any_section { options.hotspots } else { true }
1298 || options.snapshot_requested
1299 || options.trend,
1300 targets: if any_section { options.targets } else { true },
1301 css: options.css,
1302 score: effective_score,
1303 force_full,
1304 score_only_output: is_health_score_only_output(options, score),
1305 }
1306}
1307
1308#[must_use]
1310pub fn derive_complexity_sections(options: &ComplexitySectionOptions) -> DerivedComplexityOptions {
1311 let requested_hotspots = options.hotspots || options.ownership;
1312 let sections = derive_health_sections(&HealthSectionOptions {
1313 output: fallow_types::output_format::OutputFormat::Human,
1314 complexity: options.complexity,
1315 file_scores: options.file_scores,
1316 coverage_gaps: options.coverage_gaps,
1317 hotspots: requested_hotspots,
1318 targets: options.targets,
1319 css: options.css,
1320 score: options.score,
1321 score_gate: false,
1322 snapshot_requested: false,
1323 trend: false,
1324 });
1325
1326 DerivedComplexityOptions {
1327 any_section: sections.any_section,
1328 complexity: sections.complexity,
1329 file_scores: sections.file_scores,
1330 coverage_gaps: sections.coverage_gaps,
1331 hotspots: sections.hotspots,
1332 ownership: options.ownership && sections.hotspots,
1333 targets: sections.targets,
1334 force_full: sections.force_full,
1335 score_only_output: sections.score_only_output,
1336 score: sections.score,
1337 }
1338}
1339
1340#[must_use]
1342pub fn derive_complexity_options(options: &ComplexityOptions) -> DerivedComplexityOptions {
1343 derive_complexity_sections(&complexity_section_options(options))
1344}
1345
1346#[must_use]
1348pub fn derive_complexity_run_options(options: &ComplexityOptions) -> ComplexityRunOptions<'_> {
1349 ComplexityRunOptions {
1350 thresholds: ComplexityThresholdOverrides {
1351 max_cyclomatic: options.max_cyclomatic,
1352 max_cognitive: options.max_cognitive,
1353 max_crap: options.max_crap,
1354 },
1355 top: options.top,
1356 sort: options.sort,
1357 complexity_breakdown: options.complexity_breakdown,
1358 sections: derive_complexity_options(options),
1359 ownership_emails: options.ownership_emails,
1360 effort: options.effort,
1361 css: options.css,
1362 css_deep: options.css_deep,
1363 since: options.since.as_deref(),
1364 min_commits: options.min_commits,
1365 coverage_inputs: ComplexityCoverageInputs {
1366 coverage: options.coverage.as_deref(),
1367 coverage_root: options.coverage_root.as_deref(),
1368 coverage_relocated: options.coverage_relocated,
1369 },
1370 }
1371}
1372
1373pub fn validate_complexity_options(options: &ComplexityOptions) -> Result<(), ProgrammaticError> {
1396 if let Some(path) = &options.coverage {
1397 let resolved = fallow_engine::health::scoring::resolve_relative_to_root(
1398 path,
1399 options.analysis.root.as_deref(),
1400 );
1401 if !resolved.exists() {
1402 return Err(ProgrammaticError::new(
1403 format!("coverage path does not exist: {}", resolved.display()),
1404 2,
1405 )
1406 .with_code("FALLOW_INVALID_COVERAGE_PATH")
1407 .with_context("health.coverage"));
1408 }
1409 }
1410 if let Err(message) =
1411 fallow_engine::health::validate_coverage_root_absolute(options.coverage_root.as_deref())
1412 {
1413 return Err(ProgrammaticError::new(message, 2)
1414 .with_code("FALLOW_INVALID_COVERAGE_ROOT")
1415 .with_context("health.coverage_root"));
1416 }
1417
1418 Ok(())
1419}
1420
1421fn complexity_section_options(options: &ComplexityOptions) -> ComplexitySectionOptions {
1422 let ownership = options.ownership || options.ownership_emails.is_some();
1423 let requested_targets = options.targets || options.effort.is_some();
1424 ComplexitySectionOptions {
1425 complexity: options.complexity,
1426 file_scores: options.file_scores,
1427 coverage_gaps: options.coverage_gaps,
1428 hotspots: options.hotspots,
1429 ownership,
1430 targets: requested_targets,
1431 css: options.css,
1432 score: options.score,
1433 }
1434}
1435
1436fn is_health_score_only_output(options: &HealthSectionOptions, score: bool) -> bool {
1437 score
1438 && !options.complexity
1439 && !options.file_scores
1440 && !options.coverage_gaps
1441 && !options.hotspots
1442 && !options.targets
1443 && !options.trend
1444}
1445
1446const fn thresholds_to_engine(
1447 thresholds: ComplexityThresholdOverrides,
1448) -> fallow_engine::health::HealthThresholdOverrides {
1449 fallow_engine::health::HealthThresholdOverrides {
1450 max_cyclomatic: thresholds.max_cyclomatic,
1451 max_cognitive: thresholds.max_cognitive,
1452 max_crap: thresholds.max_crap,
1453 }
1454}
1455
1456const fn complexity_sort_to_engine(sort: ComplexitySort) -> fallow_engine::health::HealthSort {
1457 match sort {
1458 ComplexitySort::Severity => fallow_engine::health::HealthSort::Severity,
1459 ComplexitySort::Cyclomatic => fallow_engine::health::HealthSort::Cyclomatic,
1460 ComplexitySort::Cognitive => fallow_engine::health::HealthSort::Cognitive,
1461 ComplexitySort::Lines => fallow_engine::health::HealthSort::Lines,
1462 }
1463}
1464
1465const fn coverage_inputs_to_engine(
1466 coverage_inputs: ComplexityCoverageInputs<'_>,
1467) -> fallow_engine::health::HealthCoverageInputs<'_> {
1468 fallow_engine::health::HealthCoverageInputs {
1469 coverage: coverage_inputs.coverage,
1470 coverage_root: coverage_inputs.coverage_root,
1471 coverage_relocated: coverage_inputs.coverage_relocated,
1472 }
1473}
1474
1475const fn ownership_email_mode_to_config(mode: OwnershipEmailMode) -> EmailMode {
1476 match mode {
1477 OwnershipEmailMode::Raw => EmailMode::Raw,
1478 OwnershipEmailMode::Handle => EmailMode::Handle,
1479 OwnershipEmailMode::Anonymized => EmailMode::Anonymized,
1480 OwnershipEmailMode::Hash => EmailMode::Hash,
1481 }
1482}
1483
1484const fn target_effort_to_output(effort: TargetEffort) -> EffortEstimate {
1485 match effort {
1486 TargetEffort::Low => EffortEstimate::Low,
1487 TargetEffort::Medium => EffortEstimate::Medium,
1488 TargetEffort::High => EffortEstimate::High,
1489 }
1490}
1491
1492#[cfg(test)]
1493mod tests {
1494 use super::*;
1495
1496 #[test]
1497 fn duplication_defaults_match_cli_contract() {
1498 let options = DuplicationOptions::default();
1499 assert!(options.mode.is_none());
1500 assert!(options.min_tokens.is_none());
1501 assert!(options.min_lines.is_none());
1502 assert!(options.min_occurrences.is_none());
1503 }
1504
1505 #[test]
1506 fn programmatic_error_builder_keeps_optional_fields() {
1507 let error = ProgrammaticError::new("boom", 2)
1508 .with_code("FALLOW_TEST")
1509 .with_help("Try again")
1510 .with_context("analysis.root");
1511
1512 assert_eq!(error.message, "boom");
1513 assert_eq!(error.exit_code, 2);
1514 assert_eq!(error.code.as_deref(), Some("FALLOW_TEST"));
1515 assert_eq!(error.help.as_deref(), Some("Try again"));
1516 assert_eq!(error.context.as_deref(), Some("analysis.root"));
1517 }
1518
1519 #[test]
1520 fn dead_code_filters_accept_shared_registry_selectors() {
1521 for (selector, _) in fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS.iter() {
1522 let mut filters = DeadCodeFilters::default();
1523 assert!(
1524 filters.enable_registry_selector(selector),
1525 "{selector} should be accepted"
1526 );
1527 }
1528
1529 let mut filters = DeadCodeFilters::default();
1530 assert!(filters.enable_registry_selector("unused-files"));
1531 assert!(filters.unused_files);
1532 assert!(filters.enable_registry_selector("boundary-violations"));
1533 assert!(filters.boundary_violations);
1534 assert!(!filters.enable_registry_selector("not-a-real-selector"));
1535 }
1536
1537 #[test]
1538 fn default_complexity_options_match_programmatic_health_defaults() {
1539 let derived = derive_complexity_options(&ComplexityOptions::default());
1540
1541 assert!(!derived.any_section);
1542 assert!(derived.complexity);
1543 assert!(derived.file_scores);
1544 assert!(!derived.coverage_gaps);
1545 assert!(derived.hotspots);
1546 assert!(!derived.ownership);
1547 assert!(derived.targets);
1548 assert!(derived.force_full);
1549 assert!(!derived.score_only_output);
1550 assert!(derived.score);
1551 }
1552
1553 #[test]
1554 fn score_only_complexity_options_request_score_only_output() {
1555 let derived = derive_complexity_options(&ComplexityOptions {
1556 score: true,
1557 ..ComplexityOptions::default()
1558 });
1559
1560 assert!(derived.any_section);
1561 assert!(!derived.complexity);
1562 assert!(derived.file_scores);
1563 assert!(!derived.hotspots);
1564 assert!(!derived.targets);
1565 assert!(derived.force_full);
1566 assert!(derived.score_only_output);
1567 assert!(derived.score);
1568 }
1569
1570 #[test]
1571 fn ownership_implies_hotspots_when_requested() {
1572 let derived = derive_complexity_options(&ComplexityOptions {
1573 ownership: true,
1574 ..ComplexityOptions::default()
1575 });
1576
1577 assert!(derived.any_section);
1578 assert!(derived.hotspots);
1579 assert!(derived.ownership);
1580 assert!(!derived.targets);
1581 }
1582
1583 #[test]
1584 fn complexity_run_options_normalize_public_api_options() {
1585 let options = ComplexityOptions {
1586 max_cyclomatic: Some(42),
1587 max_cognitive: Some(21),
1588 max_crap: Some(18.5),
1589 top: Some(7),
1590 sort: ComplexitySort::Severity,
1591 complexity_breakdown: true,
1592 ownership_emails: Some(OwnershipEmailMode::Hash),
1593 effort: Some(TargetEffort::High),
1594 coverage: Some(PathBuf::from("coverage/coverage-final.json")),
1595 coverage_root: Some(PathBuf::from("/ci/workspace")),
1596 since: Some("30d".to_string()),
1597 min_commits: Some(4),
1598 ..ComplexityOptions::default()
1599 };
1600
1601 let run = derive_complexity_run_options(&options);
1602
1603 assert_eq!(run.thresholds.max_cyclomatic, Some(42));
1604 assert_eq!(run.thresholds.max_cognitive, Some(21));
1605 assert_eq!(run.thresholds.max_crap, Some(18.5));
1606 assert_eq!(run.top, Some(7));
1607 assert!(matches!(run.sort, ComplexitySort::Severity));
1608 assert!(run.complexity_breakdown);
1609 assert!(run.sections.hotspots);
1610 assert!(run.sections.ownership);
1611 assert!(run.sections.targets);
1612 assert!(matches!(
1613 run.ownership_emails,
1614 Some(OwnershipEmailMode::Hash)
1615 ));
1616 assert!(matches!(run.effort, Some(TargetEffort::High)));
1617 assert_eq!(run.since, Some("30d"));
1618 assert_eq!(run.min_commits, Some(4));
1619 assert_eq!(run.coverage_inputs.coverage, options.coverage.as_deref());
1620 assert_eq!(
1621 run.coverage_inputs.coverage_root,
1622 options.coverage_root.as_deref()
1623 );
1624 }
1625
1626 #[test]
1627 fn complexity_options_validation_accepts_existing_coverage_path_and_absolute_root() {
1628 let dir = tempfile::tempdir().expect("tempdir");
1629 let coverage = dir.path().join("coverage-final.json");
1630 std::fs::write(&coverage, "{}").expect("coverage fixture");
1631
1632 let result = validate_complexity_options(&ComplexityOptions {
1633 coverage: Some(coverage),
1634 coverage_root: Some(PathBuf::from("/ci/workspace")),
1635 ..ComplexityOptions::default()
1636 });
1637
1638 assert!(result.is_ok());
1639 }
1640
1641 #[test]
1642 fn complexity_options_validation_keeps_missing_coverage_error_contract() {
1643 let err = validate_complexity_options(&ComplexityOptions {
1644 coverage: Some(PathBuf::from("/missing/coverage-final.json")),
1645 ..ComplexityOptions::default()
1646 })
1647 .expect_err("missing coverage path should fail");
1648
1649 assert_eq!(err.exit_code, 2);
1650 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1651 assert_eq!(err.context.as_deref(), Some("health.coverage"));
1652 }
1653
1654 #[test]
1655 fn complexity_options_validation_keeps_relative_coverage_root_error_contract() {
1656 let err = validate_complexity_options(&ComplexityOptions {
1657 coverage_root: Some(PathBuf::from("coverage")),
1658 ..ComplexityOptions::default()
1659 })
1660 .expect_err("relative coverage root should fail");
1661
1662 assert_eq!(err.exit_code, 2);
1663 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_ROOT"));
1664 assert_eq!(err.context.as_deref(), Some("health.coverage_root"));
1665 }
1666
1667 #[test]
1671 fn complexity_options_validation_resolves_relative_coverage_against_root() {
1672 let dir = tempfile::tempdir().expect("tempdir");
1673 std::fs::create_dir_all(dir.path().join("artifacts")).expect("artifacts dir");
1674 std::fs::write(dir.path().join("artifacts/coverage-final.json"), "{}")
1675 .expect("coverage fixture");
1676 let relative = PathBuf::from("artifacts/coverage-final.json");
1677 assert!(
1678 !relative.exists(),
1679 "the fixture must not also exist under the test cwd"
1680 );
1681
1682 let result = validate_complexity_options(&ComplexityOptions {
1683 analysis: AnalysisOptions {
1684 root: Some(dir.path().to_path_buf()),
1685 ..AnalysisOptions::default()
1686 },
1687 coverage: Some(relative.clone()),
1688 ..ComplexityOptions::default()
1689 });
1690 assert!(result.is_ok(), "{result:?}");
1691
1692 let err = validate_complexity_options(&ComplexityOptions {
1693 analysis: AnalysisOptions {
1694 root: Some(dir.path().join("elsewhere")),
1695 ..AnalysisOptions::default()
1696 },
1697 coverage: Some(relative),
1698 ..ComplexityOptions::default()
1699 })
1700 .expect_err("the path does not exist under the other root");
1701 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1702 assert!(
1703 err.message.contains("elsewhere"),
1704 "the message names the resolved path: {}",
1705 err.message
1706 );
1707 }
1708
1709 #[test]
1710 fn default_health_sections_match_full_health_output() {
1711 let derived = derive_health_sections(&HealthSectionOptions {
1712 output: fallow_types::output_format::OutputFormat::Human,
1713 complexity: false,
1714 file_scores: false,
1715 coverage_gaps: false,
1716 hotspots: false,
1717 targets: false,
1718 css: false,
1719 score: false,
1720 score_gate: false,
1721 snapshot_requested: false,
1722 trend: false,
1723 });
1724
1725 assert!(!derived.any_section);
1726 assert!(derived.complexity);
1727 assert!(derived.file_scores);
1728 assert!(!derived.coverage_gaps);
1729 assert!(derived.hotspots);
1730 assert!(derived.targets);
1731 assert!(derived.score);
1732 assert!(derived.force_full);
1733 assert!(!derived.score_only_output);
1734 }
1735
1736 #[test]
1737 fn health_score_gate_requests_score_only_output() {
1738 let derived = derive_health_sections(&HealthSectionOptions {
1739 output: fallow_types::output_format::OutputFormat::Human,
1740 complexity: false,
1741 file_scores: false,
1742 coverage_gaps: false,
1743 hotspots: false,
1744 targets: false,
1745 css: false,
1746 score: false,
1747 score_gate: true,
1748 snapshot_requested: false,
1749 trend: false,
1750 });
1751
1752 assert!(derived.any_section);
1753 assert!(!derived.complexity);
1754 assert!(derived.file_scores);
1755 assert!(!derived.hotspots);
1756 assert!(!derived.targets);
1757 assert!(derived.score);
1758 assert!(derived.force_full);
1759 assert!(derived.score_only_output);
1760 }
1761
1762 #[test]
1763 fn health_snapshot_keeps_full_hidden_inputs_without_section_request() {
1764 let derived = derive_health_sections(&HealthSectionOptions {
1765 output: fallow_types::output_format::OutputFormat::Human,
1766 complexity: false,
1767 file_scores: false,
1768 coverage_gaps: false,
1769 hotspots: false,
1770 targets: false,
1771 css: true,
1772 score: false,
1773 score_gate: false,
1774 snapshot_requested: true,
1775 trend: false,
1776 });
1777
1778 assert!(!derived.any_section);
1779 assert!(derived.css);
1780 assert!(derived.file_scores);
1781 assert!(derived.hotspots);
1782 assert!(derived.score);
1783 assert!(derived.force_full);
1784 }
1785}