use std::process;
use harn_fmt::{format_source_opts, FmtOptions};
use harn_parser::DiagnosticCode as Code;
use serde::Serialize;
use crate::json_envelope::{JsonEnvelope, JsonError};
pub(crate) const FMT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct FmtReport {
pub files: Vec<FmtFileReport>,
pub summary: FmtSummary,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct FmtFileReport {
pub path: String,
pub status: FmtFileStatus,
pub diff_lines_changed: usize,
pub diagnostics: Vec<FmtDiagnostic>,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum FmtFileStatus {
Formatted,
AlreadyFormatted,
#[allow(dead_code)]
Skipped,
Error,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct FmtDiagnostic {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct FmtSummary {
pub formatted: usize,
pub already_formatted: usize,
pub skipped: usize,
pub errors: usize,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum FmtMode {
Write,
Check,
}
impl FmtMode {
pub(crate) fn from_check_flag(check: bool) -> Self {
if check {
Self::Check
} else {
Self::Write
}
}
fn is_check(self) -> bool {
matches!(self, Self::Check)
}
}
pub(crate) fn fmt_targets(targets: &[&str], mode: FmtMode, opts: &FmtOptions) {
let report = fmt_targets_report(targets, mode, opts);
print_text_report(&report);
if report.summary.errors > 0 {
process::exit(1);
}
}
pub(crate) fn fmt_targets_json(
targets: &[&str],
mode: FmtMode,
opts: &FmtOptions,
) -> JsonEnvelope<FmtReport> {
let report = fmt_targets_report(targets, mode, opts);
if report.summary.errors > 0 {
JsonEnvelope {
schema_version: FMT_SCHEMA_VERSION,
ok: false,
data: Some(report),
error: Some(JsonError {
code: "fmt_failed".to_string(),
message: "one or more files failed formatting checks".to_string(),
details: serde_json::Value::Null,
}),
warnings: Vec::new(),
}
} else {
JsonEnvelope::ok(FMT_SCHEMA_VERSION, report)
}
}
pub(crate) fn fmt_targets_report(targets: &[&str], mode: FmtMode, opts: &FmtOptions) -> FmtReport {
let mut files = Vec::new();
for target in targets {
let path = std::path::Path::new(target);
if path.is_dir() {
super::super::collect_harn_files(path, &mut files);
} else {
files.push(path.to_path_buf());
}
}
if files.is_empty() {
return FmtReport {
files: Vec::new(),
summary: FmtSummary {
errors: 1,
..FmtSummary::default()
},
};
}
let mut report = FmtReport {
files: Vec::new(),
summary: FmtSummary::default(),
};
for file in files {
let path_str = file.to_string_lossy();
let file_report = fmt_file_inner(&path_str, mode, opts);
match file_report.status {
FmtFileStatus::Formatted => report.summary.formatted += 1,
FmtFileStatus::AlreadyFormatted => report.summary.already_formatted += 1,
FmtFileStatus::Skipped => report.summary.skipped += 1,
FmtFileStatus::Error => report.summary.errors += 1,
}
report.files.push(file_report);
}
report
}
fn fmt_file_inner(path: &str, mode: FmtMode, opts: &FmtOptions) -> FmtFileReport {
let source = match std::fs::read_to_string(path) {
Ok(source) => source,
Err(error) => return fmt_error(path, "io", format!("Error reading {path}: {error}")),
};
let formatted = match format_source_opts(&source, opts) {
Ok(formatted) => formatted,
Err(error) => return fmt_error(path, "format", format!("{path}: {error}")),
};
if mode.is_check() {
if source != formatted {
return FmtFileReport {
path: path.to_string(),
status: FmtFileStatus::Error,
diff_lines_changed: diff_lines_changed(&source, &formatted),
diagnostics: vec![FmtDiagnostic {
code: Code::FormatterWouldReformat.to_string(),
message: "would be reformatted".to_string(),
}],
};
}
} else if source != formatted {
if let Err(error) = std::fs::write(path, &formatted) {
return fmt_error(path, "io", format!("Error writing {path}: {error}"));
}
return FmtFileReport {
path: path.to_string(),
status: FmtFileStatus::Formatted,
diff_lines_changed: diff_lines_changed(&source, &formatted),
diagnostics: Vec::new(),
};
}
FmtFileReport {
path: path.to_string(),
status: FmtFileStatus::AlreadyFormatted,
diff_lines_changed: 0,
diagnostics: Vec::new(),
}
}
fn fmt_error(path: &str, code: &str, message: String) -> FmtFileReport {
FmtFileReport {
path: path.to_string(),
status: FmtFileStatus::Error,
diff_lines_changed: 0,
diagnostics: vec![FmtDiagnostic {
code: code.to_string(),
message,
}],
}
}
fn print_text_report(report: &FmtReport) {
if report.files.is_empty() {
eprintln!("No .harn files found");
return;
}
for file in &report.files {
match file.status {
FmtFileStatus::Formatted => println!("formatted {}", file.path),
FmtFileStatus::Error => {
for diagnostic in &file.diagnostics {
if diagnostic.code == Code::FormatterWouldReformat.to_string() {
eprintln!(
"{}: {}: {}",
file.path,
Code::FormatterWouldReformat,
diagnostic.message
);
} else {
eprintln!("{}", diagnostic.message);
}
}
}
FmtFileStatus::AlreadyFormatted | FmtFileStatus::Skipped => {}
}
}
}
fn diff_lines_changed(before: &str, after: &str) -> usize {
let before_lines: Vec<&str> = before.lines().collect();
let after_lines: Vec<&str> = after.lines().collect();
let max_len = before_lines.len().max(after_lines.len());
(0..max_len)
.filter(|index| before_lines.get(*index) != after_lines.get(*index))
.count()
}