codestats 0.6.0

A CLI tool for analyzing code statistics across different programming languages
use std::{borrow::Cow, io::Write, path::Path};

use anyhow::Result;

use super::{FormatterContext, OutputFormatter, ReportData, ViewOptions};
use crate::{
	analysis::{AnalysisResults, stats::percentage},
	display::{
		formatting::pluralize,
		report::{LanguageRecord, Summary},
	},
};

pub struct HumanFormatter;

fn join_with_commas_and(parts: &[String]) -> Option<Cow<'_, str>> {
	match parts {
		[] => None,
		[first] => Some(Cow::Borrowed(first.as_str())),
		[first, second] => Some(Cow::Owned(format!("{first} and {second}"))),
		items => {
			let mut result = items[..items.len() - 1].join(", ");
			result.push_str(", and ");
			result.push_str(&items[items.len() - 1]);
			Some(Cow::Owned(result))
		}
	}
}

impl OutputFormatter for HumanFormatter {
	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);
		Self::write_overview(&report, &ctx, writer)?;
		if report.languages.is_empty() {
			writeln!(writer, "No recognized programming languages found.")?;
			return Ok(());
		}
		Self::write_language_breakdown(&report, &ctx, verbose, writer)?;
		Ok(())
	}
}

impl HumanFormatter {
	fn write_overview(report: &ReportData, ctx: &FormatterContext, writer: &mut dyn Write) -> Result<()> {
		let summary = &report.summary;
		let total_size_human = &summary.total_size_human;
		writeln!(
			writer,
			"Codestats for {}: {} {}, {} total {}, {} total size.",
			report.analysis_path,
			ctx.number(summary.total_files),
			pluralize(summary.total_files, "file", "files"),
			ctx.number(summary.total_lines),
			pluralize(summary.total_lines, "line", "lines"),
			total_size_human
		)?;
		let line_breakdown_parts = summary.line_breakdown_parts(true, ctx);
		if let Some(breakdown) = join_with_commas_and(&line_breakdown_parts) {
			writeln!(writer, "Line breakdown: {breakdown}.")?;
		}
		let percentage_parts = summary.percentage_parts(ctx);
		if let Some(percentages) = join_with_commas_and(&percentage_parts) {
			writeln!(writer, "Percentages: {percentages}.")?;
		}
		Ok(())
	}

	fn write_language_breakdown(
		report: &ReportData,
		ctx: &FormatterContext,
		verbose: bool,
		writer: &mut dyn Write,
	) -> Result<()> {
		writeln!(writer, "Language breakdown:")?;
		for language in &report.languages {
			Self::write_language_stats(language, &report.summary, ctx, verbose, writer)?;
		}
		Ok(())
	}

	fn write_language_stats(
		language: &LanguageRecord,
		summary: &Summary,
		ctx: &FormatterContext,
		verbose: bool,
		writer: &mut dyn Write,
	) -> Result<()> {
		let file_pct = percentage(language.files, summary.total_files);
		let line_pct = percentage(language.lines, summary.total_lines);
		let size_pct = percentage(language.size, summary.total_size);
		let size_human = &language.size_human;
		let file_pct_str = ctx.percent(file_pct);
		let line_pct_str = ctx.percent(line_pct);
		let size_pct_str = ctx.percent(size_pct);
		writeln!(writer, "{}:", language.name)?;
		writeln!(
			writer,
			"\tFiles: {} {} ({}% of total).",
			ctx.number(language.files),
			pluralize(language.files, "file", "files"),
			file_pct_str
		)?;
		writeln!(
			writer,
			"\tLines: {} {} ({}% of total).",
			ctx.number(language.lines),
			pluralize(language.lines, "line", "lines"),
			line_pct_str
		)?;
		writeln!(writer, "\tAverage lines per file: {:.1}.", language.avg_lines_per_file)?;
		writeln!(writer, "\tSize: {size_human} ({size_pct_str}% of total).")?;
		writeln!(writer, "\tLine breakdown:")?;
		for line_type in language.line_types() {
			writeln!(
				writer,
				"\t\t{}: {} lines ({}%).",
				line_type.title_label(),
				ctx.number(line_type.count),
				ctx.percent(line_type.percentage)
			)?;
		}
		if verbose {
			Self::write_file_breakdown(language, summary, ctx, writer)?;
		}
		Ok(())
	}

	fn write_file_breakdown(
		language: &LanguageRecord,
		summary: &Summary,
		ctx: &FormatterContext,
		writer: &mut dyn Write,
	) -> Result<()> {
		writeln!(writer, "\tFile breakdown:")?;
		let Some(files) = &language.files_detail else {
			return Ok(());
		};
		for file_stat in files {
			let file_pct = percentage(file_stat.total_lines, summary.total_lines);
			let file_pct_str = ctx.percent(file_pct);
			let size_human = &file_stat.size_human;
			writeln!(
				writer,
				"\t\t{}: {} lines, {} ({}% of total lines).",
				file_stat.path,
				ctx.number(file_stat.total_lines),
				size_human,
				file_pct_str
			)?;
		}
		Ok(())
	}
}