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