pub mod normalize;
pub mod prettier;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FormatOptions {
pub enabled: bool,
pub use_prettier: bool,
pub preserve_indent: bool,
pub preserve_quotes: bool,
pub preserve_semicolons: bool,
pub normalize_newlines: bool,
}
impl FormatOptions {
pub fn from_args(format: bool, prettier: bool, no_format: bool) -> Self {
if no_format {
Self::disabled()
} else {
Self {
enabled: format || prettier,
use_prettier: prettier,
preserve_indent: true,
preserve_quotes: true,
preserve_semicolons: true,
normalize_newlines: true,
}
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
use_prettier: false,
preserve_indent: false,
preserve_quotes: false,
preserve_semicolons: false,
normalize_newlines: false,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FormatStats {
pub files_formatted: usize,
pub files_preserved: usize,
pub changes_detected: usize,
pub indentation_changes: usize,
pub quote_style_changes: usize,
pub newline_changes: usize,
}
impl FormatStats {
pub fn summary(&self) -> String {
format!(
"Formatting: {} formatted, {} preserved, {} total changes",
self.files_formatted, self.files_preserved, self.changes_detected
)
}
}
pub struct FormatPipeline {
options: FormatOptions,
stats: FormatStats,
}
impl FormatPipeline {
pub fn new(options: FormatOptions) -> Self {
Self {
options,
stats: FormatStats::default(),
}
}
pub fn format(&mut self, source: &str, original_source: Option<&str>, path: &Path) -> String {
if !self.options.enabled {
return source.to_string();
}
let mut result = source.to_string();
let profile = original_source.map(normalize::analyze_style);
if self.options.normalize_newlines {
result = normalize::normalize_newlines(&result);
self.stats.newline_changes += 1;
}
if let Some(ref prof) = profile {
if self.options.preserve_indent {
result = normalize::adjust_indentation(&result, &prof.indent);
self.stats.indentation_changes += 1;
}
if self.options.preserve_quotes {
result = normalize::process_quotes(&result, prof.quote_style.clone(), prof.jsx_quote_style.clone());
self.stats.quote_style_changes += 1;
}
if self.options.preserve_semicolons && !prof.semicolons {
result = normalize::strip_trailing_semicolons(&result);
}
}
if self.options.use_prettier {
if let Ok(formatted) = prettier::format_with_prettier(&result, path) {
result = formatted;
self.stats.files_formatted += 1;
} else {
self.stats.files_preserved += 1;
if let Some(orig) = original_source {
result = normalize::restore_blank_lines(&result, orig);
}
}
} else {
self.stats.files_preserved += 1;
if let Some(orig) = original_source {
result = normalize::restore_blank_lines(&result, orig);
}
}
self.stats.changes_detected += 1;
result
}
pub fn stats(&self) -> &FormatStats {
&self.stats
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_disabled_options() {
let opts = FormatOptions::disabled();
assert!(!opts.enabled);
}
#[test]
fn test_format_args() {
let opts = FormatOptions::from_args(true, false, false);
assert!(opts.enabled);
assert!(!opts.use_prettier);
}
#[test]
fn test_prettier_args() {
let opts = FormatOptions::from_args(false, true, false);
assert!(opts.enabled);
assert!(opts.use_prettier);
}
#[test]
fn test_no_format_args() {
let opts = FormatOptions::from_args(true, true, true);
assert!(!opts.enabled);
}
#[test]
fn test_format_stats() {
let stats = FormatStats {
files_formatted: 5,
files_preserved: 3,
changes_detected: 8,
indentation_changes: 2,
quote_style_changes: 1,
newline_changes: 3,
};
let summary = stats.summary();
assert!(summary.contains("formatted"));
assert!(summary.contains("preserved"));
}
#[test]
fn test_pipeline_disabled() {
let opts = FormatOptions::disabled();
let mut pipeline = FormatPipeline::new(opts);
let result = pipeline.format("const x = 1;", None, Path::new("test.js"));
assert_eq!(result, "const x = 1;");
}
#[test]
fn test_newline_normalization() {
let opts = FormatOptions {
enabled: true,
use_prettier: false,
preserve_indent: false,
preserve_quotes: false,
preserve_semicolons: false,
normalize_newlines: true,
};
let mut pipeline = FormatPipeline::new(opts);
let result = pipeline.format("line1\r\nline2\rline3", None, Path::new("test.js"));
assert!(!result.contains("\r"));
}
}