1use std::io::Write;
4
5use crate::analyzer::stats::AnalysisResult;
6use crate::error::Result;
7
8use super::format::{OutputFormat, OutputOptions, Report};
9
10pub struct CsvOutput;
12
13impl CsvOutput {
14 pub fn new() -> Self {
16 Self
17 }
18}
19
20impl Default for CsvOutput {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl OutputFormat for CsvOutput {
27 fn name(&self) -> &'static str {
28 "csv"
29 }
30
31 fn extension(&self) -> &'static str {
32 "csv"
33 }
34
35 fn write(
36 &self,
37 report: &Report,
38 options: &OutputOptions,
39 writer: &mut dyn Write,
40 ) -> Result<()> {
41 match report {
42 Report::Analysis(result) => self.write_analysis(result, options, writer),
43 Report::Health(report) => self.write_health(report, options, writer),
44 Report::Hotspot(report) => self.write_hotspot(report, options, writer),
45 Report::Trend(report) => self.write_trend(report, options, writer),
46 }
47 }
48}
49
50impl CsvOutput {
51 fn write_analysis(
52 &self,
53 result: &AnalysisResult,
54 _options: &OutputOptions,
55 writer: &mut dyn Write,
56 ) -> Result<()> {
57 writeln!(writer, "Language,Files,Code,Comment,Blank,Total,Size")?;
59
60 for (name, stats) in &result.summary.by_language {
62 writeln!(
63 writer,
64 "{},{},{},{},{},{},{}",
65 name,
66 stats.files,
67 stats.lines.code,
68 stats.lines.comment,
69 stats.lines.blank,
70 stats.lines.total,
71 stats.size
72 )?;
73 }
74
75 Ok(())
76 }
77
78 fn write_health(
79 &self,
80 report: &crate::insight::health::HealthReport,
81 _options: &OutputOptions,
82 writer: &mut dyn Write,
83 ) -> Result<()> {
84 writeln!(writer, "File,Score,Grade,TopIssue")?;
85 for file in &report.worst_files {
86 writeln!(
87 writer,
88 "{},{:.1},{},{}",
89 file.path.display(),
90 file.score,
91 file.grade,
92 file.top_issue,
93 )?;
94 }
95 Ok(())
96 }
97
98 fn write_hotspot(
99 &self,
100 report: &crate::insight::hotspot::HotspotReport,
101 _options: &OutputOptions,
102 writer: &mut dyn Write,
103 ) -> Result<()> {
104 writeln!(writer, "File,Commits,Added,Deleted,Churn,CC,Score,Risk")?;
105 for file in &report.files {
106 writeln!(
107 writer,
108 "{},{},{},{},{},{},{:.2},{}",
109 file.path.display(),
110 file.churn.commits,
111 file.churn.lines_added,
112 file.churn.lines_deleted,
113 file.churn.lines_churn,
114 file.complexity.cyclomatic,
115 file.hotspot_score,
116 file.risk,
117 )?;
118 }
119 Ok(())
120 }
121
122 fn write_trend(
123 &self,
124 report: &crate::insight::trend::TrendReport,
125 _options: &OutputOptions,
126 writer: &mut dyn Write,
127 ) -> Result<()> {
128 writeln!(writer, "Metric,Before,After,Delta,Percent")?;
129 let deltas = [
130 ("Files", &report.delta.files),
131 ("Lines", &report.delta.lines),
132 ("Code", &report.delta.code),
133 ("Comments", &report.delta.comment),
134 ("Blank", &report.delta.blank),
135 ("Complexity", &report.delta.complexity),
136 ("Functions", &report.delta.functions),
137 ];
138 for (name, dv) in &deltas {
139 writeln!(
140 writer,
141 "{},{},{},{},{:.1}",
142 name,
143 dv.from,
144 dv.to,
145 dv.signed_delta(),
146 dv.percent,
147 )?;
148 }
149 Ok(())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::Report;
156 use super::*;
157 use crate::analyzer::stats::{FileStats, LineStats, Summary};
158 use std::path::PathBuf;
159 use std::time::Duration;
160
161 fn make_test_result() -> AnalysisResult {
162 let files = vec![
163 FileStats {
164 path: PathBuf::from("main.rs"),
165 language: "Rust".to_string(),
166 lines: LineStats {
167 total: 100,
168 code: 80,
169 comment: 10,
170 blank: 10,
171 },
172 size: 2000,
173 complexity: Default::default(),
174 },
175 FileStats {
176 path: PathBuf::from("test.py"),
177 language: "Python".to_string(),
178 lines: LineStats {
179 total: 50,
180 code: 40,
181 comment: 5,
182 blank: 5,
183 },
184 size: 1000,
185 complexity: Default::default(),
186 },
187 ];
188 AnalysisResult {
189 summary: Summary::from_file_stats(&files),
190 files,
191 elapsed: Duration::from_millis(100),
192 scanned_files: 2,
193 skipped_files: 0,
194 }
195 }
196
197 #[test]
198 fn test_csv_output_name() {
199 let output = CsvOutput::new();
200 assert_eq!(output.name(), "csv");
201 assert_eq!(output.extension(), "csv");
202 }
203
204 #[test]
205 fn test_csv_output_header() {
206 let output = CsvOutput;
207 let result = make_test_result();
208 let options = OutputOptions::default();
209
210 let mut buffer = Vec::new();
211 output
212 .write(&Report::Analysis(result), &options, &mut buffer)
213 .unwrap();
214
215 let csv_str = String::from_utf8(buffer).unwrap();
216 let lines: Vec<&str> = csv_str.lines().collect();
217
218 assert_eq!(lines[0], "Language,Files,Code,Comment,Blank,Total,Size");
219 }
220
221 #[test]
222 fn test_csv_output_data() {
223 let output = CsvOutput;
224 let result = make_test_result();
225 let options = OutputOptions::default();
226
227 let mut buffer = Vec::new();
228 output
229 .write(&Report::Analysis(result), &options, &mut buffer)
230 .unwrap();
231
232 let csv_str = String::from_utf8(buffer).unwrap();
233
234 assert!(csv_str.contains("Rust"));
236 assert!(csv_str.contains("Python"));
237 assert!(csv_str.contains(",80,")); assert!(csv_str.contains(",40,")); }
240
241 #[test]
242 fn test_csv_output_line_count() {
243 let output = CsvOutput;
244 let result = make_test_result();
245 let options = OutputOptions::default();
246
247 let mut buffer = Vec::new();
248 output
249 .write(&Report::Analysis(result), &options, &mut buffer)
250 .unwrap();
251
252 let csv_str = String::from_utf8(buffer).unwrap();
253 let lines: Vec<&str> = csv_str.lines().collect();
254
255 assert_eq!(lines.len(), 3);
257 }
258}