kcl_lib/
unparser.rs

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