Skip to main content

fallow_cli/health_types/
mod.rs

1//! Health / complexity analysis report types.
2//!
3//! Separated from the `health` command module so that report formatters
4//! (which are compiled as part of both the lib and bin targets) can
5//! reference these types without pulling in binary-only dependencies.
6
7mod coverage;
8mod scores;
9mod targets;
10mod trends;
11mod vital_signs;
12
13pub use coverage::*;
14pub use scores::*;
15pub use targets::*;
16pub use trends::*;
17pub use vital_signs::*;
18
19/// Result of complexity analysis for reporting.
20#[derive(Debug, serde::Serialize)]
21pub struct HealthReport {
22    /// Functions exceeding thresholds.
23    pub findings: Vec<HealthFinding>,
24    /// Summary statistics.
25    pub summary: HealthSummary,
26    /// Project-wide vital signs (always computed from available data).
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub vital_signs: Option<VitalSigns>,
29    /// Project-wide health score (only populated with `--score`).
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub health_score: Option<HealthScore>,
32    /// Per-file health scores (only populated with `--file-scores` or `--hotspots`).
33    #[serde(skip_serializing_if = "Vec::is_empty")]
34    pub file_scores: Vec<FileHealthScore>,
35    /// Static coverage gaps (only populated with `--coverage-gaps`).
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub coverage_gaps: Option<CoverageGaps>,
38    /// Hotspot entries (only populated with `--hotspots`).
39    #[serde(skip_serializing_if = "Vec::is_empty")]
40    pub hotspots: Vec<HotspotEntry>,
41    /// Hotspot analysis summary (only set with `--hotspots`).
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub hotspot_summary: Option<HotspotSummary>,
44    /// Functions exceeding 60 LOC (only populated when unit size very-high-risk >= 3%).
45    #[serde(skip_serializing_if = "Vec::is_empty")]
46    pub large_functions: Vec<LargeFunctionEntry>,
47    /// Ranked refactoring recommendations (only populated with `--targets`).
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub targets: Vec<RefactoringTarget>,
50    /// Adaptive thresholds used for target scoring (only set with `--targets`).
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub target_thresholds: Option<TargetThresholds>,
53    /// Health trend comparison against a previous snapshot (only set with `--trend`).
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub health_trend: Option<HealthTrend>,
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn health_report_skips_empty_collections() {
64        let report = HealthReport {
65            findings: vec![],
66            summary: HealthSummary {
67                files_analyzed: 0,
68                functions_analyzed: 0,
69                functions_above_threshold: 0,
70                max_cyclomatic_threshold: 20,
71                max_cognitive_threshold: 15,
72                files_scored: None,
73                average_maintainability: None,
74                coverage_model: None,
75                istanbul_matched: None,
76                istanbul_total: None,
77            },
78            vital_signs: None,
79            health_score: None,
80            file_scores: vec![],
81            coverage_gaps: None,
82            hotspots: vec![],
83            hotspot_summary: None,
84            large_functions: vec![],
85            targets: vec![],
86            target_thresholds: None,
87            health_trend: None,
88        };
89        let json = serde_json::to_string(&report).unwrap();
90        // Empty vecs should be omitted due to skip_serializing_if
91        assert!(!json.contains("file_scores"));
92        assert!(!json.contains("hotspots"));
93        assert!(!json.contains("hotspot_summary"));
94        assert!(!json.contains("large_functions"));
95        assert!(!json.contains("targets"));
96        assert!(!json.contains("vital_signs"));
97        assert!(!json.contains("health_score"));
98    }
99
100    #[test]
101    fn health_score_none_skipped_in_report() {
102        let report = HealthReport {
103            findings: vec![],
104            summary: HealthSummary {
105                files_analyzed: 0,
106                functions_analyzed: 0,
107                functions_above_threshold: 0,
108                max_cyclomatic_threshold: 20,
109                max_cognitive_threshold: 15,
110                files_scored: None,
111                average_maintainability: None,
112                coverage_model: None,
113                istanbul_matched: None,
114                istanbul_total: None,
115            },
116            vital_signs: None,
117            health_score: None,
118            file_scores: vec![],
119            coverage_gaps: None,
120            hotspots: vec![],
121            hotspot_summary: None,
122            large_functions: vec![],
123            targets: vec![],
124            target_thresholds: None,
125            health_trend: None,
126        };
127        let json = serde_json::to_string(&report).unwrap();
128        assert!(!json.contains("health_score"));
129    }
130}