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