kcl_lib/
unparser.rs

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