use std::{borrow::Cow, io::Write, path::Path};
use anyhow::Result;
use super::{FormatterContext, OutputFormatter, ReportData, ViewOptions};
use crate::{analysis::AnalysisResults, display::report::LanguageRecord};
pub trait FieldEscaper {
fn escape(field: &str) -> Cow<'_, str>;
}
pub struct CsvEscaper;
impl FieldEscaper for CsvEscaper {
fn escape(field: &str) -> Cow<'_, str> {
let needs_quotes = field.contains(',') || field.contains('"') || field.contains('\n') || field.contains('\r');
if !needs_quotes {
return Cow::Borrowed(field);
}
let escaped = field.replace('"', "\"\"");
Cow::Owned(format!("\"{escaped}\""))
}
}
pub struct TsvEscaper;
impl FieldEscaper for TsvEscaper {
fn escape(field: &str) -> Cow<'_, str> {
if !field.contains(&['\\', '\t', '\n', '\r'][..]) {
return Cow::Borrowed(field);
}
let mut result = String::with_capacity(field.len());
for ch in field.chars() {
match ch {
'\\' => result.push_str("\\\\"),
'\t' => result.push_str("\\t"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
_ => result.push(ch),
}
}
Cow::Owned(result)
}
}
pub struct SeparatedValuesFormatter<const DELIMITER: u8, E: FieldEscaper> {
_escaper: std::marker::PhantomData<E>,
}
impl<const DELIMITER: u8, E: FieldEscaper> Default for SeparatedValuesFormatter<DELIMITER, E> {
fn default() -> Self {
Self { _escaper: std::marker::PhantomData }
}
}
impl<const DELIMITER: u8, E: FieldEscaper> OutputFormatter for SeparatedValuesFormatter<DELIMITER, E> {
fn write_output(
&self,
results: &AnalysisResults,
path: &Path,
verbose: bool,
view_options: ViewOptions,
writer: &mut dyn Write,
) -> Result<()> {
let (ctx, report) = self.prepare_report(results, path, verbose, view_options);
if verbose {
Self::write_verbose(&report, &ctx, writer)
} else {
Self::write_simple(&report.languages, &ctx, writer)
}
}
}
impl<const DELIMITER: u8, E: FieldEscaper> SeparatedValuesFormatter<DELIMITER, E> {
fn write_verbose(report: &ReportData, ctx: &FormatterContext, writer: &mut dyn Write) -> Result<()> {
Self::write_summary_section(report, ctx, writer)?;
writer.write_all(b"\n")?;
Self::write_language_section(&report.languages, ctx, writer)?;
writer.write_all(b"\n")?;
Self::write_files_sections(&report.languages, ctx, writer)?;
Ok(())
}
fn write_summary_section(report: &ReportData, ctx: &FormatterContext, output: &mut dyn Write) -> Result<()> {
output.write_all(b"Summary:\n")?;
Self::write_record(output, &["metric", "value", "percentage", "human_readable"])?;
Self::write_record(output, &["Analysis Path", report.analysis_path.as_str(), "", ""])?;
for metric in report.summary.metrics() {
let value = ctx.number(metric.value);
let pct = metric.percentage.map(|pct| ctx.percent(pct)).unwrap_or_default();
let human = metric.human_readable.unwrap_or("");
Self::write_record(output, &[metric.label, value.as_str(), pct.as_str(), human])?;
}
Ok(())
}
fn write_language_section(
languages: &[LanguageRecord],
ctx: &FormatterContext,
output: &mut dyn Write,
) -> Result<()> {
output.write_all(b"Language breakdown:\n")?;
Self::write_language_header(output)?;
for lang in languages {
Self::write_language_row(lang, ctx, output)?;
}
output.write_all(b"\n")?;
Ok(())
}
fn write_files_sections(
languages: &[LanguageRecord],
ctx: &FormatterContext,
output: &mut dyn Write,
) -> Result<()> {
for language in languages {
let Some(files) = &language.files_detail else {
continue;
};
writeln!(output, "{} files:", language.name)?;
Self::write_record(
output,
&[
"file_path",
"total_lines",
"code_lines",
"comment_lines",
"blank_lines",
"shebang_lines",
"size",
"size_human",
],
)?;
for file_stat in files {
Self::write_record(
output,
&[
file_stat.path,
&file_stat.format_total_lines(ctx),
&file_stat.format_code_lines(ctx),
&file_stat.format_comment_lines(ctx),
&file_stat.format_blank_lines(ctx),
&file_stat.format_shebang_lines(ctx),
&file_stat.format_size(ctx),
&file_stat.size_human,
],
)?;
}
output.write_all(b"\n")?;
}
Ok(())
}
fn write_simple(languages: &[LanguageRecord], ctx: &FormatterContext, output: &mut dyn Write) -> Result<()> {
Self::write_language_header(output)?;
for lang in languages {
Self::write_language_row(lang, ctx, output)?;
}
Ok(())
}
fn write_language_header(output: &mut dyn Write) -> Result<()> {
Self::write_record(
output,
&[
"language",
"files",
"lines",
"avg_lines_per_file",
"code_lines",
"comment_lines",
"blank_lines",
"shebang_lines",
"size",
"size_human",
"code_percentage",
"comment_percentage",
"blank_percentage",
"shebang_percentage",
],
)?;
Ok(())
}
fn write_language_row(lang: &LanguageRecord, ctx: &FormatterContext, output: &mut dyn Write) -> Result<()> {
Self::write_record(
output,
&[
lang.name,
&lang.format_files(ctx),
&lang.format_lines(ctx),
&format!("{:.1}", lang.avg_lines_per_file),
&lang.format_code_lines(ctx),
&lang.format_comment_lines(ctx),
&lang.format_blank_lines(ctx),
&lang.format_shebang_lines(ctx),
&lang.format_size(ctx),
&lang.size_human,
&lang.format_code_percentage(ctx),
&lang.format_comment_percentage(ctx),
&lang.format_blank_percentage(ctx),
&lang.format_shebang_percentage(ctx),
],
)?;
Ok(())
}
fn write_record(output: &mut dyn Write, fields: &[&str]) -> Result<()> {
for (idx, field) in fields.iter().enumerate() {
if idx > 0 {
output.write_all(&[DELIMITER])?;
}
Self::write_field(output, field)?;
}
output.write_all(b"\n")?;
Ok(())
}
fn write_field(output: &mut dyn Write, field: &str) -> Result<()> {
output.write_all(E::escape(field).as_bytes())?;
Ok(())
}
}
pub type CsvFormatter = SeparatedValuesFormatter<b',', CsvEscaper>;
pub type TsvFormatter = SeparatedValuesFormatter<b'\t', TsvEscaper>;