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