Skip to main content

kcl_lib/
unparser.rs

1use std::fmt::Write;
2
3use crate::KclError;
4use crate::ModuleId;
5use crate::parsing::DeprecationKind;
6use crate::parsing::PIPE_OPERATOR;
7use crate::parsing::ast::types::Annotation;
8use crate::parsing::ast::types::ArrayExpression;
9use crate::parsing::ast::types::ArrayRangeExpression;
10use crate::parsing::ast::types::AscribedExpression;
11use crate::parsing::ast::types::Associativity;
12use crate::parsing::ast::types::BinaryExpression;
13use crate::parsing::ast::types::BinaryOperator;
14use crate::parsing::ast::types::BinaryPart;
15use crate::parsing::ast::types::Block;
16use crate::parsing::ast::types::BodyItem;
17use crate::parsing::ast::types::CallExpressionKw;
18use crate::parsing::ast::types::CommentStyle;
19use crate::parsing::ast::types::DefaultParamVal;
20use crate::parsing::ast::types::EnumDeclaration;
21use crate::parsing::ast::types::Expr;
22use crate::parsing::ast::types::FormatOptions;
23use crate::parsing::ast::types::FunctionExpression;
24use crate::parsing::ast::types::Identifier;
25use crate::parsing::ast::types::IfExpression;
26use crate::parsing::ast::types::ImportSelector;
27use crate::parsing::ast::types::ImportStatement;
28use crate::parsing::ast::types::ItemVisibility;
29use crate::parsing::ast::types::LabeledArg;
30use crate::parsing::ast::types::Literal;
31use crate::parsing::ast::types::LiteralValue;
32use crate::parsing::ast::types::MemberExpression;
33use crate::parsing::ast::types::Name;
34use crate::parsing::ast::types::Node;
35use crate::parsing::ast::types::NodeList;
36use crate::parsing::ast::types::NonCodeMeta;
37use crate::parsing::ast::types::NonCodeNode;
38use crate::parsing::ast::types::NonCodeValue;
39use crate::parsing::ast::types::NumericLiteral;
40use crate::parsing::ast::types::ObjectExpression;
41use crate::parsing::ast::types::Parameter;
42use crate::parsing::ast::types::PipeExpression;
43use crate::parsing::ast::types::Program;
44use crate::parsing::ast::types::SketchBlock;
45use crate::parsing::ast::types::SketchVar;
46use crate::parsing::ast::types::TagDeclarator;
47use crate::parsing::ast::types::TypeDeclaration;
48use crate::parsing::ast::types::TypeDeclarationDefinition;
49use crate::parsing::ast::types::UnaryExpression;
50use crate::parsing::ast::types::VariableDeclaration;
51use crate::parsing::ast::types::VariableKind;
52use crate::parsing::deprecation;
53
54#[allow(dead_code)]
55pub fn fmt(input: &str) -> Result<String, KclError> {
56    let program = crate::parsing::parse_str(input, ModuleId::default()).parse_errs_as_err()?;
57    Ok(program.recast_top(&Default::default(), 0))
58}
59
60impl Program {
61    pub fn recast_top(&self, options: &FormatOptions, indentation_level: usize) -> String {
62        let mut buf = String::with_capacity(1024);
63        self.recast(&mut buf, options, indentation_level);
64        buf
65    }
66
67    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
68        if let Some(sh) = self.shebang.as_ref() {
69            write!(buf, "{}\n\n", sh.inner.content).no_fail();
70        }
71
72        recast_body(
73            &self.body,
74            &self.non_code_meta,
75            &self.inner_attrs,
76            buf,
77            options,
78            indentation_level,
79        );
80    }
81}
82
83fn recast_body(
84    items: &[BodyItem],
85    non_code_meta: &NonCodeMeta,
86    inner_attrs: &NodeList<Annotation>,
87    buf: &mut String,
88    options: &FormatOptions,
89    indentation_level: usize,
90) {
91    let indentation = options.get_indentation(indentation_level);
92
93    let has_non_newline_start_node = non_code_meta
94        .start_nodes
95        .iter()
96        .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
97    if has_non_newline_start_node {
98        let mut pending_newline = false;
99        for start_node in &non_code_meta.start_nodes {
100            match start_node.value {
101                NonCodeValue::NewLine => pending_newline = true,
102                _ => {
103                    if pending_newline {
104                        // If the previous emission already ended with '\n', only add one more.
105                        if buf.ends_with('\n') {
106                            buf.push('\n');
107                        } else {
108                            buf.push_str("\n\n");
109                        }
110                        pending_newline = false;
111                    }
112                    let noncode_recast = start_node.recast(options, indentation_level);
113                    buf.push_str(&noncode_recast);
114                }
115            }
116        }
117        // Handle any trailing newlines that weren't flushed yet.
118        if pending_newline {
119            if buf.ends_with('\n') {
120                buf.push('\n');
121            } else {
122                buf.push_str("\n\n");
123            }
124        }
125    }
126
127    for attr in inner_attrs {
128        options.write_indentation(buf, indentation_level);
129        attr.recast(buf, options, indentation_level);
130    }
131    if !inner_attrs.is_empty() {
132        buf.push('\n');
133    }
134
135    let body_item_lines = items.iter().map(|body_item| {
136        let mut result = String::with_capacity(256);
137        for comment in body_item.get_comments() {
138            if !comment.is_empty() {
139                result.push_str(&indentation);
140                result.push_str(comment);
141            }
142            if comment.is_empty() && !result.ends_with("\n") {
143                result.push('\n');
144            }
145            if !result.ends_with("\n\n") && result != "\n" {
146                result.push('\n');
147            }
148        }
149        for attr in body_item.get_attrs() {
150            attr.recast(&mut result, options, indentation_level);
151        }
152        match body_item {
153            BodyItem::ImportStatement(stmt) => {
154                result.push_str(&stmt.recast(options, indentation_level));
155            }
156            BodyItem::ExpressionStatement(expression_statement) => {
157                let mut tmp_buf = String::new();
158                expression_statement
159                    .expression
160                    .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
161                options.write_indentation(&mut result, indentation_level);
162                result.push_str(tmp_buf.trim_start());
163            }
164            BodyItem::VariableDeclaration(variable_declaration) => {
165                variable_declaration.recast(&mut result, options, indentation_level);
166            }
167            BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(&mut result, options, indentation_level),
168            BodyItem::ReturnStatement(return_statement) => {
169                write!(&mut result, "{indentation}return ").no_fail();
170                let mut tmp_buf = String::with_capacity(256);
171                return_statement
172                    .argument
173                    .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
174                write!(&mut result, "{}", tmp_buf.trim_start()).no_fail();
175            }
176        };
177        result
178    });
179    for (index, recast_str) in body_item_lines.enumerate() {
180        write!(buf, "{recast_str}").no_fail();
181
182        // determine the value of the end string
183        // basically if we are inside a nested function we want to end with a new line
184        let needs_line_break = !(index == items.len() - 1 && indentation_level == 0);
185
186        let custom_white_space_or_comment = non_code_meta.non_code_nodes.get(&index).map(|noncodes| {
187            noncodes.iter().enumerate().map(|(i, custom_white_space_or_comment)| {
188                let formatted = custom_white_space_or_comment.recast(options, indentation_level);
189                if i == 0 && !formatted.trim().is_empty() {
190                    if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
191                        format!("\n{formatted}")
192                    } else {
193                        formatted
194                    }
195                } else {
196                    formatted
197                }
198            })
199        });
200
201        if let Some(custom) = custom_white_space_or_comment {
202            for to_write in custom {
203                write!(buf, "{to_write}").no_fail();
204            }
205        } else if needs_line_break {
206            buf.push('\n')
207        }
208    }
209    trim_end(buf);
210
211    // Insert a final new line if the user wants it.
212    if options.insert_final_newline && !buf.is_empty() {
213        buf.push('\n');
214    }
215}
216
217impl NonCodeValue {
218    fn should_cause_array_newline(&self) -> bool {
219        match self {
220            Self::InlineComment { .. } => false,
221            Self::BlockComment { .. } | Self::NewLine => true,
222        }
223    }
224}
225
226impl Node<NonCodeNode> {
227    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
228        let indentation = options.get_indentation(indentation_level);
229        match &self.value {
230            NonCodeValue::InlineComment {
231                value,
232                style: CommentStyle::Line,
233            } => format!(" // {value}\n"),
234            NonCodeValue::InlineComment {
235                value,
236                style: CommentStyle::Block,
237            } => format!(" /* {value} */"),
238            NonCodeValue::BlockComment { value, style } => match style {
239                CommentStyle::Block => format!("{indentation}/* {value} */"),
240                CommentStyle::Line => {
241                    if value.trim().is_empty() {
242                        format!("{indentation}//\n")
243                    } else {
244                        format!("{}// {}\n", indentation, value.trim())
245                    }
246                }
247            },
248            NonCodeValue::NewLine => "\n\n".to_string(),
249        }
250    }
251}
252
253impl Node<Annotation> {
254    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
255        let indentation = options.get_indentation(indentation_level);
256        let mut result = String::new();
257        for comment in &self.pre_comments {
258            if !comment.is_empty() {
259                result.push_str(&indentation);
260                result.push_str(comment);
261            }
262            if !result.ends_with("\n\n") && result != "\n" {
263                result.push('\n');
264            }
265        }
266        result.push('@');
267        if let Some(name) = &self.name {
268            result.push_str(&name.name);
269        }
270        if let Some(properties) = &self.properties {
271            result.push('(');
272            result.push_str(
273                &properties
274                    .iter()
275                    .map(|prop| {
276                        let mut temp = format!("{} = ", prop.key.name);
277                        prop.value
278                            .recast(&mut temp, options, indentation_level + 1, ExprContext::Other);
279                        temp.trim().to_owned()
280                    })
281                    .collect::<Vec<String>>()
282                    .join(", "),
283            );
284            result.push(')');
285            result.push('\n');
286        }
287
288        buf.push_str(&result)
289    }
290}
291
292impl ImportStatement {
293    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
294        let indentation = options.get_indentation(indentation_level);
295        let vis = if self.visibility == ItemVisibility::Export {
296            "export "
297        } else {
298            ""
299        };
300        let mut string = format!("{vis}{indentation}import ");
301        match &self.selector {
302            ImportSelector::List { items } => {
303                for (i, item) in items.iter().enumerate() {
304                    if i > 0 {
305                        string.push_str(", ");
306                    }
307                    string.push_str(&item.name.name);
308                    if let Some(alias) = &item.alias {
309                        // If the alias is the same, don't output it.
310                        if item.name.name != alias.name {
311                            string.push_str(&format!(" as {}", alias.name));
312                        }
313                    }
314                }
315                string.push_str(" from ");
316            }
317            ImportSelector::Glob(_) => string.push_str("* from "),
318            ImportSelector::None { .. } => {}
319        }
320        string.push_str(&format!("\"{}\"", self.path));
321
322        if let ImportSelector::None { alias: Some(alias) } = &self.selector {
323            string.push_str(" as ");
324            string.push_str(&alias.name);
325        }
326        string
327    }
328}
329
330#[derive(Copy, Clone, Debug, Eq, PartialEq)]
331pub(crate) enum ExprContext {
332    Pipe,
333    /// The first expression in a pipe. It should not get pipe-specific child indentation,
334    /// but it also should not emit a leading indent before the expression itself.
335    PipeHead,
336    FnDecl,
337    /// Being used as an argument to a call expression, which is in a pipe expression.
338    PipeCallArg,
339    /// Being used as an argument to a call expression.
340    CallArg,
341    Other,
342}
343
344impl ExprContext {
345    fn in_pipe(self) -> bool {
346        matches!(self, ExprContext::Pipe | ExprContext::PipeCallArg)
347    }
348
349    fn needs_leading_indent(self) -> bool {
350        !matches!(
351            self,
352            ExprContext::PipeHead | ExprContext::CallArg | ExprContext::PipeCallArg
353        )
354    }
355
356    fn call_arg_context(self) -> ExprContext {
357        if self.in_pipe() {
358            ExprContext::PipeCallArg
359        } else {
360            ExprContext::CallArg
361        }
362    }
363}
364
365impl Expr {
366    pub(crate) fn recast(
367        &self,
368        buf: &mut String,
369        options: &FormatOptions,
370        indentation_level: usize,
371        mut ctxt: ExprContext,
372    ) {
373        let is_decl = matches!(ctxt, ExprContext::FnDecl);
374        if is_decl {
375            // Just because this expression is being bound to a variable, doesn't mean that every child
376            // expression is being bound. So, reset the expression context if necessary.
377            // This will still preserve the "::Pipe" context though.
378            ctxt = ExprContext::Other;
379        }
380        match &self {
381            Expr::BinaryExpression(bin_exp) => bin_exp.recast(buf, options, indentation_level, ctxt),
382            Expr::ArrayExpression(array_exp) => array_exp.recast(buf, options, indentation_level, ctxt),
383            Expr::ArrayRangeExpression(range_exp) => range_exp.recast(buf, options, indentation_level, ctxt),
384            Expr::ObjectExpression(obj_exp) => obj_exp.recast(buf, options, indentation_level, ctxt),
385            Expr::MemberExpression(mem_exp) => mem_exp.recast(buf, options, indentation_level, ctxt),
386            Expr::Literal(literal) => {
387                literal.recast(buf);
388            }
389            Expr::FunctionExpression(func_exp) => {
390                if !is_decl {
391                    buf.push_str("fn");
392                    if let Some(name) = &func_exp.name {
393                        buf.push(' ');
394                        buf.push_str(&name.name);
395                    }
396                }
397                func_exp.recast(buf, options, indentation_level);
398            }
399            Expr::CallExpressionKw(call_exp) => call_exp.recast(buf, options, indentation_level, ctxt),
400            Expr::Name(name) => {
401                let result = &name.inner.name.inner.name;
402                match deprecation(result, DeprecationKind::Const) {
403                    Some(suggestion) => buf.push_str(suggestion),
404                    None => {
405                        for prefix in &name.path {
406                            buf.push_str(&prefix.name);
407                            buf.push(':');
408                            buf.push(':');
409                        }
410                        buf.push_str(result);
411                    }
412                }
413            }
414            Expr::TagDeclarator(tag) => tag.recast(buf),
415            Expr::PipeExpression(pipe_exp) => {
416                pipe_exp.recast(buf, options, indentation_level, !is_decl && ctxt.needs_leading_indent())
417            }
418            Expr::UnaryExpression(unary_exp) => unary_exp.recast(buf, options, indentation_level, ctxt),
419            Expr::IfExpression(e) => e.recast(buf, options, indentation_level, ctxt),
420            Expr::PipeSubstitution(_) => buf.push_str(crate::parsing::PIPE_SUBSTITUTION_OPERATOR),
421            Expr::LabelledExpression(e) => {
422                e.expr.recast(buf, options, indentation_level, ctxt);
423                buf.push_str(" as ");
424                buf.push_str(&e.label.name);
425            }
426            Expr::AscribedExpression(e) => e.recast(buf, options, indentation_level, ctxt),
427            Expr::SketchBlock(e) => e.recast(buf, options, indentation_level, ctxt),
428            Expr::SketchVar(e) => e.recast(buf),
429            Expr::None(_) => {
430                unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
431            }
432        }
433    }
434}
435
436impl AscribedExpression {
437    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
438        if matches!(
439            self.expr,
440            Expr::BinaryExpression(..) | Expr::PipeExpression(..) | Expr::UnaryExpression(..)
441        ) {
442            buf.push('(');
443            self.expr.recast(buf, options, indentation_level, ctxt);
444            buf.push(')');
445        } else {
446            self.expr.recast(buf, options, indentation_level, ctxt);
447        }
448        buf.push_str(": ");
449        write!(buf, "{}", self.ty).no_fail();
450    }
451}
452
453impl BinaryPart {
454    pub(crate) fn recast(
455        &self,
456        buf: &mut String,
457        options: &FormatOptions,
458        indentation_level: usize,
459        ctxt: ExprContext,
460    ) {
461        match &self {
462            BinaryPart::Literal(literal) => {
463                literal.recast(buf);
464            }
465            BinaryPart::Name(name) => match deprecation(&name.inner.name.inner.name, DeprecationKind::Const) {
466                Some(suggestion) => write!(buf, "{suggestion}").no_fail(),
467                None => name.write_to(buf).no_fail(),
468            },
469            BinaryPart::BinaryExpression(binary_expression) => {
470                binary_expression.recast(buf, options, indentation_level, ctxt)
471            }
472            BinaryPart::CallExpressionKw(call_expression) => {
473                call_expression.recast(buf, options, indentation_level, ExprContext::Other)
474            }
475            BinaryPart::UnaryExpression(unary_expression) => {
476                unary_expression.recast(buf, options, indentation_level, ctxt)
477            }
478            BinaryPart::MemberExpression(member_expression) => {
479                member_expression.recast(buf, options, indentation_level, ctxt)
480            }
481            BinaryPart::ArrayExpression(e) => e.recast(buf, options, indentation_level, ctxt),
482            BinaryPart::ArrayRangeExpression(e) => e.recast(buf, options, indentation_level, ctxt),
483            BinaryPart::ObjectExpression(e) => e.recast(buf, options, indentation_level, ctxt),
484            BinaryPart::IfExpression(e) => e.recast(buf, options, indentation_level, ExprContext::Other),
485            BinaryPart::AscribedExpression(e) => e.recast(buf, options, indentation_level, ExprContext::Other),
486            BinaryPart::SketchVar(e) => e.recast(buf),
487        }
488    }
489}
490
491impl CallExpressionKw {
492    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
493        recast_call(
494            &self.callee,
495            self.unlabeled.as_ref(),
496            &self.arguments,
497            &self.non_code_meta,
498            buf,
499            options,
500            indentation_level,
501            ctxt,
502        );
503    }
504}
505
506fn recast_args(
507    unlabeled: Option<&Expr>,
508    arguments: &[LabeledArg],
509    options: &FormatOptions,
510    indentation_level: usize,
511    ctxt: ExprContext,
512) -> Vec<String> {
513    let arg_ctxt = ctxt.call_arg_context();
514    let mut arg_list = if let Some(first_arg) = unlabeled {
515        let mut first = String::with_capacity(256);
516        first_arg.recast(&mut first, options, indentation_level, arg_ctxt);
517        vec![first.trim().to_owned()]
518    } else {
519        Vec::with_capacity(arguments.len())
520    };
521    arg_list.extend(arguments.iter().map(|arg| {
522        let mut buf = String::with_capacity(256);
523        arg.recast(&mut buf, options, indentation_level, arg_ctxt);
524        buf
525    }));
526    arg_list
527}
528
529#[allow(clippy::too_many_arguments)]
530fn recast_call(
531    callee: &Name,
532    unlabeled: Option<&Expr>,
533    arguments: &[LabeledArg],
534    non_code_meta: &NonCodeMeta,
535    buf: &mut String,
536    options: &FormatOptions,
537    indentation_level: usize,
538    ctxt: ExprContext,
539) {
540    let smart_indent_level = if ctxt.in_pipe() { 0 } else { indentation_level };
541    let name = callee;
542
543    if let Some(suggestion) = deprecation(&name.name.inner.name, DeprecationKind::Function) {
544        options.write_indentation(buf, smart_indent_level);
545        return write!(buf, "{suggestion}").no_fail();
546    }
547
548    // An item is an argument (possibly with block comments glued before it),
549    // or a comment that needs its own line.
550    struct FormatItem {
551        text: String,
552        is_arg: bool,
553    }
554
555    // Reconstruct the order of arguments and non-code items (e.g. comments).
556    // Non-code nodes are keyed by their position in the source argument
557    // sequence, the same scheme as ArrayExpression. The unlabeled argument is
558    // not part of that sequence; it always comes first. Iterate enough slots
559    // that no argument or comment is dropped, even if AST edits left the
560    // indices inconsistent.
561    let build_items = |arg_indent: usize| -> Vec<FormatItem> {
562        let arg_list = recast_args(unlabeled, arguments, options, arg_indent, ctxt);
563        let mut arg_iter = arg_list.into_iter();
564        let mut items = Vec::with_capacity(arguments.len() + non_code_meta.non_code_nodes_len() + 1);
565        if unlabeled.is_some()
566            && let Some(first_arg) = arg_iter.next()
567        {
568            items.push(FormatItem {
569                text: first_arg,
570                is_arg: true,
571            });
572        }
573        let num_items = arguments.len() + non_code_meta.non_code_nodes_len();
574        let num_slots = non_code_meta
575            .non_code_nodes
576            .keys()
577            .max()
578            .map_or(num_items, |max| num_items.max(max + 1));
579        // Block comments don't need a line break, so they glue to whatever
580        // comes next instead of getting their own line.
581        let mut pending_block_comments = String::new();
582        for i in 0..num_slots {
583            if let Some(noncode) = non_code_meta.non_code_nodes.get(&i) {
584                for nc in noncode {
585                    match &nc.value {
586                        NonCodeValue::BlockComment {
587                            style: CommentStyle::Block,
588                            ..
589                        }
590                        | NonCodeValue::InlineComment {
591                            style: CommentStyle::Block,
592                            ..
593                        } => {
594                            pending_block_comments.push_str(nc.recast(options, 0).trim());
595                            pending_block_comments.push(' ');
596                        }
597                        _ => {
598                            // Line comments (and blank lines) need their own
599                            // line.
600                            let mut text = std::mem::take(&mut pending_block_comments);
601                            text.push_str(nc.recast(options, 0).trim_end_matches('\n'));
602                            items.push(FormatItem {
603                                text: text.trim().to_owned(),
604                                is_arg: false,
605                            });
606                        }
607                    }
608                }
609            } else if let Some(arg) = arg_iter.next() {
610                let mut text = std::mem::take(&mut pending_block_comments);
611                text.push_str(&arg);
612                items.push(FormatItem { text, is_arg: true });
613            }
614        }
615        items.extend(arg_iter.map(|arg| FormatItem {
616            text: arg,
617            is_arg: true,
618        }));
619        // Trailing block comments have no argument to glue to.
620        if !pending_block_comments.is_empty() {
621            items.push(FormatItem {
622                text: pending_block_comments.trim_end().to_owned(),
623                is_arg: false,
624            });
625        }
626        items
627    };
628
629    let items = build_items(indentation_level);
630    let has_lots_of_args = items.iter().filter(|item| item.is_arg).count() >= 4;
631    // Comments which need their own line force the multi-line layout.
632    let has_own_line_comment = items.iter().any(|item| !item.is_arg);
633    let some_arg_is_already_multiline = items.len() > 1 && items.iter().any(|item| item.text.contains('\n'));
634    let multiline = has_lots_of_args || some_arg_is_already_multiline || has_own_line_comment;
635    if multiline {
636        let next_indent = indentation_level + 1;
637        let inner_indentation = if ctxt.in_pipe() {
638            options.get_indentation_offset_pipe(next_indent)
639        } else {
640            options.get_indentation(next_indent)
641        };
642        let items = build_items(next_indent);
643        let end_indent = if ctxt.in_pipe() {
644            options.get_indentation_offset_pipe(indentation_level)
645        } else {
646            options.get_indentation(indentation_level)
647        };
648        if ctxt.needs_leading_indent() {
649            options.write_indentation(buf, smart_indent_level);
650        }
651        name.write_to(buf).no_fail();
652        buf.push('(');
653        buf.push('\n');
654        for item in items {
655            if item.is_arg {
656                writeln!(buf, "{inner_indentation}{},", item.text).no_fail();
657            } else if item.text.is_empty() {
658                // A blank line between arguments.
659                buf.push('\n');
660            } else {
661                writeln!(buf, "{inner_indentation}{}", item.text).no_fail();
662            }
663        }
664        write!(buf, "{end_indent}").no_fail();
665        buf.push(')');
666    } else {
667        if ctxt.needs_leading_indent() {
668            options.write_indentation(buf, smart_indent_level);
669        }
670        name.write_to(buf).no_fail();
671        buf.push('(');
672        let args = items
673            .iter()
674            .map(|item| item.text.as_str())
675            .collect::<Vec<_>>()
676            .join(", ");
677        write!(buf, "{args}").no_fail();
678        buf.push(')');
679    }
680}
681
682impl LabeledArg {
683    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
684        if let Some(l) = &self.label {
685            buf.push_str(&l.name);
686            buf.push_str(" = ");
687        }
688        self.arg.recast(buf, options, indentation_level, ctxt);
689    }
690}
691
692impl VariableDeclaration {
693    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
694        options.write_indentation(buf, indentation_level);
695        match self.visibility {
696            ItemVisibility::Default => {}
697            ItemVisibility::Export => buf.push_str("export "),
698        };
699
700        let (keyword, eq, ctxt) = match self.kind {
701            VariableKind::Fn => ("fn ", "", ExprContext::FnDecl),
702            VariableKind::Const => ("", " = ", ExprContext::Other),
703        };
704        buf.push_str(keyword);
705        buf.push_str(&self.declaration.id.name);
706        buf.push_str(eq);
707
708        // Unfortunately, allocate a temporary buffer here so that we can trim the start.
709        // Otherwise, some expression kinds will write indentation at the start, because
710        // they don't know they're inside a declaration.
711        // TODO: Pass the ExprContext throughout every Expr kind, so that they can conditionally
712        // emit whitespace in an ExprStmt and not when they're in a DeclarationStmt.
713        let mut tmp_buf = String::new();
714        self.declaration
715            .init
716            .recast(&mut tmp_buf, options, indentation_level, ctxt);
717        buf.push_str(tmp_buf.trim_start());
718    }
719}
720
721impl TypeDeclaration {
722    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
723        options.write_indentation(buf, indentation_level);
724        match self.visibility {
725            ItemVisibility::Default => {}
726            ItemVisibility::Export => buf.push_str("export "),
727        };
728        buf.push_str("type ");
729        buf.push_str(&self.name.name);
730
731        if let Some(args) = &self.args {
732            buf.push('(');
733            for (i, a) in args.iter().enumerate() {
734                buf.push_str(&a.name);
735                if i < args.len() - 1 {
736                    buf.push_str(", ");
737                }
738            }
739            buf.push(')');
740        }
741        match &self.definition {
742            TypeDeclarationDefinition::Bare => {}
743            TypeDeclarationDefinition::Alias { ty } => {
744                buf.push_str(" = ");
745                write!(buf, "{ty}").no_fail();
746            }
747            TypeDeclarationDefinition::Enum(e) => e.recast(buf, options, indentation_level),
748        }
749    }
750}
751
752impl EnumDeclaration {
753    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
754        // Only comments decide the layout: blank lines in a body with no arms
755        // carry no information and are not re-emitted, so counting them here
756        // would make formatting non-idempotent (the multi-line output would
757        // reparse with no non-code at all and then collapse).
758        let has_start_comment = self
759            .non_code_meta
760            .start_nodes
761            .iter()
762            .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
763
764        // An enum with no variants and no comments stays on one line, keeping
765        // the standalone arm marker which classifies the body as an enum.
766        if self.variants.is_empty() && !has_start_comment {
767            buf.push_str(" { | }");
768            return;
769        }
770
771        let body_level = indentation_level + 1;
772        let body_indentation = options.get_indentation(body_level);
773        buf.push_str(" {\n");
774
775        // Comments before the first arm (or around a standalone `|`),
776        // mirroring how `recast_body` renders `start_nodes`.
777        if has_start_comment {
778            let mut pending_newline = false;
779            for start_node in &self.non_code_meta.start_nodes {
780                if matches!(start_node.value, NonCodeValue::NewLine) {
781                    pending_newline = true;
782                    continue;
783                }
784                if pending_newline {
785                    buf.push('\n');
786                    pending_newline = false;
787                }
788                buf.push_str(&start_node.recast(options, body_level));
789                if !buf.ends_with('\n') {
790                    buf.push('\n');
791                }
792            }
793            if pending_newline {
794                buf.push('\n');
795            }
796        }
797
798        if self.variants.is_empty() {
799            buf.push_str(&body_indentation);
800            buf.push_str("|\n");
801        }
802
803        for (index, variant) in self.variants.iter().enumerate() {
804            // Comments strongly associated with this arm, mirroring how
805            // `recast_body` renders body-item pre-comments.
806            let mut arm = String::new();
807            for comment in &variant.pre_comments {
808                if !comment.is_empty() {
809                    arm.push_str(&body_indentation);
810                    arm.push_str(comment);
811                }
812                if comment.is_empty() && !arm.ends_with('\n') {
813                    arm.push('\n');
814                }
815                if !arm.ends_with("\n\n") && arm != "\n" {
816                    arm.push('\n');
817                }
818            }
819            arm.push_str(&body_indentation);
820            arm.push_str("| ");
821            arm.push_str(&variant.name.name);
822            buf.push_str(&arm);
823
824            // Trailing comments and blank lines after this arm, mirroring how
825            // `recast_body` renders per-item non-code nodes.
826            if let Some(noncodes) = self.non_code_meta.non_code_nodes.get(&index) {
827                for (i, noncode) in noncodes.iter().enumerate() {
828                    let formatted = noncode.recast(options, body_level);
829                    if i == 0
830                        && !formatted.trim().is_empty()
831                        && matches!(noncode.value, NonCodeValue::BlockComment { .. })
832                    {
833                        buf.push('\n');
834                    }
835                    buf.push_str(&formatted);
836                }
837                if !buf.ends_with('\n') {
838                    buf.push('\n');
839                }
840            } else {
841                buf.push('\n');
842            }
843        }
844
845        options.write_indentation(buf, indentation_level);
846        buf.push('}');
847    }
848}
849
850fn write<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Display) {
851    f.write_fmt(format_args!("{s}"))
852        .expect("writing to a string should always succeed")
853}
854
855fn write_dbg<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Debug) {
856    f.write_fmt(format_args!("{s:?}"))
857        .expect("writing to a string should always succeed")
858}
859
860impl NumericLiteral {
861    fn recast(&self, buf: &mut String) {
862        if self.raw.contains('.') && self.value.fract() == 0.0 {
863            write_dbg(buf, self.value);
864            write(buf, self.suffix);
865        } else {
866            write(buf, &self.raw);
867        }
868    }
869}
870
871impl Literal {
872    fn recast(&self, buf: &mut String) {
873        match self.value {
874            LiteralValue::Number { value, suffix } => {
875                if self.raw.contains('.') && value.fract() == 0.0 {
876                    write_dbg(buf, value);
877                    write(buf, suffix);
878                } else {
879                    write(buf, &self.raw);
880                }
881            }
882            LiteralValue::String(ref s) => {
883                if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
884                    return write!(buf, "{suggestion}").unwrap();
885                }
886                let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
887                write(buf, quote);
888                write(buf, s);
889                write(buf, quote);
890            }
891            LiteralValue::Bool(_) => {
892                write(buf, &self.raw);
893            }
894        }
895    }
896}
897
898impl TagDeclarator {
899    pub fn recast(&self, buf: &mut String) {
900        // TagDeclarators are always prefixed with a dollar sign.
901        buf.push('$');
902        buf.push_str(&self.name);
903    }
904}
905
906impl ArrayExpression {
907    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
908        fn indent_multiline_item(item: &str, indent: &str) -> String {
909            if !item.contains('\n') {
910                return item.to_owned();
911            }
912            let mut out = String::with_capacity(item.len() + indent.len() * 2);
913            let mut first = true;
914            for segment in item.split_inclusive('\n') {
915                if first {
916                    out.push_str(segment);
917                    first = false;
918                    continue;
919                }
920                out.push_str(indent);
921                out.push_str(segment);
922            }
923            out
924        }
925
926        // Reconstruct the order of items in the array.
927        // An item can be an element (i.e. an expression for a KCL value),
928        // or a non-code item (e.g. a comment)
929        let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
930        let mut elems = self.elements.iter();
931        let mut found_line_comment = false;
932        let mut format_items: Vec<_> = Vec::with_capacity(num_items);
933        for i in 0..num_items {
934            if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
935                format_items.extend(noncode.iter().map(|nc| {
936                    found_line_comment |= nc.value.should_cause_array_newline();
937                    nc.recast(options, 0)
938                }));
939            } else {
940                let el = elems.next().unwrap();
941                let mut s = String::with_capacity(256);
942                el.recast(&mut s, options, 0, ExprContext::Other);
943                s.push_str(", ");
944                format_items.push(s);
945            }
946        }
947
948        // Format these items into a one-line array.
949        if let Some(item) = format_items.last_mut()
950            && let Some(norm) = item.strip_suffix(", ")
951        {
952            *item = norm.to_owned();
953        }
954        let mut flat_recast = String::with_capacity(256);
955        flat_recast.push('[');
956        for fi in &format_items {
957            flat_recast.push_str(fi)
958        }
959        flat_recast.push(']');
960
961        // We might keep the one-line representation, if it's short enough.
962        let max_array_length = 40;
963        let multi_line = flat_recast.len() > max_array_length || found_line_comment;
964        if !multi_line {
965            buf.push_str(&flat_recast);
966            return;
967        }
968
969        // Otherwise, we format a multi-line representation.
970        buf.push_str("[\n");
971        let inner_indentation = if ctxt.in_pipe() {
972            options.get_indentation_offset_pipe(indentation_level + 1)
973        } else {
974            options.get_indentation(indentation_level + 1)
975        };
976        for format_item in format_items {
977            let item = if let Some(x) = format_item.strip_suffix(" ") {
978                x
979            } else {
980                &format_item
981            };
982            let item = indent_multiline_item(item, &inner_indentation);
983            buf.push_str(&inner_indentation);
984            buf.push_str(&item);
985            if !format_item.ends_with('\n') {
986                buf.push('\n')
987            }
988        }
989        let end_indent = if ctxt.in_pipe() {
990            options.get_indentation_offset_pipe(indentation_level)
991        } else {
992            options.get_indentation(indentation_level)
993        };
994        buf.push_str(&end_indent);
995        buf.push(']');
996    }
997}
998
999/// An expression is syntactically trivial: i.e., a literal, identifier, or similar.
1000fn expr_is_trivial(expr: &Expr) -> bool {
1001    matches!(
1002        expr,
1003        Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
1004    )
1005}
1006
1007trait CannotActuallyFail {
1008    fn no_fail(self);
1009}
1010
1011impl CannotActuallyFail for std::fmt::Result {
1012    fn no_fail(self) {
1013        self.expect("writing to a string cannot fail, there's no IO happening")
1014    }
1015}
1016
1017impl ArrayRangeExpression {
1018    fn recast(&self, buf: &mut String, options: &FormatOptions, _: usize, _: ExprContext) {
1019        buf.push('[');
1020        self.start_element.recast(buf, options, 0, ExprContext::Other);
1021
1022        let range_op = if self.end_inclusive { ".." } else { "..<" };
1023        // Format these items into a one-line array. Put spaces around the `..` if either expression
1024        // is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
1025        // tightly, but this is a misleading visual representation of the precedence if the range
1026        // components are compound expressions.
1027        let no_spaces = expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element);
1028        if no_spaces {
1029            write!(buf, "{range_op}").no_fail()
1030        } else {
1031            write!(buf, " {range_op} ").no_fail()
1032        }
1033        self.end_element.recast(buf, options, 0, ExprContext::Other);
1034        buf.push(']');
1035        // Assume a range expression fits on one line.
1036    }
1037}
1038
1039fn trim_end(buf: &mut String) {
1040    buf.truncate(buf.trim_end().len())
1041}
1042
1043impl ObjectExpression {
1044    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1045        if self
1046            .non_code_meta
1047            .non_code_nodes
1048            .values()
1049            .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
1050        {
1051            return self.recast_multi_line(buf, options, indentation_level, ctxt);
1052        }
1053        let mut flat_recast_buf = String::new();
1054        flat_recast_buf.push_str("{ ");
1055        for (i, prop) in self.properties.iter().enumerate() {
1056            let obj_key = &prop.key.name;
1057            write!(flat_recast_buf, "{obj_key} = ").no_fail();
1058            prop.value
1059                .recast(&mut flat_recast_buf, options, indentation_level, ctxt);
1060            if i < self.properties.len() - 1 {
1061                flat_recast_buf.push_str(", ");
1062            }
1063        }
1064        flat_recast_buf.push_str(" }");
1065        let max_array_length = 40;
1066        let needs_multiple_lines = flat_recast_buf.len() > max_array_length;
1067        if !needs_multiple_lines {
1068            buf.push_str(&flat_recast_buf);
1069        } else {
1070            self.recast_multi_line(buf, options, indentation_level, ctxt);
1071        }
1072    }
1073
1074    /// Recast, but always outputs the object with newlines between each property.
1075    fn recast_multi_line(
1076        &self,
1077        buf: &mut String,
1078        options: &FormatOptions,
1079        indentation_level: usize,
1080        ctxt: ExprContext,
1081    ) {
1082        let inner_indentation = if ctxt.in_pipe() {
1083            options.get_indentation_offset_pipe(indentation_level + 1)
1084        } else {
1085            options.get_indentation(indentation_level + 1)
1086        };
1087        let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
1088        let mut props = self.properties.iter();
1089        let format_items: Vec<_> = (0..num_items)
1090            .flat_map(|i| {
1091                if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
1092                    noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
1093                } else {
1094                    let prop = props.next().unwrap();
1095                    // Use a comma unless it's the last item
1096                    let comma = if i == num_items - 1 { "" } else { ",\n" };
1097                    let mut s = String::new();
1098                    prop.value.recast(&mut s, options, indentation_level + 1, ctxt);
1099                    // TODO: Get rid of this vector allocation
1100                    vec![format!("{} = {}{comma}", prop.key.name, s.trim())]
1101                }
1102            })
1103            .collect();
1104        let end_indent = if ctxt.in_pipe() {
1105            options.get_indentation_offset_pipe(indentation_level)
1106        } else {
1107            options.get_indentation(indentation_level)
1108        };
1109        write!(
1110            buf,
1111            "{{\n{inner_indentation}{}\n{end_indent}}}",
1112            format_items.join(&inner_indentation),
1113        )
1114        .no_fail();
1115    }
1116}
1117
1118impl MemberExpression {
1119    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1120        // The object
1121        self.object.recast(buf, options, indentation_level, ctxt);
1122        // The key
1123        if self.computed {
1124            buf.push('[');
1125            self.property.recast(buf, options, indentation_level, ctxt);
1126            buf.push(']');
1127        } else {
1128            buf.push('.');
1129            self.property.recast(buf, options, indentation_level, ctxt);
1130        };
1131    }
1132}
1133
1134impl BinaryExpression {
1135    fn recast(&self, buf: &mut String, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) {
1136        let maybe_wrap_it = |a: String, doit: bool| -> String { if doit { format!("({a})") } else { a } };
1137
1138        // It would be better to always preserve the user's parentheses but since we've dropped that
1139        // info from the AST, we bracket expressions as necessary.
1140        let should_wrap_left = match &self.left {
1141            BinaryPart::BinaryExpression(bin_exp) => {
1142                self.precedence() > bin_exp.precedence()
1143                    || ((self.precedence() == bin_exp.precedence())
1144                        && (!(self.operator.associative() && self.operator == bin_exp.operator)
1145                            && self.operator.associativity() == Associativity::Right))
1146            }
1147            _ => false,
1148        };
1149
1150        let should_wrap_right = match &self.right {
1151            BinaryPart::BinaryExpression(bin_exp) => {
1152                self.precedence() > bin_exp.precedence()
1153                    // These two lines preserve previous reformatting behaviour.
1154                    || self.operator == BinaryOperator::Sub
1155                    || self.operator == BinaryOperator::Div
1156                    || ((self.precedence() == bin_exp.precedence())
1157                        && (!(self.operator.associative() && self.operator == bin_exp.operator)
1158                            && self.operator.associativity() == Associativity::Left))
1159            }
1160            _ => false,
1161        };
1162
1163        let mut left = String::new();
1164        self.left.recast(&mut left, options, 0, ctxt);
1165        let mut right = String::new();
1166        self.right.recast(&mut right, options, 0, ctxt);
1167        write!(
1168            buf,
1169            "{} {} {}",
1170            maybe_wrap_it(left, should_wrap_left),
1171            self.operator,
1172            maybe_wrap_it(right, should_wrap_right)
1173        )
1174        .no_fail();
1175    }
1176}
1177
1178impl UnaryExpression {
1179    fn recast(&self, buf: &mut String, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) {
1180        match self.argument {
1181            BinaryPart::Literal(_)
1182            | BinaryPart::Name(_)
1183            | BinaryPart::MemberExpression(_)
1184            | BinaryPart::ArrayExpression(_)
1185            | BinaryPart::ArrayRangeExpression(_)
1186            | BinaryPart::ObjectExpression(_)
1187            | BinaryPart::IfExpression(_)
1188            | BinaryPart::AscribedExpression(_)
1189            | BinaryPart::CallExpressionKw(_) => {
1190                write!(buf, "{}", self.operator).no_fail();
1191                self.argument.recast(buf, options, 0, ctxt)
1192            }
1193            BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) | BinaryPart::SketchVar(_) => {
1194                write!(buf, "{}", self.operator).no_fail();
1195                buf.push('(');
1196                self.argument.recast(buf, options, 0, ctxt);
1197                buf.push(')');
1198            }
1199        }
1200    }
1201}
1202
1203impl IfExpression {
1204    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1205        // We can calculate how many lines this will take, so let's do it and avoid growing the vec.
1206        // Total lines = starting lines, else-if lines, ending lines.
1207        let n = 2 + (self.else_ifs.len() * 2) + 3;
1208        let mut lines = Vec::with_capacity(n);
1209
1210        let cond = {
1211            let mut tmp_buf = String::new();
1212            self.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
1213            tmp_buf
1214        };
1215        lines.push((0, format!("if {cond} {{")));
1216        lines.push((1, {
1217            let mut tmp_buf = String::new();
1218            self.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
1219            tmp_buf
1220        }));
1221        for else_if in &self.else_ifs {
1222            let cond = {
1223                let mut tmp_buf = String::new();
1224                else_if.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
1225                tmp_buf
1226            };
1227            lines.push((0, format!("}} else if {cond} {{")));
1228            lines.push((1, {
1229                let mut tmp_buf = String::new();
1230                else_if.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
1231                tmp_buf
1232            }));
1233        }
1234        lines.push((0, "} else {".to_owned()));
1235        lines.push((1, {
1236            let mut tmp_buf = String::new();
1237            self.final_else.recast(&mut tmp_buf, options, indentation_level + 1);
1238            tmp_buf
1239        }));
1240        lines.push((0, "}".to_owned()));
1241        let out = lines
1242            .into_iter()
1243            .enumerate()
1244            .map(|(idx, (ind, line))| {
1245                let indentation = if ctxt.in_pipe() && idx == 0 {
1246                    String::new()
1247                } else {
1248                    options.get_indentation(indentation_level + ind)
1249                };
1250                format!("{indentation}{}", line.trim())
1251            })
1252            .collect::<Vec<_>>()
1253            .join("\n");
1254        buf.push_str(&out);
1255    }
1256}
1257
1258impl Node<PipeExpression> {
1259    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, preceding_indent: bool) {
1260        if preceding_indent {
1261            options.write_indentation(buf, indentation_level);
1262        }
1263        for (index, statement) in self.body.iter().enumerate() {
1264            let (statement_indentation, statement_ctxt) = if index == 0 {
1265                (indentation_level, ExprContext::PipeHead)
1266            } else {
1267                (indentation_level + 1, ExprContext::Pipe)
1268            };
1269            statement.recast(buf, options, statement_indentation, statement_ctxt);
1270            let non_code_meta = &self.non_code_meta;
1271            if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
1272                for val in non_code_meta_value {
1273                    if let NonCodeValue::NewLine = val.value {
1274                        buf.push('\n');
1275                        continue;
1276                    }
1277                    // TODO: Remove allocation here by switching val.recast to accept buf.
1278                    let formatted = if val.end == self.end {
1279                        val.recast(options, indentation_level)
1280                            .trim_end_matches('\n')
1281                            .to_string()
1282                    } else {
1283                        val.recast(options, indentation_level + 1)
1284                            .trim_end_matches('\n')
1285                            .to_string()
1286                    };
1287                    if let NonCodeValue::BlockComment { .. } = val.value
1288                        && !buf.ends_with('\n')
1289                    {
1290                        buf.push('\n');
1291                    }
1292                    buf.push_str(&formatted);
1293                }
1294            }
1295
1296            if index != self.body.len() - 1 {
1297                buf.push('\n');
1298                options.write_indentation(buf, indentation_level + 1);
1299                buf.push_str(PIPE_OPERATOR);
1300                buf.push(' ');
1301            }
1302        }
1303    }
1304}
1305
1306impl FunctionExpression {
1307    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1308        // We don't want to end with a new line inside nested functions.
1309        let mut new_options = options.clone();
1310        new_options.insert_final_newline = false;
1311
1312        buf.push('(');
1313        for (i, param) in self.params.iter().enumerate() {
1314            param.recast(buf, options, indentation_level);
1315            if i < self.params.len() - 1 {
1316                buf.push_str(", ");
1317            }
1318        }
1319        buf.push(')');
1320        if let Some(return_type) = &self.return_type {
1321            write!(buf, ": {return_type}").no_fail();
1322        }
1323        writeln!(buf, " {{").no_fail();
1324        self.body.recast(buf, &new_options, indentation_level + 1);
1325        buf.push('\n');
1326        options.write_indentation(buf, indentation_level);
1327        buf.push('}');
1328    }
1329}
1330
1331impl Parameter {
1332    pub fn recast(&self, buf: &mut String, _options: &FormatOptions, _indentation_level: usize) {
1333        if !self.labeled {
1334            buf.push('@');
1335        }
1336        buf.push_str(&self.identifier.name);
1337        if self.default_value.is_some() {
1338            buf.push('?');
1339        };
1340        if let Some(ty) = &self.param_type {
1341            buf.push_str(": ");
1342            write!(buf, "{ty}").no_fail();
1343        }
1344        if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
1345            buf.push_str(" = ");
1346            literal.recast(buf);
1347        };
1348    }
1349}
1350
1351impl SketchBlock {
1352    pub(crate) fn recast(
1353        &self,
1354        buf: &mut String,
1355        options: &FormatOptions,
1356        indentation_level: usize,
1357        ctxt: ExprContext,
1358    ) {
1359        let name = Name {
1360            name: Node {
1361                inner: Identifier {
1362                    name: SketchBlock::CALLEE_NAME.to_owned(),
1363                    digest: None,
1364                },
1365                start: Default::default(),
1366                end: Default::default(),
1367                module_id: Default::default(),
1368                node_path: None,
1369                outer_attrs: Default::default(),
1370                pre_comments: Default::default(),
1371                comment_start: Default::default(),
1372            },
1373            path: Vec::new(),
1374            abs_path: false,
1375            digest: None,
1376        };
1377        recast_call(
1378            &name,
1379            None,
1380            &self.arguments,
1381            &self.non_code_meta,
1382            buf,
1383            options,
1384            indentation_level,
1385            ctxt,
1386        );
1387
1388        // We don't want to end with a new line inside nested blocks.
1389        let mut new_options = options.clone();
1390        new_options.insert_final_newline = false;
1391
1392        writeln!(buf, " {{").no_fail();
1393        self.body.recast(buf, &new_options, indentation_level + 1);
1394        buf.push('\n');
1395        options.write_indentation(buf, indentation_level);
1396        buf.push('}');
1397    }
1398}
1399
1400impl Block {
1401    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1402        recast_body(
1403            &self.items,
1404            &self.non_code_meta,
1405            &self.inner_attrs,
1406            buf,
1407            options,
1408            indentation_level,
1409        );
1410    }
1411}
1412
1413impl SketchVar {
1414    fn recast(&self, buf: &mut String) {
1415        if let Some(initial) = &self.initial {
1416            write!(buf, "var ").no_fail();
1417            initial.recast(buf);
1418        } else {
1419            write!(buf, "var").no_fail();
1420        }
1421    }
1422}
1423
1424/// Collect all the kcl (and other relevant) files in a directory, recursively.
1425#[cfg(not(target_arch = "wasm32"))]
1426#[async_recursion::async_recursion]
1427pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
1428    // Make sure we actually have a directory.
1429    if !dir.is_dir() {
1430        anyhow::bail!("`{}` is not a directory", dir.display());
1431    }
1432
1433    let mut entries = tokio::fs::read_dir(dir).await?;
1434
1435    let mut files = Vec::new();
1436    while let Some(entry) = entries.next_entry().await? {
1437        let path = entry.path();
1438
1439        if path.is_dir() {
1440            files.extend(walk_dir(&path).await?);
1441        } else if path
1442            .extension()
1443            .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_lowercase()))
1444        {
1445            files.push(path);
1446        }
1447    }
1448
1449    Ok(files)
1450}
1451
1452/// Recast all the kcl files in a directory, recursively.
1453#[cfg(not(target_arch = "wasm32"))]
1454pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
1455    let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
1456        crate::KclError::new_internal(crate::errors::KclErrorDetails::new(
1457            format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
1458            vec![crate::SourceRange::default()],
1459        ))
1460    })?;
1461
1462    let futures = files
1463        .into_iter()
1464        .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) // We only care about kcl
1465        // files here.
1466        .map(|file| {
1467            let options = options.clone();
1468            tokio::spawn(async move {
1469                let contents = tokio::fs::read_to_string(&file)
1470                    .await
1471                    .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
1472                let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
1473                    let report = crate::Report {
1474                        kcl_source: contents.to_string(),
1475                        error: err,
1476                        filename: file.to_string_lossy().to_string(),
1477                        label: file.to_string_lossy().to_string(),
1478                    };
1479                    let report = miette::Report::new(report);
1480                    anyhow::anyhow!("{:?}", report)
1481                })?;
1482                for ce in &ces {
1483                    if ce.severity != crate::errors::Severity::Warning {
1484                        let report = crate::Report {
1485                            kcl_source: contents.to_string(),
1486                            error: crate::KclError::new_semantic(ce.clone().into()),
1487                            filename: file.to_string_lossy().to_string(),
1488                            label: file.to_string_lossy().to_string(),
1489                        };
1490                        let report = miette::Report::new(report);
1491                        anyhow::bail!("{:?}", report);
1492                    }
1493                }
1494                let Some(program) = program else {
1495                    anyhow::bail!("Failed to parse file `{}`", file.display());
1496                };
1497                let recast = program.recast_with_options(&options);
1498                tokio::fs::write(&file, recast)
1499                    .await
1500                    .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
1501
1502                Ok::<(), anyhow::Error>(())
1503            })
1504        })
1505        .collect::<Vec<_>>();
1506
1507    // Join all futures and await their completion
1508    let results = futures::future::join_all(futures).await;
1509
1510    // Check if any of the futures failed.
1511    let mut errors = Vec::new();
1512    for result in results {
1513        if let Err(err) = result? {
1514            errors.push(err);
1515        }
1516    }
1517
1518    if !errors.is_empty() {
1519        anyhow::bail!("Failed to recast some files: {:?}", errors);
1520    }
1521
1522    Ok(())
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527    use pretty_assertions::assert_eq;
1528
1529    use super::*;
1530    use crate::ModuleId;
1531    use crate::parsing::ast::types::FormatOptions;
1532
1533    #[test]
1534    fn test_recast_annotations_without_body_items() {
1535        let input = r#"@settings(defaultLengthUnit = in)
1536"#;
1537        let program = crate::parsing::top_level_parse(input).unwrap();
1538        let output = program.recast_top(&Default::default(), 0);
1539        assert_eq!(output, input);
1540    }
1541
1542    #[test]
1543    fn test_recast_annotations_in_function_body() {
1544        let input = r#"fn myFunc() {
1545  @meta(yes = true)
1546
1547  x = 2
1548}
1549"#;
1550        let program = crate::parsing::top_level_parse(input).unwrap();
1551        let output = program.recast_top(&Default::default(), 0);
1552        assert_eq!(output, input);
1553    }
1554
1555    #[test]
1556    fn test_recast_annotations_in_function_body_without_items() {
1557        let input = "\
1558fn myFunc() {
1559  @meta(yes = true)
1560}
1561";
1562        let program = crate::parsing::top_level_parse(input).unwrap();
1563        let output = program.recast_top(&Default::default(), 0);
1564        assert_eq!(output, input);
1565    }
1566
1567    #[test]
1568    fn recast_annotations_with_comments() {
1569        let input = r#"// Start comment
1570
1571// Comment on attr
1572@settings(defaultLengthUnit = in)
1573
1574// Comment on item
1575foo = 42
1576
1577// Comment on another item
1578@(impl = kcl)
1579bar = 0
1580"#;
1581        let program = crate::parsing::top_level_parse(input).unwrap();
1582        let output = program.recast_top(&Default::default(), 0);
1583        assert_eq!(output, input);
1584    }
1585
1586    #[test]
1587    fn recast_annotations_with_block_comment() {
1588        let input = r#"/* Start comment
1589
1590sdfsdfsdfs */
1591@settings(defaultLengthUnit = in)
1592
1593foo = 42
1594"#;
1595        let program = crate::parsing::top_level_parse(input).unwrap();
1596        let output = program.recast_top(&Default::default(), 0);
1597        assert_eq!(output, input);
1598    }
1599
1600    /// Assert that `input` formats to `expected`, and that formatting is
1601    /// idempotent: recasting the output parses and formats to itself.
1602    #[track_caller]
1603    fn assert_recast(input: &str, expected: &str) {
1604        let program = crate::parsing::top_level_parse(input).unwrap();
1605        let output = program.recast_top(&Default::default(), 0);
1606        assert_eq!(output, expected);
1607        let reparsed = crate::parsing::top_level_parse(&output).unwrap();
1608        assert_eq!(reparsed.recast_top(&Default::default(), 0), output);
1609    }
1610
1611    #[test]
1612    fn recast_enum_multi_line_is_stable() {
1613        let input = r#"@settings(experimentalFeatures = allow)
1614
1615type Color {
1616  | Red
1617  | Green
1618  | Blue
1619}
1620"#;
1621        assert_recast(input, input);
1622    }
1623
1624    #[test]
1625    fn recast_enum_expands_single_line() {
1626        let input = r#"@settings(experimentalFeatures = allow)
1627
1628type Color { | Red | Green | Blue }
1629"#;
1630        let expected = r#"@settings(experimentalFeatures = allow)
1631
1632type Color {
1633  | Red
1634  | Green
1635  | Blue
1636}
1637"#;
1638        assert_recast(input, expected);
1639    }
1640
1641    #[test]
1642    fn recast_enum_export() {
1643        let input = r#"@settings(experimentalFeatures = allow)
1644
1645export type Color {
1646  | Red
1647}
1648"#;
1649        assert_recast(input, input);
1650    }
1651
1652    #[test]
1653    fn recast_enum_no_variants() {
1654        let input = r#"@settings(experimentalFeatures = allow)
1655
1656type Empty { | }
1657"#;
1658        assert_recast(input, input);
1659    }
1660
1661    #[test]
1662    fn recast_enum_no_variants_collapses_blank_lines() {
1663        // Blank lines around a lone arm marker are not content: the body
1664        // collapses in one pass, not two.
1665        let input = r#"@settings(experimentalFeatures = allow)
1666
1667type Empty {
1668
1669  |
1670
1671}
1672"#;
1673        let expected = r#"@settings(experimentalFeatures = allow)
1674
1675type Empty { | }
1676"#;
1677        assert_recast(input, expected);
1678    }
1679
1680    #[test]
1681    fn recast_enum_no_variants_with_comments() {
1682        let input = r#"@settings(experimentalFeatures = allow)
1683
1684type Empty { /* a */ | /* b */ }
1685"#;
1686        let expected = r#"@settings(experimentalFeatures = allow)
1687
1688type Empty {
1689  /* a */
1690  /* b */
1691  |
1692}
1693"#;
1694        assert_recast(input, expected);
1695    }
1696
1697    #[test]
1698    fn recast_enum_with_comments() {
1699        let input = r#"@settings(experimentalFeatures = allow)
1700
1701type Color {
1702  // before red
1703  | Red // after red
1704  | /* inside green arm */ Green
1705
1706  | Blue
1707  // trailing
1708}
1709"#;
1710        // Comments between an arm's `|` and its name normalize to a line
1711        // above the arm; everything else keeps its position.
1712        let expected = r#"@settings(experimentalFeatures = allow)
1713
1714type Color {
1715  // before red
1716  | Red // after red
1717  /* inside green arm */
1718  | Green
1719
1720  | Blue
1721  // trailing
1722}
1723"#;
1724        assert_recast(input, expected);
1725    }
1726
1727    #[test]
1728    fn recast_enum_with_comment_above_declaration() {
1729        let input = r#"@settings(experimentalFeatures = allow)
1730
1731// palette
1732type Color {
1733  // before red
1734  | Red
1735}
1736"#;
1737        assert_recast(input, input);
1738    }
1739
1740    #[test]
1741    fn recast_enum_with_outer_annotation() {
1742        let input = r#"@settings(experimentalFeatures = allow)
1743
1744@(impl = kcl)
1745type Color {
1746  | Red
1747}
1748"#;
1749        assert_recast(input, input);
1750    }
1751
1752    #[test]
1753    fn recast_enum_export_annotation_and_comments() {
1754        let input = r#"@settings(experimentalFeatures = allow)
1755
1756// palette
1757@(impl = kcl)
1758export type Color {
1759  | Red // warm
1760}
1761"#;
1762        assert_recast(input, input);
1763    }
1764
1765    #[test]
1766    fn recast_enum_in_function_body() {
1767        let input = r#"@settings(experimentalFeatures = allow)
1768
1769fn palette() {
1770  type Color {
1771    | Red
1772    | Green
1773  }
1774  return 0
1775}
1776"#;
1777        assert_recast(input, input);
1778    }
1779
1780    #[test]
1781    fn test_recast_if_else_if_same() {
1782        let input = r#"b = if false {
1783  3
1784} else if true {
1785  4
1786} else {
1787  5
1788}
1789"#;
1790        let program = crate::parsing::top_level_parse(input).unwrap();
1791        let output = program.recast_top(&Default::default(), 0);
1792        assert_eq!(output, input);
1793    }
1794
1795    #[test]
1796    fn test_recast_if_same() {
1797        let input = r#"b = if false {
1798  3
1799} else {
1800  5
1801}
1802"#;
1803        let program = crate::parsing::top_level_parse(input).unwrap();
1804        let output = program.recast_top(&Default::default(), 0);
1805        assert_eq!(output, input);
1806    }
1807
1808    #[test]
1809    fn test_recast_import() {
1810        let input = r#"import a from "a.kcl"
1811import a as aaa from "a.kcl"
1812import a, b from "a.kcl"
1813import a as aaa, b from "a.kcl"
1814import a, b as bbb from "a.kcl"
1815import a as aaa, b as bbb from "a.kcl"
1816import "a_b.kcl"
1817import "a-b.kcl" as b
1818import * from "a.kcl"
1819export import a as aaa from "a.kcl"
1820export import a, b from "a.kcl"
1821export import a as aaa, b from "a.kcl"
1822export import a, b as bbb from "a.kcl"
1823"#;
1824        let program = crate::parsing::top_level_parse(input).unwrap();
1825        let output = program.recast_top(&Default::default(), 0);
1826        assert_eq!(output, input);
1827    }
1828
1829    #[test]
1830    fn test_recast_import_as_same_name() {
1831        let input = r#"import a as a from "a.kcl"
1832"#;
1833        let program = crate::parsing::top_level_parse(input).unwrap();
1834        let output = program.recast_top(&Default::default(), 0);
1835        let expected = r#"import a from "a.kcl"
1836"#;
1837        assert_eq!(output, expected);
1838    }
1839
1840    #[test]
1841    fn test_recast_export_fn() {
1842        let input = r#"export fn a() {
1843  return 0
1844}
1845"#;
1846        let program = crate::parsing::top_level_parse(input).unwrap();
1847        let output = program.recast_top(&Default::default(), 0);
1848        assert_eq!(output, input);
1849    }
1850
1851    #[test]
1852    fn test_recast_sketch_block_with_no_args() {
1853        let input = r#"sketch() {
1854  return 0
1855}
1856"#;
1857        let program = crate::parsing::top_level_parse(input).unwrap();
1858        let output = program.recast_top(&Default::default(), 0);
1859        assert_eq!(output, input);
1860    }
1861
1862    #[test]
1863    fn test_recast_sketch_block_with_labeled_args() {
1864        let input = r#"sketch(on = XY) {
1865  return 0
1866}
1867"#;
1868        let program = crate::parsing::top_level_parse(input).unwrap();
1869        let output = program.recast_top(&Default::default(), 0);
1870        assert_eq!(output, input);
1871    }
1872
1873    #[test]
1874    fn test_recast_sketch_block_with_arg_shorthand() {
1875        let input = r#"on = XY
1876sketch(on) {
1877  return 0
1878}
1879"#;
1880        let program = crate::parsing::top_level_parse(input).unwrap();
1881        let output = program.recast_top(&Default::default(), 0);
1882        assert_eq!(output, input);
1883    }
1884
1885    #[test]
1886    fn test_recast_sketch_block_with_arg_shorthand_and_comment() {
1887        // The comment stays after the shorthand argument: the parser moves the
1888        // shorthand `on` from the unlabeled slot into the argument list, which
1889        // must also shift the comment positions.
1890        let input = r#"on = XY
1891sketch(
1892  on,
1893  // plane
1894) {
1895  return 0
1896}
1897"#;
1898        let program = crate::parsing::top_level_parse(input).unwrap();
1899        let output = program.recast_top(&Default::default(), 0);
1900        assert_eq!(output, input);
1901
1902        // Check idempotency.
1903        let program = crate::parsing::top_level_parse(&output).unwrap();
1904        let output2 = program.recast_top(&Default::default(), 0);
1905        assert_eq!(output2, output);
1906    }
1907
1908    #[test]
1909    fn test_recast_sketch_block_with_statements_in_block() {
1910        let input = r#"sketch() {
1911  // Comments inside block.
1912  x = 5
1913  y = 2
1914}
1915"#;
1916        let program = crate::parsing::top_level_parse(input).unwrap();
1917        let output = program.recast_top(&Default::default(), 0);
1918        assert_eq!(output, input);
1919    }
1920
1921    #[test]
1922    fn test_recast_bug_fn_in_fn() {
1923        let some_program_string = r#"// Start point (top left)
1924zoo_x = -20
1925zoo_y = 7
1926// Scale
1927s = 1 // s = 1 -> height of Z is 13.4mm
1928// Depth
1929d = 1
1930
1931fn rect(x, y, w, h) {
1932  startSketchOn(XY)
1933    |> startProfile(at = [x, y])
1934    |> xLine(length = w)
1935    |> yLine(length = h)
1936    |> xLine(length = -w)
1937    |> close()
1938    |> extrude(d)
1939}
1940
1941fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1942  startSketchOn(XY)
1943    |> startProfile(at = [x1, y1])
1944    |> line(endAbsolute = [x2, y2])
1945    |> line(endAbsolute = [x3, y3])
1946    |> line(endAbsolute = [x4, y4])
1947    |> close()
1948    |> extrude(d)
1949}
1950
1951fn crosshair(x, y) {
1952  startSketchOn(XY)
1953    |> startProfile(at = [x, y])
1954    |> yLine(length = 1)
1955    |> yLine(length = -2)
1956    |> yLine(length = 1)
1957    |> xLine(length = 1)
1958    |> xLine(length = -2)
1959}
1960
1961fn z(z_x, z_y) {
1962  z_end_w = s * 8.4
1963  z_end_h = s * 3
1964  z_corner = s * 2
1965  z_w = z_end_w + 2 * z_corner
1966  z_h = z_w * 1.08130081300813
1967  rect(
1968    z_x,
1969    a = z_y,
1970    b = z_end_w,
1971    c = -z_end_h,
1972  )
1973  rect(
1974    z_x + z_w,
1975    a = z_y,
1976    b = -z_corner,
1977    c = -z_corner,
1978  )
1979  rect(
1980    z_x + z_w,
1981    a = z_y - z_h,
1982    b = -z_end_w,
1983    c = z_end_h,
1984  )
1985  rect(
1986    z_x,
1987    a = z_y - z_h,
1988    b = z_corner,
1989    c = z_corner,
1990  )
1991}
1992
1993fn o(c_x, c_y) {
1994  // Outer and inner radii
1995  o_r = s * 6.95
1996  i_r = 0.5652173913043478 * o_r
1997
1998  // Angle offset for diagonal break
1999  a = 7
2000
2001  // Start point for the top sketch
2002  o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
2003  o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
2004
2005  // Start point for the bottom sketch
2006  o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
2007  o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
2008
2009  // End point for the bottom startSketch
2010  o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
2011  o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
2012
2013  // Where is the center?
2014  // crosshair(c_x, c_y)
2015
2016
2017  startSketchOn(XY)
2018    |> startProfile(at = [o_x1, o_y1])
2019    |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
2020    |> angledLine(angle = 45, length = o_r - i_r)
2021    |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
2022    |> close()
2023    |> extrude(d)
2024
2025  startSketchOn(XY)
2026    |> startProfile(at = [o_x2, o_y2])
2027    |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
2028    |> angledLine(angle = 225, length = o_r - i_r)
2029    |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
2030    |> close()
2031    |> extrude(d)
2032}
2033
2034fn zoo(x0, y0) {
2035  z(x = x0, y = y0)
2036  o(x = x0 + s * 20, y = y0 - (s * 6.7))
2037  o(x = x0 + s * 35, y = y0 - (s * 6.7))
2038}
2039
2040zoo(x = zoo_x, y = zoo_y)
2041"#;
2042        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2043
2044        let recasted = program.recast_top(&Default::default(), 0);
2045        assert_eq!(recasted, some_program_string);
2046    }
2047
2048    #[test]
2049    fn test_nested_fns_indent() {
2050        let some_program_string = "\
2051x = 1
2052fn rect(x, y, w, h) {
2053  y = 2
2054  z = 3
2055  startSketchOn(XY)
2056    |> startProfile(at = [x, y])
2057    |> xLine(length = w)
2058    |> yLine(length = h)
2059    |> xLine(length = -w)
2060    |> close()
2061    |> extrude(d)
2062}
2063";
2064        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2065
2066        let recasted = program.recast_top(&Default::default(), 0);
2067        assert_eq!(recasted, some_program_string);
2068    }
2069
2070    #[test]
2071    fn test_recast_bug_extra_parens() {
2072        let some_program_string = r#"// Ball Bearing
2073// 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. 
2074
2075// Define constants like ball diameter, inside diameter, overhange length, and thickness
2076sphereDia = 0.5
2077insideDia = 1
2078thickness = 0.25
2079overHangLength = .4
2080
2081// Sketch and revolve the inside bearing piece
2082insideRevolve = startSketchOn(XZ)
2083  |> startProfile(at = [insideDia / 2, 0])
2084  |> line(end = [0, thickness + sphereDia / 2])
2085  |> line(end = [overHangLength, 0])
2086  |> line(end = [0, -thickness])
2087  |> line(end = [-overHangLength + thickness, 0])
2088  |> line(end = [0, -sphereDia])
2089  |> line(end = [overHangLength - thickness, 0])
2090  |> line(end = [0, -thickness])
2091  |> line(end = [-overHangLength, 0])
2092  |> close()
2093  |> revolve(axis = Y)
2094
2095// 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)
2096sphere = startSketchOn(XZ)
2097  |> startProfile(at = [
2098       0.05 + insideDia / 2 + thickness,
2099       0 - 0.05
2100     ])
2101  |> line(end = [sphereDia - 0.1, 0])
2102  |> arc(
2103       angle_start = 0,
2104       angle_end = -180,
2105       radius = sphereDia / 2 - 0.05
2106     )
2107  |> close()
2108  |> revolve(axis = X)
2109  |> patternCircular3d(
2110       axis = [0, 0, 1],
2111       center = [0, 0, 0],
2112       repetitions = 10,
2113       arcDegrees = 360,
2114       rotateDuplicates = true
2115     )
2116
2117// Sketch and revolve the outside bearing
2118outsideRevolve = startSketchOn(XZ)
2119  |> startProfile(at = [
2120       insideDia / 2 + thickness + sphereDia,
2121       0
2122       ]
2123     )
2124  |> line(end = [0, sphereDia / 2])
2125  |> line(end = [-overHangLength + thickness, 0])
2126  |> line(end = [0, thickness])
2127  |> line(end = [overHangLength, 0])
2128  |> line(end = [0, -2 * thickness - sphereDia])
2129  |> line(end = [-overHangLength, 0])
2130  |> line(end = [0, thickness])
2131  |> line(end = [overHangLength - thickness, 0])
2132  |> close()
2133  |> revolve(axis = Y)"#;
2134        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2135
2136        let recasted = program.recast_top(&Default::default(), 0);
2137        assert_eq!(
2138            recasted,
2139            r#"// Ball Bearing
2140// 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.
2141
2142// Define constants like ball diameter, inside diameter, overhange length, and thickness
2143sphereDia = 0.5
2144insideDia = 1
2145thickness = 0.25
2146overHangLength = .4
2147
2148// Sketch and revolve the inside bearing piece
2149insideRevolve = startSketchOn(XZ)
2150  |> startProfile(at = [insideDia / 2, 0])
2151  |> line(end = [0, thickness + sphereDia / 2])
2152  |> line(end = [overHangLength, 0])
2153  |> line(end = [0, -thickness])
2154  |> line(end = [-overHangLength + thickness, 0])
2155  |> line(end = [0, -sphereDia])
2156  |> line(end = [overHangLength - thickness, 0])
2157  |> line(end = [0, -thickness])
2158  |> line(end = [-overHangLength, 0])
2159  |> close()
2160  |> revolve(axis = Y)
2161
2162// 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)
2163sphere = startSketchOn(XZ)
2164  |> startProfile(at = [
2165       0.05 + insideDia / 2 + thickness,
2166       0 - 0.05
2167     ])
2168  |> line(end = [sphereDia - 0.1, 0])
2169  |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
2170  |> close()
2171  |> revolve(axis = X)
2172  |> patternCircular3d(
2173       axis = [0, 0, 1],
2174       center = [0, 0, 0],
2175       repetitions = 10,
2176       arcDegrees = 360,
2177       rotateDuplicates = true,
2178     )
2179
2180// Sketch and revolve the outside bearing
2181outsideRevolve = startSketchOn(XZ)
2182  |> startProfile(at = [
2183       insideDia / 2 + thickness + sphereDia,
2184       0
2185     ])
2186  |> line(end = [0, sphereDia / 2])
2187  |> line(end = [-overHangLength + thickness, 0])
2188  |> line(end = [0, thickness])
2189  |> line(end = [overHangLength, 0])
2190  |> line(end = [0, -2 * thickness - sphereDia])
2191  |> line(end = [-overHangLength, 0])
2192  |> line(end = [0, thickness])
2193  |> line(end = [overHangLength - thickness, 0])
2194  |> close()
2195  |> revolve(axis = Y)
2196"#
2197        );
2198    }
2199
2200    #[test]
2201    fn test_recast_fn_in_object() {
2202        let some_program_string = r#"bing = { yo = 55 }
2203myNestedVar = [{ prop = callExp(bing.yo) }]
2204"#;
2205        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2206
2207        let recasted = program.recast_top(&Default::default(), 0);
2208        assert_eq!(recasted, some_program_string);
2209    }
2210
2211    #[test]
2212    fn test_recast_fn_in_array() {
2213        let some_program_string = r#"bing = { yo = 55 }
2214myNestedVar = [callExp(bing.yo)]
2215"#;
2216        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2217
2218        let recasted = program.recast_top(&Default::default(), 0);
2219        assert_eq!(recasted, some_program_string);
2220    }
2221
2222    #[test]
2223    fn test_recast_ranges() {
2224        let some_program_string = r#"foo = [0..10]
2225ten = 10
2226bar = [0 + 1 .. ten]
2227"#;
2228        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2229
2230        let recasted = program.recast_top(&Default::default(), 0);
2231        assert_eq!(recasted, some_program_string);
2232    }
2233
2234    #[test]
2235    fn test_recast_space_in_fn_call() {
2236        let some_program_string = r#"fn thing (x) {
2237    return x + 1
2238}
2239
2240thing ( 1 )
2241"#;
2242        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2243
2244        let recasted = program.recast_top(&Default::default(), 0);
2245        assert_eq!(
2246            recasted,
2247            r#"fn thing(x) {
2248  return x + 1
2249}
2250
2251thing(1)
2252"#
2253        );
2254    }
2255
2256    #[test]
2257    fn test_recast_typed_fn() {
2258        let some_program_string = r#"fn thing(x: string, y: [bool]): number {
2259  return x + 1
2260}
2261"#;
2262        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2263
2264        let recasted = program.recast_top(&Default::default(), 0);
2265        assert_eq!(recasted, some_program_string);
2266    }
2267
2268    #[test]
2269    fn test_recast_typed_consts() {
2270        let some_program_string = r#"a = 42: number
2271export b = 3.2: number(ft)
2272c = "dsfds": A | B | C
2273d = [1]: [number]
2274e = foo: [number; 3]
2275f = [1, 2, 3]: [number; 1+]
2276f = [1, 2, 3]: [number; 3+]
2277"#;
2278        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2279
2280        let recasted = program.recast_top(&Default::default(), 0);
2281        assert_eq!(recasted, some_program_string);
2282    }
2283
2284    #[test]
2285    fn test_recast_object_fn_in_array_weird_bracket() {
2286        let some_program_string = r#"bing = { yo = 55 }
2287myNestedVar = [
2288  {
2289  prop:   line(a = [bing.yo, 21], b = sketch001)
2290}
2291]
2292"#;
2293        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2294
2295        let recasted = program.recast_top(&Default::default(), 0);
2296        let expected = r#"bing = { yo = 55 }
2297myNestedVar = [
2298  {
2299    prop = line(a = [bing.yo, 21], b = sketch001)
2300  }
2301]
2302"#;
2303        assert_eq!(recasted, expected,);
2304    }
2305
2306    #[test]
2307    fn test_recast_empty_file() {
2308        let some_program_string = r#""#;
2309        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2310
2311        let recasted = program.recast_top(&Default::default(), 0);
2312        // Its VERY important this comes back with zero new lines.
2313        assert_eq!(recasted, r#""#);
2314    }
2315
2316    #[test]
2317    fn test_recast_empty_file_new_line() {
2318        let some_program_string = r#"
2319"#;
2320        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2321
2322        let recasted = program.recast_top(&Default::default(), 0);
2323        // Its VERY important this comes back with zero new lines.
2324        assert_eq!(recasted, r#""#);
2325    }
2326
2327    #[test]
2328    fn test_recast_shebang() {
2329        let some_program_string = r#"#!/usr/local/env zoo kcl
2330part001 = startSketchOn(XY)
2331  |> startProfile(at = [-10, -10])
2332  |> line(end = [20, 0])
2333  |> line(end = [0, 20])
2334  |> line(end = [-20, 0])
2335  |> close()
2336"#;
2337
2338        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2339
2340        let recasted = program.recast_top(&Default::default(), 0);
2341        assert_eq!(
2342            recasted,
2343            r#"#!/usr/local/env zoo kcl
2344
2345part001 = startSketchOn(XY)
2346  |> startProfile(at = [-10, -10])
2347  |> line(end = [20, 0])
2348  |> line(end = [0, 20])
2349  |> line(end = [-20, 0])
2350  |> close()
2351"#
2352        );
2353    }
2354
2355    #[test]
2356    fn test_recast_shebang_new_lines() {
2357        let some_program_string = r#"#!/usr/local/env zoo kcl
2358        
2359
2360
2361part001 = startSketchOn(XY)
2362  |> startProfile(at = [-10, -10])
2363  |> line(end = [20, 0])
2364  |> line(end = [0, 20])
2365  |> line(end = [-20, 0])
2366  |> close()
2367"#;
2368
2369        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2370
2371        let recasted = program.recast_top(&Default::default(), 0);
2372        assert_eq!(
2373            recasted,
2374            r#"#!/usr/local/env zoo kcl
2375
2376part001 = startSketchOn(XY)
2377  |> startProfile(at = [-10, -10])
2378  |> line(end = [20, 0])
2379  |> line(end = [0, 20])
2380  |> line(end = [-20, 0])
2381  |> close()
2382"#
2383        );
2384    }
2385
2386    #[test]
2387    fn test_recast_shebang_with_comments() {
2388        let some_program_string = r#"#!/usr/local/env zoo kcl
2389        
2390// Yo yo my comments.
2391part001 = startSketchOn(XY)
2392  |> startProfile(at = [-10, -10])
2393  |> line(end = [20, 0])
2394  |> line(end = [0, 20])
2395  |> line(end = [-20, 0])
2396  |> close()
2397"#;
2398
2399        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2400
2401        let recasted = program.recast_top(&Default::default(), 0);
2402        assert_eq!(
2403            recasted,
2404            r#"#!/usr/local/env zoo kcl
2405
2406// Yo yo my comments.
2407part001 = startSketchOn(XY)
2408  |> startProfile(at = [-10, -10])
2409  |> line(end = [20, 0])
2410  |> line(end = [0, 20])
2411  |> line(end = [-20, 0])
2412  |> close()
2413"#
2414        );
2415    }
2416
2417    #[test]
2418    fn test_recast_empty_function_body_with_comments() {
2419        let input = r#"fn myFunc() {
2420  // Yo yo my comments.
2421}
2422"#;
2423
2424        let program = crate::parsing::top_level_parse(input).unwrap();
2425        let output = program.recast_top(&Default::default(), 0);
2426        assert_eq!(output, input);
2427    }
2428
2429    #[test]
2430    fn test_recast_large_file() {
2431        let some_program_string = r#"@settings(units=mm)
2432// define nts
2433radius = 6.0
2434width = 144.0
2435length = 83.0
2436depth = 45.0
2437thk = 5
2438hole_diam = 5
2439// define a rectangular shape func
2440fn rectShape(pos, w, l) {
2441  rr = startSketchOn(XY)
2442    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2443    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2444    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2445    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2446    |> close($edge4)
2447  return rr
2448}
2449// build the body of the focusrite scarlett solo gen 4
2450// only used for visualization
2451scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2452  |> extrude(depth)
2453  |> fillet(
2454       radius = radius,
2455       tags = [
2456  edge2,
2457  edge4,
2458  getOppositeEdge(edge2),
2459  getOppositeEdge(edge4)
2460]
2461   )
2462  // build the bracket sketch around the body
2463fn bracketSketch(w, d, t) {
2464  s = startSketchOn({
2465         plane = {
2466  origin = { x = 0, y = length / 2 + thk, z = 0 },
2467  x_axis = { x = 1, y = 0, z = 0 },
2468  y_axis = { x = 0, y = 0, z = 1 },
2469  z_axis = { x = 0, y = 1, z = 0 }
2470}
2471       })
2472    |> startProfile(at = [-w / 2 - t, d + t])
2473    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2474    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2475    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2476    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2477    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2478    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2479    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2480    |> close($edge8)
2481  return s
2482}
2483// build the body of the bracket
2484bracket_body = bracketSketch(w = width, d = depth, t = thk)
2485  |> extrude(length + 10)
2486  |> fillet(
2487       radius = radius,
2488       tags = [
2489  getNextAdjacentEdge(edge7),
2490  getNextAdjacentEdge(edge2),
2491  getNextAdjacentEdge(edge3),
2492  getNextAdjacentEdge(edge6)
2493]
2494     )
2495  // build the tabs of the mounting bracket (right side)
2496tabs_r = startSketchOn({
2497       plane = {
2498  origin = { x = 0, y = 0, z = depth + thk },
2499  x_axis = { x = 1, y = 0, z = 0 },
2500  y_axis = { x = 0, y = 1, z = 0 },
2501  z_axis = { x = 0, y = 0, z = 1 }
2502}
2503     })
2504  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2505  |> line(end = [10, -5])
2506  |> line(end = [0, -10])
2507  |> line(end = [-10, -5])
2508  |> close()
2509  |> subtract2d(tool = circle(
2510       center = [
2511         width / 2 + thk + hole_diam,
2512         length / 2 - hole_diam
2513       ],
2514       radius = hole_diam / 2
2515     ))
2516  |> extrude(-thk)
2517  |> patternLinear3d(
2518       axis = [0, -1, 0],
2519       repetitions = 1,
2520       distance = length - 10
2521     )
2522  // build the tabs of the mounting bracket (left side)
2523tabs_l = startSketchOn({
2524       plane = {
2525  origin = { x = 0, y = 0, z = depth + thk },
2526  x_axis = { x = 1, y = 0, z = 0 },
2527  y_axis = { x = 0, y = 1, z = 0 },
2528  z_axis = { x = 0, y = 0, z = 1 }
2529}
2530     })
2531  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2532  |> line(end = [-10, -5])
2533  |> line(end = [0, -10])
2534  |> line(end = [10, -5])
2535  |> close()
2536  |> subtract2d(tool = circle(
2537       center = [
2538         -width / 2 - thk - hole_diam,
2539         length / 2 - hole_diam
2540       ],
2541       radius = hole_diam / 2
2542     ))
2543  |> extrude(-thk)
2544  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2545"#;
2546        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2547
2548        let recasted = program.recast_top(&Default::default(), 0);
2549        // Its VERY important this comes back with zero new lines.
2550        assert_eq!(
2551            recasted,
2552            r#"@settings(units = mm)
2553
2554// define nts
2555radius = 6.0
2556width = 144.0
2557length = 83.0
2558depth = 45.0
2559thk = 5
2560hole_diam = 5
2561// define a rectangular shape func
2562fn rectShape(pos, w, l) {
2563  rr = startSketchOn(XY)
2564    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2565    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2566    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2567    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2568    |> close($edge4)
2569  return rr
2570}
2571// build the body of the focusrite scarlett solo gen 4
2572// only used for visualization
2573scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2574  |> extrude(depth)
2575  |> fillet(
2576       radius = radius,
2577       tags = [
2578         edge2,
2579         edge4,
2580         getOppositeEdge(edge2),
2581         getOppositeEdge(edge4)
2582       ],
2583     )
2584// build the bracket sketch around the body
2585fn bracketSketch(w, d, t) {
2586  s = startSketchOn({
2587    plane = {
2588      origin = { x = 0, y = length / 2 + thk, z = 0 },
2589      x_axis = { x = 1, y = 0, z = 0 },
2590      y_axis = { x = 0, y = 0, z = 1 },
2591      z_axis = { x = 0, y = 1, z = 0 }
2592    }
2593  })
2594    |> startProfile(at = [-w / 2 - t, d + t])
2595    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2596    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2597    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2598    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2599    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2600    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2601    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2602    |> close($edge8)
2603  return s
2604}
2605// build the body of the bracket
2606bracket_body = bracketSketch(w = width, d = depth, t = thk)
2607  |> extrude(length + 10)
2608  |> fillet(
2609       radius = radius,
2610       tags = [
2611         getNextAdjacentEdge(edge7),
2612         getNextAdjacentEdge(edge2),
2613         getNextAdjacentEdge(edge3),
2614         getNextAdjacentEdge(edge6)
2615       ],
2616     )
2617// build the tabs of the mounting bracket (right side)
2618tabs_r = startSketchOn({
2619  plane = {
2620    origin = { x = 0, y = 0, z = depth + thk },
2621    x_axis = { x = 1, y = 0, z = 0 },
2622    y_axis = { x = 0, y = 1, z = 0 },
2623    z_axis = { x = 0, y = 0, z = 1 }
2624  }
2625})
2626  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2627  |> line(end = [10, -5])
2628  |> line(end = [0, -10])
2629  |> line(end = [-10, -5])
2630  |> close()
2631  |> subtract2d(tool = circle(
2632       center = [
2633         width / 2 + thk + hole_diam,
2634         length / 2 - hole_diam
2635       ],
2636       radius = hole_diam / 2,
2637     ))
2638  |> extrude(-thk)
2639  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2640// build the tabs of the mounting bracket (left side)
2641tabs_l = startSketchOn({
2642  plane = {
2643    origin = { x = 0, y = 0, z = depth + thk },
2644    x_axis = { x = 1, y = 0, z = 0 },
2645    y_axis = { x = 0, y = 1, z = 0 },
2646    z_axis = { x = 0, y = 0, z = 1 }
2647  }
2648})
2649  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2650  |> line(end = [-10, -5])
2651  |> line(end = [0, -10])
2652  |> line(end = [10, -5])
2653  |> close()
2654  |> subtract2d(tool = circle(
2655       center = [
2656         -width / 2 - thk - hole_diam,
2657         length / 2 - hole_diam
2658       ],
2659       radius = hole_diam / 2,
2660     ))
2661  |> extrude(-thk)
2662  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2663"#
2664        );
2665    }
2666
2667    #[test]
2668    fn test_recast_nested_var_declaration_in_fn_body() {
2669        let some_program_string = r#"fn cube(pos, scale) {
2670   sg = startSketchOn(XY)
2671  |> startProfile(at = pos)
2672  |> line(end = [0, scale])
2673  |> line(end = [scale, 0])
2674  |> line(end = [0, -scale])
2675  |> close()
2676  |> extrude(scale)
2677}"#;
2678        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2679
2680        let recasted = program.recast_top(&Default::default(), 0);
2681        assert_eq!(
2682            recasted,
2683            r#"fn cube(pos, scale) {
2684  sg = startSketchOn(XY)
2685    |> startProfile(at = pos)
2686    |> line(end = [0, scale])
2687    |> line(end = [scale, 0])
2688    |> line(end = [0, -scale])
2689    |> close()
2690    |> extrude(scale)
2691}
2692"#
2693        );
2694    }
2695
2696    #[test]
2697    fn test_as() {
2698        let some_program_string = r#"fn cube(pos, scale) {
2699  x = dfsfs + dfsfsd as y
2700
2701  sg = startSketchOn(XY)
2702    |> startProfile(at = pos) as foo
2703    |> line([0, scale])
2704    |> line([scale, 0]) as bar
2705    |> line([0 as baz, -scale] as qux)
2706    |> close()
2707    |> extrude(length = scale)
2708}
2709
2710cube(pos = 0, scale = 0) as cub
2711"#;
2712        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2713
2714        let recasted = program.recast_top(&Default::default(), 0);
2715        assert_eq!(recasted, some_program_string,);
2716    }
2717
2718    #[test]
2719    fn test_recast_with_bad_indentation() {
2720        let some_program_string = r#"part001 = startSketchOn(XY)
2721  |> startProfile(at = [0.0, 5.0])
2722              |> line(end = [0.4900857016, -0.0240763666])
2723    |> line(end = [0.6804562304, 0.9087880491])"#;
2724        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2725
2726        let recasted = program.recast_top(&Default::default(), 0);
2727        assert_eq!(
2728            recasted,
2729            r#"part001 = startSketchOn(XY)
2730  |> startProfile(at = [0.0, 5.0])
2731  |> line(end = [0.4900857016, -0.0240763666])
2732  |> line(end = [0.6804562304, 0.9087880491])
2733"#
2734        );
2735    }
2736
2737    #[test]
2738    fn test_recast_with_bad_indentation_and_inline_comment() {
2739        let some_program_string = r#"part001 = startSketchOn(XY)
2740  |> startProfile(at = [0.0, 5.0])
2741              |> line(end = [0.4900857016, -0.0240763666]) // hello world
2742    |> line(end = [0.6804562304, 0.9087880491])"#;
2743        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2744
2745        let recasted = program.recast_top(&Default::default(), 0);
2746        assert_eq!(
2747            recasted,
2748            r#"part001 = startSketchOn(XY)
2749  |> startProfile(at = [0.0, 5.0])
2750  |> line(end = [0.4900857016, -0.0240763666]) // hello world
2751  |> line(end = [0.6804562304, 0.9087880491])
2752"#
2753        );
2754    }
2755    #[test]
2756    fn test_recast_with_bad_indentation_and_line_comment() {
2757        let some_program_string = r#"part001 = startSketchOn(XY)
2758  |> startProfile(at = [0.0, 5.0])
2759              |> line(end = [0.4900857016, -0.0240763666])
2760        // hello world
2761    |> line(end = [0.6804562304, 0.9087880491])"#;
2762        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2763
2764        let recasted = program.recast_top(&Default::default(), 0);
2765        assert_eq!(
2766            recasted,
2767            r#"part001 = startSketchOn(XY)
2768  |> startProfile(at = [0.0, 5.0])
2769  |> line(end = [0.4900857016, -0.0240763666])
2770  // hello world
2771  |> line(end = [0.6804562304, 0.9087880491])
2772"#
2773        );
2774    }
2775
2776    #[test]
2777    fn test_recast_comment_in_a_fn_block() {
2778        let some_program_string = r#"fn myFn() {
2779  // this is a comment
2780  yo = { a = { b = { c = '123' } } } /* block
2781  comment */
2782
2783  key = 'c'
2784  // this is also a comment
2785    return things
2786}"#;
2787        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2788
2789        let recasted = program.recast_top(&Default::default(), 0);
2790        assert_eq!(
2791            recasted,
2792            r#"fn myFn() {
2793  // this is a comment
2794  yo = { a = { b = { c = '123' } } } /* block
2795  comment */
2796
2797  key = 'c'
2798  // this is also a comment
2799  return things
2800}
2801"#
2802        );
2803    }
2804
2805    #[test]
2806    fn test_recast_comment_under_variable() {
2807        let some_program_string = r#"key = 'c'
2808// this is also a comment
2809thing = 'foo'
2810"#;
2811        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2812
2813        let recasted = program.recast_top(&Default::default(), 0);
2814        assert_eq!(
2815            recasted,
2816            r#"key = 'c'
2817// this is also a comment
2818thing = 'foo'
2819"#
2820        );
2821    }
2822
2823    #[test]
2824    fn test_recast_multiline_comment_start_file() {
2825        let some_program_string = r#"// hello world
2826// I am a comment
2827key = 'c'
2828// this is also a comment
2829// hello
2830thing = 'foo'
2831"#;
2832        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2833
2834        let recasted = program.recast_top(&Default::default(), 0);
2835        assert_eq!(
2836            recasted,
2837            r#"// hello world
2838// I am a comment
2839key = 'c'
2840// this is also a comment
2841// hello
2842thing = 'foo'
2843"#
2844        );
2845    }
2846
2847    #[test]
2848    fn test_recast_empty_comment() {
2849        let some_program_string = r#"// hello world
2850//
2851// I am a comment
2852key = 'c'
2853
2854//
2855// I am a comment
2856thing = 'c'
2857
2858foo = 'bar' //
2859"#;
2860        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2861
2862        let recasted = program.recast_top(&Default::default(), 0);
2863        assert_eq!(
2864            recasted,
2865            r#"// hello world
2866//
2867// I am a comment
2868key = 'c'
2869
2870//
2871// I am a comment
2872thing = 'c'
2873
2874foo = 'bar' //
2875"#
2876        );
2877    }
2878
2879    #[test]
2880    fn test_recast_multiline_comment_under_variable() {
2881        let some_program_string = r#"key = 'c'
2882// this is also a comment
2883// hello
2884thing = 'foo'
2885"#;
2886        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2887
2888        let recasted = program.recast_top(&Default::default(), 0);
2889        assert_eq!(
2890            recasted,
2891            r#"key = 'c'
2892// this is also a comment
2893// hello
2894thing = 'foo'
2895"#
2896        );
2897    }
2898
2899    #[test]
2900    fn test_recast_only_line_comments() {
2901        let code = r#"// comment at start
2902"#;
2903        let program = crate::parsing::top_level_parse(code).unwrap();
2904
2905        assert_eq!(program.recast_top(&Default::default(), 0), code);
2906    }
2907
2908    #[test]
2909    fn test_recast_comment_at_start() {
2910        let test_program = r#"
2911/* comment at start */
2912
2913mySk1 = startSketchOn(XY)
2914  |> startProfile(at = [0, 0])"#;
2915        let program = crate::parsing::top_level_parse(test_program).unwrap();
2916
2917        let recasted = program.recast_top(&Default::default(), 0);
2918        assert_eq!(
2919            recasted,
2920            r#"/* comment at start */
2921
2922mySk1 = startSketchOn(XY)
2923  |> startProfile(at = [0, 0])
2924"#
2925        );
2926    }
2927
2928    #[test]
2929    fn test_recast_lots_of_comments() {
2930        let some_program_string = r#"// comment at start
2931mySk1 = startSketchOn(XY)
2932  |> startProfile(at = [0, 0])
2933  |> line(endAbsolute = [1, 1])
2934  // comment here
2935  |> line(endAbsolute = [0, 1], tag = $myTag)
2936  |> line(endAbsolute = [1, 1])
2937  /* and
2938  here
2939  */
2940  // a comment between pipe expression statements
2941  |> rx(90)
2942  // and another with just white space between others below
2943  |> ry(45)
2944  |> rx(45)
2945// one more for good measure"#;
2946        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2947
2948        let recasted = program.recast_top(&Default::default(), 0);
2949        assert_eq!(
2950            recasted,
2951            r#"// comment at start
2952mySk1 = startSketchOn(XY)
2953  |> startProfile(at = [0, 0])
2954  |> line(endAbsolute = [1, 1])
2955  // comment here
2956  |> line(endAbsolute = [0, 1], tag = $myTag)
2957  |> line(endAbsolute = [1, 1])
2958  /* and
2959  here */
2960  // a comment between pipe expression statements
2961  |> rx(90)
2962  // and another with just white space between others below
2963  |> ry(45)
2964  |> rx(45)
2965// one more for good measure
2966"#
2967        );
2968    }
2969
2970    #[test]
2971    fn test_recast_multiline_object() {
2972        let some_program_string = r#"x = {
2973  a = 1000000000,
2974  b = 2000000000,
2975  c = 3000000000,
2976  d = 4000000000,
2977  e = 5000000000
2978}"#;
2979        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2980
2981        let recasted = program.recast_top(&Default::default(), 0);
2982        assert_eq!(recasted.trim(), some_program_string);
2983    }
2984
2985    #[test]
2986    fn test_recast_first_level_object() {
2987        let some_program_string = r#"three = 3
2988
2989yo = {
2990  aStr = 'str',
2991  anum = 2,
2992  identifier = three,
2993  binExp = 4 + 5
2994}
2995yo = [
2996  1,
2997  "  2,",
2998  "three",
2999  4 + 5,
3000  "  hey oooooo really long long long"
3001]
3002"#;
3003        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3004
3005        let recasted = program.recast_top(&Default::default(), 0);
3006        assert_eq!(recasted, some_program_string);
3007    }
3008
3009    #[test]
3010    fn test_recast_new_line_before_comment() {
3011        let some_program_string = r#"
3012// this is a comment
3013yo = { a = { b = { c = '123' } } }
3014
3015key = 'c'
3016things = "things"
3017
3018// this is also a comment"#;
3019        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3020
3021        let recasted = program.recast_top(&Default::default(), 0);
3022        let expected = some_program_string.trim();
3023        // Currently new parser removes an empty line
3024        let actual = recasted.trim();
3025        assert_eq!(actual, expected);
3026    }
3027
3028    #[test]
3029    fn test_recast_comment_tokens_inside_strings() {
3030        let some_program_string = r#"b = {
3031  end = 141,
3032  start = 125,
3033  type_ = "NonCodeNode",
3034  value = "
3035 // a comment
3036   "
3037}"#;
3038        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3039
3040        let recasted = program.recast_top(&Default::default(), 0);
3041        assert_eq!(recasted.trim(), some_program_string.trim());
3042    }
3043
3044    #[test]
3045    fn test_recast_array_new_line_in_pipe() {
3046        let some_program_string = r#"myVar = 3
3047myVar2 = 5
3048myVar3 = 6
3049myAng = 40
3050myAng2 = 134
3051part001 = startSketchOn(XY)
3052  |> startProfile(at = [0, 0])
3053  |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3054  |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3055  |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
3056        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3057
3058        let recasted = program.recast_top(&Default::default(), 0);
3059        assert_eq!(recasted.trim(), some_program_string);
3060    }
3061
3062    #[test]
3063    fn test_recast_array_new_line_in_pipe_custom() {
3064        let some_program_string = r#"myVar = 3
3065myVar2 = 5
3066myVar3 = 6
3067myAng = 40
3068myAng2 = 134
3069part001 = startSketchOn(XY)
3070   |> startProfile(at = [0, 0])
3071   |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3072   |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3073   |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
3074"#;
3075        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3076
3077        let recasted = program.recast_top(
3078            &FormatOptions {
3079                tab_size: 3,
3080                use_tabs: false,
3081                insert_final_newline: true,
3082            },
3083            0,
3084        );
3085        assert_eq!(recasted, some_program_string);
3086    }
3087
3088    #[test]
3089    fn test_recast_after_rename_std() {
3090        let some_program_string = r#"part001 = startSketchOn(XY)
3091  |> startProfile(at = [0.0000000000, 5.0000000000])
3092    |> line(end = [0.4900857016, -0.0240763666])
3093
3094part002 = "part002"
3095things = [part001, 0.0]
3096blah = 1
3097foo = false
3098baz = {a: 1, part001: "thing"}
3099
3100fn ghi(part001) {
3101  return part001
3102}
3103"#;
3104        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3105        assert!(program.rename_symbol("mySuperCoolPart", 6));
3106
3107        let recasted = program.recast_top(&Default::default(), 0);
3108        assert_eq!(
3109            recasted,
3110            r#"mySuperCoolPart = startSketchOn(XY)
3111  |> startProfile(at = [0.0, 5.0])
3112  |> line(end = [0.4900857016, -0.0240763666])
3113
3114part002 = "part002"
3115things = [mySuperCoolPart, 0.0]
3116blah = 1
3117foo = false
3118baz = { a = 1, part001 = "thing" }
3119
3120fn ghi(part001) {
3121  return part001
3122}
3123"#
3124        );
3125    }
3126
3127    #[test]
3128    fn test_recast_after_rename_fn_args() {
3129        let some_program_string = r#"fn ghi(x, y, z) {
3130  return x
3131}"#;
3132        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3133        assert!(program.rename_symbol("newName", 7));
3134
3135        let recasted = program.recast_top(&Default::default(), 0);
3136        assert_eq!(
3137            recasted,
3138            r#"fn ghi(newName, y, z) {
3139  return newName
3140}
3141"#
3142        );
3143    }
3144
3145    #[test]
3146    fn test_recast_trailing_comma() {
3147        let some_program_string = r#"startSketchOn(XY)
3148  |> startProfile(at = [0, 0])
3149  |> arc({
3150    radius = 1,
3151    angle_start = 0,
3152    angle_end = 180,
3153  })"#;
3154        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3155
3156        let recasted = program.recast_top(&Default::default(), 0);
3157        assert_eq!(
3158            recasted,
3159            r#"startSketchOn(XY)
3160  |> startProfile(at = [0, 0])
3161  |> arc({
3162       radius = 1,
3163       angle_start = 0,
3164       angle_end = 180
3165     })
3166"#
3167        );
3168    }
3169
3170    #[test]
3171    fn test_recast_array_no_trailing_comma_with_comments() {
3172        let some_program_string = r#"[
3173  1, // one
3174  2, // two
3175  3  // three
3176]"#;
3177        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3178
3179        let recasted = program.recast_top(&Default::default(), 0);
3180        assert_eq!(
3181            recasted,
3182            r#"[
3183  1,
3184  // one
3185  2,
3186  // two
3187  3,
3188  // three
3189]
3190"#
3191        );
3192    }
3193
3194    #[test]
3195    fn test_recast_object_no_trailing_comma_with_comments() {
3196        let some_program_string = r#"{
3197  x=1, // one
3198  y=2, // two
3199  z=3  // three
3200}"#;
3201        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3202
3203        let recasted = program.recast_top(&Default::default(), 0);
3204        // TODO: We should probably not add an extra new line after the last
3205        // comment.
3206        assert_eq!(
3207            recasted,
3208            r#"{
3209  x = 1,
3210  // one
3211  y = 2,
3212  // two
3213  z = 3,
3214  // three
3215
3216}
3217"#
3218        );
3219    }
3220
3221    #[test]
3222    fn test_recast_comment_between_call_args() {
3223        let some_program_string = r#"rounded = fillet(
3224  body,
3225  radius = 1mm,
3226  // Keep this comment
3227  tags = [tag1, tag2],
3228)
3229"#;
3230        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3231
3232        let recasted = program.recast_top(&Default::default(), 0);
3233        assert_eq!(recasted, some_program_string);
3234
3235        // Check idempotency.
3236        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3237        let recasted2 = program.recast_top(&Default::default(), 0);
3238        assert_eq!(recasted2, recasted);
3239    }
3240
3241    #[test]
3242    fn test_recast_line_comments_in_call_args_force_multiline() {
3243        let some_program_string = r#"edged = chamfer(
3244  body,
3245  // leading
3246  length = 1mm,
3247  tags = [tag1],
3248  // trailing
3249)
3250"#;
3251        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3252
3253        let recasted = program.recast_top(&Default::default(), 0);
3254        assert_eq!(recasted, some_program_string);
3255
3256        // Check idempotency.
3257        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3258        let recasted2 = program.recast_top(&Default::default(), 0);
3259        assert_eq!(recasted2, recasted);
3260    }
3261
3262    #[test]
3263    fn test_recast_block_comment_in_call_args_stays_inline() {
3264        // A block comment shouldn't force the call onto multiple lines.
3265        let some_program_string = r#"rounded = fillet(body, radius = 1mm, /* mid */ tags = [tag1])
3266"#;
3267        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3268
3269        let recasted = program.recast_top(&Default::default(), 0);
3270        assert_eq!(recasted, some_program_string);
3271
3272        // Check idempotency.
3273        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3274        let recasted2 = program.recast_top(&Default::default(), 0);
3275        assert_eq!(recasted2, recasted);
3276    }
3277
3278    #[test]
3279    fn test_recast_block_comment_in_multiline_call_args_stays_inline() {
3280        // In a call that's multi-line anyway, a block comment stays glued to
3281        // the argument it precedes.
3282        let some_program_string = r#"rounded = fillet(
3283  body,
3284  radius = 1mm,
3285  /* mid */ tags = [tag1],
3286  tag = $x,
3287)
3288"#;
3289        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3290
3291        let recasted = program.recast_top(&Default::default(), 0);
3292        assert_eq!(recasted, some_program_string);
3293
3294        // Check idempotency.
3295        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3296        let recasted2 = program.recast_top(&Default::default(), 0);
3297        assert_eq!(recasted2, recasted);
3298    }
3299
3300    #[test]
3301    fn test_recast_comment_in_call_args_in_pipe() {
3302        let some_program_string = r#"part = startSketchOn(XY)
3303  |> startProfile(at = [0, 0])
3304  |> fillet(
3305       radius = 1,
3306       // why
3307       tags = [a],
3308     )
3309"#;
3310        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3311
3312        let recasted = program.recast_top(&Default::default(), 0);
3313        assert_eq!(recasted, some_program_string);
3314    }
3315
3316    #[test]
3317    fn test_recast_negative_var() {
3318        let some_program_string = r#"w = 20
3319l = 8
3320h = 10
3321
3322firstExtrude = startSketchOn(XY)
3323  |> startProfile(at = [0,0])
3324  |> line(end = [0, l])
3325  |> line(end = [w, 0])
3326  |> line(end = [0, -l])
3327  |> close()
3328  |> extrude(h)
3329"#;
3330        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3331
3332        let recasted = program.recast_top(&Default::default(), 0);
3333        assert_eq!(
3334            recasted,
3335            r#"w = 20
3336l = 8
3337h = 10
3338
3339firstExtrude = startSketchOn(XY)
3340  |> startProfile(at = [0, 0])
3341  |> line(end = [0, l])
3342  |> line(end = [w, 0])
3343  |> line(end = [0, -l])
3344  |> close()
3345  |> extrude(h)
3346"#
3347        );
3348    }
3349
3350    #[test]
3351    fn test_recast_multiline_comment() {
3352        let some_program_string = r#"w = 20
3353l = 8
3354h = 10
3355
3356// This is my comment
3357// It has multiple lines
3358// And it's really long
3359firstExtrude = startSketchOn(XY)
3360  |> startProfile(at = [0,0])
3361  |> line(end = [0, l])
3362  |> line(end = [w, 0])
3363  |> line(end = [0, -l])
3364  |> close()
3365  |> extrude(h)
3366"#;
3367        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3368
3369        let recasted = program.recast_top(&Default::default(), 0);
3370        assert_eq!(
3371            recasted,
3372            r#"w = 20
3373l = 8
3374h = 10
3375
3376// This is my comment
3377// It has multiple lines
3378// And it's really long
3379firstExtrude = startSketchOn(XY)
3380  |> startProfile(at = [0, 0])
3381  |> line(end = [0, l])
3382  |> line(end = [w, 0])
3383  |> line(end = [0, -l])
3384  |> close()
3385  |> extrude(h)
3386"#
3387        );
3388    }
3389
3390    #[test]
3391    fn test_recast_math_start_negative() {
3392        let some_program_string = r#"myVar = -5 + 6"#;
3393        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3394
3395        let recasted = program.recast_top(&Default::default(), 0);
3396        assert_eq!(recasted.trim(), some_program_string);
3397    }
3398
3399    #[test]
3400    fn test_recast_math_negate_parens() {
3401        let some_program_string = r#"wallMountL = 3.82
3402thickness = 0.5
3403
3404startSketchOn(XY)
3405  |> startProfile(at = [0, 0])
3406  |> line(end = [0, -(wallMountL - thickness)])
3407  |> line(end = [0, -(5 - thickness)])
3408  |> line(end = [0, -(5 - 1)])
3409  |> line(end = [0, -(-5 - 1)])"#;
3410        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3411
3412        let recasted = program.recast_top(&Default::default(), 0);
3413        assert_eq!(recasted.trim(), some_program_string);
3414    }
3415
3416    #[test]
3417    fn test_recast_math_nested_parens() {
3418        let some_program_string = r#"distance = 5
3419p = 3: Plane
3420FOS = { a = 3, b = 42 }: Sketch
3421sigmaAllow = 8: number(mm)
3422width = 20
3423thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
3424        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3425
3426        let recasted = program.recast_top(&Default::default(), 0);
3427        assert_eq!(recasted.trim(), some_program_string);
3428    }
3429
3430    #[test]
3431    fn no_vardec_keyword() {
3432        let some_program_string = r#"distance = 5"#;
3433        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3434
3435        let recasted = program.recast_top(&Default::default(), 0);
3436        assert_eq!(recasted.trim(), some_program_string);
3437    }
3438
3439    #[test]
3440    fn recast_types() {
3441        let some_program_string = r#"type foo
3442
3443// A comment
3444@(impl = primitive)
3445export type bar(unit, baz)
3446type baz = Foo | Bar
3447type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
3448"#;
3449        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3450        let recasted = program.recast_top(&Default::default(), 0);
3451        assert_eq!(recasted, some_program_string);
3452    }
3453
3454    #[test]
3455    fn recast_nested_fn() {
3456        let some_program_string = r#"fn f() {
3457  return fn() {
3458  return 1
3459}
3460}"#;
3461        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3462        let recasted = program.recast_top(&Default::default(), 0);
3463        let expected = "\
3464fn f() {
3465  return fn() {
3466    return 1
3467  }
3468}";
3469        assert_eq!(recasted.trim(), expected);
3470    }
3471
3472    #[test]
3473    fn recast_literal() {
3474        use winnow::Parser;
3475        for (i, (raw, expected, reason)) in [
3476            (
3477                "5.0",
3478                "5.0",
3479                "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
3480            ),
3481            (
3482                "5",
3483                "5",
3484                "integers should stay integral, i.e. don't reformat this to '5.0'",
3485            ),
3486            (
3487                "5.0000000",
3488                "5.0",
3489                "if the number is f64 but not fractional, use its canonical format",
3490            ),
3491            ("5.1", "5.1", "straightforward case works"),
3492        ]
3493        .into_iter()
3494        .enumerate()
3495        {
3496            let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
3497            let literal = crate::parsing::parser::unsigned_number_literal
3498                .parse(tokens.as_slice())
3499                .unwrap();
3500            let mut actual = String::new();
3501            literal.recast(&mut actual);
3502            assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
3503        }
3504    }
3505
3506    #[test]
3507    fn recast_objects_no_comments() {
3508        let input = r#"
3509sketch002 = startSketchOn({
3510       plane: {
3511    origin: { x = 1, y = 2, z = 3 },
3512    x_axis = { x = 4, y = 5, z = 6 },
3513    y_axis = { x = 7, y = 8, z = 9 },
3514    z_axis = { x = 10, y = 11, z = 12 }
3515       }
3516  })
3517"#;
3518        let expected = r#"sketch002 = startSketchOn({
3519  plane = {
3520    origin = { x = 1, y = 2, z = 3 },
3521    x_axis = { x = 4, y = 5, z = 6 },
3522    y_axis = { x = 7, y = 8, z = 9 },
3523    z_axis = { x = 10, y = 11, z = 12 }
3524  }
3525})
3526"#;
3527        let ast = crate::parsing::top_level_parse(input).unwrap();
3528        let actual = ast.recast_top(&FormatOptions::new(), 0);
3529        assert_eq!(actual, expected);
3530    }
3531
3532    #[test]
3533    fn unparse_fn_unnamed() {
3534        let input = "\
3535squares_out = reduce(
3536  arr,
3537  n = 0: number,
3538  f = fn(@i, accum) {
3539    return 1
3540  },
3541)
3542";
3543        let ast = crate::parsing::top_level_parse(input).unwrap();
3544        let actual = ast.recast_top(&FormatOptions::new(), 0);
3545        assert_eq!(actual, input);
3546    }
3547
3548    #[test]
3549    fn unparse_fn_named() {
3550        let input = r#"fn f(x) {
3551  return 1
3552}
3553"#;
3554        let ast = crate::parsing::top_level_parse(input).unwrap();
3555        let actual = ast.recast_top(&FormatOptions::new(), 0);
3556        assert_eq!(actual, input);
3557    }
3558
3559    #[test]
3560    fn unparse_call_inside_function_single_line() {
3561        let input = r#"fn foo() {
3562  toDegrees(atan(0.5), foo = 1)
3563  return 0
3564}
3565"#;
3566        let ast = crate::parsing::top_level_parse(input).unwrap();
3567        let actual = ast.recast_top(&FormatOptions::new(), 0);
3568        assert_eq!(actual, input);
3569    }
3570
3571    #[test]
3572    fn recast_function_types() {
3573        let input = r#"foo = x: fn
3574foo = x: fn(number)
3575fn foo(x: fn(): number): fn {
3576  return 0
3577}
3578fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
3579  return 0
3580}
3581type fn
3582type foo = fn
3583type foo = fn(a: string, b: { f: fn(): any })
3584type foo = fn([fn])
3585type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
3586"#;
3587        let ast = crate::parsing::top_level_parse(input).unwrap();
3588        let actual = ast.recast_top(&FormatOptions::new(), 0);
3589        assert_eq!(actual, input);
3590    }
3591
3592    #[test]
3593    fn unparse_call_inside_function_args_multiple_lines() {
3594        let input = r#"fn foo() {
3595  toDegrees(
3596    atan(0.5),
3597    foo = 1,
3598    bar = 2,
3599    baz = 3,
3600    qux = 4,
3601  )
3602  return 0
3603}
3604"#;
3605        let ast = crate::parsing::top_level_parse(input).unwrap();
3606        let actual = ast.recast_top(&FormatOptions::new(), 0);
3607        assert_eq!(actual, input);
3608    }
3609
3610    #[test]
3611    fn unparse_call_inside_function_single_arg_multiple_lines() {
3612        let input = r#"fn foo() {
3613  toDegrees(
3614    [
3615      profile0,
3616      profile1,
3617      profile2,
3618      profile3,
3619      profile4,
3620      profile5
3621    ],
3622    key = 1,
3623  )
3624  return 0
3625}
3626"#;
3627        let ast = crate::parsing::top_level_parse(input).unwrap();
3628        let actual = ast.recast_top(&FormatOptions::new(), 0);
3629        assert_eq!(actual, input);
3630    }
3631
3632    #[test]
3633    fn recast_objects_with_comments() {
3634        use winnow::Parser;
3635        for (i, (input, expected, reason)) in [(
3636            "\
3637{
3638  a = 1,
3639  // b = 2,
3640  c = 3
3641}",
3642            "\
3643{
3644  a = 1,
3645  // b = 2,
3646  c = 3
3647}",
3648            "preserves comments",
3649        )]
3650        .into_iter()
3651        .enumerate()
3652        {
3653            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3654            crate::parsing::parser::print_tokens(tokens.as_slice());
3655            let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3656            let mut actual = String::new();
3657            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3658            assert_eq!(
3659                actual, expected,
3660                "failed test {i}, which is testing that recasting {reason}"
3661            );
3662        }
3663    }
3664
3665    #[test]
3666    fn recast_array_with_comments() {
3667        use winnow::Parser;
3668        for (i, (input, expected, reason)) in [
3669            (
3670                "\
3671[
3672  1,
3673  2,
3674  3,
3675  4,
3676  5,
3677  6,
3678  7,
3679  8,
3680  9,
3681  10,
3682  11,
3683  12,
3684  13,
3685  14,
3686  15,
3687  16,
3688  17,
3689  18,
3690  19,
3691  20,
3692]",
3693                "\
3694[
3695  1,
3696  2,
3697  3,
3698  4,
3699  5,
3700  6,
3701  7,
3702  8,
3703  9,
3704  10,
3705  11,
3706  12,
3707  13,
3708  14,
3709  15,
3710  16,
3711  17,
3712  18,
3713  19,
3714  20
3715]",
3716                "preserves multi-line arrays",
3717            ),
3718            (
3719                "\
3720[
3721  1,
3722  // 2,
3723  3
3724]",
3725                "\
3726[
3727  1,
3728  // 2,
3729  3
3730]",
3731                "preserves comments",
3732            ),
3733            (
3734                "\
3735[
3736  1,
3737  2,
3738  // 3
3739]",
3740                "\
3741[
3742  1,
3743  2,
3744  // 3
3745]",
3746                "preserves comments at the end of the array",
3747            ),
3748        ]
3749        .into_iter()
3750        .enumerate()
3751        {
3752            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3753            let expr = crate::parsing::parser::array_elem_by_elem
3754                .parse(tokens.as_slice())
3755                .unwrap();
3756            let mut actual = String::new();
3757            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3758            assert_eq!(
3759                actual, expected,
3760                "failed test {i}, which is testing that recasting {reason}"
3761            );
3762        }
3763    }
3764
3765    #[test]
3766    fn code_with_comment_and_extra_lines() {
3767        let code = r#"yo = 'c'
3768
3769/* this is
3770a
3771comment */
3772yo = 'bing'
3773"#;
3774        let ast = crate::parsing::top_level_parse(code).unwrap();
3775        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3776        assert_eq!(recasted, code);
3777    }
3778
3779    #[test]
3780    fn comments_in_a_fn_block() {
3781        let code = r#"fn myFn() {
3782  // this is a comment
3783  yo = { a = { b = { c = '123' } } }
3784
3785  /* block
3786  comment */
3787  key = 'c'
3788  // this is also a comment
3789}
3790"#;
3791        let ast = crate::parsing::top_level_parse(code).unwrap();
3792        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3793        assert_eq!(recasted, code);
3794    }
3795
3796    #[test]
3797    fn array_range_end_exclusive() {
3798        let code = "myArray = [0..<4]\n";
3799        let ast = crate::parsing::top_level_parse(code).unwrap();
3800        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3801        assert_eq!(recasted, code);
3802    }
3803
3804    #[test]
3805    fn paren_precedence() {
3806        let code = r#"x = 1 - 2 - 3
3807x = (1 - 2) - 3
3808x = 1 - (2 - 3)
3809x = 1 + 2 + 3
3810x = (1 + 2) + 3
3811x = 1 + (2 + 3)
3812x = 2 * (y % 2)
3813x = (2 * y) % 2
3814x = 2 % (y * 2)
3815x = (2 % y) * 2
3816x = 2 * y % 2
3817"#;
3818
3819        let expected = r#"x = 1 - 2 - 3
3820x = 1 - 2 - 3
3821x = 1 - (2 - 3)
3822x = 1 + 2 + 3
3823x = 1 + 2 + 3
3824x = 1 + 2 + 3
3825x = 2 * (y % 2)
3826x = 2 * y % 2
3827x = 2 % (y * 2)
3828x = 2 % y * 2
3829x = 2 * y % 2
3830"#;
3831        let ast = crate::parsing::top_level_parse(code).unwrap();
3832        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3833        assert_eq!(recasted, expected);
3834    }
3835
3836    #[test]
3837    fn gap_between_body_item_and_documented_fn() {
3838        let code = "\
3839x = 360
3840
3841// Watermelon
3842fn myFn() {
3843}
3844";
3845        let ast = crate::parsing::top_level_parse(code).unwrap();
3846        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3847        let expected = code;
3848        assert_eq!(recasted, expected);
3849    }
3850
3851    #[test]
3852    fn simple_assignment_in_fn() {
3853        let code = "\
3854fn function001() {
3855  extrude002 = extrude()
3856}\n";
3857
3858        let ast = crate::parsing::top_level_parse(code).unwrap();
3859        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3860        let expected = code;
3861        assert_eq!(recasted, expected);
3862    }
3863
3864    #[test]
3865    fn no_weird_extra_lines() {
3866        // Regression test, this used to insert a lot of new lines
3867        // between the initial comment and the @settings.
3868        let code = "\
3869// Initial comment
3870
3871@settings(defaultLengthUnit = mm)
3872
3873x = 1
3874";
3875        let ast = crate::parsing::top_level_parse(code).unwrap();
3876        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3877        let expected = code;
3878        assert_eq!(recasted, expected);
3879    }
3880
3881    #[test]
3882    fn settings_then_code_is_stable() {
3883        let code = "\
3884@settings(defaultLengthUnit = in)
3885
3886import \"cube-inches.kcl\" as cubeIn
3887import \"cube-mm.kcl\" as cubeMm
3888
3889cubeIn
3890cubeMm
3891";
3892        let formatted_once = crate::parsing::top_level_parse(code)
3893            .unwrap()
3894            .recast_top(&FormatOptions::new(), 0);
3895        assert_eq!(formatted_once, code);
3896
3897        let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3898            .unwrap()
3899            .recast_top(&FormatOptions::new(), 0);
3900        assert_eq!(formatted_twice, formatted_once);
3901    }
3902
3903    #[test]
3904    fn settings_then_standalone_comment_is_stable() {
3905        let code = "\
3906@settings(defaultLengthUnit = mm)
3907@settings(defaultAngleUnit = deg)
3908
3909// Cap for gimbal stick
3910
3911x = 1
3912";
3913        let formatted_once = crate::parsing::top_level_parse(code)
3914            .unwrap()
3915            .recast_top(&FormatOptions::new(), 0);
3916        assert_eq!(formatted_once, code);
3917
3918        let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3919            .unwrap()
3920            .recast_top(&FormatOptions::new(), 0);
3921        assert_eq!(formatted_twice, formatted_once);
3922    }
3923
3924    #[test]
3925    fn module_prefix() {
3926        let code = "x = std::sweep::SKETCH_PLANE\n";
3927        let ast = crate::parsing::top_level_parse(code).unwrap();
3928        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3929        let expected = code;
3930        assert_eq!(recasted, expected);
3931    }
3932
3933    #[test]
3934    fn inline_ifs() {
3935        let code = "y = true
3936startSketchOn(XY)
3937  |> startProfile(at = [0, 0])
3938  |> if y {
3939    yLine(length = 1)
3940  } else {
3941    xLine(length = 1)
3942  }
3943";
3944        let ast = crate::parsing::top_level_parse(code).unwrap();
3945        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3946        let expected = code;
3947        assert_eq!(recasted, expected);
3948    }
3949
3950    #[test]
3951    fn indented_binary_expressions() {
3952        let code = "\
3953fn foo() {
3954  1 == 2
3955}
3956";
3957        let ast = crate::parsing::top_level_parse(code).unwrap();
3958        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3959        let expected = code;
3960        assert_eq!(recasted, expected);
3961    }
3962
3963    #[test]
3964    fn indented_assignment() {
3965        let code = "\
3966fn foo() {
3967  x = 1
3968}
3969";
3970        let ast = crate::parsing::top_level_parse(code).unwrap();
3971        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3972        let expected = code;
3973        assert_eq!(recasted, expected);
3974    }
3975
3976    #[test]
3977    fn indented_unary_expression() {
3978        let code = "\
3979fn foo() {
3980  -x
3981}
3982";
3983        let ast = crate::parsing::top_level_parse(code).unwrap();
3984        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3985        let expected = code;
3986        assert_eq!(recasted, expected);
3987    }
3988
3989    #[test]
3990    fn indented_array_expression() {
3991        let code = "\
3992fn foo() {
3993  [1, 2]
3994}
3995";
3996        let ast = crate::parsing::top_level_parse(code).unwrap();
3997        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3998        let expected = code;
3999        assert_eq!(recasted, expected);
4000    }
4001
4002    #[test]
4003    fn indented_name_expression() {
4004        let code = "\
4005fn foo() {
4006  x
4007}
4008";
4009        let ast = crate::parsing::top_level_parse(code).unwrap();
4010        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4011        let expected = code;
4012        assert_eq!(recasted, expected);
4013    }
4014
4015    #[test]
4016    fn indented_member_assignment() {
4017        let code = "\
4018brakcetPlane = {
4019  origin = { x = length / 2 },
4020  origin = { x = length / 2 },
4021  origin = { x = length / 2 },
4022  origin = { x = length / 2 },
4023  origin = { x = length / 2 },
4024  origin = { x = length / 2 },
4025  origin = { x = length / 2 },
4026  origin = { x = length / 2 }
4027}
4028";
4029        let ast = crate::parsing::top_level_parse(code).unwrap();
4030        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4031        let expected = code;
4032        assert_eq!(recasted, expected);
4033    }
4034
4035    #[test]
4036    fn badly_formatted_inline_calls() {
4037        let code = "\
4038return union([right, left])
4039  |> subtract(tools = [
4040       translate(axle(), y = pitchStabL + forkBaseL + wheelRGap + wheelR + addedLength),
4041       socket(rakeAngle = rearRake, xyTrans = [0, 12]),
4042       socket(
4043         rakeAngle = frontRake,
4044         xyTrans = [
4045           wheelW / 2 + wheelWGap + forkTineW / 2,
4046           40 + addedLength
4047         ],
4048       )
4049     ])
4050";
4051        let ast = crate::parsing::top_level_parse(code).unwrap();
4052        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4053        let expected = code;
4054        assert_eq!(recasted, expected);
4055    }
4056
4057    #[test]
4058    fn fn_args_prefixed_with_spaces() {
4059        let code = "holeAt(
4060  [cube1, cube2],
4061  plane = XY,
4062  holeBottom =   hole::flat(),
4063  holeBody =   hole::blind(depth = 2, diameter = 1),
4064  holeType =   hole::counterbore(diameter = 1.4, depth = 1),
4065  cutAt = [1, 1],
4066)";
4067        let expected = "holeAt(
4068  [cube1, cube2],
4069  plane = XY,
4070  holeBottom = hole::flat(),
4071  holeBody = hole::blind(depth = 2, diameter = 1),
4072  holeType = hole::counterbore(diameter = 1.4, depth = 1),
4073  cutAt = [1, 1],
4074)
4075";
4076        let ast = crate::parsing::top_level_parse(code).unwrap();
4077        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4078        assert_eq!(recasted, expected);
4079    }
4080
4081    #[test]
4082    fn some_fn_args_still_prefixed() {
4083        let code = "a
4084  |> b()
4085  |> subtract(
4086       tools =     startSketchOn(XY)
4087      |> circle(diameter = hubDiameter)
4088      |> extrude(length = hubThickness * 5, symmetric = true),
4089       tolerance,
4090     )
4091";
4092        let ast = crate::parsing::top_level_parse(code).unwrap();
4093        let actual_recasted = ast.recast_top(&FormatOptions::new(), 0);
4094        let expected_recasted = "a
4095  |> b()
4096  |> subtract(
4097       tools = startSketchOn(XY)
4098      |> circle(diameter = hubDiameter)
4099      |> extrude(length = hubThickness * 5, symmetric = true),
4100       tolerance,
4101     )
4102";
4103        assert_eq!(actual_recasted, expected_recasted);
4104    }
4105
4106    #[test]
4107    fn first_in_pipeline_indent() {
4108        // These code snippets are identical, except that one passes the gear into a clone,
4109        // and the other doesn't.
4110        let not_clone = "gear::helical(
4111  nTeeth = 12,
4112  module = 1.5,
4113  pressureAngle = 14deg,
4114  helixAngle = 25deg,
4115  gearHeight = 5,
4116)
4117";
4118        let yes_clone = "gear::helical(
4119  nTeeth = 12,
4120  module = 1.5,
4121  pressureAngle = 14deg,
4122  helixAngle = 25deg,
4123  gearHeight = 5,
4124)
4125|> clone()
4126";
4127        // Both these should format their gear::helical parameters the same.
4128        let not_clone_recasted = crate::parsing::top_level_parse(not_clone)
4129            .unwrap()
4130            .recast_top(&FormatOptions::new(), 0);
4131        let yes_clone_recasted = crate::parsing::top_level_parse(yes_clone)
4132            .unwrap()
4133            .recast_top(&FormatOptions::new(), 0);
4134        assert!(not_clone_recasted.contains("\n  nTeeth"));
4135        assert!(!yes_clone_recasted.contains("\n       nTeeth"));
4136        assert!(yes_clone_recasted.contains("\n  nTeeth"));
4137    }
4138}