Skip to main content

codelens_core/output/
csv.rs

1//! CSV output format.
2
3use std::io::Write;
4
5use crate::analyzer::stats::AnalysisResult;
6use crate::error::Result;
7
8use super::format::{OutputFormat, OutputOptions, Report};
9
10/// CSV output formatter.
11pub struct CsvOutput;
12
13impl CsvOutput {
14    /// Create a new CSV output formatter.
15    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        // Header
58        writeln!(writer, "Language,Files,Code,Comment,Blank,Total,Size")?;
59
60        // Data rows
61        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        // Should contain language data
235        assert!(csv_str.contains("Rust"));
236        assert!(csv_str.contains("Python"));
237        assert!(csv_str.contains(",80,")); // Rust code lines
238        assert!(csv_str.contains(",40,")); // Python code lines
239    }
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        // 1 header + 2 languages = 3 lines
256        assert_eq!(lines.len(), 3);
257    }
258}