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