kcl_lib/
unparser.rs

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