kcl_lib/
unparser.rs

1use std::fmt::Write;
2
3use crate::parsing::{
4    ast::types::{
5        Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
6        CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FormatOptions, FunctionExpression, IfExpression,
7        ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue,
8        MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter, PipeExpression,
9        Program, TagDeclarator, TypeDeclaration, UnaryExpression, VariableDeclaration, VariableKind,
10    },
11    deprecation, DeprecationKind, PIPE_OPERATOR,
12};
13
14impl Program {
15    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
16        let indentation = options.get_indentation(indentation_level);
17
18        let mut result = self
19            .shebang
20            .as_ref()
21            .map(|sh| format!("{}\n\n", sh.inner.content))
22            .unwrap_or_default();
23
24        for start in &self.non_code_meta.start_nodes {
25            result.push_str(&start.recast(options, indentation_level));
26        }
27        for attr in &self.inner_attrs {
28            result.push_str(&attr.recast(options, indentation_level));
29        }
30        if !self.inner_attrs.is_empty() {
31            result.push('\n');
32        }
33
34        let result = result; // Remove mutation.
35        let result = self
36            .body
37            .iter()
38            .map(|body_item| {
39                let mut result = String::new();
40                for comment in body_item.get_comments() {
41                    if !comment.is_empty() {
42                        result.push_str(&indentation);
43                        result.push_str(comment);
44                    }
45                    if !result.ends_with("\n\n") && result != "\n" {
46                        result.push('\n');
47                    }
48                }
49                for attr in body_item.get_attrs() {
50                    result.push_str(&attr.recast(options, indentation_level));
51                }
52                result.push_str(&match body_item.clone() {
53                    BodyItem::ImportStatement(stmt) => stmt.recast(options, indentation_level),
54                    BodyItem::ExpressionStatement(expression_statement) => {
55                        expression_statement
56                            .expression
57                            .recast(options, indentation_level, ExprContext::Other)
58                    }
59                    BodyItem::VariableDeclaration(variable_declaration) => {
60                        variable_declaration.recast(options, indentation_level)
61                    }
62                    BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(),
63                    BodyItem::ReturnStatement(return_statement) => {
64                        format!(
65                            "{}return {}",
66                            indentation,
67                            return_statement
68                                .argument
69                                .recast(options, indentation_level, ExprContext::Other)
70                                .trim_start()
71                        )
72                    }
73                });
74                result
75            })
76            .enumerate()
77            .fold(result, |mut output, (index, recast_str)| {
78                let start_string =
79                    if index == 0 && self.non_code_meta.start_nodes.is_empty() && self.inner_attrs.is_empty() {
80                        // We need to indent.
81                        indentation.to_string()
82                    } else {
83                        // Do nothing, we already applied the indentation elsewhere.
84                        String::new()
85                    };
86
87                // determine the value of the end string
88                // basically if we are inside a nested function we want to end with a new line
89                let maybe_line_break: String = if index == self.body.len() - 1 && indentation_level == 0 {
90                    String::new()
91                } else {
92                    "\n".to_string()
93                };
94
95                let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
96                    Some(noncodes) => noncodes
97                        .iter()
98                        .enumerate()
99                        .map(|(i, custom_white_space_or_comment)| {
100                            let formatted = custom_white_space_or_comment.recast(options, indentation_level);
101                            if i == 0 && !formatted.trim().is_empty() {
102                                if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
103                                    format!("\n{}", formatted)
104                                } else {
105                                    formatted
106                                }
107                            } else {
108                                formatted
109                            }
110                        })
111                        .collect::<String>(),
112                    None => String::new(),
113                };
114                let end_string = if custom_white_space_or_comment.is_empty() {
115                    maybe_line_break
116                } else {
117                    custom_white_space_or_comment
118                };
119
120                let _ = write!(output, "{}{}{}", start_string, recast_str, end_string);
121                output
122            })
123            .trim()
124            .to_string();
125
126        // Insert a final new line if the user wants it.
127        if options.insert_final_newline && !result.is_empty() {
128            format!("{}\n", result)
129        } else {
130            result
131        }
132    }
133}
134
135impl NonCodeValue {
136    fn should_cause_array_newline(&self) -> bool {
137        match self {
138            Self::InlineComment { .. } => false,
139            Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
140        }
141    }
142}
143
144impl Node<NonCodeNode> {
145    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
146        let indentation = options.get_indentation(indentation_level);
147        match &self.value {
148            NonCodeValue::InlineComment {
149                value,
150                style: CommentStyle::Line,
151            } => format!(" // {}\n", value),
152            NonCodeValue::InlineComment {
153                value,
154                style: CommentStyle::Block,
155            } => format!(" /* {} */", value),
156            NonCodeValue::BlockComment { value, style } => match style {
157                CommentStyle::Block => format!("{}/* {} */", indentation, value),
158                CommentStyle::Line => {
159                    if value.trim().is_empty() {
160                        format!("{}//\n", indentation)
161                    } else {
162                        format!("{}// {}\n", indentation, value.trim())
163                    }
164                }
165            },
166            NonCodeValue::NewLineBlockComment { value, style } => {
167                let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
168                match style {
169                    CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
170                    CommentStyle::Line => {
171                        if value.trim().is_empty() {
172                            format!("{}{}//\n", add_start_new_line, indentation)
173                        } else {
174                            format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
175                        }
176                    }
177                }
178            }
179            NonCodeValue::NewLine => "\n\n".to_string(),
180        }
181    }
182}
183
184impl Node<Annotation> {
185    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
186        let indentation = options.get_indentation(indentation_level);
187        let mut result = String::new();
188        for comment in &self.pre_comments {
189            if !comment.is_empty() {
190                result.push_str(&indentation);
191                result.push_str(comment);
192            }
193            if !result.ends_with("\n\n") && result != "\n" {
194                result.push('\n');
195            }
196        }
197        result.push('@');
198        if let Some(name) = &self.name {
199            result.push_str(&name.name);
200        }
201        if let Some(properties) = &self.properties {
202            result.push('(');
203            result.push_str(
204                &properties
205                    .iter()
206                    .map(|prop| {
207                        format!(
208                            "{} = {}",
209                            prop.key.name,
210                            prop.value
211                                .recast(options, indentation_level + 1, ExprContext::Other)
212                                .trim()
213                        )
214                    })
215                    .collect::<Vec<String>>()
216                    .join(", "),
217            );
218            result.push(')');
219            result.push('\n');
220        }
221
222        result
223    }
224}
225
226impl ImportStatement {
227    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
228        let indentation = options.get_indentation(indentation_level);
229        let vis = if self.visibility == ItemVisibility::Export {
230            "export "
231        } else {
232            ""
233        };
234        let mut string = format!("{}{}import ", vis, indentation);
235        match &self.selector {
236            ImportSelector::List { items } => {
237                for (i, item) in items.iter().enumerate() {
238                    if i > 0 {
239                        string.push_str(", ");
240                    }
241                    string.push_str(&item.name.name);
242                    if let Some(alias) = &item.alias {
243                        // If the alias is the same, don't output it.
244                        if item.name.name != alias.name {
245                            string.push_str(&format!(" as {}", alias.name));
246                        }
247                    }
248                }
249                string.push_str(" from ");
250            }
251            ImportSelector::Glob(_) => string.push_str("* from "),
252            ImportSelector::None { .. } => {}
253        }
254        string.push_str(&format!("\"{}\"", self.path));
255
256        if let ImportSelector::None { alias: Some(alias) } = &self.selector {
257            string.push_str(" as ");
258            string.push_str(&alias.name);
259        }
260        string
261    }
262}
263
264#[derive(Copy, Clone, Debug, Eq, PartialEq)]
265pub(crate) enum ExprContext {
266    Pipe,
267    Decl,
268    Other,
269}
270
271impl Expr {
272    pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String {
273        let is_decl = matches!(ctxt, ExprContext::Decl);
274        if is_decl {
275            // Just because this expression is being bound to a variable, doesn't mean that every child
276            // expression is being bound. So, reset the expression context if necessary.
277            // This will still preserve the "::Pipe" context though.
278            ctxt = ExprContext::Other;
279        }
280        match &self {
281            Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
282            Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
283            Expr::ArrayRangeExpression(range_exp) => range_exp.recast(options, indentation_level, ctxt),
284            Expr::ObjectExpression(ref obj_exp) => obj_exp.recast(options, indentation_level, ctxt),
285            Expr::MemberExpression(mem_exp) => mem_exp.recast(),
286            Expr::Literal(literal) => literal.recast(),
287            Expr::FunctionExpression(func_exp) => {
288                let mut result = if is_decl { String::new() } else { "fn".to_owned() };
289                result += &func_exp.recast(options, indentation_level);
290                result
291            }
292            Expr::CallExpressionKw(call_exp) => call_exp.recast(options, indentation_level, ctxt),
293            Expr::Name(name) => {
294                let result = name.to_string();
295                match deprecation(&result, DeprecationKind::Const) {
296                    Some(suggestion) => suggestion.to_owned(),
297                    None => result,
298                }
299            }
300            Expr::TagDeclarator(tag) => tag.recast(),
301            Expr::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
302            Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
303            Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
304            Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
305            Expr::LabelledExpression(e) => {
306                let mut result = e.expr.recast(options, indentation_level, ctxt);
307                result += " as ";
308                result += &e.label.name;
309                result
310            }
311            Expr::AscribedExpression(e) => {
312                let mut result = e.expr.recast(options, indentation_level, ctxt);
313                if matches!(
314                    e.expr,
315                    Expr::BinaryExpression(..) | Expr::PipeExpression(..) | Expr::UnaryExpression(..)
316                ) {
317                    result = format!("({result})");
318                }
319                result += ": ";
320                result += &e.ty.to_string();
321                result
322            }
323            Expr::None(_) => {
324                unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
325            }
326        }
327    }
328}
329
330impl BinaryPart {
331    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
332        match &self {
333            BinaryPart::Literal(literal) => literal.recast(),
334            BinaryPart::Name(name) => {
335                let result = name.to_string();
336                match deprecation(&result, DeprecationKind::Const) {
337                    Some(suggestion) => suggestion.to_owned(),
338                    None => result,
339                }
340            }
341            BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
342            BinaryPart::CallExpressionKw(call_expression) => {
343                call_expression.recast(options, indentation_level, ExprContext::Other)
344            }
345            BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
346            BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
347            BinaryPart::IfExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
348        }
349    }
350}
351
352impl CallExpressionKw {
353    fn recast_args(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> Vec<String> {
354        let mut arg_list = if let Some(first_arg) = &self.unlabeled {
355            vec![first_arg.recast(options, indentation_level, ctxt).trim().to_owned()]
356        } else {
357            Vec::with_capacity(self.arguments.len())
358        };
359        arg_list.extend(
360            self.arguments
361                .iter()
362                .map(|arg| arg.recast(options, indentation_level, ctxt)),
363        );
364        arg_list
365    }
366    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
367        let indent = if ctxt == ExprContext::Pipe {
368            "".to_string()
369        } else {
370            options.get_indentation(indentation_level)
371        };
372        let name = self.callee.to_string();
373
374        if let Some(suggestion) = deprecation(&name, DeprecationKind::Function) {
375            return format!("{indent}{suggestion}");
376        }
377
378        let arg_list = self.recast_args(options, indentation_level, ctxt);
379        let args = arg_list.clone().join(", ");
380        let has_lots_of_args = arg_list.len() >= 4;
381        let some_arg_is_already_multiline = arg_list.len() > 1 && arg_list.iter().any(|arg| arg.contains('\n'));
382        let multiline = has_lots_of_args || some_arg_is_already_multiline;
383        if multiline {
384            let next_indent = indentation_level + 1;
385            let inner_indentation = if ctxt == ExprContext::Pipe {
386                options.get_indentation_offset_pipe(next_indent)
387            } else {
388                options.get_indentation(next_indent)
389            };
390            let arg_list = self.recast_args(options, next_indent, ctxt);
391            let mut args = arg_list.join(&format!(",\n{inner_indentation}"));
392            args.push(',');
393            let args = args;
394            let end_indent = if ctxt == ExprContext::Pipe {
395                options.get_indentation_offset_pipe(indentation_level)
396            } else {
397                options.get_indentation(indentation_level)
398            };
399            format!("{indent}{name}(\n{inner_indentation}{args}\n{end_indent})")
400        } else {
401            format!("{indent}{name}({args})")
402        }
403    }
404}
405
406impl LabeledArg {
407    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
408        let label = &self.label.name;
409        let arg = self.arg.recast(options, indentation_level, ctxt);
410        format!("{label} = {arg}")
411    }
412}
413
414impl VariableDeclaration {
415    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
416        let indentation = options.get_indentation(indentation_level);
417        let mut output = match self.visibility {
418            ItemVisibility::Default => String::new(),
419            ItemVisibility::Export => "export ".to_owned(),
420        };
421
422        let (keyword, eq) = match self.kind {
423            VariableKind::Fn => ("fn ", ""),
424            VariableKind::Const => ("", " = "),
425        };
426        let _ = write!(
427            output,
428            "{}{keyword}{}{eq}{}",
429            indentation,
430            self.declaration.id.name,
431            self.declaration
432                .init
433                .recast(options, indentation_level, ExprContext::Decl)
434                .trim()
435        );
436        output
437    }
438}
439
440impl TypeDeclaration {
441    pub fn recast(&self) -> String {
442        let vis = match self.visibility {
443            ItemVisibility::Default => String::new(),
444            ItemVisibility::Export => "export ".to_owned(),
445        };
446
447        let mut arg_str = String::new();
448        if let Some(args) = &self.args {
449            arg_str.push('(');
450            for a in args {
451                if arg_str.len() > 1 {
452                    arg_str.push_str(", ");
453                }
454                arg_str.push_str(&a.name);
455            }
456            arg_str.push(')');
457        }
458        if let Some(alias) = &self.alias {
459            arg_str.push_str(" = ");
460            arg_str.push_str(&alias.to_string());
461        }
462        format!("{}type {}{}", vis, self.name.name, arg_str)
463    }
464}
465
466impl Literal {
467    fn recast(&self) -> String {
468        match self.value {
469            LiteralValue::Number { value, suffix } => {
470                if self.raw.contains('.') && value.fract() == 0.0 {
471                    format!("{value:?}{suffix}")
472                } else {
473                    self.raw.clone()
474                }
475            }
476            LiteralValue::String(ref s) => {
477                if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
478                    return suggestion.to_owned();
479                }
480                let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
481                format!("{quote}{s}{quote}")
482            }
483            LiteralValue::Bool(_) => self.raw.clone(),
484        }
485    }
486}
487
488impl TagDeclarator {
489    pub fn recast(&self) -> String {
490        // TagDeclarators are always prefixed with a dollar sign.
491        format!("${}", self.name)
492    }
493}
494
495impl ArrayExpression {
496    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
497        // Reconstruct the order of items in the array.
498        // An item can be an element (i.e. an expression for a KCL value),
499        // or a non-code item (e.g. a comment)
500        let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
501        let mut elems = self.elements.iter();
502        let mut found_line_comment = false;
503        let mut format_items: Vec<_> = (0..num_items)
504            .flat_map(|i| {
505                if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
506                    noncode
507                        .iter()
508                        .map(|nc| {
509                            found_line_comment |= nc.value.should_cause_array_newline();
510                            nc.recast(options, 0)
511                        })
512                        .collect::<Vec<_>>()
513                } else {
514                    let el = elems.next().unwrap();
515                    let s = format!("{}, ", el.recast(options, 0, ExprContext::Other));
516                    vec![s]
517                }
518            })
519            .collect();
520
521        // Format these items into a one-line array.
522        if let Some(item) = format_items.last_mut() {
523            if let Some(norm) = item.strip_suffix(", ") {
524                *item = norm.to_owned();
525            }
526        }
527        let format_items = format_items; // Remove mutability
528        let flat_recast = format!("[{}]", format_items.join(""));
529
530        // We might keep the one-line representation, if it's short enough.
531        let max_array_length = 40;
532        let multi_line = flat_recast.len() > max_array_length || found_line_comment;
533        if !multi_line {
534            return flat_recast;
535        }
536
537        // Otherwise, we format a multi-line representation.
538        let inner_indentation = if ctxt == ExprContext::Pipe {
539            options.get_indentation_offset_pipe(indentation_level + 1)
540        } else {
541            options.get_indentation(indentation_level + 1)
542        };
543        let formatted_array_lines = format_items
544            .iter()
545            .map(|s| {
546                format!(
547                    "{inner_indentation}{}{}",
548                    if let Some(x) = s.strip_suffix(" ") { x } else { s },
549                    if s.ends_with('\n') { "" } else { "\n" }
550                )
551            })
552            .collect::<Vec<String>>()
553            .join("")
554            .to_owned();
555        let end_indent = if ctxt == ExprContext::Pipe {
556            options.get_indentation_offset_pipe(indentation_level)
557        } else {
558            options.get_indentation(indentation_level)
559        };
560        format!("[\n{formatted_array_lines}{end_indent}]")
561    }
562}
563
564/// An expression is syntactically trivial: i.e., a literal, identifier, or similar.
565fn expr_is_trivial(expr: &Expr) -> bool {
566    matches!(
567        expr,
568        Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
569    )
570}
571
572impl ArrayRangeExpression {
573    fn recast(&self, options: &FormatOptions, _: usize, _: ExprContext) -> String {
574        let s1 = self.start_element.recast(options, 0, ExprContext::Other);
575        let s2 = self.end_element.recast(options, 0, ExprContext::Other);
576
577        // Format these items into a one-line array. Put spaces around the `..` if either expression
578        // is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
579        // tightly, but this is a misleading visual representation of the precedence if the range
580        // components are compound expressions.
581        if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
582            format!("[{s1}..{s2}]")
583        } else {
584            format!("[{s1} .. {s2}]")
585        }
586
587        // Assume a range expression fits on one line.
588    }
589}
590
591impl ObjectExpression {
592    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
593        if self
594            .non_code_meta
595            .non_code_nodes
596            .values()
597            .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
598        {
599            return self.recast_multi_line(options, indentation_level, ctxt);
600        }
601        let flat_recast = format!(
602            "{{ {} }}",
603            self.properties
604                .iter()
605                .map(|prop| {
606                    format!(
607                        "{} = {}",
608                        prop.key.name,
609                        prop.value.recast(options, indentation_level + 1, ctxt).trim()
610                    )
611                })
612                .collect::<Vec<String>>()
613                .join(", ")
614        );
615        let max_array_length = 40;
616        let needs_multiple_lines = flat_recast.len() > max_array_length;
617        if !needs_multiple_lines {
618            return flat_recast;
619        }
620        self.recast_multi_line(options, indentation_level, ctxt)
621    }
622
623    /// Recast, but always outputs the object with newlines between each property.
624    fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
625        let inner_indentation = if ctxt == ExprContext::Pipe {
626            options.get_indentation_offset_pipe(indentation_level + 1)
627        } else {
628            options.get_indentation(indentation_level + 1)
629        };
630        let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
631        let mut props = self.properties.iter();
632        let format_items: Vec<_> = (0..num_items)
633            .flat_map(|i| {
634                if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
635                    noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
636                } else {
637                    let prop = props.next().unwrap();
638                    // Use a comma unless it's the last item
639                    let comma = if i == num_items - 1 { "" } else { ",\n" };
640                    let s = format!(
641                        "{} = {}{comma}",
642                        prop.key.name,
643                        prop.value.recast(options, indentation_level + 1, ctxt).trim()
644                    );
645                    vec![s]
646                }
647            })
648            .collect();
649        let end_indent = if ctxt == ExprContext::Pipe {
650            options.get_indentation_offset_pipe(indentation_level)
651        } else {
652            options.get_indentation(indentation_level)
653        };
654        format!(
655            "{{\n{inner_indentation}{}\n{end_indent}}}",
656            format_items.join(&inner_indentation),
657        )
658    }
659}
660
661impl MemberExpression {
662    fn recast(&self) -> String {
663        let key_str = match &self.property {
664            LiteralIdentifier::Identifier(identifier) => {
665                if self.computed {
666                    format!("[{}]", &(*identifier.name))
667                } else {
668                    format!(".{}", &(*identifier.name))
669                }
670            }
671            LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
672        };
673
674        match &self.object {
675            MemberObject::MemberExpression(member_exp) => member_exp.recast() + key_str.as_str(),
676            MemberObject::Identifier(identifier) => identifier.name.to_string() + key_str.as_str(),
677        }
678    }
679}
680
681impl BinaryExpression {
682    fn recast(&self, options: &FormatOptions) -> String {
683        let maybe_wrap_it = |a: String, doit: bool| -> String {
684            if doit {
685                format!("({})", a)
686            } else {
687                a
688            }
689        };
690
691        let should_wrap_right = match &self.right {
692            BinaryPart::BinaryExpression(bin_exp) => {
693                self.precedence() > bin_exp.precedence()
694                    || self.operator == BinaryOperator::Sub
695                    || self.operator == BinaryOperator::Div
696            }
697            _ => false,
698        };
699
700        let should_wrap_left = match &self.left {
701            BinaryPart::BinaryExpression(bin_exp) => self.precedence() > bin_exp.precedence(),
702            _ => false,
703        };
704
705        format!(
706            "{} {} {}",
707            maybe_wrap_it(self.left.recast(options, 0), should_wrap_left),
708            self.operator,
709            maybe_wrap_it(self.right.recast(options, 0), should_wrap_right)
710        )
711    }
712}
713
714impl UnaryExpression {
715    fn recast(&self, options: &FormatOptions) -> String {
716        match self.argument {
717            BinaryPart::Literal(_)
718            | BinaryPart::Name(_)
719            | BinaryPart::MemberExpression(_)
720            | BinaryPart::IfExpression(_)
721            | BinaryPart::CallExpressionKw(_) => {
722                format!("{}{}", &self.operator, self.argument.recast(options, 0))
723            }
724            BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
725                format!("{}({})", &self.operator, self.argument.recast(options, 0))
726            }
727        }
728    }
729}
730
731impl IfExpression {
732    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
733        // We can calculate how many lines this will take, so let's do it and avoid growing the vec.
734        // Total lines = starting lines, else-if lines, ending lines.
735        let n = 2 + (self.else_ifs.len() * 2) + 3;
736        let mut lines = Vec::with_capacity(n);
737
738        let cond = self.cond.recast(options, indentation_level, ctxt);
739        lines.push((0, format!("if {cond} {{")));
740        lines.push((1, self.then_val.recast(options, indentation_level + 1)));
741        for else_if in &self.else_ifs {
742            let cond = else_if.cond.recast(options, indentation_level, ctxt);
743            lines.push((0, format!("}} else if {cond} {{")));
744            lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
745        }
746        lines.push((0, "} else {".to_owned()));
747        lines.push((1, self.final_else.recast(options, indentation_level + 1)));
748        lines.push((0, "}".to_owned()));
749        lines
750            .into_iter()
751            .map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
752            .collect::<Vec<_>>()
753            .join("\n")
754    }
755}
756
757impl Node<PipeExpression> {
758    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
759        let pipe = self
760            .body
761            .iter()
762            .enumerate()
763            .map(|(index, statement)| {
764                let indentation = options.get_indentation(indentation_level + 1);
765                let mut s = statement.recast(options, indentation_level + 1, ExprContext::Pipe);
766                let non_code_meta = self.non_code_meta.clone();
767                if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
768                    for val in non_code_meta_value {
769                        let formatted = if val.end == self.end {
770                            val.recast(options, indentation_level)
771                                .trim_end_matches('\n')
772                                .to_string()
773                        } else {
774                            val.recast(options, indentation_level + 1)
775                                .trim_end_matches('\n')
776                                .to_string()
777                        };
778                        if let NonCodeValue::BlockComment { .. } = val.value {
779                            s += "\n";
780                            s += &formatted;
781                        } else {
782                            s += &formatted;
783                        }
784                    }
785                }
786
787                if index != self.body.len() - 1 {
788                    s += "\n";
789                    s += &indentation;
790                    s += PIPE_OPERATOR;
791                    s += " ";
792                }
793                s
794            })
795            .collect::<String>();
796        format!("{}{}", options.get_indentation(indentation_level), pipe)
797    }
798}
799
800impl FunctionExpression {
801    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
802        // We don't want to end with a new line inside nested functions.
803        let mut new_options = options.clone();
804        new_options.insert_final_newline = false;
805        let param_list = self
806            .params
807            .iter()
808            .map(|param| param.recast(options, indentation_level))
809            .collect::<Vec<String>>()
810            .join(", ");
811        let tab0 = options.get_indentation(indentation_level);
812        let tab1 = options.get_indentation(indentation_level + 1);
813        let return_type = match &self.return_type {
814            Some(rt) => format!(": {rt}"),
815            None => String::new(),
816        };
817        let body = self.body.recast(&new_options, indentation_level + 1);
818
819        format!("({param_list}){return_type} {{\n{tab1}{body}\n{tab0}}}")
820    }
821}
822
823impl Parameter {
824    pub fn recast(&self, _options: &FormatOptions, _indentation_level: usize) -> String {
825        let at_sign = if self.labeled { "" } else { "@" };
826        let identifier = &self.identifier.name;
827        let question_mark = if self.default_value.is_some() { "?" } else { "" };
828        let mut result = format!("{at_sign}{identifier}{question_mark}");
829        if let Some(ty) = &self.type_ {
830            result += ": ";
831            result += &ty.to_string();
832        }
833        if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
834            let lit = literal.recast();
835            result.push_str(&format!(" = {lit}"));
836        };
837
838        result
839    }
840}
841
842/// Collect all the kcl (and other relevant) files in a directory, recursively.
843#[cfg(not(target_arch = "wasm32"))]
844#[async_recursion::async_recursion]
845pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
846    // Make sure we actually have a directory.
847    if !dir.is_dir() {
848        anyhow::bail!("`{}` is not a directory", dir.display());
849    }
850
851    let mut entries = tokio::fs::read_dir(dir).await?;
852
853    let mut files = Vec::new();
854    while let Some(entry) = entries.next_entry().await? {
855        let path = entry.path();
856
857        if path.is_dir() {
858            files.extend(walk_dir(&path).await?);
859        } else if path
860            .extension()
861            .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
862        {
863            files.push(path);
864        }
865    }
866
867    Ok(files)
868}
869
870/// Recast all the kcl files in a directory, recursively.
871#[cfg(not(target_arch = "wasm32"))]
872pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
873    let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
874        crate::KclError::Internal(crate::errors::KclErrorDetails {
875            message: format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
876            source_ranges: vec![crate::SourceRange::default()],
877        })
878    })?;
879
880    let futures = files
881        .into_iter()
882        .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) // We only care about kcl
883        // files here.
884        .map(|file| {
885            let options = options.clone();
886            tokio::spawn(async move {
887                let contents = tokio::fs::read_to_string(&file)
888                    .await
889                    .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
890                let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
891                    let report = crate::Report {
892                        kcl_source: contents.to_string(),
893                        error: err.clone(),
894                        filename: file.to_string_lossy().to_string(),
895                    };
896                    let report = miette::Report::new(report);
897                    anyhow::anyhow!("{:?}", report)
898                })?;
899                for ce in &ces {
900                    if ce.severity != crate::errors::Severity::Warning {
901                        let report = crate::Report {
902                            kcl_source: contents.to_string(),
903                            error: crate::KclError::Semantic(ce.clone().into()),
904                            filename: file.to_string_lossy().to_string(),
905                        };
906                        let report = miette::Report::new(report);
907                        anyhow::bail!("{:?}", report);
908                    }
909                }
910                let Some(program) = program else {
911                    anyhow::bail!("Failed to parse file `{}`", file.display());
912                };
913                let recast = program.recast_with_options(&options);
914                tokio::fs::write(&file, recast)
915                    .await
916                    .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
917
918                Ok::<(), anyhow::Error>(())
919            })
920        })
921        .collect::<Vec<_>>();
922
923    // Join all futures and await their completion
924    let results = futures::future::join_all(futures).await;
925
926    // Check if any of the futures failed.
927    let mut errors = Vec::new();
928    for result in results {
929        if let Err(err) = result? {
930            errors.push(err);
931        }
932    }
933
934    if !errors.is_empty() {
935        anyhow::bail!("Failed to recast some files: {:?}", errors);
936    }
937
938    Ok(())
939}
940
941#[cfg(test)]
942mod tests {
943    use pretty_assertions::assert_eq;
944
945    use super::*;
946    use crate::{parsing::ast::types::FormatOptions, ModuleId};
947
948    #[test]
949    fn test_recast_annotations_without_body_items() {
950        let input = r#"@settings(defaultLengthUnit = in)
951"#;
952        let program = crate::parsing::top_level_parse(input).unwrap();
953        let output = program.recast(&Default::default(), 0);
954        assert_eq!(output, input);
955    }
956
957    #[test]
958    fn test_recast_annotations_in_function_body() {
959        let input = r#"fn myFunc() {
960  @meta(yes = true)
961
962  x = 2
963}
964"#;
965        let program = crate::parsing::top_level_parse(input).unwrap();
966        let output = program.recast(&Default::default(), 0);
967        assert_eq!(output, input);
968    }
969
970    #[test]
971    fn test_recast_annotations_in_function_body_without_items() {
972        let input = r#"fn myFunc() {
973  @meta(yes = true)
974}
975"#;
976        let program = crate::parsing::top_level_parse(input).unwrap();
977        let output = program.recast(&Default::default(), 0);
978        assert_eq!(output, input);
979    }
980
981    #[test]
982    fn recast_annotations_with_comments() {
983        let input = r#"// Start comment
984
985// Comment on attr
986@settings(defaultLengthUnit = in)
987
988// Comment on item
989foo = 42
990
991// Comment on another item
992@(impl = kcl)
993bar = 0
994"#;
995        let program = crate::parsing::top_level_parse(input).unwrap();
996        let output = program.recast(&Default::default(), 0);
997        assert_eq!(output, input);
998    }
999
1000    #[test]
1001    fn recast_annotations_with_block_comment() {
1002        let input = r#"/* Start comment
1003
1004sdfsdfsdfs */
1005@settings(defaultLengthUnit = in)
1006
1007foo = 42
1008"#;
1009        let program = crate::parsing::top_level_parse(input).unwrap();
1010        let output = program.recast(&Default::default(), 0);
1011        assert_eq!(output, input);
1012    }
1013
1014    #[test]
1015    fn test_recast_if_else_if_same() {
1016        let input = r#"b = if false {
1017  3
1018} else if true {
1019  4
1020} else {
1021  5
1022}
1023"#;
1024        let program = crate::parsing::top_level_parse(input).unwrap();
1025        let output = program.recast(&Default::default(), 0);
1026        assert_eq!(output, input);
1027    }
1028
1029    #[test]
1030    fn test_recast_if_same() {
1031        let input = r#"b = if false {
1032  3
1033} else {
1034  5
1035}
1036"#;
1037        let program = crate::parsing::top_level_parse(input).unwrap();
1038        let output = program.recast(&Default::default(), 0);
1039        assert_eq!(output, input);
1040    }
1041
1042    #[test]
1043    fn test_recast_import() {
1044        let input = r#"import a from "a.kcl"
1045import a as aaa from "a.kcl"
1046import a, b from "a.kcl"
1047import a as aaa, b from "a.kcl"
1048import a, b as bbb from "a.kcl"
1049import a as aaa, b as bbb from "a.kcl"
1050import "a_b.kcl"
1051import "a-b.kcl" as b
1052import * from "a.kcl"
1053export import a as aaa from "a.kcl"
1054export import a, b from "a.kcl"
1055export import a as aaa, b from "a.kcl"
1056export import a, b as bbb from "a.kcl"
1057"#;
1058        let program = crate::parsing::top_level_parse(input).unwrap();
1059        let output = program.recast(&Default::default(), 0);
1060        assert_eq!(output, input);
1061    }
1062
1063    #[test]
1064    fn test_recast_import_as_same_name() {
1065        let input = r#"import a as a from "a.kcl"
1066"#;
1067        let program = crate::parsing::top_level_parse(input).unwrap();
1068        let output = program.recast(&Default::default(), 0);
1069        let expected = r#"import a from "a.kcl"
1070"#;
1071        assert_eq!(output, expected);
1072    }
1073
1074    #[test]
1075    fn test_recast_export_fn() {
1076        let input = r#"export fn a() {
1077  return 0
1078}
1079"#;
1080        let program = crate::parsing::top_level_parse(input).unwrap();
1081        let output = program.recast(&Default::default(), 0);
1082        assert_eq!(output, input);
1083    }
1084
1085    #[test]
1086    fn test_recast_bug_fn_in_fn() {
1087        let some_program_string = r#"// Start point (top left)
1088zoo_x = -20
1089zoo_y = 7
1090// Scale
1091s = 1 // s = 1 -> height of Z is 13.4mm
1092// Depth
1093d = 1
1094
1095fn rect(x, y, w, h) {
1096  startSketchOn(XY)
1097    |> startProfile(at = [x, y])
1098    |> xLine(length = w)
1099    |> yLine(length = h)
1100    |> xLine(length = -w)
1101    |> close()
1102    |> extrude(d)
1103}
1104
1105fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1106  startSketchOn(XY)
1107    |> startProfile(at = [x1, y1])
1108    |> line(endAbsolute = [x2, y2])
1109    |> line(endAbsolute = [x3, y3])
1110    |> line(endAbsolute = [x4, y4])
1111    |> close()
1112    |> extrude(d)
1113}
1114
1115fn crosshair(x, y) {
1116  startSketchOn(XY)
1117    |> startProfile(at = [x, y])
1118    |> yLine(length = 1)
1119    |> yLine(length = -2)
1120    |> yLine(length = 1)
1121    |> xLine(length = 1)
1122    |> xLine(length = -2)
1123}
1124
1125fn z(z_x, z_y) {
1126  z_end_w = s * 8.4
1127  z_end_h = s * 3
1128  z_corner = s * 2
1129  z_w = z_end_w + 2 * z_corner
1130  z_h = z_w * 1.08130081300813
1131  rect(
1132    z_x,
1133    a = z_y,
1134    b = z_end_w,
1135    c = -z_end_h,
1136  )
1137  rect(
1138    z_x + z_w,
1139    a = z_y,
1140    b = -z_corner,
1141    c = -z_corner,
1142  )
1143  rect(
1144    z_x + z_w,
1145    a = z_y - z_h,
1146    b = -z_end_w,
1147    c = z_end_h,
1148  )
1149  rect(
1150    z_x,
1151    a = z_y - z_h,
1152    b = z_corner,
1153    c = z_corner,
1154  )
1155}
1156
1157fn o(c_x, c_y) {
1158  // Outer and inner radii
1159  o_r = s * 6.95
1160  i_r = 0.5652173913043478 * o_r
1161
1162  // Angle offset for diagonal break
1163  a = 7
1164
1165  // Start point for the top sketch
1166  o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
1167  o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
1168
1169  // Start point for the bottom sketch
1170  o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
1171  o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
1172
1173  // End point for the bottom startSketch
1174  o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
1175  o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
1176
1177  // Where is the center?
1178  // crosshair(c_x, c_y)
1179
1180
1181  startSketchOn(XY)
1182    |> startProfile(at = [o_x1, o_y1])
1183    |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
1184    |> angledLine(angle = 45, length = o_r - i_r)
1185    |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
1186    |> close()
1187    |> extrude(d)
1188
1189  startSketchOn(XY)
1190    |> startProfile(at = [o_x2, o_y2])
1191    |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
1192    |> angledLine(angle = 225, length = o_r - i_r)
1193    |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
1194    |> close()
1195    |> extrude(d)
1196}
1197
1198fn zoo(x0, y0) {
1199  z(x = x0, y = y0)
1200  o(x = x0 + s * 20, y = y0 - (s * 6.7))
1201  o(x = x0 + s * 35, y = y0 - (s * 6.7))
1202}
1203
1204zoo(x = zoo_x, y = zoo_y)
1205"#;
1206        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1207
1208        let recasted = program.recast(&Default::default(), 0);
1209        assert_eq!(recasted, some_program_string);
1210    }
1211
1212    #[test]
1213    fn test_recast_bug_extra_parens() {
1214        let some_program_string = r#"// Ball Bearing
1215// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads. 
1216
1217// Define constants like ball diameter, inside diameter, overhange length, and thickness
1218sphereDia = 0.5
1219insideDia = 1
1220thickness = 0.25
1221overHangLength = .4
1222
1223// Sketch and revolve the inside bearing piece
1224insideRevolve = startSketchOn(XZ)
1225  |> startProfile(at = [insideDia / 2, 0])
1226  |> line(end = [0, thickness + sphereDia / 2])
1227  |> line(end = [overHangLength, 0])
1228  |> line(end = [0, -thickness])
1229  |> line(end = [-overHangLength + thickness, 0])
1230  |> line(end = [0, -sphereDia])
1231  |> line(end = [overHangLength - thickness, 0])
1232  |> line(end = [0, -thickness])
1233  |> line(end = [-overHangLength, 0])
1234  |> close()
1235  |> revolve(axis = Y)
1236
1237// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
1238sphere = startSketchOn(XZ)
1239  |> startProfile(at = [
1240       0.05 + insideDia / 2 + thickness,
1241       0 - 0.05
1242     ])
1243  |> line(end = [sphereDia - 0.1, 0])
1244  |> arc(
1245       angle_start = 0,
1246       angle_end = -180,
1247       radius = sphereDia / 2 - 0.05
1248     )
1249  |> close()
1250  |> revolve(axis = X)
1251  |> patternCircular3d(
1252       axis = [0, 0, 1],
1253       center = [0, 0, 0],
1254       repetitions = 10,
1255       arcDegrees = 360,
1256       rotateDuplicates = true
1257     )
1258
1259// Sketch and revolve the outside bearing
1260outsideRevolve = startSketchOn(XZ)
1261  |> startProfile(at = [
1262       insideDia / 2 + thickness + sphereDia,
1263       0
1264       ]
1265     )
1266  |> line(end = [0, sphereDia / 2])
1267  |> line(end = [-overHangLength + thickness, 0])
1268  |> line(end = [0, thickness])
1269  |> line(end = [overHangLength, 0])
1270  |> line(end = [0, -2 * thickness - sphereDia])
1271  |> line(end = [-overHangLength, 0])
1272  |> line(end = [0, thickness])
1273  |> line(end = [overHangLength - thickness, 0])
1274  |> close()
1275  |> revolve(axis = Y)"#;
1276        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1277
1278        let recasted = program.recast(&Default::default(), 0);
1279        assert_eq!(
1280            recasted,
1281            r#"// Ball Bearing
1282// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
1283
1284// Define constants like ball diameter, inside diameter, overhange length, and thickness
1285sphereDia = 0.5
1286insideDia = 1
1287thickness = 0.25
1288overHangLength = .4
1289
1290// Sketch and revolve the inside bearing piece
1291insideRevolve = startSketchOn(XZ)
1292  |> startProfile(at = [insideDia / 2, 0])
1293  |> line(end = [0, thickness + sphereDia / 2])
1294  |> line(end = [overHangLength, 0])
1295  |> line(end = [0, -thickness])
1296  |> line(end = [-overHangLength + thickness, 0])
1297  |> line(end = [0, -sphereDia])
1298  |> line(end = [overHangLength - thickness, 0])
1299  |> line(end = [0, -thickness])
1300  |> line(end = [-overHangLength, 0])
1301  |> close()
1302  |> revolve(axis = Y)
1303
1304// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
1305sphere = startSketchOn(XZ)
1306  |> startProfile(at = [
1307       0.05 + insideDia / 2 + thickness,
1308       0 - 0.05
1309     ])
1310  |> line(end = [sphereDia - 0.1, 0])
1311  |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
1312  |> close()
1313  |> revolve(axis = X)
1314  |> patternCircular3d(
1315       axis = [0, 0, 1],
1316       center = [0, 0, 0],
1317       repetitions = 10,
1318       arcDegrees = 360,
1319       rotateDuplicates = true,
1320     )
1321
1322// Sketch and revolve the outside bearing
1323outsideRevolve = startSketchOn(XZ)
1324  |> startProfile(at = [
1325       insideDia / 2 + thickness + sphereDia,
1326       0
1327     ])
1328  |> line(end = [0, sphereDia / 2])
1329  |> line(end = [-overHangLength + thickness, 0])
1330  |> line(end = [0, thickness])
1331  |> line(end = [overHangLength, 0])
1332  |> line(end = [0, -2 * thickness - sphereDia])
1333  |> line(end = [-overHangLength, 0])
1334  |> line(end = [0, thickness])
1335  |> line(end = [overHangLength - thickness, 0])
1336  |> close()
1337  |> revolve(axis = Y)
1338"#
1339        );
1340    }
1341
1342    #[test]
1343    fn test_recast_fn_in_object() {
1344        let some_program_string = r#"bing = { yo = 55 }
1345myNestedVar = [{ prop = callExp(bing.yo) }]
1346"#;
1347        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1348
1349        let recasted = program.recast(&Default::default(), 0);
1350        assert_eq!(recasted, some_program_string);
1351    }
1352
1353    #[test]
1354    fn test_recast_fn_in_array() {
1355        let some_program_string = r#"bing = { yo = 55 }
1356myNestedVar = [callExp(bing.yo)]
1357"#;
1358        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1359
1360        let recasted = program.recast(&Default::default(), 0);
1361        assert_eq!(recasted, some_program_string);
1362    }
1363
1364    #[test]
1365    fn test_recast_ranges() {
1366        let some_program_string = r#"foo = [0..10]
1367ten = 10
1368bar = [0 + 1 .. ten]
1369"#;
1370        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1371
1372        let recasted = program.recast(&Default::default(), 0);
1373        assert_eq!(recasted, some_program_string);
1374    }
1375
1376    #[test]
1377    fn test_recast_space_in_fn_call() {
1378        let some_program_string = r#"fn thing (x) {
1379    return x + 1
1380}
1381
1382thing ( 1 )
1383"#;
1384        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1385
1386        let recasted = program.recast(&Default::default(), 0);
1387        assert_eq!(
1388            recasted,
1389            r#"fn thing(x) {
1390  return x + 1
1391}
1392
1393thing(1)
1394"#
1395        );
1396    }
1397
1398    #[test]
1399    fn test_recast_typed_fn() {
1400        let some_program_string = r#"fn thing(x: string, y: [bool]): number {
1401  return x + 1
1402}
1403"#;
1404        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1405
1406        let recasted = program.recast(&Default::default(), 0);
1407        assert_eq!(recasted, some_program_string);
1408    }
1409
1410    #[test]
1411    fn test_recast_typed_consts() {
1412        let some_program_string = r#"a = 42: number
1413export b = 3.2: number(ft)
1414c = "dsfds": A | B | C
1415d = [1]: [number]
1416e = foo: [number; 3]
1417f = [1, 2, 3]: [number; 1+]
1418"#;
1419        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1420
1421        let recasted = program.recast(&Default::default(), 0);
1422        assert_eq!(recasted, some_program_string);
1423    }
1424
1425    #[test]
1426    fn test_recast_object_fn_in_array_weird_bracket() {
1427        let some_program_string = r#"bing = { yo = 55 }
1428myNestedVar = [
1429  {
1430  prop:   line(a = [bing.yo, 21], b = sketch001)
1431}
1432]
1433"#;
1434        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1435
1436        let recasted = program.recast(&Default::default(), 0);
1437        assert_eq!(
1438            recasted,
1439            r#"bing = { yo = 55 }
1440myNestedVar = [
1441  {
1442  prop = line(a = [bing.yo, 21], b = sketch001)
1443}
1444]
1445"#
1446        );
1447    }
1448
1449    #[test]
1450    fn test_recast_empty_file() {
1451        let some_program_string = r#""#;
1452        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1453
1454        let recasted = program.recast(&Default::default(), 0);
1455        // Its VERY important this comes back with zero new lines.
1456        assert_eq!(recasted, r#""#);
1457    }
1458
1459    #[test]
1460    fn test_recast_empty_file_new_line() {
1461        let some_program_string = r#"
1462"#;
1463        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1464
1465        let recasted = program.recast(&Default::default(), 0);
1466        // Its VERY important this comes back with zero new lines.
1467        assert_eq!(recasted, r#""#);
1468    }
1469
1470    #[test]
1471    fn test_recast_shebang() {
1472        let some_program_string = r#"#!/usr/local/env zoo kcl
1473part001 = startSketchOn(XY)
1474  |> startProfile(at = [-10, -10])
1475  |> line(end = [20, 0])
1476  |> line(end = [0, 20])
1477  |> line(end = [-20, 0])
1478  |> close()
1479"#;
1480
1481        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1482
1483        let recasted = program.recast(&Default::default(), 0);
1484        assert_eq!(
1485            recasted,
1486            r#"#!/usr/local/env zoo kcl
1487
1488part001 = startSketchOn(XY)
1489  |> startProfile(at = [-10, -10])
1490  |> line(end = [20, 0])
1491  |> line(end = [0, 20])
1492  |> line(end = [-20, 0])
1493  |> close()
1494"#
1495        );
1496    }
1497
1498    #[test]
1499    fn test_recast_shebang_new_lines() {
1500        let some_program_string = r#"#!/usr/local/env zoo kcl
1501        
1502
1503
1504part001 = startSketchOn(XY)
1505  |> startProfile(at = [-10, -10])
1506  |> line(end = [20, 0])
1507  |> line(end = [0, 20])
1508  |> line(end = [-20, 0])
1509  |> close()
1510"#;
1511
1512        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1513
1514        let recasted = program.recast(&Default::default(), 0);
1515        assert_eq!(
1516            recasted,
1517            r#"#!/usr/local/env zoo kcl
1518
1519part001 = startSketchOn(XY)
1520  |> startProfile(at = [-10, -10])
1521  |> line(end = [20, 0])
1522  |> line(end = [0, 20])
1523  |> line(end = [-20, 0])
1524  |> close()
1525"#
1526        );
1527    }
1528
1529    #[test]
1530    fn test_recast_shebang_with_comments() {
1531        let some_program_string = r#"#!/usr/local/env zoo kcl
1532        
1533// Yo yo my comments.
1534part001 = startSketchOn(XY)
1535  |> startProfile(at = [-10, -10])
1536  |> line(end = [20, 0])
1537  |> line(end = [0, 20])
1538  |> line(end = [-20, 0])
1539  |> close()
1540"#;
1541
1542        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1543
1544        let recasted = program.recast(&Default::default(), 0);
1545        assert_eq!(
1546            recasted,
1547            r#"#!/usr/local/env zoo kcl
1548
1549// Yo yo my comments.
1550part001 = startSketchOn(XY)
1551  |> startProfile(at = [-10, -10])
1552  |> line(end = [20, 0])
1553  |> line(end = [0, 20])
1554  |> line(end = [-20, 0])
1555  |> close()
1556"#
1557        );
1558    }
1559
1560    #[test]
1561    fn test_recast_empty_function_body_with_comments() {
1562        let input = r#"fn myFunc() {
1563  // Yo yo my comments.
1564}
1565"#;
1566
1567        let program = crate::parsing::top_level_parse(input).unwrap();
1568        let output = program.recast(&Default::default(), 0);
1569        assert_eq!(output, input);
1570    }
1571
1572    #[test]
1573    fn test_recast_large_file() {
1574        let some_program_string = r#"@settings(units=mm)
1575// define nts
1576radius = 6.0
1577width = 144.0
1578length = 83.0
1579depth = 45.0
1580thk = 5
1581hole_diam = 5
1582// define a rectangular shape func
1583fn rectShape(pos, w, l) {
1584  rr = startSketchOn(XY)
1585    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1586    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1587    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1588    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1589    |> close($edge4)
1590  return rr
1591}
1592// build the body of the focusrite scarlett solo gen 4
1593// only used for visualization
1594scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1595  |> extrude(depth)
1596  |> fillet(
1597       radius = radius,
1598       tags = [
1599  edge2,
1600  edge4,
1601  getOppositeEdge(edge2),
1602  getOppositeEdge(edge4)
1603]
1604   )
1605  // build the bracket sketch around the body
1606fn bracketSketch(w, d, t) {
1607  s = startSketchOn({
1608         plane = {
1609  origin = { x = 0, y = length / 2 + thk, z = 0 },
1610  x_axis = { x = 1, y = 0, z = 0 },
1611  y_axis = { x = 0, y = 0, z = 1 },
1612  z_axis = { x = 0, y = 1, z = 0 }
1613}
1614       })
1615    |> startProfile(at = [-w / 2 - t, d + t])
1616    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1617    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1618    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1619    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1620    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1621    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1622    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1623    |> close($edge8)
1624  return s
1625}
1626// build the body of the bracket
1627bracket_body = bracketSketch(w = width, d = depth, t = thk)
1628  |> extrude(length + 10)
1629  |> fillet(
1630       radius = radius,
1631       tags = [
1632  getNextAdjacentEdge(edge7),
1633  getNextAdjacentEdge(edge2),
1634  getNextAdjacentEdge(edge3),
1635  getNextAdjacentEdge(edge6)
1636]
1637     )
1638  // build the tabs of the mounting bracket (right side)
1639tabs_r = startSketchOn({
1640       plane = {
1641  origin = { x = 0, y = 0, z = depth + thk },
1642  x_axis = { x = 1, y = 0, z = 0 },
1643  y_axis = { x = 0, y = 1, z = 0 },
1644  z_axis = { x = 0, y = 0, z = 1 }
1645}
1646     })
1647  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1648  |> line(end = [10, -5])
1649  |> line(end = [0, -10])
1650  |> line(end = [-10, -5])
1651  |> close()
1652  |> subtract2d(tool = circle(
1653       center = [
1654         width / 2 + thk + hole_diam,
1655         length / 2 - hole_diam
1656       ],
1657       radius = hole_diam / 2
1658     ))
1659  |> extrude(-thk)
1660  |> patternLinear3d(
1661       axis = [0, -1, 0],
1662       repetitions = 1,
1663       distance = length - 10
1664     )
1665  // build the tabs of the mounting bracket (left side)
1666tabs_l = startSketchOn({
1667       plane = {
1668  origin = { x = 0, y = 0, z = depth + thk },
1669  x_axis = { x = 1, y = 0, z = 0 },
1670  y_axis = { x = 0, y = 1, z = 0 },
1671  z_axis = { x = 0, y = 0, z = 1 }
1672}
1673     })
1674  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1675  |> line(end = [-10, -5])
1676  |> line(end = [0, -10])
1677  |> line(end = [10, -5])
1678  |> close()
1679  |> subtract2d(tool = circle(
1680       center = [
1681         -width / 2 - thk - hole_diam,
1682         length / 2 - hole_diam
1683       ],
1684       radius = hole_diam / 2
1685     ))
1686  |> extrude(-thk)
1687  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1688"#;
1689        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1690
1691        let recasted = program.recast(&Default::default(), 0);
1692        // Its VERY important this comes back with zero new lines.
1693        assert_eq!(
1694            recasted,
1695            r#"@settings(units = mm)
1696
1697// define nts
1698radius = 6.0
1699width = 144.0
1700length = 83.0
1701depth = 45.0
1702thk = 5
1703hole_diam = 5
1704// define a rectangular shape func
1705fn rectShape(pos, w, l) {
1706  rr = startSketchOn(XY)
1707    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1708    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1709    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1710    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1711    |> close($edge4)
1712  return rr
1713}
1714// build the body of the focusrite scarlett solo gen 4
1715// only used for visualization
1716scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1717  |> extrude(depth)
1718  |> fillet(
1719       radius = radius,
1720       tags = [
1721         edge2,
1722         edge4,
1723         getOppositeEdge(edge2),
1724         getOppositeEdge(edge4)
1725       ],
1726     )
1727// build the bracket sketch around the body
1728fn bracketSketch(w, d, t) {
1729  s = startSketchOn({
1730         plane = {
1731           origin = { x = 0, y = length / 2 + thk, z = 0 },
1732           x_axis = { x = 1, y = 0, z = 0 },
1733           y_axis = { x = 0, y = 0, z = 1 },
1734           z_axis = { x = 0, y = 1, z = 0 }
1735         }
1736       })
1737    |> startProfile(at = [-w / 2 - t, d + t])
1738    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1739    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1740    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1741    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1742    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1743    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1744    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1745    |> close($edge8)
1746  return s
1747}
1748// build the body of the bracket
1749bracket_body = bracketSketch(w = width, d = depth, t = thk)
1750  |> extrude(length + 10)
1751  |> fillet(
1752       radius = radius,
1753       tags = [
1754         getNextAdjacentEdge(edge7),
1755         getNextAdjacentEdge(edge2),
1756         getNextAdjacentEdge(edge3),
1757         getNextAdjacentEdge(edge6)
1758       ],
1759     )
1760// build the tabs of the mounting bracket (right side)
1761tabs_r = startSketchOn({
1762       plane = {
1763         origin = { x = 0, y = 0, z = depth + thk },
1764         x_axis = { x = 1, y = 0, z = 0 },
1765         y_axis = { x = 0, y = 1, z = 0 },
1766         z_axis = { x = 0, y = 0, z = 1 }
1767       }
1768     })
1769  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1770  |> line(end = [10, -5])
1771  |> line(end = [0, -10])
1772  |> line(end = [-10, -5])
1773  |> close()
1774  |> subtract2d(tool = circle(
1775       center = [
1776         width / 2 + thk + hole_diam,
1777         length / 2 - hole_diam
1778       ],
1779       radius = hole_diam / 2,
1780     ))
1781  |> extrude(-thk)
1782  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1783// build the tabs of the mounting bracket (left side)
1784tabs_l = startSketchOn({
1785       plane = {
1786         origin = { x = 0, y = 0, z = depth + thk },
1787         x_axis = { x = 1, y = 0, z = 0 },
1788         y_axis = { x = 0, y = 1, z = 0 },
1789         z_axis = { x = 0, y = 0, z = 1 }
1790       }
1791     })
1792  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1793  |> line(end = [-10, -5])
1794  |> line(end = [0, -10])
1795  |> line(end = [10, -5])
1796  |> close()
1797  |> subtract2d(tool = circle(
1798       center = [
1799         -width / 2 - thk - hole_diam,
1800         length / 2 - hole_diam
1801       ],
1802       radius = hole_diam / 2,
1803     ))
1804  |> extrude(-thk)
1805  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1806"#
1807        );
1808    }
1809
1810    #[test]
1811    fn test_recast_nested_var_declaration_in_fn_body() {
1812        let some_program_string = r#"fn cube(pos, scale) {
1813   sg = startSketchOn(XY)
1814  |> startProfile(at = pos)
1815  |> line(end = [0, scale])
1816  |> line(end = [scale, 0])
1817  |> line(end = [0, -scale])
1818  |> close()
1819  |> extrude(scale)
1820}"#;
1821        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1822
1823        let recasted = program.recast(&Default::default(), 0);
1824        assert_eq!(
1825            recasted,
1826            r#"fn cube(pos, scale) {
1827  sg = startSketchOn(XY)
1828    |> startProfile(at = pos)
1829    |> line(end = [0, scale])
1830    |> line(end = [scale, 0])
1831    |> line(end = [0, -scale])
1832    |> close()
1833    |> extrude(scale)
1834}
1835"#
1836        );
1837    }
1838
1839    #[test]
1840    fn test_as() {
1841        let some_program_string = r#"fn cube(pos, scale) {
1842  x = dfsfs + dfsfsd as y
1843
1844  sg = startSketchOn(XY)
1845    |> startProfile(at = pos) as foo
1846    |> line([0, scale])
1847    |> line([scale, 0]) as bar
1848    |> line([0 as baz, -scale] as qux)
1849    |> close()
1850    |> extrude(length = scale)
1851}
1852
1853cube(pos = 0, scale = 0) as cub
1854"#;
1855        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1856
1857        let recasted = program.recast(&Default::default(), 0);
1858        assert_eq!(recasted, some_program_string,);
1859    }
1860
1861    #[test]
1862    fn test_recast_with_bad_indentation() {
1863        let some_program_string = r#"part001 = startSketchOn(XY)
1864  |> startProfile(at = [0.0, 5.0])
1865              |> line(end = [0.4900857016, -0.0240763666])
1866    |> line(end = [0.6804562304, 0.9087880491])"#;
1867        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1868
1869        let recasted = program.recast(&Default::default(), 0);
1870        assert_eq!(
1871            recasted,
1872            r#"part001 = startSketchOn(XY)
1873  |> startProfile(at = [0.0, 5.0])
1874  |> line(end = [0.4900857016, -0.0240763666])
1875  |> line(end = [0.6804562304, 0.9087880491])
1876"#
1877        );
1878    }
1879
1880    #[test]
1881    fn test_recast_with_bad_indentation_and_inline_comment() {
1882        let some_program_string = r#"part001 = startSketchOn(XY)
1883  |> startProfile(at = [0.0, 5.0])
1884              |> line(end = [0.4900857016, -0.0240763666]) // hello world
1885    |> line(end = [0.6804562304, 0.9087880491])"#;
1886        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1887
1888        let recasted = program.recast(&Default::default(), 0);
1889        assert_eq!(
1890            recasted,
1891            r#"part001 = startSketchOn(XY)
1892  |> startProfile(at = [0.0, 5.0])
1893  |> line(end = [0.4900857016, -0.0240763666]) // hello world
1894  |> line(end = [0.6804562304, 0.9087880491])
1895"#
1896        );
1897    }
1898    #[test]
1899    fn test_recast_with_bad_indentation_and_line_comment() {
1900        let some_program_string = r#"part001 = startSketchOn(XY)
1901  |> startProfile(at = [0.0, 5.0])
1902              |> line(end = [0.4900857016, -0.0240763666])
1903        // hello world
1904    |> line(end = [0.6804562304, 0.9087880491])"#;
1905        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1906
1907        let recasted = program.recast(&Default::default(), 0);
1908        assert_eq!(
1909            recasted,
1910            r#"part001 = startSketchOn(XY)
1911  |> startProfile(at = [0.0, 5.0])
1912  |> line(end = [0.4900857016, -0.0240763666])
1913  // hello world
1914  |> line(end = [0.6804562304, 0.9087880491])
1915"#
1916        );
1917    }
1918
1919    #[test]
1920    fn test_recast_comment_in_a_fn_block() {
1921        let some_program_string = r#"fn myFn() {
1922  // this is a comment
1923  yo = { a = { b = { c = '123' } } } /* block
1924  comment */
1925
1926  key = 'c'
1927  // this is also a comment
1928    return things
1929}"#;
1930        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1931
1932        let recasted = program.recast(&Default::default(), 0);
1933        assert_eq!(
1934            recasted,
1935            r#"fn myFn() {
1936  // this is a comment
1937  yo = { a = { b = { c = '123' } } } /* block
1938  comment */
1939
1940  key = 'c'
1941  // this is also a comment
1942  return things
1943}
1944"#
1945        );
1946    }
1947
1948    #[test]
1949    fn test_recast_comment_under_variable() {
1950        let some_program_string = r#"key = 'c'
1951// this is also a comment
1952thing = 'foo'
1953"#;
1954        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1955
1956        let recasted = program.recast(&Default::default(), 0);
1957        assert_eq!(
1958            recasted,
1959            r#"key = 'c'
1960// this is also a comment
1961thing = 'foo'
1962"#
1963        );
1964    }
1965
1966    #[test]
1967    fn test_recast_multiline_comment_start_file() {
1968        let some_program_string = r#"// hello world
1969// I am a comment
1970key = 'c'
1971// this is also a comment
1972// hello
1973thing = 'foo'
1974"#;
1975        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1976
1977        let recasted = program.recast(&Default::default(), 0);
1978        assert_eq!(
1979            recasted,
1980            r#"// hello world
1981// I am a comment
1982key = 'c'
1983// this is also a comment
1984// hello
1985thing = 'foo'
1986"#
1987        );
1988    }
1989
1990    #[test]
1991    fn test_recast_empty_comment() {
1992        let some_program_string = r#"// hello world
1993//
1994// I am a comment
1995key = 'c'
1996
1997//
1998// I am a comment
1999thing = 'c'
2000
2001foo = 'bar' //
2002"#;
2003        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2004
2005        let recasted = program.recast(&Default::default(), 0);
2006        assert_eq!(
2007            recasted,
2008            r#"// hello world
2009//
2010// I am a comment
2011key = 'c'
2012
2013//
2014// I am a comment
2015thing = 'c'
2016
2017foo = 'bar' //
2018"#
2019        );
2020    }
2021
2022    #[test]
2023    fn test_recast_multiline_comment_under_variable() {
2024        let some_program_string = r#"key = 'c'
2025// this is also a comment
2026// hello
2027thing = 'foo'
2028"#;
2029        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2030
2031        let recasted = program.recast(&Default::default(), 0);
2032        assert_eq!(
2033            recasted,
2034            r#"key = 'c'
2035// this is also a comment
2036// hello
2037thing = 'foo'
2038"#
2039        );
2040    }
2041
2042    #[test]
2043    fn test_recast_only_line_comments() {
2044        let code = r#"// comment at start
2045"#;
2046        let program = crate::parsing::top_level_parse(code).unwrap();
2047
2048        assert_eq!(program.recast(&Default::default(), 0), code);
2049    }
2050
2051    #[test]
2052    fn test_recast_comment_at_start() {
2053        let test_program = r#"
2054/* comment at start */
2055
2056mySk1 = startSketchOn(XY)
2057  |> startProfile(at = [0, 0])"#;
2058        let program = crate::parsing::top_level_parse(test_program).unwrap();
2059
2060        let recasted = program.recast(&Default::default(), 0);
2061        assert_eq!(
2062            recasted,
2063            r#"/* comment at start */
2064
2065mySk1 = startSketchOn(XY)
2066  |> startProfile(at = [0, 0])
2067"#
2068        );
2069    }
2070
2071    #[test]
2072    fn test_recast_lots_of_comments() {
2073        let some_program_string = r#"// comment at start
2074mySk1 = startSketchOn(XY)
2075  |> startProfile(at = [0, 0])
2076  |> line(endAbsolute = [1, 1])
2077  // comment here
2078  |> line(endAbsolute = [0, 1], tag = $myTag)
2079  |> line(endAbsolute = [1, 1])
2080  /* and
2081  here
2082  */
2083  // a comment between pipe expression statements
2084  |> rx(90)
2085  // and another with just white space between others below
2086  |> ry(45)
2087  |> rx(45)
2088// one more for good measure"#;
2089        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2090
2091        let recasted = program.recast(&Default::default(), 0);
2092        assert_eq!(
2093            recasted,
2094            r#"// comment at start
2095mySk1 = startSketchOn(XY)
2096  |> startProfile(at = [0, 0])
2097  |> line(endAbsolute = [1, 1])
2098  // comment here
2099  |> line(endAbsolute = [0, 1], tag = $myTag)
2100  |> line(endAbsolute = [1, 1])
2101  /* and
2102  here */
2103  // a comment between pipe expression statements
2104  |> rx(90)
2105  // and another with just white space between others below
2106  |> ry(45)
2107  |> rx(45)
2108// one more for good measure
2109"#
2110        );
2111    }
2112
2113    #[test]
2114    fn test_recast_multiline_object() {
2115        let some_program_string = r#"x = {
2116  a = 1000000000,
2117  b = 2000000000,
2118  c = 3000000000,
2119  d = 4000000000,
2120  e = 5000000000
2121}"#;
2122        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2123
2124        let recasted = program.recast(&Default::default(), 0);
2125        assert_eq!(recasted.trim(), some_program_string);
2126    }
2127
2128    #[test]
2129    fn test_recast_first_level_object() {
2130        let some_program_string = r#"three = 3
2131
2132yo = {
2133  aStr = 'str',
2134  anum = 2,
2135  identifier = three,
2136  binExp = 4 + 5
2137}
2138yo = [
2139  1,
2140  "  2,",
2141  "three",
2142  4 + 5,
2143  "  hey oooooo really long long long"
2144]
2145"#;
2146        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2147
2148        let recasted = program.recast(&Default::default(), 0);
2149        assert_eq!(recasted, some_program_string);
2150    }
2151
2152    #[test]
2153    fn test_recast_new_line_before_comment() {
2154        let some_program_string = r#"
2155// this is a comment
2156yo = { a = { b = { c = '123' } } }
2157
2158key = 'c'
2159things = "things"
2160
2161// this is also a comment"#;
2162        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2163
2164        let recasted = program.recast(&Default::default(), 0);
2165        let expected = some_program_string.trim();
2166        // Currently new parser removes an empty line
2167        let actual = recasted.trim();
2168        assert_eq!(actual, expected);
2169    }
2170
2171    #[test]
2172    fn test_recast_comment_tokens_inside_strings() {
2173        let some_program_string = r#"b = {
2174  end = 141,
2175  start = 125,
2176  type_ = "NonCodeNode",
2177  value = "
2178 // a comment
2179   "
2180}"#;
2181        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2182
2183        let recasted = program.recast(&Default::default(), 0);
2184        assert_eq!(recasted.trim(), some_program_string.trim());
2185    }
2186
2187    #[test]
2188    fn test_recast_array_new_line_in_pipe() {
2189        let some_program_string = r#"myVar = 3
2190myVar2 = 5
2191myVar3 = 6
2192myAng = 40
2193myAng2 = 134
2194part001 = startSketchOn(XY)
2195  |> startProfile(at = [0, 0])
2196  |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2197  |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2198  |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2199        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2200
2201        let recasted = program.recast(&Default::default(), 0);
2202        assert_eq!(recasted.trim(), some_program_string);
2203    }
2204
2205    #[test]
2206    fn test_recast_array_new_line_in_pipe_custom() {
2207        let some_program_string = r#"myVar = 3
2208myVar2 = 5
2209myVar3 = 6
2210myAng = 40
2211myAng2 = 134
2212part001 = startSketchOn(XY)
2213   |> startProfile(at = [0, 0])
2214   |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2215   |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2216   |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2217"#;
2218        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2219
2220        let recasted = program.recast(
2221            &FormatOptions {
2222                tab_size: 3,
2223                use_tabs: false,
2224                insert_final_newline: true,
2225            },
2226            0,
2227        );
2228        assert_eq!(recasted, some_program_string);
2229    }
2230
2231    #[test]
2232    fn test_recast_after_rename_std() {
2233        let some_program_string = r#"part001 = startSketchOn(XY)
2234  |> startProfile(at = [0.0000000000, 5.0000000000])
2235    |> line(end = [0.4900857016, -0.0240763666])
2236
2237part002 = "part002"
2238things = [part001, 0.0]
2239blah = 1
2240foo = false
2241baz = {a: 1, part001: "thing"}
2242
2243fn ghi(part001) {
2244  return part001
2245}
2246"#;
2247        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2248        program.rename_symbol("mySuperCoolPart", 6);
2249
2250        let recasted = program.recast(&Default::default(), 0);
2251        assert_eq!(
2252            recasted,
2253            r#"mySuperCoolPart = startSketchOn(XY)
2254  |> startProfile(at = [0.0, 5.0])
2255  |> line(end = [0.4900857016, -0.0240763666])
2256
2257part002 = "part002"
2258things = [mySuperCoolPart, 0.0]
2259blah = 1
2260foo = false
2261baz = { a = 1, part001 = "thing" }
2262
2263fn ghi(part001) {
2264  return part001
2265}
2266"#
2267        );
2268    }
2269
2270    #[test]
2271    fn test_recast_after_rename_fn_args() {
2272        let some_program_string = r#"fn ghi(x, y, z) {
2273  return x
2274}"#;
2275        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2276        program.rename_symbol("newName", 7);
2277
2278        let recasted = program.recast(&Default::default(), 0);
2279        assert_eq!(
2280            recasted,
2281            r#"fn ghi(newName, y, z) {
2282  return newName
2283}
2284"#
2285        );
2286    }
2287
2288    #[test]
2289    fn test_recast_trailing_comma() {
2290        let some_program_string = r#"startSketchOn(XY)
2291  |> startProfile(at = [0, 0])
2292  |> arc({
2293    radius = 1,
2294    angle_start = 0,
2295    angle_end = 180,
2296  })"#;
2297        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2298
2299        let recasted = program.recast(&Default::default(), 0);
2300        assert_eq!(
2301            recasted,
2302            r#"startSketchOn(XY)
2303  |> startProfile(at = [0, 0])
2304  |> arc({
2305       radius = 1,
2306       angle_start = 0,
2307       angle_end = 180
2308     })
2309"#
2310        );
2311    }
2312
2313    #[test]
2314    fn test_recast_negative_var() {
2315        let some_program_string = r#"w = 20
2316l = 8
2317h = 10
2318
2319firstExtrude = startSketchOn(XY)
2320  |> startProfile(at = [0,0])
2321  |> line(end = [0, l])
2322  |> line(end = [w, 0])
2323  |> line(end = [0, -l])
2324  |> close()
2325  |> extrude(h)
2326"#;
2327        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2328
2329        let recasted = program.recast(&Default::default(), 0);
2330        assert_eq!(
2331            recasted,
2332            r#"w = 20
2333l = 8
2334h = 10
2335
2336firstExtrude = startSketchOn(XY)
2337  |> startProfile(at = [0, 0])
2338  |> line(end = [0, l])
2339  |> line(end = [w, 0])
2340  |> line(end = [0, -l])
2341  |> close()
2342  |> extrude(h)
2343"#
2344        );
2345    }
2346
2347    #[test]
2348    fn test_recast_multiline_comment() {
2349        let some_program_string = r#"w = 20
2350l = 8
2351h = 10
2352
2353// This is my comment
2354// It has multiple lines
2355// And it's really long
2356firstExtrude = startSketchOn(XY)
2357  |> startProfile(at = [0,0])
2358  |> line(end = [0, l])
2359  |> line(end = [w, 0])
2360  |> line(end = [0, -l])
2361  |> close()
2362  |> extrude(h)
2363"#;
2364        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2365
2366        let recasted = program.recast(&Default::default(), 0);
2367        assert_eq!(
2368            recasted,
2369            r#"w = 20
2370l = 8
2371h = 10
2372
2373// This is my comment
2374// It has multiple lines
2375// And it's really long
2376firstExtrude = startSketchOn(XY)
2377  |> startProfile(at = [0, 0])
2378  |> line(end = [0, l])
2379  |> line(end = [w, 0])
2380  |> line(end = [0, -l])
2381  |> close()
2382  |> extrude(h)
2383"#
2384        );
2385    }
2386
2387    #[test]
2388    fn test_recast_math_start_negative() {
2389        let some_program_string = r#"myVar = -5 + 6"#;
2390        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2391
2392        let recasted = program.recast(&Default::default(), 0);
2393        assert_eq!(recasted.trim(), some_program_string);
2394    }
2395
2396    #[test]
2397    fn test_recast_math_negate_parens() {
2398        let some_program_string = r#"wallMountL = 3.82
2399thickness = 0.5
2400
2401startSketchOn(XY)
2402  |> startProfile(at = [0, 0])
2403  |> line(end = [0, -(wallMountL - thickness)])
2404  |> line(end = [0, -(5 - thickness)])
2405  |> line(end = [0, -(5 - 1)])
2406  |> line(end = [0, -(-5 - 1)])"#;
2407        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2408
2409        let recasted = program.recast(&Default::default(), 0);
2410        assert_eq!(recasted.trim(), some_program_string);
2411    }
2412
2413    #[test]
2414    fn test_recast_math_nested_parens() {
2415        let some_program_string = r#"distance = 5
2416p = 3: Plane
2417FOS = { a = 3, b = 42 }: Sketch
2418sigmaAllow = 8: number(mm)
2419width = 20
2420thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2421        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2422
2423        let recasted = program.recast(&Default::default(), 0);
2424        assert_eq!(recasted.trim(), some_program_string);
2425    }
2426
2427    #[test]
2428    fn no_vardec_keyword() {
2429        let some_program_string = r#"distance = 5"#;
2430        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2431
2432        let recasted = program.recast(&Default::default(), 0);
2433        assert_eq!(recasted.trim(), some_program_string);
2434    }
2435
2436    #[test]
2437    fn recast_types() {
2438        let some_program_string = r#"type foo
2439
2440// A comment
2441@(impl = primitive)
2442export type bar(unit, baz)
2443type baz = Foo | Bar
2444"#;
2445        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2446        let recasted = program.recast(&Default::default(), 0);
2447        assert_eq!(recasted, some_program_string);
2448    }
2449
2450    #[test]
2451    fn recast_nested_fn() {
2452        let some_program_string = r#"fn f() {
2453  return fn() {
2454  return 1
2455}
2456}"#;
2457        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2458        let recasted = program.recast(&Default::default(), 0);
2459        let expected = "\
2460fn f() {
2461  return fn() {
2462    return 1
2463  }
2464}";
2465        assert_eq!(recasted.trim(), expected);
2466    }
2467
2468    #[test]
2469    fn recast_literal() {
2470        use winnow::Parser;
2471        for (i, (raw, expected, reason)) in [
2472            (
2473                "5.0",
2474                "5.0",
2475                "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2476            ),
2477            (
2478                "5",
2479                "5",
2480                "integers should stay integral, i.e. don't reformat this to '5.0'",
2481            ),
2482            (
2483                "5.0000000",
2484                "5.0",
2485                "if the number is f64 but not fractional, use its canonical format",
2486            ),
2487            ("5.1", "5.1", "straightforward case works"),
2488        ]
2489        .into_iter()
2490        .enumerate()
2491        {
2492            let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2493            let literal = crate::parsing::parser::unsigned_number_literal
2494                .parse(tokens.as_slice())
2495                .unwrap();
2496            assert_eq!(
2497                literal.recast(),
2498                expected,
2499                "failed test {i}, which is testing that {reason}"
2500            );
2501        }
2502    }
2503
2504    #[test]
2505    fn recast_objects_no_comments() {
2506        let input = r#"
2507sketch002 = startSketchOn({
2508       plane: {
2509    origin: { x = 1, y = 2, z = 3 },
2510    x_axis = { x = 4, y = 5, z = 6 },
2511    y_axis = { x = 7, y = 8, z = 9 },
2512    z_axis = { x = 10, y = 11, z = 12 }
2513       }
2514  })
2515"#;
2516        let expected = r#"sketch002 = startSketchOn({
2517  plane = {
2518    origin = { x = 1, y = 2, z = 3 },
2519    x_axis = { x = 4, y = 5, z = 6 },
2520    y_axis = { x = 7, y = 8, z = 9 },
2521    z_axis = { x = 10, y = 11, z = 12 }
2522  }
2523})
2524"#;
2525        let ast = crate::parsing::top_level_parse(input).unwrap();
2526        let actual = ast.recast(&FormatOptions::new(), 0);
2527        assert_eq!(actual, expected);
2528    }
2529
2530    #[test]
2531    fn unparse_fn_unnamed() {
2532        let input = r#"squares_out = reduce(
2533  arr,
2534  n = 0: number,
2535  f = fn(@i, accum) {
2536    return 1
2537  },
2538)
2539"#;
2540        let ast = crate::parsing::top_level_parse(input).unwrap();
2541        let actual = ast.recast(&FormatOptions::new(), 0);
2542        assert_eq!(actual, input);
2543    }
2544
2545    #[test]
2546    fn unparse_fn_named() {
2547        let input = r#"fn f(x) {
2548  return 1
2549}
2550"#;
2551        let ast = crate::parsing::top_level_parse(input).unwrap();
2552        let actual = ast.recast(&FormatOptions::new(), 0);
2553        assert_eq!(actual, input);
2554    }
2555
2556    #[test]
2557    fn unparse_call_inside_function_single_line() {
2558        let input = r#"fn foo() {
2559  toDegrees(atan(0.5), foo = 1)
2560  return 0
2561}
2562"#;
2563        let ast = crate::parsing::top_level_parse(input).unwrap();
2564        let actual = ast.recast(&FormatOptions::new(), 0);
2565        assert_eq!(actual, input);
2566    }
2567
2568    #[test]
2569    fn unparse_call_inside_function_args_multiple_lines() {
2570        let input = r#"fn foo() {
2571  toDegrees(
2572    atan(0.5),
2573    foo = 1,
2574    bar = 2,
2575    baz = 3,
2576    qux = 4,
2577  )
2578  return 0
2579}
2580"#;
2581        let ast = crate::parsing::top_level_parse(input).unwrap();
2582        let actual = ast.recast(&FormatOptions::new(), 0);
2583        assert_eq!(actual, input);
2584    }
2585
2586    #[test]
2587    fn unparse_call_inside_function_single_arg_multiple_lines() {
2588        let input = r#"fn foo() {
2589  toDegrees(
2590    [
2591      profile0,
2592      profile1,
2593      profile2,
2594      profile3,
2595      profile4,
2596      profile5
2597    ],
2598    key = 1,
2599  )
2600  return 0
2601}
2602"#;
2603        let ast = crate::parsing::top_level_parse(input).unwrap();
2604        let actual = ast.recast(&FormatOptions::new(), 0);
2605        assert_eq!(actual, input);
2606    }
2607
2608    #[test]
2609    fn recast_objects_with_comments() {
2610        use winnow::Parser;
2611        for (i, (input, expected, reason)) in [(
2612            "\
2613{
2614  a = 1,
2615  // b = 2,
2616  c = 3
2617}",
2618            "\
2619{
2620  a = 1,
2621  // b = 2,
2622  c = 3
2623}",
2624            "preserves comments",
2625        )]
2626        .into_iter()
2627        .enumerate()
2628        {
2629            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2630            crate::parsing::parser::print_tokens(tokens.as_slice());
2631            let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
2632            assert_eq!(
2633                expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2634                expected,
2635                "failed test {i}, which is testing that recasting {reason}"
2636            );
2637        }
2638    }
2639
2640    #[test]
2641    fn recast_array_with_comments() {
2642        use winnow::Parser;
2643        for (i, (input, expected, reason)) in [
2644            (
2645                "\
2646[
2647  1,
2648  2,
2649  3,
2650  4,
2651  5,
2652  6,
2653  7,
2654  8,
2655  9,
2656  10,
2657  11,
2658  12,
2659  13,
2660  14,
2661  15,
2662  16,
2663  17,
2664  18,
2665  19,
2666  20,
2667]",
2668                "\
2669[
2670  1,
2671  2,
2672  3,
2673  4,
2674  5,
2675  6,
2676  7,
2677  8,
2678  9,
2679  10,
2680  11,
2681  12,
2682  13,
2683  14,
2684  15,
2685  16,
2686  17,
2687  18,
2688  19,
2689  20
2690]",
2691                "preserves multi-line arrays",
2692            ),
2693            (
2694                "\
2695[
2696  1,
2697  // 2,
2698  3
2699]",
2700                "\
2701[
2702  1,
2703  // 2,
2704  3
2705]",
2706                "preserves comments",
2707            ),
2708            (
2709                "\
2710[
2711  1,
2712  2,
2713  // 3
2714]",
2715                "\
2716[
2717  1,
2718  2,
2719  // 3
2720]",
2721                "preserves comments at the end of the array",
2722            ),
2723        ]
2724        .into_iter()
2725        .enumerate()
2726        {
2727            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2728            let expr = crate::parsing::parser::array_elem_by_elem
2729                .parse(tokens.as_slice())
2730                .unwrap();
2731            assert_eq!(
2732                expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2733                expected,
2734                "failed test {i}, which is testing that recasting {reason}"
2735            );
2736        }
2737    }
2738
2739    #[test]
2740    fn code_with_comment_and_extra_lines() {
2741        let code = r#"yo = 'c'
2742
2743/* this is
2744a
2745comment */
2746yo = 'bing'
2747"#;
2748        let ast = crate::parsing::top_level_parse(code).unwrap();
2749        let recasted = ast.recast(&FormatOptions::new(), 0);
2750        assert_eq!(recasted, code);
2751    }
2752
2753    #[test]
2754    fn comments_in_a_fn_block() {
2755        let code = r#"fn myFn() {
2756  // this is a comment
2757  yo = { a = { b = { c = '123' } } }
2758
2759  /* block
2760  comment */
2761  key = 'c'
2762  // this is also a comment
2763}
2764"#;
2765        let ast = crate::parsing::top_level_parse(code).unwrap();
2766        let recasted = ast.recast(&FormatOptions::new(), 0);
2767        assert_eq!(recasted, code);
2768    }
2769}