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 Report::Estimation(report) => self.write_estimation(report, options, writer),
47 Report::EstimationComparison(report) => {
48 self.write_estimation_comparison(report, writer)
49 }
50 }
51 }
52}
53
54impl CsvOutput {
55 fn write_analysis(
56 &self,
57 result: &AnalysisResult,
58 _options: &OutputOptions,
59 writer: &mut dyn Write,
60 ) -> Result<()> {
61 writeln!(writer, "Language,Files,Code,Comment,Blank,Total,Size")?;
63
64 for (name, stats) in &result.summary.by_language {
66 writeln!(
67 writer,
68 "{},{},{},{},{},{},{}",
69 name,
70 stats.files,
71 stats.lines.code,
72 stats.lines.comment,
73 stats.lines.blank,
74 stats.lines.total,
75 stats.size
76 )?;
77 }
78
79 Ok(())
80 }
81
82 fn write_health(
83 &self,
84 report: &crate::insight::health::HealthReport,
85 _options: &OutputOptions,
86 writer: &mut dyn Write,
87 ) -> Result<()> {
88 writeln!(writer, "File,Score,Grade,TopIssue")?;
89 for file in &report.worst_files {
90 writeln!(
91 writer,
92 "{},{:.1},{},{}",
93 file.path.display(),
94 file.score,
95 file.grade,
96 file.top_issue,
97 )?;
98 }
99 Ok(())
100 }
101
102 fn write_hotspot(
103 &self,
104 report: &crate::insight::hotspot::HotspotReport,
105 _options: &OutputOptions,
106 writer: &mut dyn Write,
107 ) -> Result<()> {
108 writeln!(writer, "File,Commits,Added,Deleted,Churn,CC,Score,Risk")?;
109 for file in &report.files {
110 writeln!(
111 writer,
112 "{},{},{},{},{},{},{:.2},{}",
113 file.path.display(),
114 file.churn.commits,
115 file.churn.lines_added,
116 file.churn.lines_deleted,
117 file.churn.lines_churn,
118 file.complexity.cyclomatic,
119 file.hotspot_score,
120 file.risk,
121 )?;
122 }
123 Ok(())
124 }
125
126 fn write_trend(
127 &self,
128 report: &crate::insight::trend::TrendReport,
129 _options: &OutputOptions,
130 writer: &mut dyn Write,
131 ) -> Result<()> {
132 writeln!(writer, "Metric,Before,After,Delta,Percent")?;
133 let deltas = [
134 ("Files", &report.delta.files),
135 ("Lines", &report.delta.lines),
136 ("Code", &report.delta.code),
137 ("Comments", &report.delta.comment),
138 ("Blank", &report.delta.blank),
139 ("Complexity", &report.delta.complexity),
140 ("Functions", &report.delta.functions),
141 ];
142 for (name, dv) in &deltas {
143 writeln!(
144 writer,
145 "{},{},{},{},{:.1}",
146 name,
147 dv.from,
148 dv.to,
149 dv.signed_delta(),
150 dv.percent,
151 )?;
152 }
153 Ok(())
154 }
155
156 fn write_estimation(
157 &self,
158 report: &crate::insight::estimation::EstimationReport,
159 _options: &OutputOptions,
160 writer: &mut dyn Write,
161 ) -> Result<()> {
162 writeln!(writer, "Language,Code,Effort(PM),Cost")?;
163 for lang in &report.by_language {
164 writeln!(
165 writer,
166 "{},{},{:.2},{:.2}",
167 lang.language, lang.code_lines, lang.effort_months, lang.cost,
168 )?;
169 }
170 Ok(())
171 }
172
173 fn write_estimation_comparison(
174 &self,
175 report: &crate::insight::estimation::EstimationComparison,
176 writer: &mut dyn Write,
177 ) -> Result<()> {
178 writeln!(writer, "Model,SLOC,Effort(PM),Schedule(M),People,Cost")?;
179 for r in &report.reports {
180 writeln!(
181 writer,
182 "{},{},{:.2},{:.2},{:.2},{:.2}",
183 r.model,
184 r.total_sloc,
185 r.effort_months,
186 r.schedule_months,
187 r.people_required,
188 r.estimated_cost,
189 )?;
190 }
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::Report;
198 use super::*;
199 use crate::analyzer::stats::{FileStats, LineStats, Summary};
200 use std::path::PathBuf;
201 use std::time::Duration;
202
203 fn make_test_result() -> AnalysisResult {
204 let files = vec![
205 FileStats {
206 path: PathBuf::from("main.rs"),
207 language: "Rust".to_string(),
208 lines: LineStats {
209 total: 100,
210 code: 80,
211 comment: 10,
212 blank: 10,
213 },
214 size: 2000,
215 complexity: Default::default(),
216 },
217 FileStats {
218 path: PathBuf::from("test.py"),
219 language: "Python".to_string(),
220 lines: LineStats {
221 total: 50,
222 code: 40,
223 comment: 5,
224 blank: 5,
225 },
226 size: 1000,
227 complexity: Default::default(),
228 },
229 ];
230 AnalysisResult {
231 summary: Summary::from_file_stats(&files),
232 files,
233 elapsed: Duration::from_millis(100),
234 scanned_files: 2,
235 skipped_files: 0,
236 }
237 }
238
239 #[test]
240 fn test_csv_output_name() {
241 let output = CsvOutput::new();
242 assert_eq!(output.name(), "csv");
243 assert_eq!(output.extension(), "csv");
244 }
245
246 #[test]
247 fn test_csv_output_header() {
248 let output = CsvOutput;
249 let result = make_test_result();
250 let options = OutputOptions::default();
251
252 let mut buffer = Vec::new();
253 output
254 .write(&Report::Analysis(result), &options, &mut buffer)
255 .unwrap();
256
257 let csv_str = String::from_utf8(buffer).unwrap();
258 let lines: Vec<&str> = csv_str.lines().collect();
259
260 assert_eq!(lines[0], "Language,Files,Code,Comment,Blank,Total,Size");
261 }
262
263 #[test]
264 fn test_csv_output_data() {
265 let output = CsvOutput;
266 let result = make_test_result();
267 let options = OutputOptions::default();
268
269 let mut buffer = Vec::new();
270 output
271 .write(&Report::Analysis(result), &options, &mut buffer)
272 .unwrap();
273
274 let csv_str = String::from_utf8(buffer).unwrap();
275
276 assert!(csv_str.contains("Rust"));
278 assert!(csv_str.contains("Python"));
279 assert!(csv_str.contains(",80,")); assert!(csv_str.contains(",40,")); }
282
283 #[test]
284 fn test_csv_output_line_count() {
285 let output = CsvOutput;
286 let result = make_test_result();
287 let options = OutputOptions::default();
288
289 let mut buffer = Vec::new();
290 output
291 .write(&Report::Analysis(result), &options, &mut buffer)
292 .unwrap();
293
294 let csv_str = String::from_utf8(buffer).unwrap();
295 let lines: Vec<&str> = csv_str.lines().collect();
296
297 assert_eq!(lines.len(), 3);
299 }
300}