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        // Format these items into a one-line array. Put spaces around the `..` if either expression
587        // is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
588        // tightly, but this is a misleading visual representation of the precedence if the range
589        // components are compound expressions.
590        if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
591            format!("[{s1}..{s2}]")
592        } else {
593            format!("[{s1} .. {s2}]")
594        }
595
596        // Assume a range expression fits on one line.
597    }
598}
599
600impl ObjectExpression {
601    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
602        if self
603            .non_code_meta
604            .non_code_nodes
605            .values()
606            .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
607        {
608            return self.recast_multi_line(options, indentation_level, ctxt);
609        }
610        let flat_recast = format!(
611            "{{ {} }}",
612            self.properties
613                .iter()
614                .map(|prop| {
615                    format!(
616                        "{} = {}",
617                        prop.key.name,
618                        prop.value.recast(options, indentation_level + 1, ctxt).trim()
619                    )
620                })
621                .collect::<Vec<String>>()
622                .join(", ")
623        );
624        let max_array_length = 40;
625        let needs_multiple_lines = flat_recast.len() > max_array_length;
626        if !needs_multiple_lines {
627            return flat_recast;
628        }
629        self.recast_multi_line(options, indentation_level, ctxt)
630    }
631
632    /// Recast, but always outputs the object with newlines between each property.
633    fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
634        let inner_indentation = if ctxt == ExprContext::Pipe {
635            options.get_indentation_offset_pipe(indentation_level + 1)
636        } else {
637            options.get_indentation(indentation_level + 1)
638        };
639        let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
640        let mut props = self.properties.iter();
641        let format_items: Vec<_> = (0..num_items)
642            .flat_map(|i| {
643                if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
644                    noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
645                } else {
646                    let prop = props.next().unwrap();
647                    // Use a comma unless it's the last item
648                    let comma = if i == num_items - 1 { "" } else { ",\n" };
649                    let s = format!(
650                        "{} = {}{comma}",
651                        prop.key.name,
652                        prop.value.recast(options, indentation_level + 1, ctxt).trim()
653                    );
654                    vec![s]
655                }
656            })
657            .collect();
658        let end_indent = if ctxt == ExprContext::Pipe {
659            options.get_indentation_offset_pipe(indentation_level)
660        } else {
661            options.get_indentation(indentation_level)
662        };
663        format!(
664            "{{\n{inner_indentation}{}\n{end_indent}}}",
665            format_items.join(&inner_indentation),
666        )
667    }
668}
669
670impl MemberExpression {
671    fn recast(&self) -> String {
672        let key_str = match &self.property {
673            LiteralIdentifier::Identifier(identifier) => {
674                if self.computed {
675                    format!("[{}]", &(*identifier.name))
676                } else {
677                    format!(".{}", &(*identifier.name))
678                }
679            }
680            LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
681        };
682
683        match &self.object {
684            MemberObject::MemberExpression(member_exp) => member_exp.recast() + key_str.as_str(),
685            MemberObject::Identifier(identifier) => identifier.name.to_string() + key_str.as_str(),
686        }
687    }
688}
689
690impl BinaryExpression {
691    fn recast(&self, options: &FormatOptions) -> String {
692        let maybe_wrap_it = |a: String, doit: bool| -> String {
693            if doit {
694                format!("({})", a)
695            } else {
696                a
697            }
698        };
699
700        let should_wrap_right = match &self.right {
701            BinaryPart::BinaryExpression(bin_exp) => {
702                self.precedence() > bin_exp.precedence()
703                    || self.operator == BinaryOperator::Sub
704                    || self.operator == BinaryOperator::Div
705            }
706            _ => false,
707        };
708
709        let should_wrap_left = match &self.left {
710            BinaryPart::BinaryExpression(bin_exp) => self.precedence() > bin_exp.precedence(),
711            _ => false,
712        };
713
714        format!(
715            "{} {} {}",
716            maybe_wrap_it(self.left.recast(options, 0), should_wrap_left),
717            self.operator,
718            maybe_wrap_it(self.right.recast(options, 0), should_wrap_right)
719        )
720    }
721}
722
723impl UnaryExpression {
724    fn recast(&self, options: &FormatOptions) -> String {
725        match self.argument {
726            BinaryPart::Literal(_)
727            | BinaryPart::Name(_)
728            | BinaryPart::MemberExpression(_)
729            | BinaryPart::IfExpression(_)
730            | BinaryPart::AscribedExpression(_)
731            | BinaryPart::CallExpressionKw(_) => {
732                format!("{}{}", &self.operator, self.argument.recast(options, 0))
733            }
734            BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
735                format!("{}({})", &self.operator, self.argument.recast(options, 0))
736            }
737        }
738    }
739}
740
741impl IfExpression {
742    fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
743        // We can calculate how many lines this will take, so let's do it and avoid growing the vec.
744        // Total lines = starting lines, else-if lines, ending lines.
745        let n = 2 + (self.else_ifs.len() * 2) + 3;
746        let mut lines = Vec::with_capacity(n);
747
748        let cond = self.cond.recast(options, indentation_level, ctxt);
749        lines.push((0, format!("if {cond} {{")));
750        lines.push((1, self.then_val.recast(options, indentation_level + 1)));
751        for else_if in &self.else_ifs {
752            let cond = else_if.cond.recast(options, indentation_level, ctxt);
753            lines.push((0, format!("}} else if {cond} {{")));
754            lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
755        }
756        lines.push((0, "} else {".to_owned()));
757        lines.push((1, self.final_else.recast(options, indentation_level + 1)));
758        lines.push((0, "}".to_owned()));
759        lines
760            .into_iter()
761            .map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
762            .collect::<Vec<_>>()
763            .join("\n")
764    }
765}
766
767impl Node<PipeExpression> {
768    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
769        let pipe = self
770            .body
771            .iter()
772            .enumerate()
773            .map(|(index, statement)| {
774                let indentation = options.get_indentation(indentation_level + 1);
775                let mut s = statement.recast(options, indentation_level + 1, ExprContext::Pipe);
776                let non_code_meta = self.non_code_meta.clone();
777                if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
778                    for val in non_code_meta_value {
779                        let formatted = if val.end == self.end {
780                            val.recast(options, indentation_level)
781                                .trim_end_matches('\n')
782                                .to_string()
783                        } else {
784                            val.recast(options, indentation_level + 1)
785                                .trim_end_matches('\n')
786                                .to_string()
787                        };
788                        if let NonCodeValue::BlockComment { .. } = val.value {
789                            s += "\n";
790                            s += &formatted;
791                        } else {
792                            s += &formatted;
793                        }
794                    }
795                }
796
797                if index != self.body.len() - 1 {
798                    s += "\n";
799                    s += &indentation;
800                    s += PIPE_OPERATOR;
801                    s += " ";
802                }
803                s
804            })
805            .collect::<String>();
806        format!("{}{}", options.get_indentation(indentation_level), pipe)
807    }
808}
809
810impl FunctionExpression {
811    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
812        // We don't want to end with a new line inside nested functions.
813        let mut new_options = options.clone();
814        new_options.insert_final_newline = false;
815        let param_list = self
816            .params
817            .iter()
818            .map(|param| param.recast(options, indentation_level))
819            .collect::<Vec<String>>()
820            .join(", ");
821        let tab0 = options.get_indentation(indentation_level);
822        let tab1 = options.get_indentation(indentation_level + 1);
823        let return_type = match &self.return_type {
824            Some(rt) => format!(": {rt}"),
825            None => String::new(),
826        };
827        let body = self.body.recast(&new_options, indentation_level + 1);
828
829        format!("({param_list}){return_type} {{\n{tab1}{body}\n{tab0}}}")
830    }
831}
832
833impl Parameter {
834    pub fn recast(&self, _options: &FormatOptions, _indentation_level: usize) -> String {
835        let at_sign = if self.labeled { "" } else { "@" };
836        let identifier = &self.identifier.name;
837        let question_mark = if self.default_value.is_some() { "?" } else { "" };
838        let mut result = format!("{at_sign}{identifier}{question_mark}");
839        if let Some(ty) = &self.type_ {
840            result += ": ";
841            result += &ty.to_string();
842        }
843        if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
844            let lit = literal.recast();
845            result.push_str(&format!(" = {lit}"));
846        };
847
848        result
849    }
850}
851
852/// Collect all the kcl (and other relevant) files in a directory, recursively.
853#[cfg(not(target_arch = "wasm32"))]
854#[async_recursion::async_recursion]
855pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
856    // Make sure we actually have a directory.
857    if !dir.is_dir() {
858        anyhow::bail!("`{}` is not a directory", dir.display());
859    }
860
861    let mut entries = tokio::fs::read_dir(dir).await?;
862
863    let mut files = Vec::new();
864    while let Some(entry) = entries.next_entry().await? {
865        let path = entry.path();
866
867        if path.is_dir() {
868            files.extend(walk_dir(&path).await?);
869        } else if path
870            .extension()
871            .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
872        {
873            files.push(path);
874        }
875    }
876
877    Ok(files)
878}
879
880/// Recast all the kcl files in a directory, recursively.
881#[cfg(not(target_arch = "wasm32"))]
882pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
883    let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
884        crate::KclError::Internal(crate::errors::KclErrorDetails::new(
885            format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
886            vec![crate::SourceRange::default()],
887        ))
888    })?;
889
890    let futures = files
891        .into_iter()
892        .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) // We only care about kcl
893        // files here.
894        .map(|file| {
895            let options = options.clone();
896            tokio::spawn(async move {
897                let contents = tokio::fs::read_to_string(&file)
898                    .await
899                    .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
900                let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
901                    let report = crate::Report {
902                        kcl_source: contents.to_string(),
903                        error: err.clone(),
904                        filename: file.to_string_lossy().to_string(),
905                    };
906                    let report = miette::Report::new(report);
907                    anyhow::anyhow!("{:?}", report)
908                })?;
909                for ce in &ces {
910                    if ce.severity != crate::errors::Severity::Warning {
911                        let report = crate::Report {
912                            kcl_source: contents.to_string(),
913                            error: crate::KclError::Semantic(ce.clone().into()),
914                            filename: file.to_string_lossy().to_string(),
915                        };
916                        let report = miette::Report::new(report);
917                        anyhow::bail!("{:?}", report);
918                    }
919                }
920                let Some(program) = program else {
921                    anyhow::bail!("Failed to parse file `{}`", file.display());
922                };
923                let recast = program.recast_with_options(&options);
924                tokio::fs::write(&file, recast)
925                    .await
926                    .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
927
928                Ok::<(), anyhow::Error>(())
929            })
930        })
931        .collect::<Vec<_>>();
932
933    // Join all futures and await their completion
934    let results = futures::future::join_all(futures).await;
935
936    // Check if any of the futures failed.
937    let mut errors = Vec::new();
938    for result in results {
939        if let Err(err) = result? {
940            errors.push(err);
941        }
942    }
943
944    if !errors.is_empty() {
945        anyhow::bail!("Failed to recast some files: {:?}", errors);
946    }
947
948    Ok(())
949}
950
951#[cfg(test)]
952mod tests {
953    use pretty_assertions::assert_eq;
954
955    use super::*;
956    use crate::{parsing::ast::types::FormatOptions, ModuleId};
957
958    #[test]
959    fn test_recast_annotations_without_body_items() {
960        let input = r#"@settings(defaultLengthUnit = in)
961"#;
962        let program = crate::parsing::top_level_parse(input).unwrap();
963        let output = program.recast(&Default::default(), 0);
964        assert_eq!(output, input);
965    }
966
967    #[test]
968    fn test_recast_annotations_in_function_body() {
969        let input = r#"fn myFunc() {
970  @meta(yes = true)
971
972  x = 2
973}
974"#;
975        let program = crate::parsing::top_level_parse(input).unwrap();
976        let output = program.recast(&Default::default(), 0);
977        assert_eq!(output, input);
978    }
979
980    #[test]
981    fn test_recast_annotations_in_function_body_without_items() {
982        let input = r#"fn myFunc() {
983  @meta(yes = true)
984}
985"#;
986        let program = crate::parsing::top_level_parse(input).unwrap();
987        let output = program.recast(&Default::default(), 0);
988        assert_eq!(output, input);
989    }
990
991    #[test]
992    fn recast_annotations_with_comments() {
993        let input = r#"// Start comment
994
995// Comment on attr
996@settings(defaultLengthUnit = in)
997
998// Comment on item
999foo = 42
1000
1001// Comment on another item
1002@(impl = kcl)
1003bar = 0
1004"#;
1005        let program = crate::parsing::top_level_parse(input).unwrap();
1006        let output = program.recast(&Default::default(), 0);
1007        assert_eq!(output, input);
1008    }
1009
1010    #[test]
1011    fn recast_annotations_with_block_comment() {
1012        let input = r#"/* Start comment
1013
1014sdfsdfsdfs */
1015@settings(defaultLengthUnit = in)
1016
1017foo = 42
1018"#;
1019        let program = crate::parsing::top_level_parse(input).unwrap();
1020        let output = program.recast(&Default::default(), 0);
1021        assert_eq!(output, input);
1022    }
1023
1024    #[test]
1025    fn test_recast_if_else_if_same() {
1026        let input = r#"b = if false {
1027  3
1028} else if true {
1029  4
1030} else {
1031  5
1032}
1033"#;
1034        let program = crate::parsing::top_level_parse(input).unwrap();
1035        let output = program.recast(&Default::default(), 0);
1036        assert_eq!(output, input);
1037    }
1038
1039    #[test]
1040    fn test_recast_if_same() {
1041        let input = r#"b = if false {
1042  3
1043} else {
1044  5
1045}
1046"#;
1047        let program = crate::parsing::top_level_parse(input).unwrap();
1048        let output = program.recast(&Default::default(), 0);
1049        assert_eq!(output, input);
1050    }
1051
1052    #[test]
1053    fn test_recast_import() {
1054        let input = r#"import a from "a.kcl"
1055import a as aaa from "a.kcl"
1056import a, b from "a.kcl"
1057import a as aaa, b from "a.kcl"
1058import a, b as bbb from "a.kcl"
1059import a as aaa, b as bbb from "a.kcl"
1060import "a_b.kcl"
1061import "a-b.kcl" as b
1062import * from "a.kcl"
1063export import a as aaa from "a.kcl"
1064export import a, b from "a.kcl"
1065export import a as aaa, b from "a.kcl"
1066export import a, b as bbb from "a.kcl"
1067"#;
1068        let program = crate::parsing::top_level_parse(input).unwrap();
1069        let output = program.recast(&Default::default(), 0);
1070        assert_eq!(output, input);
1071    }
1072
1073    #[test]
1074    fn test_recast_import_as_same_name() {
1075        let input = r#"import a as a from "a.kcl"
1076"#;
1077        let program = crate::parsing::top_level_parse(input).unwrap();
1078        let output = program.recast(&Default::default(), 0);
1079        let expected = r#"import a from "a.kcl"
1080"#;
1081        assert_eq!(output, expected);
1082    }
1083
1084    #[test]
1085    fn test_recast_export_fn() {
1086        let input = r#"export fn a() {
1087  return 0
1088}
1089"#;
1090        let program = crate::parsing::top_level_parse(input).unwrap();
1091        let output = program.recast(&Default::default(), 0);
1092        assert_eq!(output, input);
1093    }
1094
1095    #[test]
1096    fn test_recast_bug_fn_in_fn() {
1097        let some_program_string = r#"// Start point (top left)
1098zoo_x = -20
1099zoo_y = 7
1100// Scale
1101s = 1 // s = 1 -> height of Z is 13.4mm
1102// Depth
1103d = 1
1104
1105fn rect(x, y, w, h) {
1106  startSketchOn(XY)
1107    |> startProfile(at = [x, y])
1108    |> xLine(length = w)
1109    |> yLine(length = h)
1110    |> xLine(length = -w)
1111    |> close()
1112    |> extrude(d)
1113}
1114
1115fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1116  startSketchOn(XY)
1117    |> startProfile(at = [x1, y1])
1118    |> line(endAbsolute = [x2, y2])
1119    |> line(endAbsolute = [x3, y3])
1120    |> line(endAbsolute = [x4, y4])
1121    |> close()
1122    |> extrude(d)
1123}
1124
1125fn crosshair(x, y) {
1126  startSketchOn(XY)
1127    |> startProfile(at = [x, y])
1128    |> yLine(length = 1)
1129    |> yLine(length = -2)
1130    |> yLine(length = 1)
1131    |> xLine(length = 1)
1132    |> xLine(length = -2)
1133}
1134
1135fn z(z_x, z_y) {
1136  z_end_w = s * 8.4
1137  z_end_h = s * 3
1138  z_corner = s * 2
1139  z_w = z_end_w + 2 * z_corner
1140  z_h = z_w * 1.08130081300813
1141  rect(
1142    z_x,
1143    a = z_y,
1144    b = z_end_w,
1145    c = -z_end_h,
1146  )
1147  rect(
1148    z_x + z_w,
1149    a = z_y,
1150    b = -z_corner,
1151    c = -z_corner,
1152  )
1153  rect(
1154    z_x + z_w,
1155    a = z_y - z_h,
1156    b = -z_end_w,
1157    c = z_end_h,
1158  )
1159  rect(
1160    z_x,
1161    a = z_y - z_h,
1162    b = z_corner,
1163    c = z_corner,
1164  )
1165}
1166
1167fn o(c_x, c_y) {
1168  // Outer and inner radii
1169  o_r = s * 6.95
1170  i_r = 0.5652173913043478 * o_r
1171
1172  // Angle offset for diagonal break
1173  a = 7
1174
1175  // Start point for the top sketch
1176  o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
1177  o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
1178
1179  // Start point for the bottom sketch
1180  o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
1181  o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
1182
1183  // End point for the bottom startSketch
1184  o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
1185  o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
1186
1187  // Where is the center?
1188  // crosshair(c_x, c_y)
1189
1190
1191  startSketchOn(XY)
1192    |> startProfile(at = [o_x1, o_y1])
1193    |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
1194    |> angledLine(angle = 45, length = o_r - i_r)
1195    |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
1196    |> close()
1197    |> extrude(d)
1198
1199  startSketchOn(XY)
1200    |> startProfile(at = [o_x2, o_y2])
1201    |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
1202    |> angledLine(angle = 225, length = o_r - i_r)
1203    |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
1204    |> close()
1205    |> extrude(d)
1206}
1207
1208fn zoo(x0, y0) {
1209  z(x = x0, y = y0)
1210  o(x = x0 + s * 20, y = y0 - (s * 6.7))
1211  o(x = x0 + s * 35, y = y0 - (s * 6.7))
1212}
1213
1214zoo(x = zoo_x, y = zoo_y)
1215"#;
1216        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1217
1218        let recasted = program.recast(&Default::default(), 0);
1219        assert_eq!(recasted, some_program_string);
1220    }
1221
1222    #[test]
1223    fn test_recast_bug_extra_parens() {
1224        let some_program_string = r#"// Ball Bearing
1225// 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. 
1226
1227// Define constants like ball diameter, inside diameter, overhange length, and thickness
1228sphereDia = 0.5
1229insideDia = 1
1230thickness = 0.25
1231overHangLength = .4
1232
1233// Sketch and revolve the inside bearing piece
1234insideRevolve = startSketchOn(XZ)
1235  |> startProfile(at = [insideDia / 2, 0])
1236  |> line(end = [0, thickness + sphereDia / 2])
1237  |> line(end = [overHangLength, 0])
1238  |> line(end = [0, -thickness])
1239  |> line(end = [-overHangLength + thickness, 0])
1240  |> line(end = [0, -sphereDia])
1241  |> line(end = [overHangLength - thickness, 0])
1242  |> line(end = [0, -thickness])
1243  |> line(end = [-overHangLength, 0])
1244  |> close()
1245  |> revolve(axis = Y)
1246
1247// 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)
1248sphere = startSketchOn(XZ)
1249  |> startProfile(at = [
1250       0.05 + insideDia / 2 + thickness,
1251       0 - 0.05
1252     ])
1253  |> line(end = [sphereDia - 0.1, 0])
1254  |> arc(
1255       angle_start = 0,
1256       angle_end = -180,
1257       radius = sphereDia / 2 - 0.05
1258     )
1259  |> close()
1260  |> revolve(axis = X)
1261  |> patternCircular3d(
1262       axis = [0, 0, 1],
1263       center = [0, 0, 0],
1264       repetitions = 10,
1265       arcDegrees = 360,
1266       rotateDuplicates = true
1267     )
1268
1269// Sketch and revolve the outside bearing
1270outsideRevolve = startSketchOn(XZ)
1271  |> startProfile(at = [
1272       insideDia / 2 + thickness + sphereDia,
1273       0
1274       ]
1275     )
1276  |> line(end = [0, sphereDia / 2])
1277  |> line(end = [-overHangLength + thickness, 0])
1278  |> line(end = [0, thickness])
1279  |> line(end = [overHangLength, 0])
1280  |> line(end = [0, -2 * thickness - sphereDia])
1281  |> line(end = [-overHangLength, 0])
1282  |> line(end = [0, thickness])
1283  |> line(end = [overHangLength - thickness, 0])
1284  |> close()
1285  |> revolve(axis = Y)"#;
1286        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1287
1288        let recasted = program.recast(&Default::default(), 0);
1289        assert_eq!(
1290            recasted,
1291            r#"// Ball Bearing
1292// 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.
1293
1294// Define constants like ball diameter, inside diameter, overhange length, and thickness
1295sphereDia = 0.5
1296insideDia = 1
1297thickness = 0.25
1298overHangLength = .4
1299
1300// Sketch and revolve the inside bearing piece
1301insideRevolve = startSketchOn(XZ)
1302  |> startProfile(at = [insideDia / 2, 0])
1303  |> line(end = [0, thickness + sphereDia / 2])
1304  |> line(end = [overHangLength, 0])
1305  |> line(end = [0, -thickness])
1306  |> line(end = [-overHangLength + thickness, 0])
1307  |> line(end = [0, -sphereDia])
1308  |> line(end = [overHangLength - thickness, 0])
1309  |> line(end = [0, -thickness])
1310  |> line(end = [-overHangLength, 0])
1311  |> close()
1312  |> revolve(axis = Y)
1313
1314// 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)
1315sphere = startSketchOn(XZ)
1316  |> startProfile(at = [
1317       0.05 + insideDia / 2 + thickness,
1318       0 - 0.05
1319     ])
1320  |> line(end = [sphereDia - 0.1, 0])
1321  |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
1322  |> close()
1323  |> revolve(axis = X)
1324  |> patternCircular3d(
1325       axis = [0, 0, 1],
1326       center = [0, 0, 0],
1327       repetitions = 10,
1328       arcDegrees = 360,
1329       rotateDuplicates = true,
1330     )
1331
1332// Sketch and revolve the outside bearing
1333outsideRevolve = startSketchOn(XZ)
1334  |> startProfile(at = [
1335       insideDia / 2 + thickness + sphereDia,
1336       0
1337     ])
1338  |> line(end = [0, sphereDia / 2])
1339  |> line(end = [-overHangLength + thickness, 0])
1340  |> line(end = [0, thickness])
1341  |> line(end = [overHangLength, 0])
1342  |> line(end = [0, -2 * thickness - sphereDia])
1343  |> line(end = [-overHangLength, 0])
1344  |> line(end = [0, thickness])
1345  |> line(end = [overHangLength - thickness, 0])
1346  |> close()
1347  |> revolve(axis = Y)
1348"#
1349        );
1350    }
1351
1352    #[test]
1353    fn test_recast_fn_in_object() {
1354        let some_program_string = r#"bing = { yo = 55 }
1355myNestedVar = [{ prop = callExp(bing.yo) }]
1356"#;
1357        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1358
1359        let recasted = program.recast(&Default::default(), 0);
1360        assert_eq!(recasted, some_program_string);
1361    }
1362
1363    #[test]
1364    fn test_recast_fn_in_array() {
1365        let some_program_string = r#"bing = { yo = 55 }
1366myNestedVar = [callExp(bing.yo)]
1367"#;
1368        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1369
1370        let recasted = program.recast(&Default::default(), 0);
1371        assert_eq!(recasted, some_program_string);
1372    }
1373
1374    #[test]
1375    fn test_recast_ranges() {
1376        let some_program_string = r#"foo = [0..10]
1377ten = 10
1378bar = [0 + 1 .. ten]
1379"#;
1380        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1381
1382        let recasted = program.recast(&Default::default(), 0);
1383        assert_eq!(recasted, some_program_string);
1384    }
1385
1386    #[test]
1387    fn test_recast_space_in_fn_call() {
1388        let some_program_string = r#"fn thing (x) {
1389    return x + 1
1390}
1391
1392thing ( 1 )
1393"#;
1394        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1395
1396        let recasted = program.recast(&Default::default(), 0);
1397        assert_eq!(
1398            recasted,
1399            r#"fn thing(x) {
1400  return x + 1
1401}
1402
1403thing(1)
1404"#
1405        );
1406    }
1407
1408    #[test]
1409    fn test_recast_typed_fn() {
1410        let some_program_string = r#"fn thing(x: string, y: [bool]): number {
1411  return x + 1
1412}
1413"#;
1414        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1415
1416        let recasted = program.recast(&Default::default(), 0);
1417        assert_eq!(recasted, some_program_string);
1418    }
1419
1420    #[test]
1421    fn test_recast_typed_consts() {
1422        let some_program_string = r#"a = 42: number
1423export b = 3.2: number(ft)
1424c = "dsfds": A | B | C
1425d = [1]: [number]
1426e = foo: [number; 3]
1427f = [1, 2, 3]: [number; 1+]
1428"#;
1429        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1430
1431        let recasted = program.recast(&Default::default(), 0);
1432        assert_eq!(recasted, some_program_string);
1433    }
1434
1435    #[test]
1436    fn test_recast_object_fn_in_array_weird_bracket() {
1437        let some_program_string = r#"bing = { yo = 55 }
1438myNestedVar = [
1439  {
1440  prop:   line(a = [bing.yo, 21], b = sketch001)
1441}
1442]
1443"#;
1444        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1445
1446        let recasted = program.recast(&Default::default(), 0);
1447        assert_eq!(
1448            recasted,
1449            r#"bing = { yo = 55 }
1450myNestedVar = [
1451  {
1452  prop = line(a = [bing.yo, 21], b = sketch001)
1453}
1454]
1455"#
1456        );
1457    }
1458
1459    #[test]
1460    fn test_recast_empty_file() {
1461        let some_program_string = r#""#;
1462        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1463
1464        let recasted = program.recast(&Default::default(), 0);
1465        // Its VERY important this comes back with zero new lines.
1466        assert_eq!(recasted, r#""#);
1467    }
1468
1469    #[test]
1470    fn test_recast_empty_file_new_line() {
1471        let some_program_string = r#"
1472"#;
1473        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1474
1475        let recasted = program.recast(&Default::default(), 0);
1476        // Its VERY important this comes back with zero new lines.
1477        assert_eq!(recasted, r#""#);
1478    }
1479
1480    #[test]
1481    fn test_recast_shebang() {
1482        let some_program_string = r#"#!/usr/local/env zoo kcl
1483part001 = startSketchOn(XY)
1484  |> startProfile(at = [-10, -10])
1485  |> line(end = [20, 0])
1486  |> line(end = [0, 20])
1487  |> line(end = [-20, 0])
1488  |> close()
1489"#;
1490
1491        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1492
1493        let recasted = program.recast(&Default::default(), 0);
1494        assert_eq!(
1495            recasted,
1496            r#"#!/usr/local/env zoo kcl
1497
1498part001 = startSketchOn(XY)
1499  |> startProfile(at = [-10, -10])
1500  |> line(end = [20, 0])
1501  |> line(end = [0, 20])
1502  |> line(end = [-20, 0])
1503  |> close()
1504"#
1505        );
1506    }
1507
1508    #[test]
1509    fn test_recast_shebang_new_lines() {
1510        let some_program_string = r#"#!/usr/local/env zoo kcl
1511        
1512
1513
1514part001 = startSketchOn(XY)
1515  |> startProfile(at = [-10, -10])
1516  |> line(end = [20, 0])
1517  |> line(end = [0, 20])
1518  |> line(end = [-20, 0])
1519  |> close()
1520"#;
1521
1522        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1523
1524        let recasted = program.recast(&Default::default(), 0);
1525        assert_eq!(
1526            recasted,
1527            r#"#!/usr/local/env zoo kcl
1528
1529part001 = startSketchOn(XY)
1530  |> startProfile(at = [-10, -10])
1531  |> line(end = [20, 0])
1532  |> line(end = [0, 20])
1533  |> line(end = [-20, 0])
1534  |> close()
1535"#
1536        );
1537    }
1538
1539    #[test]
1540    fn test_recast_shebang_with_comments() {
1541        let some_program_string = r#"#!/usr/local/env zoo kcl
1542        
1543// Yo yo my comments.
1544part001 = startSketchOn(XY)
1545  |> startProfile(at = [-10, -10])
1546  |> line(end = [20, 0])
1547  |> line(end = [0, 20])
1548  |> line(end = [-20, 0])
1549  |> close()
1550"#;
1551
1552        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1553
1554        let recasted = program.recast(&Default::default(), 0);
1555        assert_eq!(
1556            recasted,
1557            r#"#!/usr/local/env zoo kcl
1558
1559// Yo yo my comments.
1560part001 = startSketchOn(XY)
1561  |> startProfile(at = [-10, -10])
1562  |> line(end = [20, 0])
1563  |> line(end = [0, 20])
1564  |> line(end = [-20, 0])
1565  |> close()
1566"#
1567        );
1568    }
1569
1570    #[test]
1571    fn test_recast_empty_function_body_with_comments() {
1572        let input = r#"fn myFunc() {
1573  // Yo yo my comments.
1574}
1575"#;
1576
1577        let program = crate::parsing::top_level_parse(input).unwrap();
1578        let output = program.recast(&Default::default(), 0);
1579        assert_eq!(output, input);
1580    }
1581
1582    #[test]
1583    fn test_recast_large_file() {
1584        let some_program_string = r#"@settings(units=mm)
1585// define nts
1586radius = 6.0
1587width = 144.0
1588length = 83.0
1589depth = 45.0
1590thk = 5
1591hole_diam = 5
1592// define a rectangular shape func
1593fn rectShape(pos, w, l) {
1594  rr = startSketchOn(XY)
1595    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1596    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1597    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1598    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1599    |> close($edge4)
1600  return rr
1601}
1602// build the body of the focusrite scarlett solo gen 4
1603// only used for visualization
1604scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1605  |> extrude(depth)
1606  |> fillet(
1607       radius = radius,
1608       tags = [
1609  edge2,
1610  edge4,
1611  getOppositeEdge(edge2),
1612  getOppositeEdge(edge4)
1613]
1614   )
1615  // build the bracket sketch around the body
1616fn bracketSketch(w, d, t) {
1617  s = startSketchOn({
1618         plane = {
1619  origin = { x = 0, y = length / 2 + thk, z = 0 },
1620  x_axis = { x = 1, y = 0, z = 0 },
1621  y_axis = { x = 0, y = 0, z = 1 },
1622  z_axis = { x = 0, y = 1, z = 0 }
1623}
1624       })
1625    |> startProfile(at = [-w / 2 - t, d + t])
1626    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1627    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1628    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1629    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1630    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1631    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1632    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1633    |> close($edge8)
1634  return s
1635}
1636// build the body of the bracket
1637bracket_body = bracketSketch(w = width, d = depth, t = thk)
1638  |> extrude(length + 10)
1639  |> fillet(
1640       radius = radius,
1641       tags = [
1642  getNextAdjacentEdge(edge7),
1643  getNextAdjacentEdge(edge2),
1644  getNextAdjacentEdge(edge3),
1645  getNextAdjacentEdge(edge6)
1646]
1647     )
1648  // build the tabs of the mounting bracket (right side)
1649tabs_r = startSketchOn({
1650       plane = {
1651  origin = { x = 0, y = 0, z = depth + thk },
1652  x_axis = { x = 1, y = 0, z = 0 },
1653  y_axis = { x = 0, y = 1, z = 0 },
1654  z_axis = { x = 0, y = 0, z = 1 }
1655}
1656     })
1657  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1658  |> line(end = [10, -5])
1659  |> line(end = [0, -10])
1660  |> line(end = [-10, -5])
1661  |> close()
1662  |> subtract2d(tool = circle(
1663       center = [
1664         width / 2 + thk + hole_diam,
1665         length / 2 - hole_diam
1666       ],
1667       radius = hole_diam / 2
1668     ))
1669  |> extrude(-thk)
1670  |> patternLinear3d(
1671       axis = [0, -1, 0],
1672       repetitions = 1,
1673       distance = length - 10
1674     )
1675  // build the tabs of the mounting bracket (left side)
1676tabs_l = startSketchOn({
1677       plane = {
1678  origin = { x = 0, y = 0, z = depth + thk },
1679  x_axis = { x = 1, y = 0, z = 0 },
1680  y_axis = { x = 0, y = 1, z = 0 },
1681  z_axis = { x = 0, y = 0, z = 1 }
1682}
1683     })
1684  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1685  |> line(end = [-10, -5])
1686  |> line(end = [0, -10])
1687  |> line(end = [10, -5])
1688  |> close()
1689  |> subtract2d(tool = circle(
1690       center = [
1691         -width / 2 - thk - hole_diam,
1692         length / 2 - hole_diam
1693       ],
1694       radius = hole_diam / 2
1695     ))
1696  |> extrude(-thk)
1697  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1698"#;
1699        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1700
1701        let recasted = program.recast(&Default::default(), 0);
1702        // Its VERY important this comes back with zero new lines.
1703        assert_eq!(
1704            recasted,
1705            r#"@settings(units = mm)
1706
1707// define nts
1708radius = 6.0
1709width = 144.0
1710length = 83.0
1711depth = 45.0
1712thk = 5
1713hole_diam = 5
1714// define a rectangular shape func
1715fn rectShape(pos, w, l) {
1716  rr = startSketchOn(XY)
1717    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1718    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1719    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1720    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1721    |> close($edge4)
1722  return rr
1723}
1724// build the body of the focusrite scarlett solo gen 4
1725// only used for visualization
1726scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1727  |> extrude(depth)
1728  |> fillet(
1729       radius = radius,
1730       tags = [
1731         edge2,
1732         edge4,
1733         getOppositeEdge(edge2),
1734         getOppositeEdge(edge4)
1735       ],
1736     )
1737// build the bracket sketch around the body
1738fn bracketSketch(w, d, t) {
1739  s = startSketchOn({
1740         plane = {
1741           origin = { x = 0, y = length / 2 + thk, z = 0 },
1742           x_axis = { x = 1, y = 0, z = 0 },
1743           y_axis = { x = 0, y = 0, z = 1 },
1744           z_axis = { x = 0, y = 1, z = 0 }
1745         }
1746       })
1747    |> startProfile(at = [-w / 2 - t, d + t])
1748    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1749    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1750    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1751    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1752    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1753    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1754    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1755    |> close($edge8)
1756  return s
1757}
1758// build the body of the bracket
1759bracket_body = bracketSketch(w = width, d = depth, t = thk)
1760  |> extrude(length + 10)
1761  |> fillet(
1762       radius = radius,
1763       tags = [
1764         getNextAdjacentEdge(edge7),
1765         getNextAdjacentEdge(edge2),
1766         getNextAdjacentEdge(edge3),
1767         getNextAdjacentEdge(edge6)
1768       ],
1769     )
1770// build the tabs of the mounting bracket (right side)
1771tabs_r = startSketchOn({
1772       plane = {
1773         origin = { x = 0, y = 0, z = depth + thk },
1774         x_axis = { x = 1, y = 0, z = 0 },
1775         y_axis = { x = 0, y = 1, z = 0 },
1776         z_axis = { x = 0, y = 0, z = 1 }
1777       }
1778     })
1779  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1780  |> line(end = [10, -5])
1781  |> line(end = [0, -10])
1782  |> line(end = [-10, -5])
1783  |> close()
1784  |> subtract2d(tool = circle(
1785       center = [
1786         width / 2 + thk + hole_diam,
1787         length / 2 - hole_diam
1788       ],
1789       radius = hole_diam / 2,
1790     ))
1791  |> extrude(-thk)
1792  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1793// build the tabs of the mounting bracket (left side)
1794tabs_l = startSketchOn({
1795       plane = {
1796         origin = { x = 0, y = 0, z = depth + thk },
1797         x_axis = { x = 1, y = 0, z = 0 },
1798         y_axis = { x = 0, y = 1, z = 0 },
1799         z_axis = { x = 0, y = 0, z = 1 }
1800       }
1801     })
1802  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1803  |> line(end = [-10, -5])
1804  |> line(end = [0, -10])
1805  |> line(end = [10, -5])
1806  |> close()
1807  |> subtract2d(tool = circle(
1808       center = [
1809         -width / 2 - thk - hole_diam,
1810         length / 2 - hole_diam
1811       ],
1812       radius = hole_diam / 2,
1813     ))
1814  |> extrude(-thk)
1815  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1816"#
1817        );
1818    }
1819
1820    #[test]
1821    fn test_recast_nested_var_declaration_in_fn_body() {
1822        let some_program_string = r#"fn cube(pos, scale) {
1823   sg = startSketchOn(XY)
1824  |> startProfile(at = pos)
1825  |> line(end = [0, scale])
1826  |> line(end = [scale, 0])
1827  |> line(end = [0, -scale])
1828  |> close()
1829  |> extrude(scale)
1830}"#;
1831        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1832
1833        let recasted = program.recast(&Default::default(), 0);
1834        assert_eq!(
1835            recasted,
1836            r#"fn cube(pos, scale) {
1837  sg = startSketchOn(XY)
1838    |> startProfile(at = pos)
1839    |> line(end = [0, scale])
1840    |> line(end = [scale, 0])
1841    |> line(end = [0, -scale])
1842    |> close()
1843    |> extrude(scale)
1844}
1845"#
1846        );
1847    }
1848
1849    #[test]
1850    fn test_as() {
1851        let some_program_string = r#"fn cube(pos, scale) {
1852  x = dfsfs + dfsfsd as y
1853
1854  sg = startSketchOn(XY)
1855    |> startProfile(at = pos) as foo
1856    |> line([0, scale])
1857    |> line([scale, 0]) as bar
1858    |> line([0 as baz, -scale] as qux)
1859    |> close()
1860    |> extrude(length = scale)
1861}
1862
1863cube(pos = 0, scale = 0) as cub
1864"#;
1865        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1866
1867        let recasted = program.recast(&Default::default(), 0);
1868        assert_eq!(recasted, some_program_string,);
1869    }
1870
1871    #[test]
1872    fn test_recast_with_bad_indentation() {
1873        let some_program_string = r#"part001 = startSketchOn(XY)
1874  |> startProfile(at = [0.0, 5.0])
1875              |> line(end = [0.4900857016, -0.0240763666])
1876    |> line(end = [0.6804562304, 0.9087880491])"#;
1877        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1878
1879        let recasted = program.recast(&Default::default(), 0);
1880        assert_eq!(
1881            recasted,
1882            r#"part001 = startSketchOn(XY)
1883  |> startProfile(at = [0.0, 5.0])
1884  |> line(end = [0.4900857016, -0.0240763666])
1885  |> line(end = [0.6804562304, 0.9087880491])
1886"#
1887        );
1888    }
1889
1890    #[test]
1891    fn test_recast_with_bad_indentation_and_inline_comment() {
1892        let some_program_string = r#"part001 = startSketchOn(XY)
1893  |> startProfile(at = [0.0, 5.0])
1894              |> line(end = [0.4900857016, -0.0240763666]) // hello world
1895    |> line(end = [0.6804562304, 0.9087880491])"#;
1896        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1897
1898        let recasted = program.recast(&Default::default(), 0);
1899        assert_eq!(
1900            recasted,
1901            r#"part001 = startSketchOn(XY)
1902  |> startProfile(at = [0.0, 5.0])
1903  |> line(end = [0.4900857016, -0.0240763666]) // hello world
1904  |> line(end = [0.6804562304, 0.9087880491])
1905"#
1906        );
1907    }
1908    #[test]
1909    fn test_recast_with_bad_indentation_and_line_comment() {
1910        let some_program_string = r#"part001 = startSketchOn(XY)
1911  |> startProfile(at = [0.0, 5.0])
1912              |> line(end = [0.4900857016, -0.0240763666])
1913        // hello world
1914    |> line(end = [0.6804562304, 0.9087880491])"#;
1915        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1916
1917        let recasted = program.recast(&Default::default(), 0);
1918        assert_eq!(
1919            recasted,
1920            r#"part001 = startSketchOn(XY)
1921  |> startProfile(at = [0.0, 5.0])
1922  |> line(end = [0.4900857016, -0.0240763666])
1923  // hello world
1924  |> line(end = [0.6804562304, 0.9087880491])
1925"#
1926        );
1927    }
1928
1929    #[test]
1930    fn test_recast_comment_in_a_fn_block() {
1931        let some_program_string = r#"fn myFn() {
1932  // this is a comment
1933  yo = { a = { b = { c = '123' } } } /* block
1934  comment */
1935
1936  key = 'c'
1937  // this is also a comment
1938    return things
1939}"#;
1940        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1941
1942        let recasted = program.recast(&Default::default(), 0);
1943        assert_eq!(
1944            recasted,
1945            r#"fn myFn() {
1946  // this is a comment
1947  yo = { a = { b = { c = '123' } } } /* block
1948  comment */
1949
1950  key = 'c'
1951  // this is also a comment
1952  return things
1953}
1954"#
1955        );
1956    }
1957
1958    #[test]
1959    fn test_recast_comment_under_variable() {
1960        let some_program_string = r#"key = 'c'
1961// this is also a comment
1962thing = 'foo'
1963"#;
1964        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1965
1966        let recasted = program.recast(&Default::default(), 0);
1967        assert_eq!(
1968            recasted,
1969            r#"key = 'c'
1970// this is also a comment
1971thing = 'foo'
1972"#
1973        );
1974    }
1975
1976    #[test]
1977    fn test_recast_multiline_comment_start_file() {
1978        let some_program_string = r#"// hello world
1979// I am a comment
1980key = 'c'
1981// this is also a comment
1982// hello
1983thing = 'foo'
1984"#;
1985        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1986
1987        let recasted = program.recast(&Default::default(), 0);
1988        assert_eq!(
1989            recasted,
1990            r#"// hello world
1991// I am a comment
1992key = 'c'
1993// this is also a comment
1994// hello
1995thing = 'foo'
1996"#
1997        );
1998    }
1999
2000    #[test]
2001    fn test_recast_empty_comment() {
2002        let some_program_string = r#"// hello world
2003//
2004// I am a comment
2005key = 'c'
2006
2007//
2008// I am a comment
2009thing = 'c'
2010
2011foo = 'bar' //
2012"#;
2013        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2014
2015        let recasted = program.recast(&Default::default(), 0);
2016        assert_eq!(
2017            recasted,
2018            r#"// hello world
2019//
2020// I am a comment
2021key = 'c'
2022
2023//
2024// I am a comment
2025thing = 'c'
2026
2027foo = 'bar' //
2028"#
2029        );
2030    }
2031
2032    #[test]
2033    fn test_recast_multiline_comment_under_variable() {
2034        let some_program_string = r#"key = 'c'
2035// this is also a comment
2036// hello
2037thing = 'foo'
2038"#;
2039        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2040
2041        let recasted = program.recast(&Default::default(), 0);
2042        assert_eq!(
2043            recasted,
2044            r#"key = 'c'
2045// this is also a comment
2046// hello
2047thing = 'foo'
2048"#
2049        );
2050    }
2051
2052    #[test]
2053    fn test_recast_only_line_comments() {
2054        let code = r#"// comment at start
2055"#;
2056        let program = crate::parsing::top_level_parse(code).unwrap();
2057
2058        assert_eq!(program.recast(&Default::default(), 0), code);
2059    }
2060
2061    #[test]
2062    fn test_recast_comment_at_start() {
2063        let test_program = r#"
2064/* comment at start */
2065
2066mySk1 = startSketchOn(XY)
2067  |> startProfile(at = [0, 0])"#;
2068        let program = crate::parsing::top_level_parse(test_program).unwrap();
2069
2070        let recasted = program.recast(&Default::default(), 0);
2071        assert_eq!(
2072            recasted,
2073            r#"/* comment at start */
2074
2075mySk1 = startSketchOn(XY)
2076  |> startProfile(at = [0, 0])
2077"#
2078        );
2079    }
2080
2081    #[test]
2082    fn test_recast_lots_of_comments() {
2083        let some_program_string = r#"// comment at start
2084mySk1 = startSketchOn(XY)
2085  |> startProfile(at = [0, 0])
2086  |> line(endAbsolute = [1, 1])
2087  // comment here
2088  |> line(endAbsolute = [0, 1], tag = $myTag)
2089  |> line(endAbsolute = [1, 1])
2090  /* and
2091  here
2092  */
2093  // a comment between pipe expression statements
2094  |> rx(90)
2095  // and another with just white space between others below
2096  |> ry(45)
2097  |> rx(45)
2098// one more for good measure"#;
2099        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2100
2101        let recasted = program.recast(&Default::default(), 0);
2102        assert_eq!(
2103            recasted,
2104            r#"// comment at start
2105mySk1 = startSketchOn(XY)
2106  |> startProfile(at = [0, 0])
2107  |> line(endAbsolute = [1, 1])
2108  // comment here
2109  |> line(endAbsolute = [0, 1], tag = $myTag)
2110  |> line(endAbsolute = [1, 1])
2111  /* and
2112  here */
2113  // a comment between pipe expression statements
2114  |> rx(90)
2115  // and another with just white space between others below
2116  |> ry(45)
2117  |> rx(45)
2118// one more for good measure
2119"#
2120        );
2121    }
2122
2123    #[test]
2124    fn test_recast_multiline_object() {
2125        let some_program_string = r#"x = {
2126  a = 1000000000,
2127  b = 2000000000,
2128  c = 3000000000,
2129  d = 4000000000,
2130  e = 5000000000
2131}"#;
2132        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2133
2134        let recasted = program.recast(&Default::default(), 0);
2135        assert_eq!(recasted.trim(), some_program_string);
2136    }
2137
2138    #[test]
2139    fn test_recast_first_level_object() {
2140        let some_program_string = r#"three = 3
2141
2142yo = {
2143  aStr = 'str',
2144  anum = 2,
2145  identifier = three,
2146  binExp = 4 + 5
2147}
2148yo = [
2149  1,
2150  "  2,",
2151  "three",
2152  4 + 5,
2153  "  hey oooooo really long long long"
2154]
2155"#;
2156        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2157
2158        let recasted = program.recast(&Default::default(), 0);
2159        assert_eq!(recasted, some_program_string);
2160    }
2161
2162    #[test]
2163    fn test_recast_new_line_before_comment() {
2164        let some_program_string = r#"
2165// this is a comment
2166yo = { a = { b = { c = '123' } } }
2167
2168key = 'c'
2169things = "things"
2170
2171// this is also a comment"#;
2172        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2173
2174        let recasted = program.recast(&Default::default(), 0);
2175        let expected = some_program_string.trim();
2176        // Currently new parser removes an empty line
2177        let actual = recasted.trim();
2178        assert_eq!(actual, expected);
2179    }
2180
2181    #[test]
2182    fn test_recast_comment_tokens_inside_strings() {
2183        let some_program_string = r#"b = {
2184  end = 141,
2185  start = 125,
2186  type_ = "NonCodeNode",
2187  value = "
2188 // a comment
2189   "
2190}"#;
2191        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2192
2193        let recasted = program.recast(&Default::default(), 0);
2194        assert_eq!(recasted.trim(), some_program_string.trim());
2195    }
2196
2197    #[test]
2198    fn test_recast_array_new_line_in_pipe() {
2199        let some_program_string = r#"myVar = 3
2200myVar2 = 5
2201myVar3 = 6
2202myAng = 40
2203myAng2 = 134
2204part001 = startSketchOn(XY)
2205  |> startProfile(at = [0, 0])
2206  |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2207  |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2208  |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2209        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2210
2211        let recasted = program.recast(&Default::default(), 0);
2212        assert_eq!(recasted.trim(), some_program_string);
2213    }
2214
2215    #[test]
2216    fn test_recast_array_new_line_in_pipe_custom() {
2217        let some_program_string = r#"myVar = 3
2218myVar2 = 5
2219myVar3 = 6
2220myAng = 40
2221myAng2 = 134
2222part001 = startSketchOn(XY)
2223   |> startProfile(at = [0, 0])
2224   |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2225   |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2226   |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2227"#;
2228        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2229
2230        let recasted = program.recast(
2231            &FormatOptions {
2232                tab_size: 3,
2233                use_tabs: false,
2234                insert_final_newline: true,
2235            },
2236            0,
2237        );
2238        assert_eq!(recasted, some_program_string);
2239    }
2240
2241    #[test]
2242    fn test_recast_after_rename_std() {
2243        let some_program_string = r#"part001 = startSketchOn(XY)
2244  |> startProfile(at = [0.0000000000, 5.0000000000])
2245    |> line(end = [0.4900857016, -0.0240763666])
2246
2247part002 = "part002"
2248things = [part001, 0.0]
2249blah = 1
2250foo = false
2251baz = {a: 1, part001: "thing"}
2252
2253fn ghi(part001) {
2254  return part001
2255}
2256"#;
2257        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2258        program.rename_symbol("mySuperCoolPart", 6);
2259
2260        let recasted = program.recast(&Default::default(), 0);
2261        assert_eq!(
2262            recasted,
2263            r#"mySuperCoolPart = startSketchOn(XY)
2264  |> startProfile(at = [0.0, 5.0])
2265  |> line(end = [0.4900857016, -0.0240763666])
2266
2267part002 = "part002"
2268things = [mySuperCoolPart, 0.0]
2269blah = 1
2270foo = false
2271baz = { a = 1, part001 = "thing" }
2272
2273fn ghi(part001) {
2274  return part001
2275}
2276"#
2277        );
2278    }
2279
2280    #[test]
2281    fn test_recast_after_rename_fn_args() {
2282        let some_program_string = r#"fn ghi(x, y, z) {
2283  return x
2284}"#;
2285        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2286        program.rename_symbol("newName", 7);
2287
2288        let recasted = program.recast(&Default::default(), 0);
2289        assert_eq!(
2290            recasted,
2291            r#"fn ghi(newName, y, z) {
2292  return newName
2293}
2294"#
2295        );
2296    }
2297
2298    #[test]
2299    fn test_recast_trailing_comma() {
2300        let some_program_string = r#"startSketchOn(XY)
2301  |> startProfile(at = [0, 0])
2302  |> arc({
2303    radius = 1,
2304    angle_start = 0,
2305    angle_end = 180,
2306  })"#;
2307        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2308
2309        let recasted = program.recast(&Default::default(), 0);
2310        assert_eq!(
2311            recasted,
2312            r#"startSketchOn(XY)
2313  |> startProfile(at = [0, 0])
2314  |> arc({
2315       radius = 1,
2316       angle_start = 0,
2317       angle_end = 180
2318     })
2319"#
2320        );
2321    }
2322
2323    #[test]
2324    fn test_recast_negative_var() {
2325        let some_program_string = r#"w = 20
2326l = 8
2327h = 10
2328
2329firstExtrude = startSketchOn(XY)
2330  |> startProfile(at = [0,0])
2331  |> line(end = [0, l])
2332  |> line(end = [w, 0])
2333  |> line(end = [0, -l])
2334  |> close()
2335  |> extrude(h)
2336"#;
2337        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2338
2339        let recasted = program.recast(&Default::default(), 0);
2340        assert_eq!(
2341            recasted,
2342            r#"w = 20
2343l = 8
2344h = 10
2345
2346firstExtrude = startSketchOn(XY)
2347  |> startProfile(at = [0, 0])
2348  |> line(end = [0, l])
2349  |> line(end = [w, 0])
2350  |> line(end = [0, -l])
2351  |> close()
2352  |> extrude(h)
2353"#
2354        );
2355    }
2356
2357    #[test]
2358    fn test_recast_multiline_comment() {
2359        let some_program_string = r#"w = 20
2360l = 8
2361h = 10
2362
2363// This is my comment
2364// It has multiple lines
2365// And it's really long
2366firstExtrude = startSketchOn(XY)
2367  |> startProfile(at = [0,0])
2368  |> line(end = [0, l])
2369  |> line(end = [w, 0])
2370  |> line(end = [0, -l])
2371  |> close()
2372  |> extrude(h)
2373"#;
2374        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2375
2376        let recasted = program.recast(&Default::default(), 0);
2377        assert_eq!(
2378            recasted,
2379            r#"w = 20
2380l = 8
2381h = 10
2382
2383// This is my comment
2384// It has multiple lines
2385// And it's really long
2386firstExtrude = startSketchOn(XY)
2387  |> startProfile(at = [0, 0])
2388  |> line(end = [0, l])
2389  |> line(end = [w, 0])
2390  |> line(end = [0, -l])
2391  |> close()
2392  |> extrude(h)
2393"#
2394        );
2395    }
2396
2397    #[test]
2398    fn test_recast_math_start_negative() {
2399        let some_program_string = r#"myVar = -5 + 6"#;
2400        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2401
2402        let recasted = program.recast(&Default::default(), 0);
2403        assert_eq!(recasted.trim(), some_program_string);
2404    }
2405
2406    #[test]
2407    fn test_recast_math_negate_parens() {
2408        let some_program_string = r#"wallMountL = 3.82
2409thickness = 0.5
2410
2411startSketchOn(XY)
2412  |> startProfile(at = [0, 0])
2413  |> line(end = [0, -(wallMountL - thickness)])
2414  |> line(end = [0, -(5 - thickness)])
2415  |> line(end = [0, -(5 - 1)])
2416  |> line(end = [0, -(-5 - 1)])"#;
2417        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2418
2419        let recasted = program.recast(&Default::default(), 0);
2420        assert_eq!(recasted.trim(), some_program_string);
2421    }
2422
2423    #[test]
2424    fn test_recast_math_nested_parens() {
2425        let some_program_string = r#"distance = 5
2426p = 3: Plane
2427FOS = { a = 3, b = 42 }: Sketch
2428sigmaAllow = 8: number(mm)
2429width = 20
2430thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2431        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2432
2433        let recasted = program.recast(&Default::default(), 0);
2434        assert_eq!(recasted.trim(), some_program_string);
2435    }
2436
2437    #[test]
2438    fn no_vardec_keyword() {
2439        let some_program_string = r#"distance = 5"#;
2440        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2441
2442        let recasted = program.recast(&Default::default(), 0);
2443        assert_eq!(recasted.trim(), some_program_string);
2444    }
2445
2446    #[test]
2447    fn recast_types() {
2448        let some_program_string = r#"type foo
2449
2450// A comment
2451@(impl = primitive)
2452export type bar(unit, baz)
2453type baz = Foo | Bar
2454"#;
2455        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2456        let recasted = program.recast(&Default::default(), 0);
2457        assert_eq!(recasted, some_program_string);
2458    }
2459
2460    #[test]
2461    fn recast_nested_fn() {
2462        let some_program_string = r#"fn f() {
2463  return fn() {
2464  return 1
2465}
2466}"#;
2467        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2468        let recasted = program.recast(&Default::default(), 0);
2469        let expected = "\
2470fn f() {
2471  return fn() {
2472    return 1
2473  }
2474}";
2475        assert_eq!(recasted.trim(), expected);
2476    }
2477
2478    #[test]
2479    fn recast_literal() {
2480        use winnow::Parser;
2481        for (i, (raw, expected, reason)) in [
2482            (
2483                "5.0",
2484                "5.0",
2485                "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2486            ),
2487            (
2488                "5",
2489                "5",
2490                "integers should stay integral, i.e. don't reformat this to '5.0'",
2491            ),
2492            (
2493                "5.0000000",
2494                "5.0",
2495                "if the number is f64 but not fractional, use its canonical format",
2496            ),
2497            ("5.1", "5.1", "straightforward case works"),
2498        ]
2499        .into_iter()
2500        .enumerate()
2501        {
2502            let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2503            let literal = crate::parsing::parser::unsigned_number_literal
2504                .parse(tokens.as_slice())
2505                .unwrap();
2506            assert_eq!(
2507                literal.recast(),
2508                expected,
2509                "failed test {i}, which is testing that {reason}"
2510            );
2511        }
2512    }
2513
2514    #[test]
2515    fn recast_objects_no_comments() {
2516        let input = r#"
2517sketch002 = startSketchOn({
2518       plane: {
2519    origin: { x = 1, y = 2, z = 3 },
2520    x_axis = { x = 4, y = 5, z = 6 },
2521    y_axis = { x = 7, y = 8, z = 9 },
2522    z_axis = { x = 10, y = 11, z = 12 }
2523       }
2524  })
2525"#;
2526        let expected = r#"sketch002 = startSketchOn({
2527  plane = {
2528    origin = { x = 1, y = 2, z = 3 },
2529    x_axis = { x = 4, y = 5, z = 6 },
2530    y_axis = { x = 7, y = 8, z = 9 },
2531    z_axis = { x = 10, y = 11, z = 12 }
2532  }
2533})
2534"#;
2535        let ast = crate::parsing::top_level_parse(input).unwrap();
2536        let actual = ast.recast(&FormatOptions::new(), 0);
2537        assert_eq!(actual, expected);
2538    }
2539
2540    #[test]
2541    fn unparse_fn_unnamed() {
2542        let input = r#"squares_out = reduce(
2543  arr,
2544  n = 0: number,
2545  f = fn(@i, accum) {
2546    return 1
2547  },
2548)
2549"#;
2550        let ast = crate::parsing::top_level_parse(input).unwrap();
2551        let actual = ast.recast(&FormatOptions::new(), 0);
2552        assert_eq!(actual, input);
2553    }
2554
2555    #[test]
2556    fn unparse_fn_named() {
2557        let input = r#"fn f(x) {
2558  return 1
2559}
2560"#;
2561        let ast = crate::parsing::top_level_parse(input).unwrap();
2562        let actual = ast.recast(&FormatOptions::new(), 0);
2563        assert_eq!(actual, input);
2564    }
2565
2566    #[test]
2567    fn unparse_call_inside_function_single_line() {
2568        let input = r#"fn foo() {
2569  toDegrees(atan(0.5), foo = 1)
2570  return 0
2571}
2572"#;
2573        let ast = crate::parsing::top_level_parse(input).unwrap();
2574        let actual = ast.recast(&FormatOptions::new(), 0);
2575        assert_eq!(actual, input);
2576    }
2577
2578    #[test]
2579    fn recast_function_types() {
2580        let input = r#"foo = x: fn
2581foo = x: fn(number)
2582fn foo(x: fn(): number): fn {
2583  return 0
2584}
2585fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
2586  return 0
2587}
2588type fn
2589type foo = fn
2590type foo = fn(a: string, b: { f: fn(): any })
2591type foo = fn([fn])
2592type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
2593"#;
2594        let ast = crate::parsing::top_level_parse(input).unwrap();
2595        let actual = ast.recast(&FormatOptions::new(), 0);
2596        assert_eq!(actual, input);
2597    }
2598
2599    #[test]
2600    fn unparse_call_inside_function_args_multiple_lines() {
2601        let input = r#"fn foo() {
2602  toDegrees(
2603    atan(0.5),
2604    foo = 1,
2605    bar = 2,
2606    baz = 3,
2607    qux = 4,
2608  )
2609  return 0
2610}
2611"#;
2612        let ast = crate::parsing::top_level_parse(input).unwrap();
2613        let actual = ast.recast(&FormatOptions::new(), 0);
2614        assert_eq!(actual, input);
2615    }
2616
2617    #[test]
2618    fn unparse_call_inside_function_single_arg_multiple_lines() {
2619        let input = r#"fn foo() {
2620  toDegrees(
2621    [
2622      profile0,
2623      profile1,
2624      profile2,
2625      profile3,
2626      profile4,
2627      profile5
2628    ],
2629    key = 1,
2630  )
2631  return 0
2632}
2633"#;
2634        let ast = crate::parsing::top_level_parse(input).unwrap();
2635        let actual = ast.recast(&FormatOptions::new(), 0);
2636        assert_eq!(actual, input);
2637    }
2638
2639    #[test]
2640    fn recast_objects_with_comments() {
2641        use winnow::Parser;
2642        for (i, (input, expected, reason)) in [(
2643            "\
2644{
2645  a = 1,
2646  // b = 2,
2647  c = 3
2648}",
2649            "\
2650{
2651  a = 1,
2652  // b = 2,
2653  c = 3
2654}",
2655            "preserves comments",
2656        )]
2657        .into_iter()
2658        .enumerate()
2659        {
2660            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2661            crate::parsing::parser::print_tokens(tokens.as_slice());
2662            let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
2663            assert_eq!(
2664                expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2665                expected,
2666                "failed test {i}, which is testing that recasting {reason}"
2667            );
2668        }
2669    }
2670
2671    #[test]
2672    fn recast_array_with_comments() {
2673        use winnow::Parser;
2674        for (i, (input, expected, reason)) in [
2675            (
2676                "\
2677[
2678  1,
2679  2,
2680  3,
2681  4,
2682  5,
2683  6,
2684  7,
2685  8,
2686  9,
2687  10,
2688  11,
2689  12,
2690  13,
2691  14,
2692  15,
2693  16,
2694  17,
2695  18,
2696  19,
2697  20,
2698]",
2699                "\
2700[
2701  1,
2702  2,
2703  3,
2704  4,
2705  5,
2706  6,
2707  7,
2708  8,
2709  9,
2710  10,
2711  11,
2712  12,
2713  13,
2714  14,
2715  15,
2716  16,
2717  17,
2718  18,
2719  19,
2720  20
2721]",
2722                "preserves multi-line arrays",
2723            ),
2724            (
2725                "\
2726[
2727  1,
2728  // 2,
2729  3
2730]",
2731                "\
2732[
2733  1,
2734  // 2,
2735  3
2736]",
2737                "preserves comments",
2738            ),
2739            (
2740                "\
2741[
2742  1,
2743  2,
2744  // 3
2745]",
2746                "\
2747[
2748  1,
2749  2,
2750  // 3
2751]",
2752                "preserves comments at the end of the array",
2753            ),
2754        ]
2755        .into_iter()
2756        .enumerate()
2757        {
2758            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2759            let expr = crate::parsing::parser::array_elem_by_elem
2760                .parse(tokens.as_slice())
2761                .unwrap();
2762            assert_eq!(
2763                expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2764                expected,
2765                "failed test {i}, which is testing that recasting {reason}"
2766            );
2767        }
2768    }
2769
2770    #[test]
2771    fn code_with_comment_and_extra_lines() {
2772        let code = r#"yo = 'c'
2773
2774/* this is
2775a
2776comment */
2777yo = 'bing'
2778"#;
2779        let ast = crate::parsing::top_level_parse(code).unwrap();
2780        let recasted = ast.recast(&FormatOptions::new(), 0);
2781        assert_eq!(recasted, code);
2782    }
2783
2784    #[test]
2785    fn comments_in_a_fn_block() {
2786        let code = r#"fn myFn() {
2787  // this is a comment
2788  yo = { a = { b = { c = '123' } } }
2789
2790  /* block
2791  comment */
2792  key = 'c'
2793  // this is also a comment
2794}
2795"#;
2796        let ast = crate::parsing::top_level_parse(code).unwrap();
2797        let recasted = ast.recast(&FormatOptions::new(), 0);
2798        assert_eq!(recasted, code);
2799    }
2800}