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