morph_cli/core/format/
mod.rs1pub mod normalize;
2pub mod prettier;
3
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct FormatOptions {
9 pub enabled: bool,
10 pub use_prettier: bool,
11 pub preserve_indent: bool,
12 pub preserve_quotes: bool,
13 pub preserve_semicolons: bool,
14 pub normalize_newlines: bool,
15}
16
17impl FormatOptions {
18 pub fn from_args(format: bool, prettier: bool, no_format: bool) -> Self {
19 if no_format {
20 Self::disabled()
21 } else {
22 Self {
23 enabled: format || prettier,
24 use_prettier: prettier,
25 preserve_indent: true,
26 preserve_quotes: true,
27 preserve_semicolons: true,
28 normalize_newlines: true,
29 }
30 }
31 }
32
33 pub fn disabled() -> Self {
34 Self {
35 enabled: false,
36 use_prettier: false,
37 preserve_indent: false,
38 preserve_quotes: false,
39 preserve_semicolons: false,
40 normalize_newlines: false,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46pub struct FormatStats {
47 pub files_formatted: usize,
48 pub files_preserved: usize,
49 pub changes_detected: usize,
50 pub indentation_changes: usize,
51 pub quote_style_changes: usize,
52 pub newline_changes: usize,
53}
54
55impl FormatStats {
56 pub fn summary(&self) -> String {
57 format!(
58 "Formatting: {} formatted, {} preserved, {} total changes",
59 self.files_formatted, self.files_preserved, self.changes_detected
60 )
61 }
62}
63
64pub struct FormatPipeline {
65 options: FormatOptions,
66 stats: FormatStats,
67}
68
69impl FormatPipeline {
70 pub fn new(options: FormatOptions) -> Self {
71 Self {
72 options,
73 stats: FormatStats::default(),
74 }
75 }
76
77 pub fn format(&mut self, source: &str, original_source: Option<&str>, path: &Path) -> String {
78 if !self.options.enabled {
79 return source.to_string();
80 }
81
82 let mut result = source.to_string();
83
84 let profile = original_source.map(normalize::analyze_style);
85
86 if self.options.normalize_newlines {
87 result = normalize::normalize_newlines(&result);
88 self.stats.newline_changes += 1;
89 }
90
91 if let Some(ref prof) = profile {
92 if self.options.preserve_indent {
93 result = normalize::adjust_indentation(&result, &prof.indent);
94 self.stats.indentation_changes += 1;
95 }
96
97 if self.options.preserve_quotes {
98 result = normalize::process_quotes(&result, prof.quote_style.clone(), prof.jsx_quote_style.clone());
99 self.stats.quote_style_changes += 1;
100 }
101
102 if self.options.preserve_semicolons && !prof.semicolons {
103 result = normalize::strip_trailing_semicolons(&result);
104 }
105 }
106
107 if self.options.use_prettier {
108 if let Ok(formatted) = prettier::format_with_prettier(&result, path) {
109 result = formatted;
110 self.stats.files_formatted += 1;
111 } else {
112 self.stats.files_preserved += 1;
113 if let Some(orig) = original_source {
114 result = normalize::restore_blank_lines(&result, orig);
115 }
116 }
117 } else {
118 self.stats.files_preserved += 1;
119 if let Some(orig) = original_source {
120 result = normalize::restore_blank_lines(&result, orig);
121 }
122 }
123
124 self.stats.changes_detected += 1;
125 result
126 }
127
128 pub fn stats(&self) -> &FormatStats {
129 &self.stats
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_disabled_options() {
139 let opts = FormatOptions::disabled();
140 assert!(!opts.enabled);
141 }
142
143 #[test]
144 fn test_format_args() {
145 let opts = FormatOptions::from_args(true, false, false);
146 assert!(opts.enabled);
147 assert!(!opts.use_prettier);
148 }
149
150 #[test]
151 fn test_prettier_args() {
152 let opts = FormatOptions::from_args(false, true, false);
153 assert!(opts.enabled);
154 assert!(opts.use_prettier);
155 }
156
157 #[test]
158 fn test_no_format_args() {
159 let opts = FormatOptions::from_args(true, true, true);
160 assert!(!opts.enabled);
161 }
162
163 #[test]
164 fn test_format_stats() {
165 let stats = FormatStats {
166 files_formatted: 5,
167 files_preserved: 3,
168 changes_detected: 8,
169 indentation_changes: 2,
170 quote_style_changes: 1,
171 newline_changes: 3,
172 };
173 let summary = stats.summary();
174 assert!(summary.contains("formatted"));
175 assert!(summary.contains("preserved"));
176 }
177
178 #[test]
179 fn test_pipeline_disabled() {
180 let opts = FormatOptions::disabled();
181 let mut pipeline = FormatPipeline::new(opts);
182 let result = pipeline.format("const x = 1;", None, Path::new("test.js"));
183 assert_eq!(result, "const x = 1;");
184 }
185
186 #[test]
187 fn test_newline_normalization() {
188 let opts = FormatOptions {
189 enabled: true,
190 use_prettier: false,
191 preserve_indent: false,
192 preserve_quotes: false,
193 preserve_semicolons: false,
194 normalize_newlines: true,
195 };
196 let mut pipeline = FormatPipeline::new(opts);
197 let result = pipeline.format("line1\r\nline2\rline3", None, Path::new("test.js"));
198 assert!(!result.contains("\r"));
199 }
200}