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                    };
1478                    let report = miette::Report::new(report);
1479                    anyhow::anyhow!("{:?}", report)
1480                })?;
1481                for ce in &ces {
1482                    if ce.severity != crate::errors::Severity::Warning {
1483                        let report = crate::Report {
1484                            kcl_source: contents.to_string(),
1485                            error: crate::KclError::new_semantic(ce.clone().into()),
1486                            filename: file.to_string_lossy().to_string(),
1487                        };
1488                        let report = miette::Report::new(report);
1489                        anyhow::bail!("{:?}", report);
1490                    }
1491                }
1492                let Some(program) = program else {
1493                    anyhow::bail!("Failed to parse file `{}`", file.display());
1494                };
1495                let recast = program.recast_with_options(&options);
1496                tokio::fs::write(&file, recast)
1497                    .await
1498                    .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
1499
1500                Ok::<(), anyhow::Error>(())
1501            })
1502        })
1503        .collect::<Vec<_>>();
1504
1505    // Join all futures and await their completion
1506    let results = futures::future::join_all(futures).await;
1507
1508    // Check if any of the futures failed.
1509    let mut errors = Vec::new();
1510    for result in results {
1511        if let Err(err) = result? {
1512            errors.push(err);
1513        }
1514    }
1515
1516    if !errors.is_empty() {
1517        anyhow::bail!("Failed to recast some files: {:?}", errors);
1518    }
1519
1520    Ok(())
1521}
1522
1523#[cfg(test)]
1524mod tests {
1525    use pretty_assertions::assert_eq;
1526
1527    use super::*;
1528    use crate::ModuleId;
1529    use crate::parsing::ast::types::FormatOptions;
1530
1531    #[test]
1532    fn test_recast_annotations_without_body_items() {
1533        let input = r#"@settings(defaultLengthUnit = in)
1534"#;
1535        let program = crate::parsing::top_level_parse(input).unwrap();
1536        let output = program.recast_top(&Default::default(), 0);
1537        assert_eq!(output, input);
1538    }
1539
1540    #[test]
1541    fn test_recast_annotations_in_function_body() {
1542        let input = r#"fn myFunc() {
1543  @meta(yes = true)
1544
1545  x = 2
1546}
1547"#;
1548        let program = crate::parsing::top_level_parse(input).unwrap();
1549        let output = program.recast_top(&Default::default(), 0);
1550        assert_eq!(output, input);
1551    }
1552
1553    #[test]
1554    fn test_recast_annotations_in_function_body_without_items() {
1555        let input = "\
1556fn myFunc() {
1557  @meta(yes = true)
1558}
1559";
1560        let program = crate::parsing::top_level_parse(input).unwrap();
1561        let output = program.recast_top(&Default::default(), 0);
1562        assert_eq!(output, input);
1563    }
1564
1565    #[test]
1566    fn recast_annotations_with_comments() {
1567        let input = r#"// Start comment
1568
1569// Comment on attr
1570@settings(defaultLengthUnit = in)
1571
1572// Comment on item
1573foo = 42
1574
1575// Comment on another item
1576@(impl = kcl)
1577bar = 0
1578"#;
1579        let program = crate::parsing::top_level_parse(input).unwrap();
1580        let output = program.recast_top(&Default::default(), 0);
1581        assert_eq!(output, input);
1582    }
1583
1584    #[test]
1585    fn recast_annotations_with_block_comment() {
1586        let input = r#"/* Start comment
1587
1588sdfsdfsdfs */
1589@settings(defaultLengthUnit = in)
1590
1591foo = 42
1592"#;
1593        let program = crate::parsing::top_level_parse(input).unwrap();
1594        let output = program.recast_top(&Default::default(), 0);
1595        assert_eq!(output, input);
1596    }
1597
1598    /// Assert that `input` formats to `expected`, and that formatting is
1599    /// idempotent: recasting the output parses and formats to itself.
1600    #[track_caller]
1601    fn assert_recast(input: &str, expected: &str) {
1602        let program = crate::parsing::top_level_parse(input).unwrap();
1603        let output = program.recast_top(&Default::default(), 0);
1604        assert_eq!(output, expected);
1605        let reparsed = crate::parsing::top_level_parse(&output).unwrap();
1606        assert_eq!(reparsed.recast_top(&Default::default(), 0), output);
1607    }
1608
1609    #[test]
1610    fn recast_enum_multi_line_is_stable() {
1611        let input = r#"@settings(experimentalFeatures = allow)
1612
1613type Color {
1614  | Red
1615  | Green
1616  | Blue
1617}
1618"#;
1619        assert_recast(input, input);
1620    }
1621
1622    #[test]
1623    fn recast_enum_expands_single_line() {
1624        let input = r#"@settings(experimentalFeatures = allow)
1625
1626type Color { | Red | Green | Blue }
1627"#;
1628        let expected = r#"@settings(experimentalFeatures = allow)
1629
1630type Color {
1631  | Red
1632  | Green
1633  | Blue
1634}
1635"#;
1636        assert_recast(input, expected);
1637    }
1638
1639    #[test]
1640    fn recast_enum_export() {
1641        let input = r#"@settings(experimentalFeatures = allow)
1642
1643export type Color {
1644  | Red
1645}
1646"#;
1647        assert_recast(input, input);
1648    }
1649
1650    #[test]
1651    fn recast_enum_no_variants() {
1652        let input = r#"@settings(experimentalFeatures = allow)
1653
1654type Empty { | }
1655"#;
1656        assert_recast(input, input);
1657    }
1658
1659    #[test]
1660    fn recast_enum_no_variants_collapses_blank_lines() {
1661        // Blank lines around a lone arm marker are not content: the body
1662        // collapses in one pass, not two.
1663        let input = r#"@settings(experimentalFeatures = allow)
1664
1665type Empty {
1666
1667  |
1668
1669}
1670"#;
1671        let expected = r#"@settings(experimentalFeatures = allow)
1672
1673type Empty { | }
1674"#;
1675        assert_recast(input, expected);
1676    }
1677
1678    #[test]
1679    fn recast_enum_no_variants_with_comments() {
1680        let input = r#"@settings(experimentalFeatures = allow)
1681
1682type Empty { /* a */ | /* b */ }
1683"#;
1684        let expected = r#"@settings(experimentalFeatures = allow)
1685
1686type Empty {
1687  /* a */
1688  /* b */
1689  |
1690}
1691"#;
1692        assert_recast(input, expected);
1693    }
1694
1695    #[test]
1696    fn recast_enum_with_comments() {
1697        let input = r#"@settings(experimentalFeatures = allow)
1698
1699type Color {
1700  // before red
1701  | Red // after red
1702  | /* inside green arm */ Green
1703
1704  | Blue
1705  // trailing
1706}
1707"#;
1708        // Comments between an arm's `|` and its name normalize to a line
1709        // above the arm; everything else keeps its position.
1710        let expected = r#"@settings(experimentalFeatures = allow)
1711
1712type Color {
1713  // before red
1714  | Red // after red
1715  /* inside green arm */
1716  | Green
1717
1718  | Blue
1719  // trailing
1720}
1721"#;
1722        assert_recast(input, expected);
1723    }
1724
1725    #[test]
1726    fn recast_enum_with_comment_above_declaration() {
1727        let input = r#"@settings(experimentalFeatures = allow)
1728
1729// palette
1730type Color {
1731  // before red
1732  | Red
1733}
1734"#;
1735        assert_recast(input, input);
1736    }
1737
1738    #[test]
1739    fn recast_enum_with_outer_annotation() {
1740        let input = r#"@settings(experimentalFeatures = allow)
1741
1742@(impl = kcl)
1743type Color {
1744  | Red
1745}
1746"#;
1747        assert_recast(input, input);
1748    }
1749
1750    #[test]
1751    fn recast_enum_export_annotation_and_comments() {
1752        let input = r#"@settings(experimentalFeatures = allow)
1753
1754// palette
1755@(impl = kcl)
1756export type Color {
1757  | Red // warm
1758}
1759"#;
1760        assert_recast(input, input);
1761    }
1762
1763    #[test]
1764    fn recast_enum_in_function_body() {
1765        let input = r#"@settings(experimentalFeatures = allow)
1766
1767fn palette() {
1768  type Color {
1769    | Red
1770    | Green
1771  }
1772  return 0
1773}
1774"#;
1775        assert_recast(input, input);
1776    }
1777
1778    #[test]
1779    fn test_recast_if_else_if_same() {
1780        let input = r#"b = if false {
1781  3
1782} else if true {
1783  4
1784} else {
1785  5
1786}
1787"#;
1788        let program = crate::parsing::top_level_parse(input).unwrap();
1789        let output = program.recast_top(&Default::default(), 0);
1790        assert_eq!(output, input);
1791    }
1792
1793    #[test]
1794    fn test_recast_if_same() {
1795        let input = r#"b = if false {
1796  3
1797} else {
1798  5
1799}
1800"#;
1801        let program = crate::parsing::top_level_parse(input).unwrap();
1802        let output = program.recast_top(&Default::default(), 0);
1803        assert_eq!(output, input);
1804    }
1805
1806    #[test]
1807    fn test_recast_import() {
1808        let input = r#"import a from "a.kcl"
1809import a as aaa from "a.kcl"
1810import a, b from "a.kcl"
1811import a as aaa, b from "a.kcl"
1812import a, b as bbb from "a.kcl"
1813import a as aaa, b as bbb from "a.kcl"
1814import "a_b.kcl"
1815import "a-b.kcl" as b
1816import * from "a.kcl"
1817export import a as aaa from "a.kcl"
1818export import a, b from "a.kcl"
1819export import a as aaa, b from "a.kcl"
1820export import a, b as bbb from "a.kcl"
1821"#;
1822        let program = crate::parsing::top_level_parse(input).unwrap();
1823        let output = program.recast_top(&Default::default(), 0);
1824        assert_eq!(output, input);
1825    }
1826
1827    #[test]
1828    fn test_recast_import_as_same_name() {
1829        let input = r#"import a as a from "a.kcl"
1830"#;
1831        let program = crate::parsing::top_level_parse(input).unwrap();
1832        let output = program.recast_top(&Default::default(), 0);
1833        let expected = r#"import a from "a.kcl"
1834"#;
1835        assert_eq!(output, expected);
1836    }
1837
1838    #[test]
1839    fn test_recast_export_fn() {
1840        let input = r#"export fn a() {
1841  return 0
1842}
1843"#;
1844        let program = crate::parsing::top_level_parse(input).unwrap();
1845        let output = program.recast_top(&Default::default(), 0);
1846        assert_eq!(output, input);
1847    }
1848
1849    #[test]
1850    fn test_recast_sketch_block_with_no_args() {
1851        let input = r#"sketch() {
1852  return 0
1853}
1854"#;
1855        let program = crate::parsing::top_level_parse(input).unwrap();
1856        let output = program.recast_top(&Default::default(), 0);
1857        assert_eq!(output, input);
1858    }
1859
1860    #[test]
1861    fn test_recast_sketch_block_with_labeled_args() {
1862        let input = r#"sketch(on = XY) {
1863  return 0
1864}
1865"#;
1866        let program = crate::parsing::top_level_parse(input).unwrap();
1867        let output = program.recast_top(&Default::default(), 0);
1868        assert_eq!(output, input);
1869    }
1870
1871    #[test]
1872    fn test_recast_sketch_block_with_arg_shorthand() {
1873        let input = r#"on = XY
1874sketch(on) {
1875  return 0
1876}
1877"#;
1878        let program = crate::parsing::top_level_parse(input).unwrap();
1879        let output = program.recast_top(&Default::default(), 0);
1880        assert_eq!(output, input);
1881    }
1882
1883    #[test]
1884    fn test_recast_sketch_block_with_arg_shorthand_and_comment() {
1885        // The comment stays after the shorthand argument: the parser moves the
1886        // shorthand `on` from the unlabeled slot into the argument list, which
1887        // must also shift the comment positions.
1888        let input = r#"on = XY
1889sketch(
1890  on,
1891  // plane
1892) {
1893  return 0
1894}
1895"#;
1896        let program = crate::parsing::top_level_parse(input).unwrap();
1897        let output = program.recast_top(&Default::default(), 0);
1898        assert_eq!(output, input);
1899
1900        // Check idempotency.
1901        let program = crate::parsing::top_level_parse(&output).unwrap();
1902        let output2 = program.recast_top(&Default::default(), 0);
1903        assert_eq!(output2, output);
1904    }
1905
1906    #[test]
1907    fn test_recast_sketch_block_with_statements_in_block() {
1908        let input = r#"sketch() {
1909  // Comments inside block.
1910  x = 5
1911  y = 2
1912}
1913"#;
1914        let program = crate::parsing::top_level_parse(input).unwrap();
1915        let output = program.recast_top(&Default::default(), 0);
1916        assert_eq!(output, input);
1917    }
1918
1919    #[test]
1920    fn test_recast_bug_fn_in_fn() {
1921        let some_program_string = r#"// Start point (top left)
1922zoo_x = -20
1923zoo_y = 7
1924// Scale
1925s = 1 // s = 1 -> height of Z is 13.4mm
1926// Depth
1927d = 1
1928
1929fn rect(x, y, w, h) {
1930  startSketchOn(XY)
1931    |> startProfile(at = [x, y])
1932    |> xLine(length = w)
1933    |> yLine(length = h)
1934    |> xLine(length = -w)
1935    |> close()
1936    |> extrude(d)
1937}
1938
1939fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1940  startSketchOn(XY)
1941    |> startProfile(at = [x1, y1])
1942    |> line(endAbsolute = [x2, y2])
1943    |> line(endAbsolute = [x3, y3])
1944    |> line(endAbsolute = [x4, y4])
1945    |> close()
1946    |> extrude(d)
1947}
1948
1949fn crosshair(x, y) {
1950  startSketchOn(XY)
1951    |> startProfile(at = [x, y])
1952    |> yLine(length = 1)
1953    |> yLine(length = -2)
1954    |> yLine(length = 1)
1955    |> xLine(length = 1)
1956    |> xLine(length = -2)
1957}
1958
1959fn z(z_x, z_y) {
1960  z_end_w = s * 8.4
1961  z_end_h = s * 3
1962  z_corner = s * 2
1963  z_w = z_end_w + 2 * z_corner
1964  z_h = z_w * 1.08130081300813
1965  rect(
1966    z_x,
1967    a = z_y,
1968    b = z_end_w,
1969    c = -z_end_h,
1970  )
1971  rect(
1972    z_x + z_w,
1973    a = z_y,
1974    b = -z_corner,
1975    c = -z_corner,
1976  )
1977  rect(
1978    z_x + z_w,
1979    a = z_y - z_h,
1980    b = -z_end_w,
1981    c = z_end_h,
1982  )
1983  rect(
1984    z_x,
1985    a = z_y - z_h,
1986    b = z_corner,
1987    c = z_corner,
1988  )
1989}
1990
1991fn o(c_x, c_y) {
1992  // Outer and inner radii
1993  o_r = s * 6.95
1994  i_r = 0.5652173913043478 * o_r
1995
1996  // Angle offset for diagonal break
1997  a = 7
1998
1999  // Start point for the top sketch
2000  o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
2001  o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
2002
2003  // Start point for the bottom sketch
2004  o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
2005  o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
2006
2007  // End point for the bottom startSketch
2008  o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
2009  o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
2010
2011  // Where is the center?
2012  // crosshair(c_x, c_y)
2013
2014
2015  startSketchOn(XY)
2016    |> startProfile(at = [o_x1, o_y1])
2017    |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
2018    |> angledLine(angle = 45, length = o_r - i_r)
2019    |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
2020    |> close()
2021    |> extrude(d)
2022
2023  startSketchOn(XY)
2024    |> startProfile(at = [o_x2, o_y2])
2025    |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
2026    |> angledLine(angle = 225, length = o_r - i_r)
2027    |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
2028    |> close()
2029    |> extrude(d)
2030}
2031
2032fn zoo(x0, y0) {
2033  z(x = x0, y = y0)
2034  o(x = x0 + s * 20, y = y0 - (s * 6.7))
2035  o(x = x0 + s * 35, y = y0 - (s * 6.7))
2036}
2037
2038zoo(x = zoo_x, y = zoo_y)
2039"#;
2040        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2041
2042        let recasted = program.recast_top(&Default::default(), 0);
2043        assert_eq!(recasted, some_program_string);
2044    }
2045
2046    #[test]
2047    fn test_nested_fns_indent() {
2048        let some_program_string = "\
2049x = 1
2050fn rect(x, y, w, h) {
2051  y = 2
2052  z = 3
2053  startSketchOn(XY)
2054    |> startProfile(at = [x, y])
2055    |> xLine(length = w)
2056    |> yLine(length = h)
2057    |> xLine(length = -w)
2058    |> close()
2059    |> extrude(d)
2060}
2061";
2062        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2063
2064        let recasted = program.recast_top(&Default::default(), 0);
2065        assert_eq!(recasted, some_program_string);
2066    }
2067
2068    #[test]
2069    fn test_recast_bug_extra_parens() {
2070        let some_program_string = r#"// Ball Bearing
2071// 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. 
2072
2073// Define constants like ball diameter, inside diameter, overhange length, and thickness
2074sphereDia = 0.5
2075insideDia = 1
2076thickness = 0.25
2077overHangLength = .4
2078
2079// Sketch and revolve the inside bearing piece
2080insideRevolve = startSketchOn(XZ)
2081  |> startProfile(at = [insideDia / 2, 0])
2082  |> line(end = [0, thickness + sphereDia / 2])
2083  |> line(end = [overHangLength, 0])
2084  |> line(end = [0, -thickness])
2085  |> line(end = [-overHangLength + thickness, 0])
2086  |> line(end = [0, -sphereDia])
2087  |> line(end = [overHangLength - thickness, 0])
2088  |> line(end = [0, -thickness])
2089  |> line(end = [-overHangLength, 0])
2090  |> close()
2091  |> revolve(axis = Y)
2092
2093// 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)
2094sphere = startSketchOn(XZ)
2095  |> startProfile(at = [
2096       0.05 + insideDia / 2 + thickness,
2097       0 - 0.05
2098     ])
2099  |> line(end = [sphereDia - 0.1, 0])
2100  |> arc(
2101       angle_start = 0,
2102       angle_end = -180,
2103       radius = sphereDia / 2 - 0.05
2104     )
2105  |> close()
2106  |> revolve(axis = X)
2107  |> patternCircular3d(
2108       axis = [0, 0, 1],
2109       center = [0, 0, 0],
2110       repetitions = 10,
2111       arcDegrees = 360,
2112       rotateDuplicates = true
2113     )
2114
2115// Sketch and revolve the outside bearing
2116outsideRevolve = startSketchOn(XZ)
2117  |> startProfile(at = [
2118       insideDia / 2 + thickness + sphereDia,
2119       0
2120       ]
2121     )
2122  |> line(end = [0, sphereDia / 2])
2123  |> line(end = [-overHangLength + thickness, 0])
2124  |> line(end = [0, thickness])
2125  |> line(end = [overHangLength, 0])
2126  |> line(end = [0, -2 * thickness - sphereDia])
2127  |> line(end = [-overHangLength, 0])
2128  |> line(end = [0, thickness])
2129  |> line(end = [overHangLength - thickness, 0])
2130  |> close()
2131  |> revolve(axis = Y)"#;
2132        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2133
2134        let recasted = program.recast_top(&Default::default(), 0);
2135        assert_eq!(
2136            recasted,
2137            r#"// Ball Bearing
2138// 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.
2139
2140// Define constants like ball diameter, inside diameter, overhange length, and thickness
2141sphereDia = 0.5
2142insideDia = 1
2143thickness = 0.25
2144overHangLength = .4
2145
2146// Sketch and revolve the inside bearing piece
2147insideRevolve = startSketchOn(XZ)
2148  |> startProfile(at = [insideDia / 2, 0])
2149  |> line(end = [0, thickness + sphereDia / 2])
2150  |> line(end = [overHangLength, 0])
2151  |> line(end = [0, -thickness])
2152  |> line(end = [-overHangLength + thickness, 0])
2153  |> line(end = [0, -sphereDia])
2154  |> line(end = [overHangLength - thickness, 0])
2155  |> line(end = [0, -thickness])
2156  |> line(end = [-overHangLength, 0])
2157  |> close()
2158  |> revolve(axis = Y)
2159
2160// 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)
2161sphere = startSketchOn(XZ)
2162  |> startProfile(at = [
2163       0.05 + insideDia / 2 + thickness,
2164       0 - 0.05
2165     ])
2166  |> line(end = [sphereDia - 0.1, 0])
2167  |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
2168  |> close()
2169  |> revolve(axis = X)
2170  |> patternCircular3d(
2171       axis = [0, 0, 1],
2172       center = [0, 0, 0],
2173       repetitions = 10,
2174       arcDegrees = 360,
2175       rotateDuplicates = true,
2176     )
2177
2178// Sketch and revolve the outside bearing
2179outsideRevolve = startSketchOn(XZ)
2180  |> startProfile(at = [
2181       insideDia / 2 + thickness + sphereDia,
2182       0
2183     ])
2184  |> line(end = [0, sphereDia / 2])
2185  |> line(end = [-overHangLength + thickness, 0])
2186  |> line(end = [0, thickness])
2187  |> line(end = [overHangLength, 0])
2188  |> line(end = [0, -2 * thickness - sphereDia])
2189  |> line(end = [-overHangLength, 0])
2190  |> line(end = [0, thickness])
2191  |> line(end = [overHangLength - thickness, 0])
2192  |> close()
2193  |> revolve(axis = Y)
2194"#
2195        );
2196    }
2197
2198    #[test]
2199    fn test_recast_fn_in_object() {
2200        let some_program_string = r#"bing = { yo = 55 }
2201myNestedVar = [{ prop = callExp(bing.yo) }]
2202"#;
2203        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2204
2205        let recasted = program.recast_top(&Default::default(), 0);
2206        assert_eq!(recasted, some_program_string);
2207    }
2208
2209    #[test]
2210    fn test_recast_fn_in_array() {
2211        let some_program_string = r#"bing = { yo = 55 }
2212myNestedVar = [callExp(bing.yo)]
2213"#;
2214        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2215
2216        let recasted = program.recast_top(&Default::default(), 0);
2217        assert_eq!(recasted, some_program_string);
2218    }
2219
2220    #[test]
2221    fn test_recast_ranges() {
2222        let some_program_string = r#"foo = [0..10]
2223ten = 10
2224bar = [0 + 1 .. ten]
2225"#;
2226        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2227
2228        let recasted = program.recast_top(&Default::default(), 0);
2229        assert_eq!(recasted, some_program_string);
2230    }
2231
2232    #[test]
2233    fn test_recast_space_in_fn_call() {
2234        let some_program_string = r#"fn thing (x) {
2235    return x + 1
2236}
2237
2238thing ( 1 )
2239"#;
2240        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2241
2242        let recasted = program.recast_top(&Default::default(), 0);
2243        assert_eq!(
2244            recasted,
2245            r#"fn thing(x) {
2246  return x + 1
2247}
2248
2249thing(1)
2250"#
2251        );
2252    }
2253
2254    #[test]
2255    fn test_recast_typed_fn() {
2256        let some_program_string = r#"fn thing(x: string, y: [bool]): number {
2257  return x + 1
2258}
2259"#;
2260        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2261
2262        let recasted = program.recast_top(&Default::default(), 0);
2263        assert_eq!(recasted, some_program_string);
2264    }
2265
2266    #[test]
2267    fn test_recast_typed_consts() {
2268        let some_program_string = r#"a = 42: number
2269export b = 3.2: number(ft)
2270c = "dsfds": A | B | C
2271d = [1]: [number]
2272e = foo: [number; 3]
2273f = [1, 2, 3]: [number; 1+]
2274f = [1, 2, 3]: [number; 3+]
2275"#;
2276        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2277
2278        let recasted = program.recast_top(&Default::default(), 0);
2279        assert_eq!(recasted, some_program_string);
2280    }
2281
2282    #[test]
2283    fn test_recast_object_fn_in_array_weird_bracket() {
2284        let some_program_string = r#"bing = { yo = 55 }
2285myNestedVar = [
2286  {
2287  prop:   line(a = [bing.yo, 21], b = sketch001)
2288}
2289]
2290"#;
2291        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2292
2293        let recasted = program.recast_top(&Default::default(), 0);
2294        let expected = r#"bing = { yo = 55 }
2295myNestedVar = [
2296  {
2297    prop = line(a = [bing.yo, 21], b = sketch001)
2298  }
2299]
2300"#;
2301        assert_eq!(recasted, expected,);
2302    }
2303
2304    #[test]
2305    fn test_recast_empty_file() {
2306        let some_program_string = r#""#;
2307        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2308
2309        let recasted = program.recast_top(&Default::default(), 0);
2310        // Its VERY important this comes back with zero new lines.
2311        assert_eq!(recasted, r#""#);
2312    }
2313
2314    #[test]
2315    fn test_recast_empty_file_new_line() {
2316        let some_program_string = r#"
2317"#;
2318        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2319
2320        let recasted = program.recast_top(&Default::default(), 0);
2321        // Its VERY important this comes back with zero new lines.
2322        assert_eq!(recasted, r#""#);
2323    }
2324
2325    #[test]
2326    fn test_recast_shebang() {
2327        let some_program_string = r#"#!/usr/local/env zoo kcl
2328part001 = startSketchOn(XY)
2329  |> startProfile(at = [-10, -10])
2330  |> line(end = [20, 0])
2331  |> line(end = [0, 20])
2332  |> line(end = [-20, 0])
2333  |> close()
2334"#;
2335
2336        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2337
2338        let recasted = program.recast_top(&Default::default(), 0);
2339        assert_eq!(
2340            recasted,
2341            r#"#!/usr/local/env zoo kcl
2342
2343part001 = startSketchOn(XY)
2344  |> startProfile(at = [-10, -10])
2345  |> line(end = [20, 0])
2346  |> line(end = [0, 20])
2347  |> line(end = [-20, 0])
2348  |> close()
2349"#
2350        );
2351    }
2352
2353    #[test]
2354    fn test_recast_shebang_new_lines() {
2355        let some_program_string = r#"#!/usr/local/env zoo kcl
2356        
2357
2358
2359part001 = startSketchOn(XY)
2360  |> startProfile(at = [-10, -10])
2361  |> line(end = [20, 0])
2362  |> line(end = [0, 20])
2363  |> line(end = [-20, 0])
2364  |> close()
2365"#;
2366
2367        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2368
2369        let recasted = program.recast_top(&Default::default(), 0);
2370        assert_eq!(
2371            recasted,
2372            r#"#!/usr/local/env zoo kcl
2373
2374part001 = startSketchOn(XY)
2375  |> startProfile(at = [-10, -10])
2376  |> line(end = [20, 0])
2377  |> line(end = [0, 20])
2378  |> line(end = [-20, 0])
2379  |> close()
2380"#
2381        );
2382    }
2383
2384    #[test]
2385    fn test_recast_shebang_with_comments() {
2386        let some_program_string = r#"#!/usr/local/env zoo kcl
2387        
2388// Yo yo my comments.
2389part001 = startSketchOn(XY)
2390  |> startProfile(at = [-10, -10])
2391  |> line(end = [20, 0])
2392  |> line(end = [0, 20])
2393  |> line(end = [-20, 0])
2394  |> close()
2395"#;
2396
2397        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2398
2399        let recasted = program.recast_top(&Default::default(), 0);
2400        assert_eq!(
2401            recasted,
2402            r#"#!/usr/local/env zoo kcl
2403
2404// Yo yo my comments.
2405part001 = startSketchOn(XY)
2406  |> startProfile(at = [-10, -10])
2407  |> line(end = [20, 0])
2408  |> line(end = [0, 20])
2409  |> line(end = [-20, 0])
2410  |> close()
2411"#
2412        );
2413    }
2414
2415    #[test]
2416    fn test_recast_empty_function_body_with_comments() {
2417        let input = r#"fn myFunc() {
2418  // Yo yo my comments.
2419}
2420"#;
2421
2422        let program = crate::parsing::top_level_parse(input).unwrap();
2423        let output = program.recast_top(&Default::default(), 0);
2424        assert_eq!(output, input);
2425    }
2426
2427    #[test]
2428    fn test_recast_large_file() {
2429        let some_program_string = r#"@settings(units=mm)
2430// define nts
2431radius = 6.0
2432width = 144.0
2433length = 83.0
2434depth = 45.0
2435thk = 5
2436hole_diam = 5
2437// define a rectangular shape func
2438fn rectShape(pos, w, l) {
2439  rr = startSketchOn(XY)
2440    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2441    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2442    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2443    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2444    |> close($edge4)
2445  return rr
2446}
2447// build the body of the focusrite scarlett solo gen 4
2448// only used for visualization
2449scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2450  |> extrude(depth)
2451  |> fillet(
2452       radius = radius,
2453       tags = [
2454  edge2,
2455  edge4,
2456  getOppositeEdge(edge2),
2457  getOppositeEdge(edge4)
2458]
2459   )
2460  // build the bracket sketch around the body
2461fn bracketSketch(w, d, t) {
2462  s = startSketchOn({
2463         plane = {
2464  origin = { x = 0, y = length / 2 + thk, z = 0 },
2465  x_axis = { x = 1, y = 0, z = 0 },
2466  y_axis = { x = 0, y = 0, z = 1 },
2467  z_axis = { x = 0, y = 1, z = 0 }
2468}
2469       })
2470    |> startProfile(at = [-w / 2 - t, d + t])
2471    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2472    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2473    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2474    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2475    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2476    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2477    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2478    |> close($edge8)
2479  return s
2480}
2481// build the body of the bracket
2482bracket_body = bracketSketch(w = width, d = depth, t = thk)
2483  |> extrude(length + 10)
2484  |> fillet(
2485       radius = radius,
2486       tags = [
2487  getNextAdjacentEdge(edge7),
2488  getNextAdjacentEdge(edge2),
2489  getNextAdjacentEdge(edge3),
2490  getNextAdjacentEdge(edge6)
2491]
2492     )
2493  // build the tabs of the mounting bracket (right side)
2494tabs_r = startSketchOn({
2495       plane = {
2496  origin = { x = 0, y = 0, z = depth + thk },
2497  x_axis = { x = 1, y = 0, z = 0 },
2498  y_axis = { x = 0, y = 1, z = 0 },
2499  z_axis = { x = 0, y = 0, z = 1 }
2500}
2501     })
2502  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2503  |> line(end = [10, -5])
2504  |> line(end = [0, -10])
2505  |> line(end = [-10, -5])
2506  |> close()
2507  |> subtract2d(tool = circle(
2508       center = [
2509         width / 2 + thk + hole_diam,
2510         length / 2 - hole_diam
2511       ],
2512       radius = hole_diam / 2
2513     ))
2514  |> extrude(-thk)
2515  |> patternLinear3d(
2516       axis = [0, -1, 0],
2517       repetitions = 1,
2518       distance = length - 10
2519     )
2520  // build the tabs of the mounting bracket (left side)
2521tabs_l = startSketchOn({
2522       plane = {
2523  origin = { x = 0, y = 0, z = depth + thk },
2524  x_axis = { x = 1, y = 0, z = 0 },
2525  y_axis = { x = 0, y = 1, z = 0 },
2526  z_axis = { x = 0, y = 0, z = 1 }
2527}
2528     })
2529  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2530  |> line(end = [-10, -5])
2531  |> line(end = [0, -10])
2532  |> line(end = [10, -5])
2533  |> close()
2534  |> subtract2d(tool = circle(
2535       center = [
2536         -width / 2 - thk - hole_diam,
2537         length / 2 - hole_diam
2538       ],
2539       radius = hole_diam / 2
2540     ))
2541  |> extrude(-thk)
2542  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2543"#;
2544        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2545
2546        let recasted = program.recast_top(&Default::default(), 0);
2547        // Its VERY important this comes back with zero new lines.
2548        assert_eq!(
2549            recasted,
2550            r#"@settings(units = mm)
2551
2552// define nts
2553radius = 6.0
2554width = 144.0
2555length = 83.0
2556depth = 45.0
2557thk = 5
2558hole_diam = 5
2559// define a rectangular shape func
2560fn rectShape(pos, w, l) {
2561  rr = startSketchOn(XY)
2562    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2563    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2564    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2565    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2566    |> close($edge4)
2567  return rr
2568}
2569// build the body of the focusrite scarlett solo gen 4
2570// only used for visualization
2571scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2572  |> extrude(depth)
2573  |> fillet(
2574       radius = radius,
2575       tags = [
2576         edge2,
2577         edge4,
2578         getOppositeEdge(edge2),
2579         getOppositeEdge(edge4)
2580       ],
2581     )
2582// build the bracket sketch around the body
2583fn bracketSketch(w, d, t) {
2584  s = startSketchOn({
2585    plane = {
2586      origin = { x = 0, y = length / 2 + thk, z = 0 },
2587      x_axis = { x = 1, y = 0, z = 0 },
2588      y_axis = { x = 0, y = 0, z = 1 },
2589      z_axis = { x = 0, y = 1, z = 0 }
2590    }
2591  })
2592    |> startProfile(at = [-w / 2 - t, d + t])
2593    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2594    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2595    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2596    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2597    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2598    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2599    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2600    |> close($edge8)
2601  return s
2602}
2603// build the body of the bracket
2604bracket_body = bracketSketch(w = width, d = depth, t = thk)
2605  |> extrude(length + 10)
2606  |> fillet(
2607       radius = radius,
2608       tags = [
2609         getNextAdjacentEdge(edge7),
2610         getNextAdjacentEdge(edge2),
2611         getNextAdjacentEdge(edge3),
2612         getNextAdjacentEdge(edge6)
2613       ],
2614     )
2615// build the tabs of the mounting bracket (right side)
2616tabs_r = startSketchOn({
2617  plane = {
2618    origin = { x = 0, y = 0, z = depth + thk },
2619    x_axis = { x = 1, y = 0, z = 0 },
2620    y_axis = { x = 0, y = 1, z = 0 },
2621    z_axis = { x = 0, y = 0, z = 1 }
2622  }
2623})
2624  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2625  |> line(end = [10, -5])
2626  |> line(end = [0, -10])
2627  |> line(end = [-10, -5])
2628  |> close()
2629  |> subtract2d(tool = circle(
2630       center = [
2631         width / 2 + thk + hole_diam,
2632         length / 2 - hole_diam
2633       ],
2634       radius = hole_diam / 2,
2635     ))
2636  |> extrude(-thk)
2637  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2638// build the tabs of the mounting bracket (left side)
2639tabs_l = startSketchOn({
2640  plane = {
2641    origin = { x = 0, y = 0, z = depth + thk },
2642    x_axis = { x = 1, y = 0, z = 0 },
2643    y_axis = { x = 0, y = 1, z = 0 },
2644    z_axis = { x = 0, y = 0, z = 1 }
2645  }
2646})
2647  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2648  |> line(end = [-10, -5])
2649  |> line(end = [0, -10])
2650  |> line(end = [10, -5])
2651  |> close()
2652  |> subtract2d(tool = circle(
2653       center = [
2654         -width / 2 - thk - hole_diam,
2655         length / 2 - hole_diam
2656       ],
2657       radius = hole_diam / 2,
2658     ))
2659  |> extrude(-thk)
2660  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2661"#
2662        );
2663    }
2664
2665    #[test]
2666    fn test_recast_nested_var_declaration_in_fn_body() {
2667        let some_program_string = r#"fn cube(pos, scale) {
2668   sg = startSketchOn(XY)
2669  |> startProfile(at = pos)
2670  |> line(end = [0, scale])
2671  |> line(end = [scale, 0])
2672  |> line(end = [0, -scale])
2673  |> close()
2674  |> extrude(scale)
2675}"#;
2676        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2677
2678        let recasted = program.recast_top(&Default::default(), 0);
2679        assert_eq!(
2680            recasted,
2681            r#"fn cube(pos, scale) {
2682  sg = startSketchOn(XY)
2683    |> startProfile(at = pos)
2684    |> line(end = [0, scale])
2685    |> line(end = [scale, 0])
2686    |> line(end = [0, -scale])
2687    |> close()
2688    |> extrude(scale)
2689}
2690"#
2691        );
2692    }
2693
2694    #[test]
2695    fn test_as() {
2696        let some_program_string = r#"fn cube(pos, scale) {
2697  x = dfsfs + dfsfsd as y
2698
2699  sg = startSketchOn(XY)
2700    |> startProfile(at = pos) as foo
2701    |> line([0, scale])
2702    |> line([scale, 0]) as bar
2703    |> line([0 as baz, -scale] as qux)
2704    |> close()
2705    |> extrude(length = scale)
2706}
2707
2708cube(pos = 0, scale = 0) as cub
2709"#;
2710        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2711
2712        let recasted = program.recast_top(&Default::default(), 0);
2713        assert_eq!(recasted, some_program_string,);
2714    }
2715
2716    #[test]
2717    fn test_recast_with_bad_indentation() {
2718        let some_program_string = r#"part001 = startSketchOn(XY)
2719  |> startProfile(at = [0.0, 5.0])
2720              |> line(end = [0.4900857016, -0.0240763666])
2721    |> line(end = [0.6804562304, 0.9087880491])"#;
2722        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2723
2724        let recasted = program.recast_top(&Default::default(), 0);
2725        assert_eq!(
2726            recasted,
2727            r#"part001 = startSketchOn(XY)
2728  |> startProfile(at = [0.0, 5.0])
2729  |> line(end = [0.4900857016, -0.0240763666])
2730  |> line(end = [0.6804562304, 0.9087880491])
2731"#
2732        );
2733    }
2734
2735    #[test]
2736    fn test_recast_with_bad_indentation_and_inline_comment() {
2737        let some_program_string = r#"part001 = startSketchOn(XY)
2738  |> startProfile(at = [0.0, 5.0])
2739              |> line(end = [0.4900857016, -0.0240763666]) // hello world
2740    |> line(end = [0.6804562304, 0.9087880491])"#;
2741        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2742
2743        let recasted = program.recast_top(&Default::default(), 0);
2744        assert_eq!(
2745            recasted,
2746            r#"part001 = startSketchOn(XY)
2747  |> startProfile(at = [0.0, 5.0])
2748  |> line(end = [0.4900857016, -0.0240763666]) // hello world
2749  |> line(end = [0.6804562304, 0.9087880491])
2750"#
2751        );
2752    }
2753    #[test]
2754    fn test_recast_with_bad_indentation_and_line_comment() {
2755        let some_program_string = r#"part001 = startSketchOn(XY)
2756  |> startProfile(at = [0.0, 5.0])
2757              |> line(end = [0.4900857016, -0.0240763666])
2758        // hello world
2759    |> line(end = [0.6804562304, 0.9087880491])"#;
2760        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2761
2762        let recasted = program.recast_top(&Default::default(), 0);
2763        assert_eq!(
2764            recasted,
2765            r#"part001 = startSketchOn(XY)
2766  |> startProfile(at = [0.0, 5.0])
2767  |> line(end = [0.4900857016, -0.0240763666])
2768  // hello world
2769  |> line(end = [0.6804562304, 0.9087880491])
2770"#
2771        );
2772    }
2773
2774    #[test]
2775    fn test_recast_comment_in_a_fn_block() {
2776        let some_program_string = r#"fn myFn() {
2777  // this is a comment
2778  yo = { a = { b = { c = '123' } } } /* block
2779  comment */
2780
2781  key = 'c'
2782  // this is also a comment
2783    return things
2784}"#;
2785        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2786
2787        let recasted = program.recast_top(&Default::default(), 0);
2788        assert_eq!(
2789            recasted,
2790            r#"fn myFn() {
2791  // this is a comment
2792  yo = { a = { b = { c = '123' } } } /* block
2793  comment */
2794
2795  key = 'c'
2796  // this is also a comment
2797  return things
2798}
2799"#
2800        );
2801    }
2802
2803    #[test]
2804    fn test_recast_comment_under_variable() {
2805        let some_program_string = r#"key = 'c'
2806// this is also a comment
2807thing = 'foo'
2808"#;
2809        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2810
2811        let recasted = program.recast_top(&Default::default(), 0);
2812        assert_eq!(
2813            recasted,
2814            r#"key = 'c'
2815// this is also a comment
2816thing = 'foo'
2817"#
2818        );
2819    }
2820
2821    #[test]
2822    fn test_recast_multiline_comment_start_file() {
2823        let some_program_string = r#"// hello world
2824// I am a comment
2825key = 'c'
2826// this is also a comment
2827// hello
2828thing = 'foo'
2829"#;
2830        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2831
2832        let recasted = program.recast_top(&Default::default(), 0);
2833        assert_eq!(
2834            recasted,
2835            r#"// hello world
2836// I am a comment
2837key = 'c'
2838// this is also a comment
2839// hello
2840thing = 'foo'
2841"#
2842        );
2843    }
2844
2845    #[test]
2846    fn test_recast_empty_comment() {
2847        let some_program_string = r#"// hello world
2848//
2849// I am a comment
2850key = 'c'
2851
2852//
2853// I am a comment
2854thing = 'c'
2855
2856foo = 'bar' //
2857"#;
2858        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2859
2860        let recasted = program.recast_top(&Default::default(), 0);
2861        assert_eq!(
2862            recasted,
2863            r#"// hello world
2864//
2865// I am a comment
2866key = 'c'
2867
2868//
2869// I am a comment
2870thing = 'c'
2871
2872foo = 'bar' //
2873"#
2874        );
2875    }
2876
2877    #[test]
2878    fn test_recast_multiline_comment_under_variable() {
2879        let some_program_string = r#"key = 'c'
2880// this is also a comment
2881// hello
2882thing = 'foo'
2883"#;
2884        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2885
2886        let recasted = program.recast_top(&Default::default(), 0);
2887        assert_eq!(
2888            recasted,
2889            r#"key = 'c'
2890// this is also a comment
2891// hello
2892thing = 'foo'
2893"#
2894        );
2895    }
2896
2897    #[test]
2898    fn test_recast_only_line_comments() {
2899        let code = r#"// comment at start
2900"#;
2901        let program = crate::parsing::top_level_parse(code).unwrap();
2902
2903        assert_eq!(program.recast_top(&Default::default(), 0), code);
2904    }
2905
2906    #[test]
2907    fn test_recast_comment_at_start() {
2908        let test_program = r#"
2909/* comment at start */
2910
2911mySk1 = startSketchOn(XY)
2912  |> startProfile(at = [0, 0])"#;
2913        let program = crate::parsing::top_level_parse(test_program).unwrap();
2914
2915        let recasted = program.recast_top(&Default::default(), 0);
2916        assert_eq!(
2917            recasted,
2918            r#"/* comment at start */
2919
2920mySk1 = startSketchOn(XY)
2921  |> startProfile(at = [0, 0])
2922"#
2923        );
2924    }
2925
2926    #[test]
2927    fn test_recast_lots_of_comments() {
2928        let some_program_string = r#"// comment at start
2929mySk1 = startSketchOn(XY)
2930  |> startProfile(at = [0, 0])
2931  |> line(endAbsolute = [1, 1])
2932  // comment here
2933  |> line(endAbsolute = [0, 1], tag = $myTag)
2934  |> line(endAbsolute = [1, 1])
2935  /* and
2936  here
2937  */
2938  // a comment between pipe expression statements
2939  |> rx(90)
2940  // and another with just white space between others below
2941  |> ry(45)
2942  |> rx(45)
2943// one more for good measure"#;
2944        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2945
2946        let recasted = program.recast_top(&Default::default(), 0);
2947        assert_eq!(
2948            recasted,
2949            r#"// comment at start
2950mySk1 = startSketchOn(XY)
2951  |> startProfile(at = [0, 0])
2952  |> line(endAbsolute = [1, 1])
2953  // comment here
2954  |> line(endAbsolute = [0, 1], tag = $myTag)
2955  |> line(endAbsolute = [1, 1])
2956  /* and
2957  here */
2958  // a comment between pipe expression statements
2959  |> rx(90)
2960  // and another with just white space between others below
2961  |> ry(45)
2962  |> rx(45)
2963// one more for good measure
2964"#
2965        );
2966    }
2967
2968    #[test]
2969    fn test_recast_multiline_object() {
2970        let some_program_string = r#"x = {
2971  a = 1000000000,
2972  b = 2000000000,
2973  c = 3000000000,
2974  d = 4000000000,
2975  e = 5000000000
2976}"#;
2977        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2978
2979        let recasted = program.recast_top(&Default::default(), 0);
2980        assert_eq!(recasted.trim(), some_program_string);
2981    }
2982
2983    #[test]
2984    fn test_recast_first_level_object() {
2985        let some_program_string = r#"three = 3
2986
2987yo = {
2988  aStr = 'str',
2989  anum = 2,
2990  identifier = three,
2991  binExp = 4 + 5
2992}
2993yo = [
2994  1,
2995  "  2,",
2996  "three",
2997  4 + 5,
2998  "  hey oooooo really long long long"
2999]
3000"#;
3001        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3002
3003        let recasted = program.recast_top(&Default::default(), 0);
3004        assert_eq!(recasted, some_program_string);
3005    }
3006
3007    #[test]
3008    fn test_recast_new_line_before_comment() {
3009        let some_program_string = r#"
3010// this is a comment
3011yo = { a = { b = { c = '123' } } }
3012
3013key = 'c'
3014things = "things"
3015
3016// this is also a comment"#;
3017        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3018
3019        let recasted = program.recast_top(&Default::default(), 0);
3020        let expected = some_program_string.trim();
3021        // Currently new parser removes an empty line
3022        let actual = recasted.trim();
3023        assert_eq!(actual, expected);
3024    }
3025
3026    #[test]
3027    fn test_recast_comment_tokens_inside_strings() {
3028        let some_program_string = r#"b = {
3029  end = 141,
3030  start = 125,
3031  type_ = "NonCodeNode",
3032  value = "
3033 // a comment
3034   "
3035}"#;
3036        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3037
3038        let recasted = program.recast_top(&Default::default(), 0);
3039        assert_eq!(recasted.trim(), some_program_string.trim());
3040    }
3041
3042    #[test]
3043    fn test_recast_array_new_line_in_pipe() {
3044        let some_program_string = r#"myVar = 3
3045myVar2 = 5
3046myVar3 = 6
3047myAng = 40
3048myAng2 = 134
3049part001 = startSketchOn(XY)
3050  |> startProfile(at = [0, 0])
3051  |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3052  |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3053  |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
3054        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3055
3056        let recasted = program.recast_top(&Default::default(), 0);
3057        assert_eq!(recasted.trim(), some_program_string);
3058    }
3059
3060    #[test]
3061    fn test_recast_array_new_line_in_pipe_custom() {
3062        let some_program_string = r#"myVar = 3
3063myVar2 = 5
3064myVar3 = 6
3065myAng = 40
3066myAng2 = 134
3067part001 = startSketchOn(XY)
3068   |> startProfile(at = [0, 0])
3069   |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3070   |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3071   |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
3072"#;
3073        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3074
3075        let recasted = program.recast_top(
3076            &FormatOptions {
3077                tab_size: 3,
3078                use_tabs: false,
3079                insert_final_newline: true,
3080            },
3081            0,
3082        );
3083        assert_eq!(recasted, some_program_string);
3084    }
3085
3086    #[test]
3087    fn test_recast_after_rename_std() {
3088        let some_program_string = r#"part001 = startSketchOn(XY)
3089  |> startProfile(at = [0.0000000000, 5.0000000000])
3090    |> line(end = [0.4900857016, -0.0240763666])
3091
3092part002 = "part002"
3093things = [part001, 0.0]
3094blah = 1
3095foo = false
3096baz = {a: 1, part001: "thing"}
3097
3098fn ghi(part001) {
3099  return part001
3100}
3101"#;
3102        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3103        program.rename_symbol("mySuperCoolPart", 6);
3104
3105        let recasted = program.recast_top(&Default::default(), 0);
3106        assert_eq!(
3107            recasted,
3108            r#"mySuperCoolPart = startSketchOn(XY)
3109  |> startProfile(at = [0.0, 5.0])
3110  |> line(end = [0.4900857016, -0.0240763666])
3111
3112part002 = "part002"
3113things = [mySuperCoolPart, 0.0]
3114blah = 1
3115foo = false
3116baz = { a = 1, part001 = "thing" }
3117
3118fn ghi(part001) {
3119  return part001
3120}
3121"#
3122        );
3123    }
3124
3125    #[test]
3126    fn test_recast_after_rename_fn_args() {
3127        let some_program_string = r#"fn ghi(x, y, z) {
3128  return x
3129}"#;
3130        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3131        program.rename_symbol("newName", 7);
3132
3133        let recasted = program.recast_top(&Default::default(), 0);
3134        assert_eq!(
3135            recasted,
3136            r#"fn ghi(newName, y, z) {
3137  return newName
3138}
3139"#
3140        );
3141    }
3142
3143    #[test]
3144    fn test_recast_trailing_comma() {
3145        let some_program_string = r#"startSketchOn(XY)
3146  |> startProfile(at = [0, 0])
3147  |> arc({
3148    radius = 1,
3149    angle_start = 0,
3150    angle_end = 180,
3151  })"#;
3152        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3153
3154        let recasted = program.recast_top(&Default::default(), 0);
3155        assert_eq!(
3156            recasted,
3157            r#"startSketchOn(XY)
3158  |> startProfile(at = [0, 0])
3159  |> arc({
3160       radius = 1,
3161       angle_start = 0,
3162       angle_end = 180
3163     })
3164"#
3165        );
3166    }
3167
3168    #[test]
3169    fn test_recast_array_no_trailing_comma_with_comments() {
3170        let some_program_string = r#"[
3171  1, // one
3172  2, // two
3173  3  // three
3174]"#;
3175        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3176
3177        let recasted = program.recast_top(&Default::default(), 0);
3178        assert_eq!(
3179            recasted,
3180            r#"[
3181  1,
3182  // one
3183  2,
3184  // two
3185  3,
3186  // three
3187]
3188"#
3189        );
3190    }
3191
3192    #[test]
3193    fn test_recast_object_no_trailing_comma_with_comments() {
3194        let some_program_string = r#"{
3195  x=1, // one
3196  y=2, // two
3197  z=3  // three
3198}"#;
3199        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3200
3201        let recasted = program.recast_top(&Default::default(), 0);
3202        // TODO: We should probably not add an extra new line after the last
3203        // comment.
3204        assert_eq!(
3205            recasted,
3206            r#"{
3207  x = 1,
3208  // one
3209  y = 2,
3210  // two
3211  z = 3,
3212  // three
3213
3214}
3215"#
3216        );
3217    }
3218
3219    #[test]
3220    fn test_recast_comment_between_call_args() {
3221        let some_program_string = r#"rounded = fillet(
3222  body,
3223  radius = 1mm,
3224  // Keep this comment
3225  tags = [tag1, tag2],
3226)
3227"#;
3228        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3229
3230        let recasted = program.recast_top(&Default::default(), 0);
3231        assert_eq!(recasted, some_program_string);
3232
3233        // Check idempotency.
3234        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3235        let recasted2 = program.recast_top(&Default::default(), 0);
3236        assert_eq!(recasted2, recasted);
3237    }
3238
3239    #[test]
3240    fn test_recast_line_comments_in_call_args_force_multiline() {
3241        let some_program_string = r#"edged = chamfer(
3242  body,
3243  // leading
3244  length = 1mm,
3245  tags = [tag1],
3246  // trailing
3247)
3248"#;
3249        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3250
3251        let recasted = program.recast_top(&Default::default(), 0);
3252        assert_eq!(recasted, some_program_string);
3253
3254        // Check idempotency.
3255        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3256        let recasted2 = program.recast_top(&Default::default(), 0);
3257        assert_eq!(recasted2, recasted);
3258    }
3259
3260    #[test]
3261    fn test_recast_block_comment_in_call_args_stays_inline() {
3262        // A block comment shouldn't force the call onto multiple lines.
3263        let some_program_string = r#"rounded = fillet(body, radius = 1mm, /* mid */ tags = [tag1])
3264"#;
3265        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3266
3267        let recasted = program.recast_top(&Default::default(), 0);
3268        assert_eq!(recasted, some_program_string);
3269
3270        // Check idempotency.
3271        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3272        let recasted2 = program.recast_top(&Default::default(), 0);
3273        assert_eq!(recasted2, recasted);
3274    }
3275
3276    #[test]
3277    fn test_recast_block_comment_in_multiline_call_args_stays_inline() {
3278        // In a call that's multi-line anyway, a block comment stays glued to
3279        // the argument it precedes.
3280        let some_program_string = r#"rounded = fillet(
3281  body,
3282  radius = 1mm,
3283  /* mid */ tags = [tag1],
3284  tag = $x,
3285)
3286"#;
3287        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3288
3289        let recasted = program.recast_top(&Default::default(), 0);
3290        assert_eq!(recasted, some_program_string);
3291
3292        // Check idempotency.
3293        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3294        let recasted2 = program.recast_top(&Default::default(), 0);
3295        assert_eq!(recasted2, recasted);
3296    }
3297
3298    #[test]
3299    fn test_recast_comment_in_call_args_in_pipe() {
3300        let some_program_string = r#"part = startSketchOn(XY)
3301  |> startProfile(at = [0, 0])
3302  |> fillet(
3303       radius = 1,
3304       // why
3305       tags = [a],
3306     )
3307"#;
3308        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3309
3310        let recasted = program.recast_top(&Default::default(), 0);
3311        assert_eq!(recasted, some_program_string);
3312    }
3313
3314    #[test]
3315    fn test_recast_negative_var() {
3316        let some_program_string = r#"w = 20
3317l = 8
3318h = 10
3319
3320firstExtrude = startSketchOn(XY)
3321  |> startProfile(at = [0,0])
3322  |> line(end = [0, l])
3323  |> line(end = [w, 0])
3324  |> line(end = [0, -l])
3325  |> close()
3326  |> extrude(h)
3327"#;
3328        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3329
3330        let recasted = program.recast_top(&Default::default(), 0);
3331        assert_eq!(
3332            recasted,
3333            r#"w = 20
3334l = 8
3335h = 10
3336
3337firstExtrude = startSketchOn(XY)
3338  |> startProfile(at = [0, 0])
3339  |> line(end = [0, l])
3340  |> line(end = [w, 0])
3341  |> line(end = [0, -l])
3342  |> close()
3343  |> extrude(h)
3344"#
3345        );
3346    }
3347
3348    #[test]
3349    fn test_recast_multiline_comment() {
3350        let some_program_string = r#"w = 20
3351l = 8
3352h = 10
3353
3354// This is my comment
3355// It has multiple lines
3356// And it's really long
3357firstExtrude = startSketchOn(XY)
3358  |> startProfile(at = [0,0])
3359  |> line(end = [0, l])
3360  |> line(end = [w, 0])
3361  |> line(end = [0, -l])
3362  |> close()
3363  |> extrude(h)
3364"#;
3365        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3366
3367        let recasted = program.recast_top(&Default::default(), 0);
3368        assert_eq!(
3369            recasted,
3370            r#"w = 20
3371l = 8
3372h = 10
3373
3374// This is my comment
3375// It has multiple lines
3376// And it's really long
3377firstExtrude = startSketchOn(XY)
3378  |> startProfile(at = [0, 0])
3379  |> line(end = [0, l])
3380  |> line(end = [w, 0])
3381  |> line(end = [0, -l])
3382  |> close()
3383  |> extrude(h)
3384"#
3385        );
3386    }
3387
3388    #[test]
3389    fn test_recast_math_start_negative() {
3390        let some_program_string = r#"myVar = -5 + 6"#;
3391        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3392
3393        let recasted = program.recast_top(&Default::default(), 0);
3394        assert_eq!(recasted.trim(), some_program_string);
3395    }
3396
3397    #[test]
3398    fn test_recast_math_negate_parens() {
3399        let some_program_string = r#"wallMountL = 3.82
3400thickness = 0.5
3401
3402startSketchOn(XY)
3403  |> startProfile(at = [0, 0])
3404  |> line(end = [0, -(wallMountL - thickness)])
3405  |> line(end = [0, -(5 - thickness)])
3406  |> line(end = [0, -(5 - 1)])
3407  |> line(end = [0, -(-5 - 1)])"#;
3408        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3409
3410        let recasted = program.recast_top(&Default::default(), 0);
3411        assert_eq!(recasted.trim(), some_program_string);
3412    }
3413
3414    #[test]
3415    fn test_recast_math_nested_parens() {
3416        let some_program_string = r#"distance = 5
3417p = 3: Plane
3418FOS = { a = 3, b = 42 }: Sketch
3419sigmaAllow = 8: number(mm)
3420width = 20
3421thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
3422        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3423
3424        let recasted = program.recast_top(&Default::default(), 0);
3425        assert_eq!(recasted.trim(), some_program_string);
3426    }
3427
3428    #[test]
3429    fn no_vardec_keyword() {
3430        let some_program_string = r#"distance = 5"#;
3431        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3432
3433        let recasted = program.recast_top(&Default::default(), 0);
3434        assert_eq!(recasted.trim(), some_program_string);
3435    }
3436
3437    #[test]
3438    fn recast_types() {
3439        let some_program_string = r#"type foo
3440
3441// A comment
3442@(impl = primitive)
3443export type bar(unit, baz)
3444type baz = Foo | Bar
3445type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
3446"#;
3447        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3448        let recasted = program.recast_top(&Default::default(), 0);
3449        assert_eq!(recasted, some_program_string);
3450    }
3451
3452    #[test]
3453    fn recast_nested_fn() {
3454        let some_program_string = r#"fn f() {
3455  return fn() {
3456  return 1
3457}
3458}"#;
3459        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3460        let recasted = program.recast_top(&Default::default(), 0);
3461        let expected = "\
3462fn f() {
3463  return fn() {
3464    return 1
3465  }
3466}";
3467        assert_eq!(recasted.trim(), expected);
3468    }
3469
3470    #[test]
3471    fn recast_literal() {
3472        use winnow::Parser;
3473        for (i, (raw, expected, reason)) in [
3474            (
3475                "5.0",
3476                "5.0",
3477                "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
3478            ),
3479            (
3480                "5",
3481                "5",
3482                "integers should stay integral, i.e. don't reformat this to '5.0'",
3483            ),
3484            (
3485                "5.0000000",
3486                "5.0",
3487                "if the number is f64 but not fractional, use its canonical format",
3488            ),
3489            ("5.1", "5.1", "straightforward case works"),
3490        ]
3491        .into_iter()
3492        .enumerate()
3493        {
3494            let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
3495            let literal = crate::parsing::parser::unsigned_number_literal
3496                .parse(tokens.as_slice())
3497                .unwrap();
3498            let mut actual = String::new();
3499            literal.recast(&mut actual);
3500            assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
3501        }
3502    }
3503
3504    #[test]
3505    fn recast_objects_no_comments() {
3506        let input = r#"
3507sketch002 = startSketchOn({
3508       plane: {
3509    origin: { x = 1, y = 2, z = 3 },
3510    x_axis = { x = 4, y = 5, z = 6 },
3511    y_axis = { x = 7, y = 8, z = 9 },
3512    z_axis = { x = 10, y = 11, z = 12 }
3513       }
3514  })
3515"#;
3516        let expected = r#"sketch002 = startSketchOn({
3517  plane = {
3518    origin = { x = 1, y = 2, z = 3 },
3519    x_axis = { x = 4, y = 5, z = 6 },
3520    y_axis = { x = 7, y = 8, z = 9 },
3521    z_axis = { x = 10, y = 11, z = 12 }
3522  }
3523})
3524"#;
3525        let ast = crate::parsing::top_level_parse(input).unwrap();
3526        let actual = ast.recast_top(&FormatOptions::new(), 0);
3527        assert_eq!(actual, expected);
3528    }
3529
3530    #[test]
3531    fn unparse_fn_unnamed() {
3532        let input = "\
3533squares_out = reduce(
3534  arr,
3535  n = 0: number,
3536  f = fn(@i, accum) {
3537    return 1
3538  },
3539)
3540";
3541        let ast = crate::parsing::top_level_parse(input).unwrap();
3542        let actual = ast.recast_top(&FormatOptions::new(), 0);
3543        assert_eq!(actual, input);
3544    }
3545
3546    #[test]
3547    fn unparse_fn_named() {
3548        let input = r#"fn f(x) {
3549  return 1
3550}
3551"#;
3552        let ast = crate::parsing::top_level_parse(input).unwrap();
3553        let actual = ast.recast_top(&FormatOptions::new(), 0);
3554        assert_eq!(actual, input);
3555    }
3556
3557    #[test]
3558    fn unparse_call_inside_function_single_line() {
3559        let input = r#"fn foo() {
3560  toDegrees(atan(0.5), foo = 1)
3561  return 0
3562}
3563"#;
3564        let ast = crate::parsing::top_level_parse(input).unwrap();
3565        let actual = ast.recast_top(&FormatOptions::new(), 0);
3566        assert_eq!(actual, input);
3567    }
3568
3569    #[test]
3570    fn recast_function_types() {
3571        let input = r#"foo = x: fn
3572foo = x: fn(number)
3573fn foo(x: fn(): number): fn {
3574  return 0
3575}
3576fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
3577  return 0
3578}
3579type fn
3580type foo = fn
3581type foo = fn(a: string, b: { f: fn(): any })
3582type foo = fn([fn])
3583type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
3584"#;
3585        let ast = crate::parsing::top_level_parse(input).unwrap();
3586        let actual = ast.recast_top(&FormatOptions::new(), 0);
3587        assert_eq!(actual, input);
3588    }
3589
3590    #[test]
3591    fn unparse_call_inside_function_args_multiple_lines() {
3592        let input = r#"fn foo() {
3593  toDegrees(
3594    atan(0.5),
3595    foo = 1,
3596    bar = 2,
3597    baz = 3,
3598    qux = 4,
3599  )
3600  return 0
3601}
3602"#;
3603        let ast = crate::parsing::top_level_parse(input).unwrap();
3604        let actual = ast.recast_top(&FormatOptions::new(), 0);
3605        assert_eq!(actual, input);
3606    }
3607
3608    #[test]
3609    fn unparse_call_inside_function_single_arg_multiple_lines() {
3610        let input = r#"fn foo() {
3611  toDegrees(
3612    [
3613      profile0,
3614      profile1,
3615      profile2,
3616      profile3,
3617      profile4,
3618      profile5
3619    ],
3620    key = 1,
3621  )
3622  return 0
3623}
3624"#;
3625        let ast = crate::parsing::top_level_parse(input).unwrap();
3626        let actual = ast.recast_top(&FormatOptions::new(), 0);
3627        assert_eq!(actual, input);
3628    }
3629
3630    #[test]
3631    fn recast_objects_with_comments() {
3632        use winnow::Parser;
3633        for (i, (input, expected, reason)) in [(
3634            "\
3635{
3636  a = 1,
3637  // b = 2,
3638  c = 3
3639}",
3640            "\
3641{
3642  a = 1,
3643  // b = 2,
3644  c = 3
3645}",
3646            "preserves comments",
3647        )]
3648        .into_iter()
3649        .enumerate()
3650        {
3651            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3652            crate::parsing::parser::print_tokens(tokens.as_slice());
3653            let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3654            let mut actual = String::new();
3655            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3656            assert_eq!(
3657                actual, expected,
3658                "failed test {i}, which is testing that recasting {reason}"
3659            );
3660        }
3661    }
3662
3663    #[test]
3664    fn recast_array_with_comments() {
3665        use winnow::Parser;
3666        for (i, (input, expected, reason)) in [
3667            (
3668                "\
3669[
3670  1,
3671  2,
3672  3,
3673  4,
3674  5,
3675  6,
3676  7,
3677  8,
3678  9,
3679  10,
3680  11,
3681  12,
3682  13,
3683  14,
3684  15,
3685  16,
3686  17,
3687  18,
3688  19,
3689  20,
3690]",
3691                "\
3692[
3693  1,
3694  2,
3695  3,
3696  4,
3697  5,
3698  6,
3699  7,
3700  8,
3701  9,
3702  10,
3703  11,
3704  12,
3705  13,
3706  14,
3707  15,
3708  16,
3709  17,
3710  18,
3711  19,
3712  20
3713]",
3714                "preserves multi-line arrays",
3715            ),
3716            (
3717                "\
3718[
3719  1,
3720  // 2,
3721  3
3722]",
3723                "\
3724[
3725  1,
3726  // 2,
3727  3
3728]",
3729                "preserves comments",
3730            ),
3731            (
3732                "\
3733[
3734  1,
3735  2,
3736  // 3
3737]",
3738                "\
3739[
3740  1,
3741  2,
3742  // 3
3743]",
3744                "preserves comments at the end of the array",
3745            ),
3746        ]
3747        .into_iter()
3748        .enumerate()
3749        {
3750            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3751            let expr = crate::parsing::parser::array_elem_by_elem
3752                .parse(tokens.as_slice())
3753                .unwrap();
3754            let mut actual = String::new();
3755            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3756            assert_eq!(
3757                actual, expected,
3758                "failed test {i}, which is testing that recasting {reason}"
3759            );
3760        }
3761    }
3762
3763    #[test]
3764    fn code_with_comment_and_extra_lines() {
3765        let code = r#"yo = 'c'
3766
3767/* this is
3768a
3769comment */
3770yo = 'bing'
3771"#;
3772        let ast = crate::parsing::top_level_parse(code).unwrap();
3773        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3774        assert_eq!(recasted, code);
3775    }
3776
3777    #[test]
3778    fn comments_in_a_fn_block() {
3779        let code = r#"fn myFn() {
3780  // this is a comment
3781  yo = { a = { b = { c = '123' } } }
3782
3783  /* block
3784  comment */
3785  key = 'c'
3786  // this is also a comment
3787}
3788"#;
3789        let ast = crate::parsing::top_level_parse(code).unwrap();
3790        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3791        assert_eq!(recasted, code);
3792    }
3793
3794    #[test]
3795    fn array_range_end_exclusive() {
3796        let code = "myArray = [0..<4]\n";
3797        let ast = crate::parsing::top_level_parse(code).unwrap();
3798        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3799        assert_eq!(recasted, code);
3800    }
3801
3802    #[test]
3803    fn paren_precedence() {
3804        let code = r#"x = 1 - 2 - 3
3805x = (1 - 2) - 3
3806x = 1 - (2 - 3)
3807x = 1 + 2 + 3
3808x = (1 + 2) + 3
3809x = 1 + (2 + 3)
3810x = 2 * (y % 2)
3811x = (2 * y) % 2
3812x = 2 % (y * 2)
3813x = (2 % y) * 2
3814x = 2 * y % 2
3815"#;
3816
3817        let expected = r#"x = 1 - 2 - 3
3818x = 1 - 2 - 3
3819x = 1 - (2 - 3)
3820x = 1 + 2 + 3
3821x = 1 + 2 + 3
3822x = 1 + 2 + 3
3823x = 2 * (y % 2)
3824x = 2 * y % 2
3825x = 2 % (y * 2)
3826x = 2 % y * 2
3827x = 2 * y % 2
3828"#;
3829        let ast = crate::parsing::top_level_parse(code).unwrap();
3830        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3831        assert_eq!(recasted, expected);
3832    }
3833
3834    #[test]
3835    fn gap_between_body_item_and_documented_fn() {
3836        let code = "\
3837x = 360
3838
3839// Watermelon
3840fn myFn() {
3841}
3842";
3843        let ast = crate::parsing::top_level_parse(code).unwrap();
3844        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3845        let expected = code;
3846        assert_eq!(recasted, expected);
3847    }
3848
3849    #[test]
3850    fn simple_assignment_in_fn() {
3851        let code = "\
3852fn function001() {
3853  extrude002 = extrude()
3854}\n";
3855
3856        let ast = crate::parsing::top_level_parse(code).unwrap();
3857        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3858        let expected = code;
3859        assert_eq!(recasted, expected);
3860    }
3861
3862    #[test]
3863    fn no_weird_extra_lines() {
3864        // Regression test, this used to insert a lot of new lines
3865        // between the initial comment and the @settings.
3866        let code = "\
3867// Initial comment
3868
3869@settings(defaultLengthUnit = mm)
3870
3871x = 1
3872";
3873        let ast = crate::parsing::top_level_parse(code).unwrap();
3874        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3875        let expected = code;
3876        assert_eq!(recasted, expected);
3877    }
3878
3879    #[test]
3880    fn settings_then_code_is_stable() {
3881        let code = "\
3882@settings(defaultLengthUnit = in)
3883
3884import \"cube-inches.kcl\" as cubeIn
3885import \"cube-mm.kcl\" as cubeMm
3886
3887cubeIn
3888cubeMm
3889";
3890        let formatted_once = crate::parsing::top_level_parse(code)
3891            .unwrap()
3892            .recast_top(&FormatOptions::new(), 0);
3893        assert_eq!(formatted_once, code);
3894
3895        let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3896            .unwrap()
3897            .recast_top(&FormatOptions::new(), 0);
3898        assert_eq!(formatted_twice, formatted_once);
3899    }
3900
3901    #[test]
3902    fn settings_then_standalone_comment_is_stable() {
3903        let code = "\
3904@settings(defaultLengthUnit = mm)
3905@settings(defaultAngleUnit = deg)
3906
3907// Cap for gimbal stick
3908
3909x = 1
3910";
3911        let formatted_once = crate::parsing::top_level_parse(code)
3912            .unwrap()
3913            .recast_top(&FormatOptions::new(), 0);
3914        assert_eq!(formatted_once, code);
3915
3916        let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3917            .unwrap()
3918            .recast_top(&FormatOptions::new(), 0);
3919        assert_eq!(formatted_twice, formatted_once);
3920    }
3921
3922    #[test]
3923    fn module_prefix() {
3924        let code = "x = std::sweep::SKETCH_PLANE\n";
3925        let ast = crate::parsing::top_level_parse(code).unwrap();
3926        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3927        let expected = code;
3928        assert_eq!(recasted, expected);
3929    }
3930
3931    #[test]
3932    fn inline_ifs() {
3933        let code = "y = true
3934startSketchOn(XY)
3935  |> startProfile(at = [0, 0])
3936  |> if y {
3937    yLine(length = 1)
3938  } else {
3939    xLine(length = 1)
3940  }
3941";
3942        let ast = crate::parsing::top_level_parse(code).unwrap();
3943        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3944        let expected = code;
3945        assert_eq!(recasted, expected);
3946    }
3947
3948    #[test]
3949    fn indented_binary_expressions() {
3950        let code = "\
3951fn foo() {
3952  1 == 2
3953}
3954";
3955        let ast = crate::parsing::top_level_parse(code).unwrap();
3956        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3957        let expected = code;
3958        assert_eq!(recasted, expected);
3959    }
3960
3961    #[test]
3962    fn indented_assignment() {
3963        let code = "\
3964fn foo() {
3965  x = 1
3966}
3967";
3968        let ast = crate::parsing::top_level_parse(code).unwrap();
3969        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3970        let expected = code;
3971        assert_eq!(recasted, expected);
3972    }
3973
3974    #[test]
3975    fn indented_unary_expression() {
3976        let code = "\
3977fn foo() {
3978  -x
3979}
3980";
3981        let ast = crate::parsing::top_level_parse(code).unwrap();
3982        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3983        let expected = code;
3984        assert_eq!(recasted, expected);
3985    }
3986
3987    #[test]
3988    fn indented_array_expression() {
3989        let code = "\
3990fn foo() {
3991  [1, 2]
3992}
3993";
3994        let ast = crate::parsing::top_level_parse(code).unwrap();
3995        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3996        let expected = code;
3997        assert_eq!(recasted, expected);
3998    }
3999
4000    #[test]
4001    fn indented_name_expression() {
4002        let code = "\
4003fn foo() {
4004  x
4005}
4006";
4007        let ast = crate::parsing::top_level_parse(code).unwrap();
4008        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4009        let expected = code;
4010        assert_eq!(recasted, expected);
4011    }
4012
4013    #[test]
4014    fn indented_member_assignment() {
4015        let code = "\
4016brakcetPlane = {
4017  origin = { x = length / 2 },
4018  origin = { x = length / 2 },
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}
4026";
4027        let ast = crate::parsing::top_level_parse(code).unwrap();
4028        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4029        let expected = code;
4030        assert_eq!(recasted, expected);
4031    }
4032
4033    #[test]
4034    fn badly_formatted_inline_calls() {
4035        let code = "\
4036return union([right, left])
4037  |> subtract(tools = [
4038       translate(axle(), y = pitchStabL + forkBaseL + wheelRGap + wheelR + addedLength),
4039       socket(rakeAngle = rearRake, xyTrans = [0, 12]),
4040       socket(
4041         rakeAngle = frontRake,
4042         xyTrans = [
4043           wheelW / 2 + wheelWGap + forkTineW / 2,
4044           40 + addedLength
4045         ],
4046       )
4047     ])
4048";
4049        let ast = crate::parsing::top_level_parse(code).unwrap();
4050        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4051        let expected = code;
4052        assert_eq!(recasted, expected);
4053    }
4054
4055    #[test]
4056    fn fn_args_prefixed_with_spaces() {
4057        let code = "holeAt(
4058  [cube1, cube2],
4059  plane = XY,
4060  holeBottom =   hole::flat(),
4061  holeBody =   hole::blind(depth = 2, diameter = 1),
4062  holeType =   hole::counterbore(diameter = 1.4, depth = 1),
4063  cutAt = [1, 1],
4064)";
4065        let expected = "holeAt(
4066  [cube1, cube2],
4067  plane = XY,
4068  holeBottom = hole::flat(),
4069  holeBody = hole::blind(depth = 2, diameter = 1),
4070  holeType = hole::counterbore(diameter = 1.4, depth = 1),
4071  cutAt = [1, 1],
4072)
4073";
4074        let ast = crate::parsing::top_level_parse(code).unwrap();
4075        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4076        assert_eq!(recasted, expected);
4077    }
4078
4079    #[test]
4080    fn some_fn_args_still_prefixed() {
4081        let code = "a
4082  |> b()
4083  |> subtract(
4084       tools =     startSketchOn(XY)
4085      |> circle(diameter = hubDiameter)
4086      |> extrude(length = hubThickness * 5, symmetric = true),
4087       tolerance,
4088     )
4089";
4090        let ast = crate::parsing::top_level_parse(code).unwrap();
4091        let actual_recasted = ast.recast_top(&FormatOptions::new(), 0);
4092        let expected_recasted = "a
4093  |> b()
4094  |> subtract(
4095       tools = startSketchOn(XY)
4096      |> circle(diameter = hubDiameter)
4097      |> extrude(length = hubThickness * 5, symmetric = true),
4098       tolerance,
4099     )
4100";
4101        assert_eq!(actual_recasted, expected_recasted);
4102    }
4103
4104    #[test]
4105    fn first_in_pipeline_indent() {
4106        // These code snippets are identical, except that one passes the gear into a clone,
4107        // and the other doesn't.
4108        let not_clone = "gear::helical(
4109  nTeeth = 12,
4110  module = 1.5,
4111  pressureAngle = 14deg,
4112  helixAngle = 25deg,
4113  gearHeight = 5,
4114)
4115";
4116        let yes_clone = "gear::helical(
4117  nTeeth = 12,
4118  module = 1.5,
4119  pressureAngle = 14deg,
4120  helixAngle = 25deg,
4121  gearHeight = 5,
4122)
4123|> clone()
4124";
4125        // Both these should format their gear::helical parameters the same.
4126        let not_clone_recasted = crate::parsing::top_level_parse(not_clone)
4127            .unwrap()
4128            .recast_top(&FormatOptions::new(), 0);
4129        let yes_clone_recasted = crate::parsing::top_level_parse(yes_clone)
4130            .unwrap()
4131            .recast_top(&FormatOptions::new(), 0);
4132        assert!(not_clone_recasted.contains("\n  nTeeth"));
4133        assert!(!yes_clone_recasted.contains("\n       nTeeth"));
4134        assert!(yes_clone_recasted.contains("\n  nTeeth"));
4135    }
4136}