Skip to main content

vm/compiler/
format.rs

1use std::fmt;
2
3use super::{
4    CompileSourceFileOptions, ParseError, SourceFlavor, frontends, parser, source_map::SourceMap,
5};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum FormatError {
9    Parse(ParseError),
10    UnsupportedFlavor(SourceFlavor),
11}
12
13impl fmt::Display for FormatError {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        match self {
16            FormatError::Parse(err) => write!(f, "{err}"),
17            FormatError::UnsupportedFlavor(flavor) => {
18                write!(f, "formatting is unsupported for {flavor:?} source")
19            }
20        }
21    }
22}
23
24impl std::error::Error for FormatError {}
25
26pub fn format_source(source: &str) -> Result<String, FormatError> {
27    format_source_with_flavor(source, SourceFlavor::RustScript)
28}
29
30pub fn format_source_with_flavor(
31    source: &str,
32    flavor: SourceFlavor,
33) -> Result<String, FormatError> {
34    format_source_with_flavor_and_options(source, flavor, &CompileSourceFileOptions::default())
35}
36
37pub fn format_source_with_flavor_and_options(
38    source: &str,
39    flavor: SourceFlavor,
40    options: &CompileSourceFileOptions,
41) -> Result<String, FormatError> {
42    let Some(dialect) = frontends::parser_dialect_for_flavor(flavor, options) else {
43        return Err(FormatError::UnsupportedFlavor(flavor));
44    };
45
46    let mut source_map = SourceMap::new();
47    let source_id = source_map.add_source("<source>", source.to_string());
48    parser::format_source(source, dialect)
49        .map_err(|err| FormatError::Parse(err.with_line_span_from_source(&source_map, source_id)))
50}
51
52#[cfg(test)]
53mod tests {
54    use super::format_source_with_flavor;
55    use crate::compiler::SourceFlavor;
56
57    #[test]
58    fn keeps_tail_expression_addition_on_one_line() {
59        let input = "fn mix(seed) {\n    v\n        +\n        seed\n}\n";
60        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
61            .expect("formatting should succeed");
62
63        assert_eq!(formatted, "fn mix(seed) {\n    v + seed\n}\n");
64    }
65
66    #[test]
67    fn adds_space_before_closure_literals_after_assignment() {
68        let input = "let base = 7;\nlet add =|value| value + base;\n";
69        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
70            .expect("formatting should succeed");
71
72        assert_eq!(
73            formatted,
74            "let base = 7;\nlet add = |value| value + base;\n"
75        );
76    }
77
78    #[test]
79    fn adds_space_before_unary_bang_in_if_conditions() {
80        let input = concat!(
81            "use stdlib::rss::strings as string;\n\n",
82            "let total = if!string::non_empty(\"rustscript\") => {\n",
83            "    1\n",
84            "} else => {\n",
85            "    0\n",
86            "};\n"
87        );
88        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
89            .expect("formatting should succeed");
90
91        assert_eq!(
92            formatted,
93            concat!(
94                "use stdlib::rss::strings as string;\n\n",
95                "let total = if !string::non_empty(\"rustscript\") => {\n",
96                "    1\n",
97                "} else => {\n",
98                "    0\n",
99                "};\n"
100            )
101        );
102    }
103
104    #[test]
105    fn formats_rust_style_for_in_loop_head_on_one_line() {
106        let input = "for i in 0..3{\nvalue=value+i;\n}\n";
107        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
108            .expect("formatting should succeed");
109
110        assert_eq!(formatted, "for i in 0..3 {\n    value = value + i;\n}\n");
111    }
112
113    #[test]
114    fn adds_space_before_array_literals_after_assignment() {
115        let input = "let a =[1, \"a\"];\n";
116        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
117            .expect("formatting should succeed");
118
119        assert_eq!(formatted, "let a = [1, \"a\"];\n");
120    }
121
122    #[test]
123    fn adds_space_before_prefix_borrows_and_negation() {
124        let input = "let neg=-1;\nlet borrow=&mut value;\nconsume(a,&b,neg);\n";
125        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
126            .expect("formatting should succeed");
127
128        assert_eq!(
129            formatted,
130            "let neg = -1;\nlet borrow = &mut value;\nconsume(a, &b, neg);\n"
131        );
132    }
133
134    #[test]
135    fn keeps_namespace_keyword_calls_tight_to_open_paren() {
136        let input = "let regex_ok = re::match (\"(?i)^rustscript$\", \"RUSTSCRIPT\");\n";
137        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
138            .expect("formatting should succeed");
139
140        assert_eq!(
141            formatted,
142            "let regex_ok = re::match(\"(?i)^rustscript$\", \"RUSTSCRIPT\");\n"
143        );
144    }
145
146    #[test]
147    fn folds_long_comma_delimited_groups() {
148        let input = concat!(
149            "fn borrow_with_really_long_parameter_names(first_parameter, &second_parameter, third_parameter, fourth_parameter, fifth_parameter, sixth_parameter) {\n",
150            "    call_with_really_long_parameter_names(first_parameter, &second_parameter, third_parameter, fourth_parameter, fifth_parameter, sixth_parameter)\n",
151            "}\n"
152        );
153        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
154            .expect("formatting should succeed");
155
156        assert_eq!(
157            formatted,
158            concat!(
159                "fn borrow_with_really_long_parameter_names(\n",
160                "    first_parameter,\n",
161                "    &second_parameter,\n",
162                "    third_parameter,\n",
163                "    fourth_parameter,\n",
164                "    fifth_parameter,\n",
165                "    sixth_parameter\n",
166                ") {\n",
167                "    call_with_really_long_parameter_names(\n",
168                "        first_parameter,\n",
169                "        &second_parameter,\n",
170                "        third_parameter,\n",
171                "        fourth_parameter,\n",
172                "        fifth_parameter,\n",
173                "        sixth_parameter\n",
174                "    )\n",
175                "}\n"
176            )
177        );
178    }
179
180    #[test]
181    fn keeps_generic_type_params_and_turbofish_tight() {
182        let input = concat!(
183            "struct Box < T > {\n",
184            "    value: T\n",
185            "}\n",
186            "fn wrap < T >(value: Box < T >) {\n",
187            "    let decoded = json::decode:: < Box < T > >(payload);\n",
188            "    decoded\n",
189            "}\n"
190        );
191        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
192            .expect("formatting should succeed");
193
194        assert_eq!(
195            formatted,
196            concat!(
197                "struct Box<T> {\n",
198                "    value: T\n",
199                "}\n",
200                "fn wrap<T>(value: Box<T>) {\n",
201                "    let decoded = json::decode::<Box<T>>(payload);\n",
202                "    decoded\n",
203                "}\n"
204            )
205        );
206    }
207
208    #[test]
209    fn keeps_nested_generic_type_annotations_tight() {
210        let input = concat!(
211            "struct LinkState < V > {\n",
212            "    nodes: map < LruNode < V > >,\n",
213            "    head: int,\n",
214            "    tail: int\n",
215            "}\n",
216            "let cache: LruCacheState < FeedCacheItem > = new:: < FeedCacheItem >(3);\n",
217            "let relinked = append_existing_node:: < FeedCacheItem >(nodes, head, tail, id, node);\n"
218        );
219        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
220            .expect("formatting should succeed");
221
222        assert_eq!(
223            formatted,
224            concat!(
225                "struct LinkState<V> {\n",
226                "    nodes: map<LruNode<V>>,\n",
227                "    head: int,\n",
228                "    tail: int\n",
229                "}\n",
230                "let cache: LruCacheState<FeedCacheItem> = new::<FeedCacheItem>(3);\n",
231                "let relinked = append_existing_node::<FeedCacheItem>(nodes, head, tail, id, node);\n"
232            )
233        );
234    }
235
236    #[test]
237    fn keeps_multi_param_generic_fields_tight_inside_struct_bodies() {
238        let input = concat!(
239            "struct LruGetResult < K, V > {\n",
240            "    cache: LruCacheState < K,\n",
241            "    V >,\n",
242            "    found: bool,\n",
243            "    value: V\n",
244            "}\n"
245        );
246        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
247            .expect("formatting should succeed");
248
249        assert_eq!(
250            formatted,
251            concat!(
252                "struct LruGetResult<K, V> {\n",
253                "    cache: LruCacheState<K, V>,\n",
254                "    found: bool,\n",
255                "    value: V\n",
256                "}\n"
257            )
258        );
259    }
260
261    #[test]
262    fn keeps_multi_param_generic_calls_tight_inside_collection_literals() {
263        let input = concat!(
264            "let out = {\n",
265            "    cache: make_cache:: < K,\n",
266            "    V >(limit, size),\n",
267            "    found: false,\n",
268            "    value: null\n",
269            "};\n"
270        );
271        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
272            .expect("formatting should succeed");
273
274        assert_eq!(
275            formatted,
276            concat!(
277                "let out = {\n",
278                "    cache: make_cache::<K, V>(limit, size),\n",
279                "    found: false,\n",
280                "    value: null\n",
281                "};\n"
282            )
283        );
284    }
285
286    #[test]
287    fn keeps_space_before_grouped_expression_after_assignment() {
288        let input = "let mut next_node: LruNode<K, V> =(&next_nodes)[next_head];\n";
289        let formatted = format_source_with_flavor(input, SourceFlavor::RustScript)
290            .expect("formatting should succeed");
291
292        assert_eq!(
293            formatted,
294            "let mut next_node: LruNode<K, V> = (&next_nodes)[next_head];\n"
295        );
296    }
297}