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, 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 MutationRunOptions, MutationRunScope, apply_mutation_verdicts, validate_mutation_verdicts,
172};
173pub use output_contracts::{
174 AuditOutput, BoundariesListLogicalGroup, BoundariesListRule, BoundariesListZone,
175 BoundariesListing, CombinedOutput, FallowOutput, ImpactOutput, ListBoundariesOutput,
176 ListEntryPointOutput, ListOutput, ListPluginOutput, ReviewBriefWireOutput, SecurityGate,
177 SecurityOutput, SecurityOutputConfig, SecuritySummaryOutput, SimilarCodeCandidateSnapshot,
178 SimilarCodeOutput, TraceOutput, WorkspacesOutput,
179};
180pub use runtime::TYPE_AWARE_CONFIG_UNSUPPORTED;
181pub use runtime::{
182 AUDIT_CONFIG_BASELINE_UNSUPPORTED, ArchitectureOutput, ArchitectureProgrammaticOutput,
183 AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
184 BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
185 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
186 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
187 DuplicationProgrammaticOutput, EngineHealthRunner, FeatureFlagsOutput,
188 FeatureFlagsProgrammaticOutput, HealthJsonReportInput, HealthProgrammaticOutput,
189 ProgrammaticHealthAnalysis, ProgrammaticHealthNextStepFacts, ProgrammaticHealthRun,
190 ProgrammaticHealthRunner, TraceClassMemberOutput, TraceCloneBenchmarkResult, TraceCloneOutput,
191 TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
192 TraceErrorOutput, TraceErrorProgrammaticOutput, TraceExportOutput,
193 TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOutput,
194 TraceFileProgrammaticOutput, TraceImportPathOutput, TraceImportPathProgrammaticOutput,
195 benchmark_trace_clone_compact_json, benchmark_trace_graph_family_compact_json,
196 inspect_similar_code, load_health_config, parse_similar_code_candidate_snapshot, plan_mutation,
197 review_similar_code, run_architecture, run_audit, run_boundary_violations,
198 run_circular_dependencies, run_combined, run_complexity_with_runner, run_dead_code,
199 run_dead_code_with_baseline, run_decision_surface, run_duplication, run_feature_flags,
200 run_health, run_health_with_runner, run_mutation, run_similar_code, run_trace_clone,
201 run_trace_dependency, run_trace_error, run_trace_export, run_trace_file, run_trace_import_path,
202 select_similar_code_candidate_snapshot, serialize_health_report_json,
203};
204pub use runtime_json::{
205 serialize_architecture_programmatic_json, serialize_audit_programmatic_json,
206 serialize_boundary_violations_programmatic_json,
207 serialize_circular_dependencies_programmatic_json, serialize_combined_programmatic_json,
208 serialize_dead_code_programmatic_json, serialize_decision_surface_programmatic_json,
209 serialize_duplication_programmatic_json, serialize_feature_flags_programmatic_json,
210 serialize_health_programmatic_json, serialize_trace_clone_programmatic_json,
211 serialize_trace_dependency_programmatic_json, serialize_trace_error_programmatic_json,
212 serialize_trace_export_programmatic_json, serialize_trace_file_programmatic_json,
213 serialize_trace_import_path_programmatic_json,
214};
215pub use sarif_output::{
216 annotate_sarif_results, build_duplication_sarif, build_grouped_duplication_sarif,
217 build_health_sarif,
218};
219pub use security_output::SecurityGateMode;
220pub use type_aware::{
221 SemanticCouplingOutcome, SemanticDeadCodeOutcome, SemanticInspectOutcome, TypeAwareError,
222 TypeAwareFileChanges, TypeAwareOutcome, TypeAwareSession, TypeAwareStatus,
223 discard_unverified_semantic_candidates, inspect_symbol as inspect_type_aware_symbol,
224 merge_type_aware_meta,
225 refine_configured_dead_code_results as refine_type_aware_results_with_config,
226 shutdown_type_aware_sidecars, status as type_aware_status,
227 symbol_impact as run_type_aware_symbol_impact, symbol_impact as type_aware_symbol_impact,
228 terminate_active_type_aware_sidecars, trace_symbol as run_type_aware_symbol_trace,
229 trace_symbol as trace_type_aware_symbol, type_coupling as analyze_type_coupling,
230};
231
232pub const COMMON_ANALYSIS_OPTION_FLAGS: &[&str] = &[
239 "root",
240 "config",
241 "no-cache",
242 "threads",
243 "changed-since",
244 "diff-file",
245 "production",
246 "workspace",
247 "changed-workspaces",
248 "explain",
249 "allow-remote-extends",
250];
251
252#[derive(Debug, Clone, Serialize)]
254pub struct ProgrammaticError {
255 pub message: String,
257 pub exit_code: u8,
259 pub code: Option<String>,
261 pub help: Option<String>,
263 pub context: Option<String>,
265}
266
267impl ProgrammaticError {
268 #[must_use]
271 pub fn new(message: impl Into<String>, exit_code: u8) -> Self {
272 Self {
273 message: message.into(),
274 exit_code,
275 code: None,
276 help: None,
277 context: None,
278 }
279 }
280
281 #[must_use]
283 pub fn with_help(mut self, help: impl Into<String>) -> Self {
284 self.help = Some(help.into());
285 self
286 }
287
288 #[must_use]
291 pub fn with_code(mut self, code: impl Into<String>) -> Self {
292 self.code = Some(code.into());
293 self
294 }
295
296 #[must_use]
299 pub fn with_context(mut self, context: impl Into<String>) -> Self {
300 self.context = Some(context.into());
301 self
302 }
303}
304
305impl std::fmt::Display for ProgrammaticError {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 write!(f, "{}", self.message)
308 }
309}
310
311impl std::error::Error for ProgrammaticError {}
312
313#[derive(Debug, Clone, Default)]
315pub struct AnalysisOptions {
316 pub root: Option<PathBuf>,
319 pub config_path: Option<PathBuf>,
321 pub allow_remote_extends: bool,
323 pub no_cache: bool,
325 pub threads: Option<usize>,
327 pub diff_file: Option<PathBuf>,
339 pub ambient_diff_file: Option<PathBuf>,
347 pub production: bool,
350 pub production_override: Option<bool>,
353 pub changed_since: Option<String>,
358 pub ambient_changed_since: Option<String>,
366 pub no_package_baselines: bool,
370 pub workspace: Option<Vec<String>>,
372 pub changed_workspaces: Option<String>,
374 pub explain: bool,
376 pub type_aware: TypeAwareOptions,
379 pub cancellation: Option<Arc<AtomicBool>>,
409}
410
411#[derive(Debug, Clone, Default, PartialEq, Eq)]
413pub struct TypeAwareOptions {
414 pub enabled: Option<bool>,
421 pub projects: Vec<PathBuf>,
424 pub require: fallow_config::TypeAwareRequire,
426}
427
428impl TypeAwareOptions {
429 #[must_use]
431 pub const fn requested(&self) -> bool {
432 matches!(self.enabled, Some(true))
433 }
434}
435
436#[derive(Debug, Clone, Default)]
441pub struct DeadCodeFilters {
442 pub unused_files: bool,
444 pub unused_exports: bool,
446 pub unused_deps: bool,
448 pub unused_types: bool,
450 pub private_type_leaks: bool,
452 pub deprecated_exports_in_use: bool,
454 pub unused_enum_members: bool,
456 pub unused_class_members: bool,
458 pub unused_store_members: bool,
460 pub unprovided_injects: bool,
462 pub unrendered_components: bool,
464 pub unused_component_props: bool,
466 pub absent_component_props: bool,
468 pub unused_component_emits: bool,
470 pub unused_component_inputs: bool,
472 pub unused_component_outputs: bool,
474 pub unused_svelte_events: bool,
476 pub unused_effect_layers: bool,
478 pub unused_tagged_errors: bool,
480 pub unproduced_catch_tags: bool,
482 pub unused_server_actions: bool,
484 pub unused_load_data_keys: bool,
486 pub unresolved_imports: bool,
488 pub unlisted_deps: bool,
490 pub duplicate_exports: bool,
492 pub circular_deps: bool,
494 pub re_export_cycles: bool,
496 pub package_cycles: bool,
498 pub boundary_violations: bool,
500 pub policy_violations: bool,
502 pub stale_suppressions: bool,
504 pub unused_catalog_entries: bool,
506 pub empty_catalog_groups: bool,
508 pub unresolved_catalog_references: bool,
510 pub unused_dependency_overrides: bool,
512 pub misconfigured_dependency_overrides: bool,
514}
515
516impl DeadCodeFilters {
517 pub(crate) fn reports_dependency_findings(&self) -> bool {
521 !self.any_active() || self.unused_deps || self.unlisted_deps
522 }
523
524 fn any_active(&self) -> bool {
525 self.unused_files
526 || self.unused_exports
527 || self.unused_deps
528 || self.unused_types
529 || self.private_type_leaks
530 || self.deprecated_exports_in_use
531 || self.unused_enum_members
532 || self.unused_class_members
533 || self.unused_store_members
534 || self.unprovided_injects
535 || self.unrendered_components
536 || self.unused_component_props
537 || self.absent_component_props
538 || self.unused_component_emits
539 || self.unused_component_inputs
540 || self.unused_component_outputs
541 || self.unused_svelte_events
542 || self.unused_effect_layers
543 || self.unused_tagged_errors
544 || self.unproduced_catch_tags
545 || self.unused_server_actions
546 || self.unused_load_data_keys
547 || self.unresolved_imports
548 || self.unlisted_deps
549 || self.duplicate_exports
550 || self.circular_deps
551 || self.re_export_cycles
552 || self.package_cycles
553 || self.boundary_violations
554 || self.policy_violations
555 || self.stale_suppressions
556 || self.unused_catalog_entries
557 || self.empty_catalog_groups
558 || self.unresolved_catalog_references
559 || self.unused_dependency_overrides
560 || self.misconfigured_dependency_overrides
561 }
562
563 pub fn enable_registry_selector(&mut self, selector: &str) -> bool {
569 let Some(flag) = fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS
570 .iter()
571 .find_map(|&(name, flag)| (name == selector).then_some(flag))
572 else {
573 return false;
574 };
575 self.enable_cli_filter_flag(flag);
576 true
577 }
578
579 fn enable_cli_filter_flag(&mut self, flag: &str) {
580 match flag {
581 "--unused-files" => self.unused_files = true,
582 "--unused-exports" => self.unused_exports = true,
583 "--unused-types" => self.unused_types = true,
584 "--private-type-leaks" => self.private_type_leaks = true,
585 "--deprecated-exports-in-use" => self.deprecated_exports_in_use = true,
586 "--unused-deps" => self.unused_deps = true,
587 "--unused-enum-members" => self.unused_enum_members = true,
588 "--unused-class-members" => self.unused_class_members = true,
589 "--unused-store-members" => self.unused_store_members = true,
590 "--unprovided-injects" => self.unprovided_injects = true,
591 "--unrendered-components" => self.unrendered_components = true,
592 "--unused-component-props" => self.unused_component_props = true,
593 "--absent-component-props" => self.absent_component_props = true,
594 "--unused-component-emits" => self.unused_component_emits = true,
595 "--unused-component-inputs" => self.unused_component_inputs = true,
596 "--unused-component-outputs" => self.unused_component_outputs = true,
597 "--unused-svelte-events" => self.unused_svelte_events = true,
598 "--unused-effect-layers" => self.unused_effect_layers = true,
599 "--unused-tagged-errors" => self.unused_tagged_errors = true,
600 "--unproduced-catch-tags" => self.unproduced_catch_tags = true,
601 "--unused-server-actions" => self.unused_server_actions = true,
602 "--unused-load-data-keys" => self.unused_load_data_keys = true,
603 "--unresolved-imports" => self.unresolved_imports = true,
604 "--unlisted-deps" => self.unlisted_deps = true,
605 "--duplicate-exports" => self.duplicate_exports = true,
606 "--circular-deps" => self.circular_deps = true,
607 "--re-export-cycles" => self.re_export_cycles = true,
608 "--package-cycles" => self.package_cycles = true,
609 "--boundary-violations" => self.boundary_violations = true,
610 "--policy-violations" => self.policy_violations = true,
611 "--stale-suppressions" => self.stale_suppressions = true,
612 "--unused-catalog-entries" => self.unused_catalog_entries = true,
613 "--empty-catalog-groups" => self.empty_catalog_groups = true,
614 "--unresolved-catalog-references" => self.unresolved_catalog_references = true,
615 "--unused-dependency-overrides" => self.unused_dependency_overrides = true,
616 "--misconfigured-dependency-overrides" => {
617 self.misconfigured_dependency_overrides = true;
618 }
619 _ => unreachable!("registry emitted unsupported dead-code filter flag: {flag}"),
620 }
621 }
622}
623
624#[derive(Debug, Clone, Default)]
626pub struct DeadCodeOptions {
627 pub analysis: AnalysisOptions,
629 pub filters: DeadCodeFilters,
631 pub files: Vec<PathBuf>,
633 pub include_entry_exports: bool,
635 pub show_cascade: bool,
637 pub finding_ids: Vec<String>,
642}
643
644#[derive(Debug, Clone, Default)]
650pub struct ArchitectureOptions {
651 pub analysis: AnalysisOptions,
653 pub files: Vec<PathBuf>,
655 pub cycles: bool,
658 pub boundaries: bool,
660 pub policy: bool,
662}
663
664impl ArchitectureOptions {
665 #[must_use]
667 pub fn dead_code_filters(&self) -> DeadCodeFilters {
668 let all = !(self.cycles || self.boundaries || self.policy);
669 DeadCodeFilters {
670 circular_deps: self.cycles || all,
671 re_export_cycles: self.cycles || all,
672 package_cycles: self.cycles || all,
673 boundary_violations: self.boundaries || all,
674 policy_violations: self.policy || all,
675 ..DeadCodeFilters::default()
676 }
677 }
678
679 #[must_use]
681 pub fn dead_code_options(&self) -> DeadCodeOptions {
682 DeadCodeOptions {
683 analysis: self.analysis.clone(),
684 filters: self.dead_code_filters(),
685 files: self.files.clone(),
686 include_entry_exports: false,
687 show_cascade: false,
688 finding_ids: Vec::new(),
689 }
690 }
691}
692
693#[derive(Debug, Clone, Default)]
695pub struct AuditOptions {
696 pub analysis: AnalysisOptions,
698 pub base: Option<String>,
701 pub production: bool,
703 pub production_dead_code: Option<bool>,
705 pub production_health: Option<bool>,
707 pub production_dupes: Option<bool>,
710 pub dead_code: Option<bool>,
714 pub health: Option<bool>,
717 pub duplication: Option<bool>,
720 pub css: Option<bool>,
723 pub css_deep: Option<bool>,
726 pub gate: Option<fallow_config::AuditGate>,
729 pub max_crap: Option<f64>,
731 pub coverage: Option<PathBuf>,
733 pub coverage_root: Option<PathBuf>,
735 pub include_entry_exports: bool,
737 pub show_cascade: bool,
739 pub runtime_coverage: Option<PathBuf>,
741 pub min_invocations_hot: u64,
743}
744
745#[derive(Debug, Clone)]
747pub struct CombinedOptions {
748 pub analysis: AnalysisOptions,
750 pub dead_code: bool,
752 pub duplication: bool,
754 pub health: bool,
756 pub include_entry_exports: bool,
758 pub show_cascade: bool,
760 pub duplication_options: DuplicationOptions,
762 pub health_options: ComplexityOptions,
764}
765
766impl Default for CombinedOptions {
767 fn default() -> Self {
768 Self {
769 analysis: AnalysisOptions::default(),
770 dead_code: true,
771 duplication: true,
772 health: true,
773 include_entry_exports: false,
774 show_cascade: false,
775 duplication_options: DuplicationOptions::default(),
776 health_options: ComplexityOptions::default(),
777 }
778 }
779}
780
781#[derive(Debug, Clone, Default)]
783pub struct DecisionSurfaceOptions {
784 pub analysis: AnalysisOptions,
786 pub base: Option<String>,
789 pub max_decisions: Option<usize>,
791}
792
793#[derive(Debug, Clone, Default)]
795pub struct FeatureFlagsOptions {
796 pub analysis: AnalysisOptions,
798 pub top: Option<usize>,
801 pub retirement: Option<FeatureFlagsRetirementOptions>,
803}
804
805#[derive(Debug, Clone, Default)]
808pub struct FeatureFlagsRetirementOptions {
809 pub flag_age: fallow_types::flag_retirement::FlagAgeMode,
811 pub flag_state: Option<std::path::PathBuf>,
813 pub reasons: Vec<fallow_types::flag_retirement::RetirementReason>,
815 pub sort: fallow_engine::flag_retirement::RetirementSort,
817 pub min_age_days: Option<u64>,
819 pub max_flag_age: Option<u64>,
821}
822
823#[derive(Debug, Clone, Copy, Default)]
825pub enum DuplicationMode {
826 Strict,
829 #[default]
831 Mild,
832 Weak,
834 Semantic,
837}
838
839#[derive(Debug, Clone, Default)]
841pub struct DuplicationOptions {
842 pub analysis: AnalysisOptions,
844 pub mode: Option<DuplicationMode>,
846 pub near: Option<bool>,
849 pub min_tokens: Option<usize>,
851 pub min_lines: Option<usize>,
853 pub min_occurrences: Option<usize>,
856 pub threshold: Option<f64>,
859 pub skip_local: Option<bool>,
862 pub ignore_symlinks: Option<bool>,
865 pub cross_language: Option<bool>,
867 pub ignore_imports: Option<bool>,
870 pub top: Option<usize>,
872 pub include_fragments: Option<bool>,
876}
877
878#[derive(Debug, Clone, Default)]
880pub struct SimilarCodeOptions {
881 pub analysis: AnalysisOptions,
883 pub threshold: Option<f64>,
885 pub min_lines: Option<usize>,
888 pub top: Option<usize>,
890 pub files: Vec<PathBuf>,
894 #[doc(hidden)]
899 pub adapter_provider_path: Option<PathBuf>,
900}
901
902#[derive(Debug, Clone)]
904pub struct SimilarCodeInspectOptions {
905 pub analysis: AnalysisOptions,
908 pub snapshot: fallow_output::SimilarCodeCandidateSnapshot,
910}
911
912#[derive(Debug, Clone, Default)]
914pub struct TraceExportOptions {
915 pub analysis: AnalysisOptions,
917 pub file: String,
919 pub export_name: String,
921}
922
923#[derive(Debug, Clone, Default)]
925pub struct TraceFileOptions {
926 pub analysis: AnalysisOptions,
928 pub file: String,
930}
931
932#[derive(Debug, Clone, Default)]
934pub struct TraceImportPathOptions {
935 pub analysis: AnalysisOptions,
937 pub from: String,
939 pub to: String,
941 pub eager_only: bool,
944}
945
946#[derive(Debug, Clone, Default)]
948pub struct TraceErrorOptions {
949 pub analysis: AnalysisOptions,
951 pub trace: String,
953 pub source: String,
955}
956
957#[derive(Debug, Clone, Default)]
959pub struct TraceDependencyOptions {
960 pub analysis: AnalysisOptions,
962 pub package_name: String,
964 pub usage: Option<DependencyUsageQuery>,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq)]
971pub enum TraceCloneTarget {
972 Location {
974 file: String,
976 line: usize,
978 },
979 Fingerprint(String),
981}
982
983#[derive(Debug, Clone)]
985pub struct TraceCloneOptions {
986 pub duplication: DuplicationOptions,
988 pub target: TraceCloneTarget,
990}
991
992#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
994pub enum ComplexitySort {
995 #[default]
997 Cyclomatic,
998 Cognitive,
1000 Lines,
1002 Severity,
1004}
1005
1006#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1008pub enum OwnershipEmailMode {
1009 Raw,
1011 #[default]
1013 Handle,
1014 Anonymized,
1016 Hash,
1018}
1019
1020#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1022pub enum TargetEffort {
1023 Low,
1025 Medium,
1027 High,
1029}
1030
1031#[derive(Debug, Clone, Default)]
1033pub struct ComplexityOptions {
1034 pub analysis: AnalysisOptions,
1036 pub max_cyclomatic: Option<u16>,
1038 pub max_cognitive: Option<u16>,
1040 pub max_crap: Option<f64>,
1042 pub top: Option<usize>,
1044 pub sort: ComplexitySort,
1046 pub complexity_breakdown: bool,
1048 pub complexity: bool,
1050 pub file_scores: bool,
1052 pub coverage_gaps: bool,
1054 pub hotspots: bool,
1056 pub ownership: bool,
1058 pub ownership_emails: Option<OwnershipEmailMode>,
1060 pub targets: bool,
1062 pub css: bool,
1064 pub css_deep: bool,
1066 pub effort: Option<TargetEffort>,
1069 pub score: bool,
1071 pub since: Option<String>,
1073 pub min_commits: Option<u32>,
1075 pub coverage: Option<PathBuf>,
1077 pub coverage_root: Option<PathBuf>,
1079 pub coverage_relocated: bool,
1084}
1085
1086#[derive(Debug, Clone, Copy, Default, PartialEq)]
1088pub struct ComplexityThresholdOverrides {
1089 pub max_cyclomatic: Option<u16>,
1091 pub max_cognitive: Option<u16>,
1093 pub max_crap: Option<f64>,
1095}
1096
1097#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1099pub struct ComplexityCoverageInputs<'a> {
1100 pub coverage: Option<&'a Path>,
1102 pub coverage_root: Option<&'a Path>,
1104 pub coverage_relocated: bool,
1107}
1108
1109#[derive(Debug, Clone)]
1111pub struct HealthSectionOptions {
1112 pub output: fallow_types::output_format::OutputFormat,
1114 pub complexity: bool,
1116 pub file_scores: bool,
1118 pub coverage_gaps: bool,
1120 pub hotspots: bool,
1122 pub targets: bool,
1124 pub css: bool,
1126 pub score: bool,
1128 pub score_gate: bool,
1130 pub snapshot_requested: bool,
1132 pub trend: bool,
1134}
1135
1136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1138pub struct DerivedHealthSections {
1139 pub any_section: bool,
1141 pub complexity: bool,
1143 pub file_scores: bool,
1145 pub coverage_gaps: bool,
1147 pub hotspots: bool,
1149 pub targets: bool,
1151 pub css: bool,
1153 pub score: bool,
1155 pub force_full: bool,
1157 pub score_only_output: bool,
1159}
1160
1161#[derive(Debug, Clone)]
1163pub struct ComplexitySectionOptions {
1164 pub complexity: bool,
1166 pub file_scores: bool,
1168 pub coverage_gaps: bool,
1170 pub hotspots: bool,
1172 pub ownership: bool,
1174 pub targets: bool,
1176 pub css: bool,
1178 pub score: bool,
1180}
1181
1182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1184pub struct DerivedComplexityOptions {
1185 pub any_section: bool,
1187 pub complexity: bool,
1189 pub file_scores: bool,
1191 pub coverage_gaps: bool,
1193 pub hotspots: bool,
1195 pub ownership: bool,
1197 pub targets: bool,
1199 pub force_full: bool,
1201 pub score_only_output: bool,
1203 pub score: bool,
1205}
1206
1207#[derive(Debug, Clone, PartialEq)]
1209pub struct ComplexityRunOptions<'a> {
1210 pub thresholds: ComplexityThresholdOverrides,
1212 pub top: Option<usize>,
1214 pub sort: ComplexitySort,
1216 pub complexity_breakdown: bool,
1218 pub sections: DerivedComplexityOptions,
1220 pub ownership_emails: Option<OwnershipEmailMode>,
1222 pub effort: Option<TargetEffort>,
1224 pub css: bool,
1226 pub css_deep: bool,
1228 pub since: Option<&'a str>,
1230 pub min_commits: Option<u32>,
1232 pub coverage_inputs: ComplexityCoverageInputs<'a>,
1234}
1235
1236#[must_use]
1238pub fn derive_health_sections(options: &HealthSectionOptions) -> DerivedHealthSections {
1239 let score = options.score
1240 || options.score_gate
1241 || options.trend
1242 || matches!(
1243 options.output,
1244 fallow_types::output_format::OutputFormat::Badge
1245 );
1246 let any_section = options.complexity
1247 || options.file_scores
1248 || options.coverage_gaps
1249 || options.hotspots
1250 || options.targets
1251 || score;
1252 let effective_score = if any_section { score } else { true } || options.snapshot_requested;
1253 let force_full = options.snapshot_requested || effective_score;
1254
1255 DerivedHealthSections {
1256 any_section,
1257 complexity: if any_section {
1258 options.complexity
1259 } else {
1260 true
1261 },
1262 file_scores: if any_section {
1263 options.file_scores
1264 } else {
1265 true
1266 } || force_full,
1267 coverage_gaps: if any_section {
1268 options.coverage_gaps
1269 } else {
1270 false
1271 },
1272 hotspots: if any_section { options.hotspots } else { true }
1273 || options.snapshot_requested
1274 || options.trend,
1275 targets: if any_section { options.targets } else { true },
1276 css: options.css,
1277 score: effective_score,
1278 force_full,
1279 score_only_output: is_health_score_only_output(options, score),
1280 }
1281}
1282
1283#[must_use]
1285pub fn derive_complexity_sections(options: &ComplexitySectionOptions) -> DerivedComplexityOptions {
1286 let requested_hotspots = options.hotspots || options.ownership;
1287 let sections = derive_health_sections(&HealthSectionOptions {
1288 output: fallow_types::output_format::OutputFormat::Human,
1289 complexity: options.complexity,
1290 file_scores: options.file_scores,
1291 coverage_gaps: options.coverage_gaps,
1292 hotspots: requested_hotspots,
1293 targets: options.targets,
1294 css: options.css,
1295 score: options.score,
1296 score_gate: false,
1297 snapshot_requested: false,
1298 trend: false,
1299 });
1300
1301 DerivedComplexityOptions {
1302 any_section: sections.any_section,
1303 complexity: sections.complexity,
1304 file_scores: sections.file_scores,
1305 coverage_gaps: sections.coverage_gaps,
1306 hotspots: sections.hotspots,
1307 ownership: options.ownership && sections.hotspots,
1308 targets: sections.targets,
1309 force_full: sections.force_full,
1310 score_only_output: sections.score_only_output,
1311 score: sections.score,
1312 }
1313}
1314
1315#[must_use]
1317pub fn derive_complexity_options(options: &ComplexityOptions) -> DerivedComplexityOptions {
1318 derive_complexity_sections(&complexity_section_options(options))
1319}
1320
1321#[must_use]
1323pub fn derive_complexity_run_options(options: &ComplexityOptions) -> ComplexityRunOptions<'_> {
1324 ComplexityRunOptions {
1325 thresholds: ComplexityThresholdOverrides {
1326 max_cyclomatic: options.max_cyclomatic,
1327 max_cognitive: options.max_cognitive,
1328 max_crap: options.max_crap,
1329 },
1330 top: options.top,
1331 sort: options.sort,
1332 complexity_breakdown: options.complexity_breakdown,
1333 sections: derive_complexity_options(options),
1334 ownership_emails: options.ownership_emails,
1335 effort: options.effort,
1336 css: options.css,
1337 css_deep: options.css_deep,
1338 since: options.since.as_deref(),
1339 min_commits: options.min_commits,
1340 coverage_inputs: ComplexityCoverageInputs {
1341 coverage: options.coverage.as_deref(),
1342 coverage_root: options.coverage_root.as_deref(),
1343 coverage_relocated: options.coverage_relocated,
1344 },
1345 }
1346}
1347
1348pub fn validate_complexity_options(options: &ComplexityOptions) -> Result<(), ProgrammaticError> {
1371 if let Some(path) = &options.coverage {
1372 let resolved = fallow_engine::health::scoring::resolve_relative_to_root(
1373 path,
1374 options.analysis.root.as_deref(),
1375 );
1376 if !resolved.exists() {
1377 return Err(ProgrammaticError::new(
1378 format!("coverage path does not exist: {}", resolved.display()),
1379 2,
1380 )
1381 .with_code("FALLOW_INVALID_COVERAGE_PATH")
1382 .with_context("health.coverage"));
1383 }
1384 }
1385 if let Err(message) =
1386 fallow_engine::health::validate_coverage_root_absolute(options.coverage_root.as_deref())
1387 {
1388 return Err(ProgrammaticError::new(message, 2)
1389 .with_code("FALLOW_INVALID_COVERAGE_ROOT")
1390 .with_context("health.coverage_root"));
1391 }
1392
1393 Ok(())
1394}
1395
1396fn complexity_section_options(options: &ComplexityOptions) -> ComplexitySectionOptions {
1397 let ownership = options.ownership || options.ownership_emails.is_some();
1398 let requested_targets = options.targets || options.effort.is_some();
1399 ComplexitySectionOptions {
1400 complexity: options.complexity,
1401 file_scores: options.file_scores,
1402 coverage_gaps: options.coverage_gaps,
1403 hotspots: options.hotspots,
1404 ownership,
1405 targets: requested_targets,
1406 css: options.css,
1407 score: options.score,
1408 }
1409}
1410
1411fn is_health_score_only_output(options: &HealthSectionOptions, score: bool) -> bool {
1412 score
1413 && !options.complexity
1414 && !options.file_scores
1415 && !options.coverage_gaps
1416 && !options.hotspots
1417 && !options.targets
1418 && !options.trend
1419}
1420
1421const fn thresholds_to_engine(
1422 thresholds: ComplexityThresholdOverrides,
1423) -> fallow_engine::health::HealthThresholdOverrides {
1424 fallow_engine::health::HealthThresholdOverrides {
1425 max_cyclomatic: thresholds.max_cyclomatic,
1426 max_cognitive: thresholds.max_cognitive,
1427 max_crap: thresholds.max_crap,
1428 }
1429}
1430
1431const fn complexity_sort_to_engine(sort: ComplexitySort) -> fallow_engine::health::HealthSort {
1432 match sort {
1433 ComplexitySort::Severity => fallow_engine::health::HealthSort::Severity,
1434 ComplexitySort::Cyclomatic => fallow_engine::health::HealthSort::Cyclomatic,
1435 ComplexitySort::Cognitive => fallow_engine::health::HealthSort::Cognitive,
1436 ComplexitySort::Lines => fallow_engine::health::HealthSort::Lines,
1437 }
1438}
1439
1440const fn coverage_inputs_to_engine(
1441 coverage_inputs: ComplexityCoverageInputs<'_>,
1442) -> fallow_engine::health::HealthCoverageInputs<'_> {
1443 fallow_engine::health::HealthCoverageInputs {
1444 coverage: coverage_inputs.coverage,
1445 coverage_root: coverage_inputs.coverage_root,
1446 coverage_relocated: coverage_inputs.coverage_relocated,
1447 }
1448}
1449
1450const fn ownership_email_mode_to_config(mode: OwnershipEmailMode) -> EmailMode {
1451 match mode {
1452 OwnershipEmailMode::Raw => EmailMode::Raw,
1453 OwnershipEmailMode::Handle => EmailMode::Handle,
1454 OwnershipEmailMode::Anonymized => EmailMode::Anonymized,
1455 OwnershipEmailMode::Hash => EmailMode::Hash,
1456 }
1457}
1458
1459const fn target_effort_to_output(effort: TargetEffort) -> EffortEstimate {
1460 match effort {
1461 TargetEffort::Low => EffortEstimate::Low,
1462 TargetEffort::Medium => EffortEstimate::Medium,
1463 TargetEffort::High => EffortEstimate::High,
1464 }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469 use super::*;
1470
1471 #[test]
1472 fn duplication_defaults_match_cli_contract() {
1473 let options = DuplicationOptions::default();
1474 assert!(options.mode.is_none());
1475 assert!(options.min_tokens.is_none());
1476 assert!(options.min_lines.is_none());
1477 assert!(options.min_occurrences.is_none());
1478 }
1479
1480 #[test]
1481 fn programmatic_error_builder_keeps_optional_fields() {
1482 let error = ProgrammaticError::new("boom", 2)
1483 .with_code("FALLOW_TEST")
1484 .with_help("Try again")
1485 .with_context("analysis.root");
1486
1487 assert_eq!(error.message, "boom");
1488 assert_eq!(error.exit_code, 2);
1489 assert_eq!(error.code.as_deref(), Some("FALLOW_TEST"));
1490 assert_eq!(error.help.as_deref(), Some("Try again"));
1491 assert_eq!(error.context.as_deref(), Some("analysis.root"));
1492 }
1493
1494 #[test]
1495 fn dead_code_filters_accept_shared_registry_selectors() {
1496 for (selector, _) in fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS.iter() {
1497 let mut filters = DeadCodeFilters::default();
1498 assert!(
1499 filters.enable_registry_selector(selector),
1500 "{selector} should be accepted"
1501 );
1502 }
1503
1504 let mut filters = DeadCodeFilters::default();
1505 assert!(filters.enable_registry_selector("unused-files"));
1506 assert!(filters.unused_files);
1507 assert!(filters.enable_registry_selector("boundary-violations"));
1508 assert!(filters.boundary_violations);
1509 assert!(!filters.enable_registry_selector("not-a-real-selector"));
1510 }
1511
1512 #[test]
1513 fn default_complexity_options_match_programmatic_health_defaults() {
1514 let derived = derive_complexity_options(&ComplexityOptions::default());
1515
1516 assert!(!derived.any_section);
1517 assert!(derived.complexity);
1518 assert!(derived.file_scores);
1519 assert!(!derived.coverage_gaps);
1520 assert!(derived.hotspots);
1521 assert!(!derived.ownership);
1522 assert!(derived.targets);
1523 assert!(derived.force_full);
1524 assert!(!derived.score_only_output);
1525 assert!(derived.score);
1526 }
1527
1528 #[test]
1529 fn score_only_complexity_options_request_score_only_output() {
1530 let derived = derive_complexity_options(&ComplexityOptions {
1531 score: true,
1532 ..ComplexityOptions::default()
1533 });
1534
1535 assert!(derived.any_section);
1536 assert!(!derived.complexity);
1537 assert!(derived.file_scores);
1538 assert!(!derived.hotspots);
1539 assert!(!derived.targets);
1540 assert!(derived.force_full);
1541 assert!(derived.score_only_output);
1542 assert!(derived.score);
1543 }
1544
1545 #[test]
1546 fn ownership_implies_hotspots_when_requested() {
1547 let derived = derive_complexity_options(&ComplexityOptions {
1548 ownership: true,
1549 ..ComplexityOptions::default()
1550 });
1551
1552 assert!(derived.any_section);
1553 assert!(derived.hotspots);
1554 assert!(derived.ownership);
1555 assert!(!derived.targets);
1556 }
1557
1558 #[test]
1559 fn complexity_run_options_normalize_public_api_options() {
1560 let options = ComplexityOptions {
1561 max_cyclomatic: Some(42),
1562 max_cognitive: Some(21),
1563 max_crap: Some(18.5),
1564 top: Some(7),
1565 sort: ComplexitySort::Severity,
1566 complexity_breakdown: true,
1567 ownership_emails: Some(OwnershipEmailMode::Hash),
1568 effort: Some(TargetEffort::High),
1569 coverage: Some(PathBuf::from("coverage/coverage-final.json")),
1570 coverage_root: Some(PathBuf::from("/ci/workspace")),
1571 since: Some("30d".to_string()),
1572 min_commits: Some(4),
1573 ..ComplexityOptions::default()
1574 };
1575
1576 let run = derive_complexity_run_options(&options);
1577
1578 assert_eq!(run.thresholds.max_cyclomatic, Some(42));
1579 assert_eq!(run.thresholds.max_cognitive, Some(21));
1580 assert_eq!(run.thresholds.max_crap, Some(18.5));
1581 assert_eq!(run.top, Some(7));
1582 assert!(matches!(run.sort, ComplexitySort::Severity));
1583 assert!(run.complexity_breakdown);
1584 assert!(run.sections.hotspots);
1585 assert!(run.sections.ownership);
1586 assert!(run.sections.targets);
1587 assert!(matches!(
1588 run.ownership_emails,
1589 Some(OwnershipEmailMode::Hash)
1590 ));
1591 assert!(matches!(run.effort, Some(TargetEffort::High)));
1592 assert_eq!(run.since, Some("30d"));
1593 assert_eq!(run.min_commits, Some(4));
1594 assert_eq!(run.coverage_inputs.coverage, options.coverage.as_deref());
1595 assert_eq!(
1596 run.coverage_inputs.coverage_root,
1597 options.coverage_root.as_deref()
1598 );
1599 }
1600
1601 #[test]
1602 fn complexity_options_validation_accepts_existing_coverage_path_and_absolute_root() {
1603 let dir = tempfile::tempdir().expect("tempdir");
1604 let coverage = dir.path().join("coverage-final.json");
1605 std::fs::write(&coverage, "{}").expect("coverage fixture");
1606
1607 let result = validate_complexity_options(&ComplexityOptions {
1608 coverage: Some(coverage),
1609 coverage_root: Some(PathBuf::from("/ci/workspace")),
1610 ..ComplexityOptions::default()
1611 });
1612
1613 assert!(result.is_ok());
1614 }
1615
1616 #[test]
1617 fn complexity_options_validation_keeps_missing_coverage_error_contract() {
1618 let err = validate_complexity_options(&ComplexityOptions {
1619 coverage: Some(PathBuf::from("/missing/coverage-final.json")),
1620 ..ComplexityOptions::default()
1621 })
1622 .expect_err("missing coverage path should fail");
1623
1624 assert_eq!(err.exit_code, 2);
1625 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1626 assert_eq!(err.context.as_deref(), Some("health.coverage"));
1627 }
1628
1629 #[test]
1630 fn complexity_options_validation_keeps_relative_coverage_root_error_contract() {
1631 let err = validate_complexity_options(&ComplexityOptions {
1632 coverage_root: Some(PathBuf::from("coverage")),
1633 ..ComplexityOptions::default()
1634 })
1635 .expect_err("relative coverage root should fail");
1636
1637 assert_eq!(err.exit_code, 2);
1638 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_ROOT"));
1639 assert_eq!(err.context.as_deref(), Some("health.coverage_root"));
1640 }
1641
1642 #[test]
1646 fn complexity_options_validation_resolves_relative_coverage_against_root() {
1647 let dir = tempfile::tempdir().expect("tempdir");
1648 std::fs::create_dir_all(dir.path().join("artifacts")).expect("artifacts dir");
1649 std::fs::write(dir.path().join("artifacts/coverage-final.json"), "{}")
1650 .expect("coverage fixture");
1651 let relative = PathBuf::from("artifacts/coverage-final.json");
1652 assert!(
1653 !relative.exists(),
1654 "the fixture must not also exist under the test cwd"
1655 );
1656
1657 let result = validate_complexity_options(&ComplexityOptions {
1658 analysis: AnalysisOptions {
1659 root: Some(dir.path().to_path_buf()),
1660 ..AnalysisOptions::default()
1661 },
1662 coverage: Some(relative.clone()),
1663 ..ComplexityOptions::default()
1664 });
1665 assert!(result.is_ok(), "{result:?}");
1666
1667 let err = validate_complexity_options(&ComplexityOptions {
1668 analysis: AnalysisOptions {
1669 root: Some(dir.path().join("elsewhere")),
1670 ..AnalysisOptions::default()
1671 },
1672 coverage: Some(relative),
1673 ..ComplexityOptions::default()
1674 })
1675 .expect_err("the path does not exist under the other root");
1676 assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1677 assert!(
1678 err.message.contains("elsewhere"),
1679 "the message names the resolved path: {}",
1680 err.message
1681 );
1682 }
1683
1684 #[test]
1685 fn default_health_sections_match_full_health_output() {
1686 let derived = derive_health_sections(&HealthSectionOptions {
1687 output: fallow_types::output_format::OutputFormat::Human,
1688 complexity: false,
1689 file_scores: false,
1690 coverage_gaps: false,
1691 hotspots: false,
1692 targets: false,
1693 css: false,
1694 score: false,
1695 score_gate: false,
1696 snapshot_requested: false,
1697 trend: false,
1698 });
1699
1700 assert!(!derived.any_section);
1701 assert!(derived.complexity);
1702 assert!(derived.file_scores);
1703 assert!(!derived.coverage_gaps);
1704 assert!(derived.hotspots);
1705 assert!(derived.targets);
1706 assert!(derived.score);
1707 assert!(derived.force_full);
1708 assert!(!derived.score_only_output);
1709 }
1710
1711 #[test]
1712 fn health_score_gate_requests_score_only_output() {
1713 let derived = derive_health_sections(&HealthSectionOptions {
1714 output: fallow_types::output_format::OutputFormat::Human,
1715 complexity: false,
1716 file_scores: false,
1717 coverage_gaps: false,
1718 hotspots: false,
1719 targets: false,
1720 css: false,
1721 score: false,
1722 score_gate: true,
1723 snapshot_requested: false,
1724 trend: false,
1725 });
1726
1727 assert!(derived.any_section);
1728 assert!(!derived.complexity);
1729 assert!(derived.file_scores);
1730 assert!(!derived.hotspots);
1731 assert!(!derived.targets);
1732 assert!(derived.score);
1733 assert!(derived.force_full);
1734 assert!(derived.score_only_output);
1735 }
1736
1737 #[test]
1738 fn health_snapshot_keeps_full_hidden_inputs_without_section_request() {
1739 let derived = derive_health_sections(&HealthSectionOptions {
1740 output: fallow_types::output_format::OutputFormat::Human,
1741 complexity: false,
1742 file_scores: false,
1743 coverage_gaps: false,
1744 hotspots: false,
1745 targets: false,
1746 css: true,
1747 score: false,
1748 score_gate: false,
1749 snapshot_requested: true,
1750 trend: false,
1751 });
1752
1753 assert!(!derived.any_section);
1754 assert!(derived.css);
1755 assert!(derived.file_scores);
1756 assert!(derived.hotspots);
1757 assert!(derived.score);
1758 assert!(derived.force_full);
1759 }
1760}