csskit 0.0.24-canary.00879f1042

Refreshing CSS!
use crate::{CliError, CliResult, GlobalConfig, bold_green, bold_red, commands::format_diagnostic_error};
use bumpalo::Bump;
use clap::Args;
use css_ast::CssAtomSet;
use css_lexer::{DynAtomSet, Lexer, RegisteredAtomSet};
use css_parse::Parser;
use csskit_ast::{Collector, CsskitAtomSet, ResolvedDiagnosticLevel, StatType, sheet::Sheet};
use csskit_highlight::CssHighlighter;
use miette::{GraphicalReportHandler, GraphicalTheme, NamedSource, Report};
use std::{collections::HashMap, fs};

/// Report potential issues around some CSS files
#[derive(Debug, Args)]
pub struct Check {
	/// The csskit sheet file (.cks)
	#[arg(value_parser)]
	sheet: String,

	/// A list of CSS files to check
	#[arg(value_parser)]
	input: Vec<String>,

	/// Automatically apply suggested fixes
	#[arg(short, long, value_parser)]
	fix: bool,
}

impl Check {
	pub fn run(&self, config: GlobalConfig) -> CliResult {
		let Self { sheet, input, fix } = self;

		if *fix {
			todo!()
		}

		if input.is_empty() {
			hint_no_input(sheet, &config);
			return Err(CliError::ParseFailed);
		}

		let bump = Bump::new();

		// Read and parse the csskit sheet
		let rule_source = fs::read_to_string(sheet)?;
		let rule_lexer = Lexer::new(CsskitAtomSet::get_dyn_set(), &rule_source);
		let mut rule_parser = Parser::new(&bump, &rule_source, rule_lexer);
		let rule_result = rule_parser.parse_entirely::<Sheet>();
		let parsed_rules = rule_result.output.ok_or_else(|| {
			if let Some(e) = rule_result.errors.first() {
				eprintln!("{}", format_diagnostic_error(e, &rule_source, sheet));
			}
			hint_bad_sheet(sheet, input, &config);
			CliError::ParseFailed
		})?;

		// Aggregate statistics across all files
		let mut aggregated_stats = HashMap::new();
		let mut error_count = 0;

		for css_file_path in input.iter() {
			let css_source = fs::read_to_string(css_file_path)?;
			let css_lexer = Lexer::new(&CssAtomSet::ATOMS, &css_source);
			let mut css_parser = Parser::new(&bump, &css_source, css_lexer);
			let css_result = css_parser.parse_entirely();

			let stylesheet = css_result.output.ok_or_else(|| {
				if let Some(e) = css_result.errors.first() {
					eprintln!("{}", format_diagnostic_error(e, &css_source, css_file_path));
				}
				CliError::ParseFailed
			})?;

			let mut collector = Collector::new(&parsed_rules, &rule_source, &bump);
			collector.collect(&stylesheet, &css_source);

			let mut file_failed = false;

			for diagnostic in collector.diagnostics(&css_source) {
				if matches!(diagnostic.severity, ResolvedDiagnosticLevel::Error) && !file_failed {
					error_count += 1;
					file_failed = true;
				}

				let handler = if config.colors() {
					let highlighter = CssHighlighter::new(css_source.clone(), &stylesheet);
					GraphicalReportHandler::new_themed(GraphicalTheme::unicode()).with_syntax_highlighting(highlighter)
				} else {
					GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor())
				};

				let miette_diag = diagnostic.into_miette();
				let named_source = NamedSource::new(css_file_path, css_source.clone());
				let report = Report::new(miette_diag).with_source_code(named_source);
				let mut output = String::new();
				if handler.render_report(&mut output, &*report).is_ok() {
					eprint!("{}", output);
				}
			}

			for (stat_name, (stat_type, count)) in collector.stats() {
				let entry = aggregated_stats.entry(*stat_name).or_insert((*stat_type, 0));
				entry.1 += count;
			}
		}

		// Output aggregated statistics summary
		if !aggregated_stats.is_empty() {
			println!("\nStatistics:");
			let mut stat_entries: Vec<_> = aggregated_stats
				.iter()
				.map(|(name, val)| (CsskitAtomSet::get_dyn_set().bits_to_str(name.as_bits()), val))
				.collect();
			stat_entries.sort_by_key(|(name, _)| *name);
			for (name, (stat_type, count)) in stat_entries {
				let type_label = match stat_type {
					StatType::Counter => "",
					StatType::Bytes => " bytes",
					StatType::Lines => " lines",
				};
				println!("  --{}: {}{}", name, count, type_label);
			}
		}

		// Return error if we encountered any error-level diagnostics
		if error_count > 0 { Err(CliError::Checks(error_count)) } else { Ok(()) }
	}
}

/// Find the first `.cks` file in cwd, or fall back to `rules.cks`.
fn find_cks_hint() -> String {
	if let Ok(entries) = fs::read_dir(".") {
		let mut names: Vec<String> = entries
			.flatten()
			.filter_map(|e| {
				let name = e.file_name().to_string_lossy().into_owned();
				name.ends_with(".cks").then_some(name)
			})
			.collect();
		names.sort();
		if let Some(name) = names.into_iter().next() {
			return name;
		}
	}
	"rules.cks".to_string()
}

fn maybe_color<F: Fn(&str) -> String>(colors: bool, s: &str, f: F) -> String {
	if colors { f(s) } else { s.to_string() }
}

/// No CSS input files provided. The `sheet` arg may itself be a CSS file.
fn hint_no_input(sheet: &str, config: &GlobalConfig) {
	let colors = config.colors();
	let error_label = maybe_color(colors, "error", |s| bold_red(s));
	let help_label = maybe_color(colors, "help", |s| bold_green(s));
	eprintln!("{}: no CSS files to check", error_label);
	eprintln!();
	eprintln!("{}: usage: csskit check <rules.cks> <file1.css> [more.css...]", help_label);

	if sheet.ends_with(".css") {
		let cks = find_cks_hint();
		let cmd = format!("csskit check {cks} {sheet}");
		let help_label = maybe_color(colors, "help", |s| bold_green(s));
		eprintln!(
			"{}: `{}` looks like a CSS file, did you mean `{}`?",
			help_label,
			sheet,
			maybe_color(colors, &cmd, |s| bold_green(s))
		);
	}
}

/// Sheet arg failed to parse; may be a CSS file passed in the wrong position.
fn hint_bad_sheet(sheet: &str, input: &[String], config: &GlobalConfig) {
	if !sheet.ends_with(".css") {
		return;
	}
	let colors = config.colors();
	let help_label = maybe_color(colors, "help", |s| bold_green(s));
	let cks = find_cks_hint();
	let all_css = std::iter::once(sheet).chain(input.iter().map(String::as_str)).collect::<Vec<_>>().join(" ");
	let cmd = format!("csskit check {cks} {all_css}");
	eprintln!();
	eprintln!(
		"{}: `{}` looks like a CSS file, did you mean `{}`?",
		help_label,
		sheet,
		maybe_color(colors, &cmd, |s| bold_green(s))
	);
}