Skip to main content

morph_cli/core/format/
mod.rs

1pub 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}