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            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        // Header
62        writeln!(writer, "Language,Files,Code,Comment,Blank,Total,Size")?;
63
64        // Data rows
65        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        // Should contain language data
277        assert!(csv_str.contains("Rust"));
278        assert!(csv_str.contains("Python"));
279        assert!(csv_str.contains(",80,")); // Rust code lines
280        assert!(csv_str.contains(",40,")); // Python code lines
281    }
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        // 1 header + 2 languages = 3 lines
298        assert_eq!(lines.len(), 3);
299    }
300}