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