kcl_lib/
unparser.rs

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