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 let range_op = if self.end_inclusive { ".." } else { "..<" };
587
588 if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
593 format!("[{s1}{range_op}{s2}]")
594 } else {
595 format!("[{s1} {range_op} {s2}]")
596 }
597
598 }
600}
601
602impl ObjectExpression {
603 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
604 if self
605 .non_code_meta
606 .non_code_nodes
607 .values()
608 .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
609 {
610 return self.recast_multi_line(options, indentation_level, ctxt);
611 }
612 let flat_recast = format!(
613 "{{ {} }}",
614 self.properties
615 .iter()
616 .map(|prop| {
617 format!(
618 "{} = {}",
619 prop.key.name,
620 prop.value.recast(options, indentation_level + 1, ctxt).trim()
621 )
622 })
623 .collect::<Vec<String>>()
624 .join(", ")
625 );
626 let max_array_length = 40;
627 let needs_multiple_lines = flat_recast.len() > max_array_length;
628 if !needs_multiple_lines {
629 return flat_recast;
630 }
631 self.recast_multi_line(options, indentation_level, ctxt)
632 }
633
634 fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
636 let inner_indentation = if ctxt == ExprContext::Pipe {
637 options.get_indentation_offset_pipe(indentation_level + 1)
638 } else {
639 options.get_indentation(indentation_level + 1)
640 };
641 let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
642 let mut props = self.properties.iter();
643 let format_items: Vec<_> = (0..num_items)
644 .flat_map(|i| {
645 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
646 noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
647 } else {
648 let prop = props.next().unwrap();
649 let comma = if i == num_items - 1 { "" } else { ",\n" };
651 let s = format!(
652 "{} = {}{comma}",
653 prop.key.name,
654 prop.value.recast(options, indentation_level + 1, ctxt).trim()
655 );
656 vec![s]
657 }
658 })
659 .collect();
660 let end_indent = if ctxt == ExprContext::Pipe {
661 options.get_indentation_offset_pipe(indentation_level)
662 } else {
663 options.get_indentation(indentation_level)
664 };
665 format!(
666 "{{\n{inner_indentation}{}\n{end_indent}}}",
667 format_items.join(&inner_indentation),
668 )
669 }
670}
671
672impl MemberExpression {
673 fn recast(&self) -> String {
674 let key_str = match &self.property {
675 LiteralIdentifier::Identifier(identifier) => {
676 if self.computed {
677 format!("[{}]", &(*identifier.name))
678 } else {
679 format!(".{}", &(*identifier.name))
680 }
681 }
682 LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
683 };
684
685 match &self.object {
686 MemberObject::MemberExpression(member_exp) => member_exp.recast() + key_str.as_str(),
687 MemberObject::Identifier(identifier) => identifier.name.to_string() + key_str.as_str(),
688 }
689 }
690}
691
692impl BinaryExpression {
693 fn recast(&self, options: &FormatOptions) -> String {
694 let maybe_wrap_it = |a: String, doit: bool| -> String {
695 if doit {
696 format!("({})", a)
697 } else {
698 a
699 }
700 };
701
702 let should_wrap_right = match &self.right {
703 BinaryPart::BinaryExpression(bin_exp) => {
704 self.precedence() > bin_exp.precedence()
705 || self.operator == BinaryOperator::Sub
706 || self.operator == BinaryOperator::Div
707 }
708 _ => false,
709 };
710
711 let should_wrap_left = match &self.left {
712 BinaryPart::BinaryExpression(bin_exp) => self.precedence() > bin_exp.precedence(),
713 _ => false,
714 };
715
716 format!(
717 "{} {} {}",
718 maybe_wrap_it(self.left.recast(options, 0), should_wrap_left),
719 self.operator,
720 maybe_wrap_it(self.right.recast(options, 0), should_wrap_right)
721 )
722 }
723}
724
725impl UnaryExpression {
726 fn recast(&self, options: &FormatOptions) -> String {
727 match self.argument {
728 BinaryPart::Literal(_)
729 | BinaryPart::Name(_)
730 | BinaryPart::MemberExpression(_)
731 | BinaryPart::IfExpression(_)
732 | BinaryPart::AscribedExpression(_)
733 | BinaryPart::CallExpressionKw(_) => {
734 format!("{}{}", &self.operator, self.argument.recast(options, 0))
735 }
736 BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
737 format!("{}({})", &self.operator, self.argument.recast(options, 0))
738 }
739 }
740 }
741}
742
743impl IfExpression {
744 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
745 let n = 2 + (self.else_ifs.len() * 2) + 3;
748 let mut lines = Vec::with_capacity(n);
749
750 let cond = self.cond.recast(options, indentation_level, ctxt);
751 lines.push((0, format!("if {cond} {{")));
752 lines.push((1, self.then_val.recast(options, indentation_level + 1)));
753 for else_if in &self.else_ifs {
754 let cond = else_if.cond.recast(options, indentation_level, ctxt);
755 lines.push((0, format!("}} else if {cond} {{")));
756 lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
757 }
758 lines.push((0, "} else {".to_owned()));
759 lines.push((1, self.final_else.recast(options, indentation_level + 1)));
760 lines.push((0, "}".to_owned()));
761 lines
762 .into_iter()
763 .map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
764 .collect::<Vec<_>>()
765 .join("\n")
766 }
767}
768
769impl Node<PipeExpression> {
770 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
771 let pipe = self
772 .body
773 .iter()
774 .enumerate()
775 .map(|(index, statement)| {
776 let indentation = options.get_indentation(indentation_level + 1);
777 let mut s = statement.recast(options, indentation_level + 1, ExprContext::Pipe);
778 let non_code_meta = self.non_code_meta.clone();
779 if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
780 for val in non_code_meta_value {
781 let formatted = if val.end == self.end {
782 val.recast(options, indentation_level)
783 .trim_end_matches('\n')
784 .to_string()
785 } else {
786 val.recast(options, indentation_level + 1)
787 .trim_end_matches('\n')
788 .to_string()
789 };
790 if let NonCodeValue::BlockComment { .. } = val.value {
791 s += "\n";
792 s += &formatted;
793 } else {
794 s += &formatted;
795 }
796 }
797 }
798
799 if index != self.body.len() - 1 {
800 s += "\n";
801 s += &indentation;
802 s += PIPE_OPERATOR;
803 s += " ";
804 }
805 s
806 })
807 .collect::<String>();
808 format!("{}{}", options.get_indentation(indentation_level), pipe)
809 }
810}
811
812impl FunctionExpression {
813 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
814 let mut new_options = options.clone();
816 new_options.insert_final_newline = false;
817 let param_list = self
818 .params
819 .iter()
820 .map(|param| param.recast(options, indentation_level))
821 .collect::<Vec<String>>()
822 .join(", ");
823 let tab0 = options.get_indentation(indentation_level);
824 let tab1 = options.get_indentation(indentation_level + 1);
825 let return_type = match &self.return_type {
826 Some(rt) => format!(": {rt}"),
827 None => String::new(),
828 };
829 let body = self.body.recast(&new_options, indentation_level + 1);
830
831 format!("({param_list}){return_type} {{\n{tab1}{body}\n{tab0}}}")
832 }
833}
834
835impl Parameter {
836 pub fn recast(&self, _options: &FormatOptions, _indentation_level: usize) -> String {
837 let at_sign = if self.labeled { "" } else { "@" };
838 let identifier = &self.identifier.name;
839 let question_mark = if self.default_value.is_some() { "?" } else { "" };
840 let mut result = format!("{at_sign}{identifier}{question_mark}");
841 if let Some(ty) = &self.type_ {
842 result += ": ";
843 result += &ty.to_string();
844 }
845 if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
846 let lit = literal.recast();
847 result.push_str(&format!(" = {lit}"));
848 };
849
850 result
851 }
852}
853
854#[cfg(not(target_arch = "wasm32"))]
856#[async_recursion::async_recursion]
857pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
858 if !dir.is_dir() {
860 anyhow::bail!("`{}` is not a directory", dir.display());
861 }
862
863 let mut entries = tokio::fs::read_dir(dir).await?;
864
865 let mut files = Vec::new();
866 while let Some(entry) = entries.next_entry().await? {
867 let path = entry.path();
868
869 if path.is_dir() {
870 files.extend(walk_dir(&path).await?);
871 } else if path
872 .extension()
873 .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
874 {
875 files.push(path);
876 }
877 }
878
879 Ok(files)
880}
881
882#[cfg(not(target_arch = "wasm32"))]
884pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
885 let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
886 crate::KclError::Internal(crate::errors::KclErrorDetails::new(
887 format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
888 vec![crate::SourceRange::default()],
889 ))
890 })?;
891
892 let futures = files
893 .into_iter()
894 .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) .map(|file| {
897 let options = options.clone();
898 tokio::spawn(async move {
899 let contents = tokio::fs::read_to_string(&file)
900 .await
901 .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
902 let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
903 let report = crate::Report {
904 kcl_source: contents.to_string(),
905 error: err.clone(),
906 filename: file.to_string_lossy().to_string(),
907 };
908 let report = miette::Report::new(report);
909 anyhow::anyhow!("{:?}", report)
910 })?;
911 for ce in &ces {
912 if ce.severity != crate::errors::Severity::Warning {
913 let report = crate::Report {
914 kcl_source: contents.to_string(),
915 error: crate::KclError::Semantic(ce.clone().into()),
916 filename: file.to_string_lossy().to_string(),
917 };
918 let report = miette::Report::new(report);
919 anyhow::bail!("{:?}", report);
920 }
921 }
922 let Some(program) = program else {
923 anyhow::bail!("Failed to parse file `{}`", file.display());
924 };
925 let recast = program.recast_with_options(&options);
926 tokio::fs::write(&file, recast)
927 .await
928 .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
929
930 Ok::<(), anyhow::Error>(())
931 })
932 })
933 .collect::<Vec<_>>();
934
935 let results = futures::future::join_all(futures).await;
937
938 let mut errors = Vec::new();
940 for result in results {
941 if let Err(err) = result? {
942 errors.push(err);
943 }
944 }
945
946 if !errors.is_empty() {
947 anyhow::bail!("Failed to recast some files: {:?}", errors);
948 }
949
950 Ok(())
951}
952
953#[cfg(test)]
954mod tests {
955 use pretty_assertions::assert_eq;
956
957 use super::*;
958 use crate::{parsing::ast::types::FormatOptions, ModuleId};
959
960 #[test]
961 fn test_recast_annotations_without_body_items() {
962 let input = r#"@settings(defaultLengthUnit = in)
963"#;
964 let program = crate::parsing::top_level_parse(input).unwrap();
965 let output = program.recast(&Default::default(), 0);
966 assert_eq!(output, input);
967 }
968
969 #[test]
970 fn test_recast_annotations_in_function_body() {
971 let input = r#"fn myFunc() {
972 @meta(yes = true)
973
974 x = 2
975}
976"#;
977 let program = crate::parsing::top_level_parse(input).unwrap();
978 let output = program.recast(&Default::default(), 0);
979 assert_eq!(output, input);
980 }
981
982 #[test]
983 fn test_recast_annotations_in_function_body_without_items() {
984 let input = r#"fn myFunc() {
985 @meta(yes = true)
986}
987"#;
988 let program = crate::parsing::top_level_parse(input).unwrap();
989 let output = program.recast(&Default::default(), 0);
990 assert_eq!(output, input);
991 }
992
993 #[test]
994 fn recast_annotations_with_comments() {
995 let input = r#"// Start comment
996
997// Comment on attr
998@settings(defaultLengthUnit = in)
999
1000// Comment on item
1001foo = 42
1002
1003// Comment on another item
1004@(impl = kcl)
1005bar = 0
1006"#;
1007 let program = crate::parsing::top_level_parse(input).unwrap();
1008 let output = program.recast(&Default::default(), 0);
1009 assert_eq!(output, input);
1010 }
1011
1012 #[test]
1013 fn recast_annotations_with_block_comment() {
1014 let input = r#"/* Start comment
1015
1016sdfsdfsdfs */
1017@settings(defaultLengthUnit = in)
1018
1019foo = 42
1020"#;
1021 let program = crate::parsing::top_level_parse(input).unwrap();
1022 let output = program.recast(&Default::default(), 0);
1023 assert_eq!(output, input);
1024 }
1025
1026 #[test]
1027 fn test_recast_if_else_if_same() {
1028 let input = r#"b = if false {
1029 3
1030} else if true {
1031 4
1032} else {
1033 5
1034}
1035"#;
1036 let program = crate::parsing::top_level_parse(input).unwrap();
1037 let output = program.recast(&Default::default(), 0);
1038 assert_eq!(output, input);
1039 }
1040
1041 #[test]
1042 fn test_recast_if_same() {
1043 let input = r#"b = if false {
1044 3
1045} else {
1046 5
1047}
1048"#;
1049 let program = crate::parsing::top_level_parse(input).unwrap();
1050 let output = program.recast(&Default::default(), 0);
1051 assert_eq!(output, input);
1052 }
1053
1054 #[test]
1055 fn test_recast_import() {
1056 let input = r#"import a from "a.kcl"
1057import a as aaa from "a.kcl"
1058import a, b from "a.kcl"
1059import a as aaa, b from "a.kcl"
1060import a, b as bbb from "a.kcl"
1061import a as aaa, b as bbb from "a.kcl"
1062import "a_b.kcl"
1063import "a-b.kcl" as b
1064import * from "a.kcl"
1065export import a as aaa from "a.kcl"
1066export import a, b from "a.kcl"
1067export import a as aaa, b from "a.kcl"
1068export import a, b as bbb from "a.kcl"
1069"#;
1070 let program = crate::parsing::top_level_parse(input).unwrap();
1071 let output = program.recast(&Default::default(), 0);
1072 assert_eq!(output, input);
1073 }
1074
1075 #[test]
1076 fn test_recast_import_as_same_name() {
1077 let input = r#"import a as a from "a.kcl"
1078"#;
1079 let program = crate::parsing::top_level_parse(input).unwrap();
1080 let output = program.recast(&Default::default(), 0);
1081 let expected = r#"import a from "a.kcl"
1082"#;
1083 assert_eq!(output, expected);
1084 }
1085
1086 #[test]
1087 fn test_recast_export_fn() {
1088 let input = r#"export fn a() {
1089 return 0
1090}
1091"#;
1092 let program = crate::parsing::top_level_parse(input).unwrap();
1093 let output = program.recast(&Default::default(), 0);
1094 assert_eq!(output, input);
1095 }
1096
1097 #[test]
1098 fn test_recast_bug_fn_in_fn() {
1099 let some_program_string = r#"// Start point (top left)
1100zoo_x = -20
1101zoo_y = 7
1102// Scale
1103s = 1 // s = 1 -> height of Z is 13.4mm
1104// Depth
1105d = 1
1106
1107fn rect(x, y, w, h) {
1108 startSketchOn(XY)
1109 |> startProfile(at = [x, y])
1110 |> xLine(length = w)
1111 |> yLine(length = h)
1112 |> xLine(length = -w)
1113 |> close()
1114 |> extrude(d)
1115}
1116
1117fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1118 startSketchOn(XY)
1119 |> startProfile(at = [x1, y1])
1120 |> line(endAbsolute = [x2, y2])
1121 |> line(endAbsolute = [x3, y3])
1122 |> line(endAbsolute = [x4, y4])
1123 |> close()
1124 |> extrude(d)
1125}
1126
1127fn crosshair(x, y) {
1128 startSketchOn(XY)
1129 |> startProfile(at = [x, y])
1130 |> yLine(length = 1)
1131 |> yLine(length = -2)
1132 |> yLine(length = 1)
1133 |> xLine(length = 1)
1134 |> xLine(length = -2)
1135}
1136
1137fn z(z_x, z_y) {
1138 z_end_w = s * 8.4
1139 z_end_h = s * 3
1140 z_corner = s * 2
1141 z_w = z_end_w + 2 * z_corner
1142 z_h = z_w * 1.08130081300813
1143 rect(
1144 z_x,
1145 a = z_y,
1146 b = z_end_w,
1147 c = -z_end_h,
1148 )
1149 rect(
1150 z_x + z_w,
1151 a = z_y,
1152 b = -z_corner,
1153 c = -z_corner,
1154 )
1155 rect(
1156 z_x + z_w,
1157 a = z_y - z_h,
1158 b = -z_end_w,
1159 c = z_end_h,
1160 )
1161 rect(
1162 z_x,
1163 a = z_y - z_h,
1164 b = z_corner,
1165 c = z_corner,
1166 )
1167}
1168
1169fn o(c_x, c_y) {
1170 // Outer and inner radii
1171 o_r = s * 6.95
1172 i_r = 0.5652173913043478 * o_r
1173
1174 // Angle offset for diagonal break
1175 a = 7
1176
1177 // Start point for the top sketch
1178 o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
1179 o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
1180
1181 // Start point for the bottom sketch
1182 o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
1183 o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
1184
1185 // End point for the bottom startSketch
1186 o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
1187 o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
1188
1189 // Where is the center?
1190 // crosshair(c_x, c_y)
1191
1192
1193 startSketchOn(XY)
1194 |> startProfile(at = [o_x1, o_y1])
1195 |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
1196 |> angledLine(angle = 45, length = o_r - i_r)
1197 |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
1198 |> close()
1199 |> extrude(d)
1200
1201 startSketchOn(XY)
1202 |> startProfile(at = [o_x2, o_y2])
1203 |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
1204 |> angledLine(angle = 225, length = o_r - i_r)
1205 |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
1206 |> close()
1207 |> extrude(d)
1208}
1209
1210fn zoo(x0, y0) {
1211 z(x = x0, y = y0)
1212 o(x = x0 + s * 20, y = y0 - (s * 6.7))
1213 o(x = x0 + s * 35, y = y0 - (s * 6.7))
1214}
1215
1216zoo(x = zoo_x, y = zoo_y)
1217"#;
1218 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1219
1220 let recasted = program.recast(&Default::default(), 0);
1221 assert_eq!(recasted, some_program_string);
1222 }
1223
1224 #[test]
1225 fn test_recast_bug_extra_parens() {
1226 let some_program_string = r#"// Ball Bearing
1227// 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.
1228
1229// Define constants like ball diameter, inside diameter, overhange length, and thickness
1230sphereDia = 0.5
1231insideDia = 1
1232thickness = 0.25
1233overHangLength = .4
1234
1235// Sketch and revolve the inside bearing piece
1236insideRevolve = startSketchOn(XZ)
1237 |> startProfile(at = [insideDia / 2, 0])
1238 |> line(end = [0, thickness + sphereDia / 2])
1239 |> line(end = [overHangLength, 0])
1240 |> line(end = [0, -thickness])
1241 |> line(end = [-overHangLength + thickness, 0])
1242 |> line(end = [0, -sphereDia])
1243 |> line(end = [overHangLength - thickness, 0])
1244 |> line(end = [0, -thickness])
1245 |> line(end = [-overHangLength, 0])
1246 |> close()
1247 |> revolve(axis = Y)
1248
1249// 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)
1250sphere = startSketchOn(XZ)
1251 |> startProfile(at = [
1252 0.05 + insideDia / 2 + thickness,
1253 0 - 0.05
1254 ])
1255 |> line(end = [sphereDia - 0.1, 0])
1256 |> arc(
1257 angle_start = 0,
1258 angle_end = -180,
1259 radius = sphereDia / 2 - 0.05
1260 )
1261 |> close()
1262 |> revolve(axis = X)
1263 |> patternCircular3d(
1264 axis = [0, 0, 1],
1265 center = [0, 0, 0],
1266 repetitions = 10,
1267 arcDegrees = 360,
1268 rotateDuplicates = true
1269 )
1270
1271// Sketch and revolve the outside bearing
1272outsideRevolve = startSketchOn(XZ)
1273 |> startProfile(at = [
1274 insideDia / 2 + thickness + sphereDia,
1275 0
1276 ]
1277 )
1278 |> line(end = [0, sphereDia / 2])
1279 |> line(end = [-overHangLength + thickness, 0])
1280 |> line(end = [0, thickness])
1281 |> line(end = [overHangLength, 0])
1282 |> line(end = [0, -2 * thickness - sphereDia])
1283 |> line(end = [-overHangLength, 0])
1284 |> line(end = [0, thickness])
1285 |> line(end = [overHangLength - thickness, 0])
1286 |> close()
1287 |> revolve(axis = Y)"#;
1288 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1289
1290 let recasted = program.recast(&Default::default(), 0);
1291 assert_eq!(
1292 recasted,
1293 r#"// Ball Bearing
1294// 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.
1295
1296// Define constants like ball diameter, inside diameter, overhange length, and thickness
1297sphereDia = 0.5
1298insideDia = 1
1299thickness = 0.25
1300overHangLength = .4
1301
1302// Sketch and revolve the inside bearing piece
1303insideRevolve = startSketchOn(XZ)
1304 |> startProfile(at = [insideDia / 2, 0])
1305 |> line(end = [0, thickness + sphereDia / 2])
1306 |> line(end = [overHangLength, 0])
1307 |> line(end = [0, -thickness])
1308 |> line(end = [-overHangLength + thickness, 0])
1309 |> line(end = [0, -sphereDia])
1310 |> line(end = [overHangLength - thickness, 0])
1311 |> line(end = [0, -thickness])
1312 |> line(end = [-overHangLength, 0])
1313 |> close()
1314 |> revolve(axis = Y)
1315
1316// 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)
1317sphere = startSketchOn(XZ)
1318 |> startProfile(at = [
1319 0.05 + insideDia / 2 + thickness,
1320 0 - 0.05
1321 ])
1322 |> line(end = [sphereDia - 0.1, 0])
1323 |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
1324 |> close()
1325 |> revolve(axis = X)
1326 |> patternCircular3d(
1327 axis = [0, 0, 1],
1328 center = [0, 0, 0],
1329 repetitions = 10,
1330 arcDegrees = 360,
1331 rotateDuplicates = true,
1332 )
1333
1334// Sketch and revolve the outside bearing
1335outsideRevolve = startSketchOn(XZ)
1336 |> startProfile(at = [
1337 insideDia / 2 + thickness + sphereDia,
1338 0
1339 ])
1340 |> line(end = [0, sphereDia / 2])
1341 |> line(end = [-overHangLength + thickness, 0])
1342 |> line(end = [0, thickness])
1343 |> line(end = [overHangLength, 0])
1344 |> line(end = [0, -2 * thickness - sphereDia])
1345 |> line(end = [-overHangLength, 0])
1346 |> line(end = [0, thickness])
1347 |> line(end = [overHangLength - thickness, 0])
1348 |> close()
1349 |> revolve(axis = Y)
1350"#
1351 );
1352 }
1353
1354 #[test]
1355 fn test_recast_fn_in_object() {
1356 let some_program_string = r#"bing = { yo = 55 }
1357myNestedVar = [{ prop = callExp(bing.yo) }]
1358"#;
1359 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1360
1361 let recasted = program.recast(&Default::default(), 0);
1362 assert_eq!(recasted, some_program_string);
1363 }
1364
1365 #[test]
1366 fn test_recast_fn_in_array() {
1367 let some_program_string = r#"bing = { yo = 55 }
1368myNestedVar = [callExp(bing.yo)]
1369"#;
1370 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1371
1372 let recasted = program.recast(&Default::default(), 0);
1373 assert_eq!(recasted, some_program_string);
1374 }
1375
1376 #[test]
1377 fn test_recast_ranges() {
1378 let some_program_string = r#"foo = [0..10]
1379ten = 10
1380bar = [0 + 1 .. ten]
1381"#;
1382 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1383
1384 let recasted = program.recast(&Default::default(), 0);
1385 assert_eq!(recasted, some_program_string);
1386 }
1387
1388 #[test]
1389 fn test_recast_space_in_fn_call() {
1390 let some_program_string = r#"fn thing (x) {
1391 return x + 1
1392}
1393
1394thing ( 1 )
1395"#;
1396 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1397
1398 let recasted = program.recast(&Default::default(), 0);
1399 assert_eq!(
1400 recasted,
1401 r#"fn thing(x) {
1402 return x + 1
1403}
1404
1405thing(1)
1406"#
1407 );
1408 }
1409
1410 #[test]
1411 fn test_recast_typed_fn() {
1412 let some_program_string = r#"fn thing(x: string, y: [bool]): number {
1413 return x + 1
1414}
1415"#;
1416 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1417
1418 let recasted = program.recast(&Default::default(), 0);
1419 assert_eq!(recasted, some_program_string);
1420 }
1421
1422 #[test]
1423 fn test_recast_typed_consts() {
1424 let some_program_string = r#"a = 42: number
1425export b = 3.2: number(ft)
1426c = "dsfds": A | B | C
1427d = [1]: [number]
1428e = foo: [number; 3]
1429f = [1, 2, 3]: [number; 1+]
1430"#;
1431 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1432
1433 let recasted = program.recast(&Default::default(), 0);
1434 assert_eq!(recasted, some_program_string);
1435 }
1436
1437 #[test]
1438 fn test_recast_object_fn_in_array_weird_bracket() {
1439 let some_program_string = r#"bing = { yo = 55 }
1440myNestedVar = [
1441 {
1442 prop: line(a = [bing.yo, 21], b = sketch001)
1443}
1444]
1445"#;
1446 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1447
1448 let recasted = program.recast(&Default::default(), 0);
1449 assert_eq!(
1450 recasted,
1451 r#"bing = { yo = 55 }
1452myNestedVar = [
1453 {
1454 prop = line(a = [bing.yo, 21], b = sketch001)
1455}
1456]
1457"#
1458 );
1459 }
1460
1461 #[test]
1462 fn test_recast_empty_file() {
1463 let some_program_string = r#""#;
1464 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1465
1466 let recasted = program.recast(&Default::default(), 0);
1467 assert_eq!(recasted, r#""#);
1469 }
1470
1471 #[test]
1472 fn test_recast_empty_file_new_line() {
1473 let some_program_string = r#"
1474"#;
1475 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1476
1477 let recasted = program.recast(&Default::default(), 0);
1478 assert_eq!(recasted, r#""#);
1480 }
1481
1482 #[test]
1483 fn test_recast_shebang() {
1484 let some_program_string = r#"#!/usr/local/env zoo kcl
1485part001 = startSketchOn(XY)
1486 |> startProfile(at = [-10, -10])
1487 |> line(end = [20, 0])
1488 |> line(end = [0, 20])
1489 |> line(end = [-20, 0])
1490 |> close()
1491"#;
1492
1493 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1494
1495 let recasted = program.recast(&Default::default(), 0);
1496 assert_eq!(
1497 recasted,
1498 r#"#!/usr/local/env zoo kcl
1499
1500part001 = startSketchOn(XY)
1501 |> startProfile(at = [-10, -10])
1502 |> line(end = [20, 0])
1503 |> line(end = [0, 20])
1504 |> line(end = [-20, 0])
1505 |> close()
1506"#
1507 );
1508 }
1509
1510 #[test]
1511 fn test_recast_shebang_new_lines() {
1512 let some_program_string = r#"#!/usr/local/env zoo kcl
1513
1514
1515
1516part001 = startSketchOn(XY)
1517 |> startProfile(at = [-10, -10])
1518 |> line(end = [20, 0])
1519 |> line(end = [0, 20])
1520 |> line(end = [-20, 0])
1521 |> close()
1522"#;
1523
1524 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1525
1526 let recasted = program.recast(&Default::default(), 0);
1527 assert_eq!(
1528 recasted,
1529 r#"#!/usr/local/env zoo kcl
1530
1531part001 = startSketchOn(XY)
1532 |> startProfile(at = [-10, -10])
1533 |> line(end = [20, 0])
1534 |> line(end = [0, 20])
1535 |> line(end = [-20, 0])
1536 |> close()
1537"#
1538 );
1539 }
1540
1541 #[test]
1542 fn test_recast_shebang_with_comments() {
1543 let some_program_string = r#"#!/usr/local/env zoo kcl
1544
1545// Yo yo my comments.
1546part001 = startSketchOn(XY)
1547 |> startProfile(at = [-10, -10])
1548 |> line(end = [20, 0])
1549 |> line(end = [0, 20])
1550 |> line(end = [-20, 0])
1551 |> close()
1552"#;
1553
1554 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1555
1556 let recasted = program.recast(&Default::default(), 0);
1557 assert_eq!(
1558 recasted,
1559 r#"#!/usr/local/env zoo kcl
1560
1561// Yo yo my comments.
1562part001 = startSketchOn(XY)
1563 |> startProfile(at = [-10, -10])
1564 |> line(end = [20, 0])
1565 |> line(end = [0, 20])
1566 |> line(end = [-20, 0])
1567 |> close()
1568"#
1569 );
1570 }
1571
1572 #[test]
1573 fn test_recast_empty_function_body_with_comments() {
1574 let input = r#"fn myFunc() {
1575 // Yo yo my comments.
1576}
1577"#;
1578
1579 let program = crate::parsing::top_level_parse(input).unwrap();
1580 let output = program.recast(&Default::default(), 0);
1581 assert_eq!(output, input);
1582 }
1583
1584 #[test]
1585 fn test_recast_large_file() {
1586 let some_program_string = r#"@settings(units=mm)
1587// define nts
1588radius = 6.0
1589width = 144.0
1590length = 83.0
1591depth = 45.0
1592thk = 5
1593hole_diam = 5
1594// define a rectangular shape func
1595fn rectShape(pos, w, l) {
1596 rr = startSketchOn(XY)
1597 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1598 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1599 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1600 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1601 |> close($edge4)
1602 return rr
1603}
1604// build the body of the focusrite scarlett solo gen 4
1605// only used for visualization
1606scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1607 |> extrude(depth)
1608 |> fillet(
1609 radius = radius,
1610 tags = [
1611 edge2,
1612 edge4,
1613 getOppositeEdge(edge2),
1614 getOppositeEdge(edge4)
1615]
1616 )
1617 // build the bracket sketch around the body
1618fn bracketSketch(w, d, t) {
1619 s = startSketchOn({
1620 plane = {
1621 origin = { x = 0, y = length / 2 + thk, z = 0 },
1622 x_axis = { x = 1, y = 0, z = 0 },
1623 y_axis = { x = 0, y = 0, z = 1 },
1624 z_axis = { x = 0, y = 1, z = 0 }
1625}
1626 })
1627 |> startProfile(at = [-w / 2 - t, d + t])
1628 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1629 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1630 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1631 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1632 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1633 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1634 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1635 |> close($edge8)
1636 return s
1637}
1638// build the body of the bracket
1639bracket_body = bracketSketch(w = width, d = depth, t = thk)
1640 |> extrude(length + 10)
1641 |> fillet(
1642 radius = radius,
1643 tags = [
1644 getNextAdjacentEdge(edge7),
1645 getNextAdjacentEdge(edge2),
1646 getNextAdjacentEdge(edge3),
1647 getNextAdjacentEdge(edge6)
1648]
1649 )
1650 // build the tabs of the mounting bracket (right side)
1651tabs_r = startSketchOn({
1652 plane = {
1653 origin = { x = 0, y = 0, z = depth + thk },
1654 x_axis = { x = 1, y = 0, z = 0 },
1655 y_axis = { x = 0, y = 1, z = 0 },
1656 z_axis = { x = 0, y = 0, z = 1 }
1657}
1658 })
1659 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1660 |> line(end = [10, -5])
1661 |> line(end = [0, -10])
1662 |> line(end = [-10, -5])
1663 |> close()
1664 |> subtract2d(tool = circle(
1665 center = [
1666 width / 2 + thk + hole_diam,
1667 length / 2 - hole_diam
1668 ],
1669 radius = hole_diam / 2
1670 ))
1671 |> extrude(-thk)
1672 |> patternLinear3d(
1673 axis = [0, -1, 0],
1674 repetitions = 1,
1675 distance = length - 10
1676 )
1677 // build the tabs of the mounting bracket (left side)
1678tabs_l = startSketchOn({
1679 plane = {
1680 origin = { x = 0, y = 0, z = depth + thk },
1681 x_axis = { x = 1, y = 0, z = 0 },
1682 y_axis = { x = 0, y = 1, z = 0 },
1683 z_axis = { x = 0, y = 0, z = 1 }
1684}
1685 })
1686 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1687 |> line(end = [-10, -5])
1688 |> line(end = [0, -10])
1689 |> line(end = [10, -5])
1690 |> close()
1691 |> subtract2d(tool = circle(
1692 center = [
1693 -width / 2 - thk - hole_diam,
1694 length / 2 - hole_diam
1695 ],
1696 radius = hole_diam / 2
1697 ))
1698 |> extrude(-thk)
1699 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1700"#;
1701 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1702
1703 let recasted = program.recast(&Default::default(), 0);
1704 assert_eq!(
1706 recasted,
1707 r#"@settings(units = mm)
1708
1709// define nts
1710radius = 6.0
1711width = 144.0
1712length = 83.0
1713depth = 45.0
1714thk = 5
1715hole_diam = 5
1716// define a rectangular shape func
1717fn rectShape(pos, w, l) {
1718 rr = startSketchOn(XY)
1719 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1720 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1721 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1722 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1723 |> close($edge4)
1724 return rr
1725}
1726// build the body of the focusrite scarlett solo gen 4
1727// only used for visualization
1728scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1729 |> extrude(depth)
1730 |> fillet(
1731 radius = radius,
1732 tags = [
1733 edge2,
1734 edge4,
1735 getOppositeEdge(edge2),
1736 getOppositeEdge(edge4)
1737 ],
1738 )
1739// build the bracket sketch around the body
1740fn bracketSketch(w, d, t) {
1741 s = startSketchOn({
1742 plane = {
1743 origin = { x = 0, y = length / 2 + thk, z = 0 },
1744 x_axis = { x = 1, y = 0, z = 0 },
1745 y_axis = { x = 0, y = 0, z = 1 },
1746 z_axis = { x = 0, y = 1, z = 0 }
1747 }
1748 })
1749 |> startProfile(at = [-w / 2 - t, d + t])
1750 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1751 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1752 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1753 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1754 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1755 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1756 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1757 |> close($edge8)
1758 return s
1759}
1760// build the body of the bracket
1761bracket_body = bracketSketch(w = width, d = depth, t = thk)
1762 |> extrude(length + 10)
1763 |> fillet(
1764 radius = radius,
1765 tags = [
1766 getNextAdjacentEdge(edge7),
1767 getNextAdjacentEdge(edge2),
1768 getNextAdjacentEdge(edge3),
1769 getNextAdjacentEdge(edge6)
1770 ],
1771 )
1772// build the tabs of the mounting bracket (right side)
1773tabs_r = startSketchOn({
1774 plane = {
1775 origin = { x = 0, y = 0, z = depth + thk },
1776 x_axis = { x = 1, y = 0, z = 0 },
1777 y_axis = { x = 0, y = 1, z = 0 },
1778 z_axis = { x = 0, y = 0, z = 1 }
1779 }
1780 })
1781 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1782 |> line(end = [10, -5])
1783 |> line(end = [0, -10])
1784 |> line(end = [-10, -5])
1785 |> close()
1786 |> subtract2d(tool = circle(
1787 center = [
1788 width / 2 + thk + hole_diam,
1789 length / 2 - hole_diam
1790 ],
1791 radius = hole_diam / 2,
1792 ))
1793 |> extrude(-thk)
1794 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1795// build the tabs of the mounting bracket (left side)
1796tabs_l = startSketchOn({
1797 plane = {
1798 origin = { x = 0, y = 0, z = depth + thk },
1799 x_axis = { x = 1, y = 0, z = 0 },
1800 y_axis = { x = 0, y = 1, z = 0 },
1801 z_axis = { x = 0, y = 0, z = 1 }
1802 }
1803 })
1804 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1805 |> line(end = [-10, -5])
1806 |> line(end = [0, -10])
1807 |> line(end = [10, -5])
1808 |> close()
1809 |> subtract2d(tool = circle(
1810 center = [
1811 -width / 2 - thk - hole_diam,
1812 length / 2 - hole_diam
1813 ],
1814 radius = hole_diam / 2,
1815 ))
1816 |> extrude(-thk)
1817 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1818"#
1819 );
1820 }
1821
1822 #[test]
1823 fn test_recast_nested_var_declaration_in_fn_body() {
1824 let some_program_string = r#"fn cube(pos, scale) {
1825 sg = startSketchOn(XY)
1826 |> startProfile(at = pos)
1827 |> line(end = [0, scale])
1828 |> line(end = [scale, 0])
1829 |> line(end = [0, -scale])
1830 |> close()
1831 |> extrude(scale)
1832}"#;
1833 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1834
1835 let recasted = program.recast(&Default::default(), 0);
1836 assert_eq!(
1837 recasted,
1838 r#"fn cube(pos, scale) {
1839 sg = startSketchOn(XY)
1840 |> startProfile(at = pos)
1841 |> line(end = [0, scale])
1842 |> line(end = [scale, 0])
1843 |> line(end = [0, -scale])
1844 |> close()
1845 |> extrude(scale)
1846}
1847"#
1848 );
1849 }
1850
1851 #[test]
1852 fn test_as() {
1853 let some_program_string = r#"fn cube(pos, scale) {
1854 x = dfsfs + dfsfsd as y
1855
1856 sg = startSketchOn(XY)
1857 |> startProfile(at = pos) as foo
1858 |> line([0, scale])
1859 |> line([scale, 0]) as bar
1860 |> line([0 as baz, -scale] as qux)
1861 |> close()
1862 |> extrude(length = scale)
1863}
1864
1865cube(pos = 0, scale = 0) as cub
1866"#;
1867 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1868
1869 let recasted = program.recast(&Default::default(), 0);
1870 assert_eq!(recasted, some_program_string,);
1871 }
1872
1873 #[test]
1874 fn test_recast_with_bad_indentation() {
1875 let some_program_string = r#"part001 = startSketchOn(XY)
1876 |> startProfile(at = [0.0, 5.0])
1877 |> line(end = [0.4900857016, -0.0240763666])
1878 |> line(end = [0.6804562304, 0.9087880491])"#;
1879 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1880
1881 let recasted = program.recast(&Default::default(), 0);
1882 assert_eq!(
1883 recasted,
1884 r#"part001 = startSketchOn(XY)
1885 |> startProfile(at = [0.0, 5.0])
1886 |> line(end = [0.4900857016, -0.0240763666])
1887 |> line(end = [0.6804562304, 0.9087880491])
1888"#
1889 );
1890 }
1891
1892 #[test]
1893 fn test_recast_with_bad_indentation_and_inline_comment() {
1894 let some_program_string = r#"part001 = startSketchOn(XY)
1895 |> startProfile(at = [0.0, 5.0])
1896 |> line(end = [0.4900857016, -0.0240763666]) // hello world
1897 |> line(end = [0.6804562304, 0.9087880491])"#;
1898 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1899
1900 let recasted = program.recast(&Default::default(), 0);
1901 assert_eq!(
1902 recasted,
1903 r#"part001 = startSketchOn(XY)
1904 |> startProfile(at = [0.0, 5.0])
1905 |> line(end = [0.4900857016, -0.0240763666]) // hello world
1906 |> line(end = [0.6804562304, 0.9087880491])
1907"#
1908 );
1909 }
1910 #[test]
1911 fn test_recast_with_bad_indentation_and_line_comment() {
1912 let some_program_string = r#"part001 = startSketchOn(XY)
1913 |> startProfile(at = [0.0, 5.0])
1914 |> line(end = [0.4900857016, -0.0240763666])
1915 // hello world
1916 |> line(end = [0.6804562304, 0.9087880491])"#;
1917 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1918
1919 let recasted = program.recast(&Default::default(), 0);
1920 assert_eq!(
1921 recasted,
1922 r#"part001 = startSketchOn(XY)
1923 |> startProfile(at = [0.0, 5.0])
1924 |> line(end = [0.4900857016, -0.0240763666])
1925 // hello world
1926 |> line(end = [0.6804562304, 0.9087880491])
1927"#
1928 );
1929 }
1930
1931 #[test]
1932 fn test_recast_comment_in_a_fn_block() {
1933 let some_program_string = r#"fn myFn() {
1934 // this is a comment
1935 yo = { a = { b = { c = '123' } } } /* block
1936 comment */
1937
1938 key = 'c'
1939 // this is also a comment
1940 return things
1941}"#;
1942 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1943
1944 let recasted = program.recast(&Default::default(), 0);
1945 assert_eq!(
1946 recasted,
1947 r#"fn myFn() {
1948 // this is a comment
1949 yo = { a = { b = { c = '123' } } } /* block
1950 comment */
1951
1952 key = 'c'
1953 // this is also a comment
1954 return things
1955}
1956"#
1957 );
1958 }
1959
1960 #[test]
1961 fn test_recast_comment_under_variable() {
1962 let some_program_string = r#"key = 'c'
1963// this is also a comment
1964thing = 'foo'
1965"#;
1966 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1967
1968 let recasted = program.recast(&Default::default(), 0);
1969 assert_eq!(
1970 recasted,
1971 r#"key = 'c'
1972// this is also a comment
1973thing = 'foo'
1974"#
1975 );
1976 }
1977
1978 #[test]
1979 fn test_recast_multiline_comment_start_file() {
1980 let some_program_string = r#"// hello world
1981// I am a comment
1982key = 'c'
1983// this is also a comment
1984// hello
1985thing = 'foo'
1986"#;
1987 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1988
1989 let recasted = program.recast(&Default::default(), 0);
1990 assert_eq!(
1991 recasted,
1992 r#"// hello world
1993// I am a comment
1994key = 'c'
1995// this is also a comment
1996// hello
1997thing = 'foo'
1998"#
1999 );
2000 }
2001
2002 #[test]
2003 fn test_recast_empty_comment() {
2004 let some_program_string = r#"// hello world
2005//
2006// I am a comment
2007key = 'c'
2008
2009//
2010// I am a comment
2011thing = 'c'
2012
2013foo = 'bar' //
2014"#;
2015 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2016
2017 let recasted = program.recast(&Default::default(), 0);
2018 assert_eq!(
2019 recasted,
2020 r#"// hello world
2021//
2022// I am a comment
2023key = 'c'
2024
2025//
2026// I am a comment
2027thing = 'c'
2028
2029foo = 'bar' //
2030"#
2031 );
2032 }
2033
2034 #[test]
2035 fn test_recast_multiline_comment_under_variable() {
2036 let some_program_string = r#"key = 'c'
2037// this is also a comment
2038// hello
2039thing = 'foo'
2040"#;
2041 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2042
2043 let recasted = program.recast(&Default::default(), 0);
2044 assert_eq!(
2045 recasted,
2046 r#"key = 'c'
2047// this is also a comment
2048// hello
2049thing = 'foo'
2050"#
2051 );
2052 }
2053
2054 #[test]
2055 fn test_recast_only_line_comments() {
2056 let code = r#"// comment at start
2057"#;
2058 let program = crate::parsing::top_level_parse(code).unwrap();
2059
2060 assert_eq!(program.recast(&Default::default(), 0), code);
2061 }
2062
2063 #[test]
2064 fn test_recast_comment_at_start() {
2065 let test_program = r#"
2066/* comment at start */
2067
2068mySk1 = startSketchOn(XY)
2069 |> startProfile(at = [0, 0])"#;
2070 let program = crate::parsing::top_level_parse(test_program).unwrap();
2071
2072 let recasted = program.recast(&Default::default(), 0);
2073 assert_eq!(
2074 recasted,
2075 r#"/* comment at start */
2076
2077mySk1 = startSketchOn(XY)
2078 |> startProfile(at = [0, 0])
2079"#
2080 );
2081 }
2082
2083 #[test]
2084 fn test_recast_lots_of_comments() {
2085 let some_program_string = r#"// comment at start
2086mySk1 = startSketchOn(XY)
2087 |> startProfile(at = [0, 0])
2088 |> line(endAbsolute = [1, 1])
2089 // comment here
2090 |> line(endAbsolute = [0, 1], tag = $myTag)
2091 |> line(endAbsolute = [1, 1])
2092 /* and
2093 here
2094 */
2095 // a comment between pipe expression statements
2096 |> rx(90)
2097 // and another with just white space between others below
2098 |> ry(45)
2099 |> rx(45)
2100// one more for good measure"#;
2101 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2102
2103 let recasted = program.recast(&Default::default(), 0);
2104 assert_eq!(
2105 recasted,
2106 r#"// comment at start
2107mySk1 = startSketchOn(XY)
2108 |> startProfile(at = [0, 0])
2109 |> line(endAbsolute = [1, 1])
2110 // comment here
2111 |> line(endAbsolute = [0, 1], tag = $myTag)
2112 |> line(endAbsolute = [1, 1])
2113 /* and
2114 here */
2115 // a comment between pipe expression statements
2116 |> rx(90)
2117 // and another with just white space between others below
2118 |> ry(45)
2119 |> rx(45)
2120// one more for good measure
2121"#
2122 );
2123 }
2124
2125 #[test]
2126 fn test_recast_multiline_object() {
2127 let some_program_string = r#"x = {
2128 a = 1000000000,
2129 b = 2000000000,
2130 c = 3000000000,
2131 d = 4000000000,
2132 e = 5000000000
2133}"#;
2134 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2135
2136 let recasted = program.recast(&Default::default(), 0);
2137 assert_eq!(recasted.trim(), some_program_string);
2138 }
2139
2140 #[test]
2141 fn test_recast_first_level_object() {
2142 let some_program_string = r#"three = 3
2143
2144yo = {
2145 aStr = 'str',
2146 anum = 2,
2147 identifier = three,
2148 binExp = 4 + 5
2149}
2150yo = [
2151 1,
2152 " 2,",
2153 "three",
2154 4 + 5,
2155 " hey oooooo really long long long"
2156]
2157"#;
2158 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2159
2160 let recasted = program.recast(&Default::default(), 0);
2161 assert_eq!(recasted, some_program_string);
2162 }
2163
2164 #[test]
2165 fn test_recast_new_line_before_comment() {
2166 let some_program_string = r#"
2167// this is a comment
2168yo = { a = { b = { c = '123' } } }
2169
2170key = 'c'
2171things = "things"
2172
2173// this is also a comment"#;
2174 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2175
2176 let recasted = program.recast(&Default::default(), 0);
2177 let expected = some_program_string.trim();
2178 let actual = recasted.trim();
2180 assert_eq!(actual, expected);
2181 }
2182
2183 #[test]
2184 fn test_recast_comment_tokens_inside_strings() {
2185 let some_program_string = r#"b = {
2186 end = 141,
2187 start = 125,
2188 type_ = "NonCodeNode",
2189 value = "
2190 // a comment
2191 "
2192}"#;
2193 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2194
2195 let recasted = program.recast(&Default::default(), 0);
2196 assert_eq!(recasted.trim(), some_program_string.trim());
2197 }
2198
2199 #[test]
2200 fn test_recast_array_new_line_in_pipe() {
2201 let some_program_string = r#"myVar = 3
2202myVar2 = 5
2203myVar3 = 6
2204myAng = 40
2205myAng2 = 134
2206part001 = startSketchOn(XY)
2207 |> startProfile(at = [0, 0])
2208 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2209 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2210 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2211 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2212
2213 let recasted = program.recast(&Default::default(), 0);
2214 assert_eq!(recasted.trim(), some_program_string);
2215 }
2216
2217 #[test]
2218 fn test_recast_array_new_line_in_pipe_custom() {
2219 let some_program_string = r#"myVar = 3
2220myVar2 = 5
2221myVar3 = 6
2222myAng = 40
2223myAng2 = 134
2224part001 = startSketchOn(XY)
2225 |> startProfile(at = [0, 0])
2226 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2227 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2228 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2229"#;
2230 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2231
2232 let recasted = program.recast(
2233 &FormatOptions {
2234 tab_size: 3,
2235 use_tabs: false,
2236 insert_final_newline: true,
2237 },
2238 0,
2239 );
2240 assert_eq!(recasted, some_program_string);
2241 }
2242
2243 #[test]
2244 fn test_recast_after_rename_std() {
2245 let some_program_string = r#"part001 = startSketchOn(XY)
2246 |> startProfile(at = [0.0000000000, 5.0000000000])
2247 |> line(end = [0.4900857016, -0.0240763666])
2248
2249part002 = "part002"
2250things = [part001, 0.0]
2251blah = 1
2252foo = false
2253baz = {a: 1, part001: "thing"}
2254
2255fn ghi(part001) {
2256 return part001
2257}
2258"#;
2259 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2260 program.rename_symbol("mySuperCoolPart", 6);
2261
2262 let recasted = program.recast(&Default::default(), 0);
2263 assert_eq!(
2264 recasted,
2265 r#"mySuperCoolPart = startSketchOn(XY)
2266 |> startProfile(at = [0.0, 5.0])
2267 |> line(end = [0.4900857016, -0.0240763666])
2268
2269part002 = "part002"
2270things = [mySuperCoolPart, 0.0]
2271blah = 1
2272foo = false
2273baz = { a = 1, part001 = "thing" }
2274
2275fn ghi(part001) {
2276 return part001
2277}
2278"#
2279 );
2280 }
2281
2282 #[test]
2283 fn test_recast_after_rename_fn_args() {
2284 let some_program_string = r#"fn ghi(x, y, z) {
2285 return x
2286}"#;
2287 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2288 program.rename_symbol("newName", 7);
2289
2290 let recasted = program.recast(&Default::default(), 0);
2291 assert_eq!(
2292 recasted,
2293 r#"fn ghi(newName, y, z) {
2294 return newName
2295}
2296"#
2297 );
2298 }
2299
2300 #[test]
2301 fn test_recast_trailing_comma() {
2302 let some_program_string = r#"startSketchOn(XY)
2303 |> startProfile(at = [0, 0])
2304 |> arc({
2305 radius = 1,
2306 angle_start = 0,
2307 angle_end = 180,
2308 })"#;
2309 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2310
2311 let recasted = program.recast(&Default::default(), 0);
2312 assert_eq!(
2313 recasted,
2314 r#"startSketchOn(XY)
2315 |> startProfile(at = [0, 0])
2316 |> arc({
2317 radius = 1,
2318 angle_start = 0,
2319 angle_end = 180
2320 })
2321"#
2322 );
2323 }
2324
2325 #[test]
2326 fn test_recast_negative_var() {
2327 let some_program_string = r#"w = 20
2328l = 8
2329h = 10
2330
2331firstExtrude = startSketchOn(XY)
2332 |> startProfile(at = [0,0])
2333 |> line(end = [0, l])
2334 |> line(end = [w, 0])
2335 |> line(end = [0, -l])
2336 |> close()
2337 |> extrude(h)
2338"#;
2339 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2340
2341 let recasted = program.recast(&Default::default(), 0);
2342 assert_eq!(
2343 recasted,
2344 r#"w = 20
2345l = 8
2346h = 10
2347
2348firstExtrude = startSketchOn(XY)
2349 |> startProfile(at = [0, 0])
2350 |> line(end = [0, l])
2351 |> line(end = [w, 0])
2352 |> line(end = [0, -l])
2353 |> close()
2354 |> extrude(h)
2355"#
2356 );
2357 }
2358
2359 #[test]
2360 fn test_recast_multiline_comment() {
2361 let some_program_string = r#"w = 20
2362l = 8
2363h = 10
2364
2365// This is my comment
2366// It has multiple lines
2367// And it's really long
2368firstExtrude = startSketchOn(XY)
2369 |> startProfile(at = [0,0])
2370 |> line(end = [0, l])
2371 |> line(end = [w, 0])
2372 |> line(end = [0, -l])
2373 |> close()
2374 |> extrude(h)
2375"#;
2376 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2377
2378 let recasted = program.recast(&Default::default(), 0);
2379 assert_eq!(
2380 recasted,
2381 r#"w = 20
2382l = 8
2383h = 10
2384
2385// This is my comment
2386// It has multiple lines
2387// And it's really long
2388firstExtrude = startSketchOn(XY)
2389 |> startProfile(at = [0, 0])
2390 |> line(end = [0, l])
2391 |> line(end = [w, 0])
2392 |> line(end = [0, -l])
2393 |> close()
2394 |> extrude(h)
2395"#
2396 );
2397 }
2398
2399 #[test]
2400 fn test_recast_math_start_negative() {
2401 let some_program_string = r#"myVar = -5 + 6"#;
2402 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2403
2404 let recasted = program.recast(&Default::default(), 0);
2405 assert_eq!(recasted.trim(), some_program_string);
2406 }
2407
2408 #[test]
2409 fn test_recast_math_negate_parens() {
2410 let some_program_string = r#"wallMountL = 3.82
2411thickness = 0.5
2412
2413startSketchOn(XY)
2414 |> startProfile(at = [0, 0])
2415 |> line(end = [0, -(wallMountL - thickness)])
2416 |> line(end = [0, -(5 - thickness)])
2417 |> line(end = [0, -(5 - 1)])
2418 |> line(end = [0, -(-5 - 1)])"#;
2419 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2420
2421 let recasted = program.recast(&Default::default(), 0);
2422 assert_eq!(recasted.trim(), some_program_string);
2423 }
2424
2425 #[test]
2426 fn test_recast_math_nested_parens() {
2427 let some_program_string = r#"distance = 5
2428p = 3: Plane
2429FOS = { a = 3, b = 42 }: Sketch
2430sigmaAllow = 8: number(mm)
2431width = 20
2432thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2433 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2434
2435 let recasted = program.recast(&Default::default(), 0);
2436 assert_eq!(recasted.trim(), some_program_string);
2437 }
2438
2439 #[test]
2440 fn no_vardec_keyword() {
2441 let some_program_string = r#"distance = 5"#;
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 recast_types() {
2450 let some_program_string = r#"type foo
2451
2452// A comment
2453@(impl = primitive)
2454export type bar(unit, baz)
2455type baz = Foo | Bar
2456"#;
2457 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2458 let recasted = program.recast(&Default::default(), 0);
2459 assert_eq!(recasted, some_program_string);
2460 }
2461
2462 #[test]
2463 fn recast_nested_fn() {
2464 let some_program_string = r#"fn f() {
2465 return fn() {
2466 return 1
2467}
2468}"#;
2469 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2470 let recasted = program.recast(&Default::default(), 0);
2471 let expected = "\
2472fn f() {
2473 return fn() {
2474 return 1
2475 }
2476}";
2477 assert_eq!(recasted.trim(), expected);
2478 }
2479
2480 #[test]
2481 fn recast_literal() {
2482 use winnow::Parser;
2483 for (i, (raw, expected, reason)) in [
2484 (
2485 "5.0",
2486 "5.0",
2487 "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2488 ),
2489 (
2490 "5",
2491 "5",
2492 "integers should stay integral, i.e. don't reformat this to '5.0'",
2493 ),
2494 (
2495 "5.0000000",
2496 "5.0",
2497 "if the number is f64 but not fractional, use its canonical format",
2498 ),
2499 ("5.1", "5.1", "straightforward case works"),
2500 ]
2501 .into_iter()
2502 .enumerate()
2503 {
2504 let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2505 let literal = crate::parsing::parser::unsigned_number_literal
2506 .parse(tokens.as_slice())
2507 .unwrap();
2508 assert_eq!(
2509 literal.recast(),
2510 expected,
2511 "failed test {i}, which is testing that {reason}"
2512 );
2513 }
2514 }
2515
2516 #[test]
2517 fn recast_objects_no_comments() {
2518 let input = r#"
2519sketch002 = startSketchOn({
2520 plane: {
2521 origin: { x = 1, y = 2, z = 3 },
2522 x_axis = { x = 4, y = 5, z = 6 },
2523 y_axis = { x = 7, y = 8, z = 9 },
2524 z_axis = { x = 10, y = 11, z = 12 }
2525 }
2526 })
2527"#;
2528 let expected = r#"sketch002 = 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 ast = crate::parsing::top_level_parse(input).unwrap();
2538 let actual = ast.recast(&FormatOptions::new(), 0);
2539 assert_eq!(actual, expected);
2540 }
2541
2542 #[test]
2543 fn unparse_fn_unnamed() {
2544 let input = r#"squares_out = reduce(
2545 arr,
2546 n = 0: number,
2547 f = fn(@i, accum) {
2548 return 1
2549 },
2550)
2551"#;
2552 let ast = crate::parsing::top_level_parse(input).unwrap();
2553 let actual = ast.recast(&FormatOptions::new(), 0);
2554 assert_eq!(actual, input);
2555 }
2556
2557 #[test]
2558 fn unparse_fn_named() {
2559 let input = r#"fn f(x) {
2560 return 1
2561}
2562"#;
2563 let ast = crate::parsing::top_level_parse(input).unwrap();
2564 let actual = ast.recast(&FormatOptions::new(), 0);
2565 assert_eq!(actual, input);
2566 }
2567
2568 #[test]
2569 fn unparse_call_inside_function_single_line() {
2570 let input = r#"fn foo() {
2571 toDegrees(atan(0.5), foo = 1)
2572 return 0
2573}
2574"#;
2575 let ast = crate::parsing::top_level_parse(input).unwrap();
2576 let actual = ast.recast(&FormatOptions::new(), 0);
2577 assert_eq!(actual, input);
2578 }
2579
2580 #[test]
2581 fn recast_function_types() {
2582 let input = r#"foo = x: fn
2583foo = x: fn(number)
2584fn foo(x: fn(): number): fn {
2585 return 0
2586}
2587fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
2588 return 0
2589}
2590type fn
2591type foo = fn
2592type foo = fn(a: string, b: { f: fn(): any })
2593type foo = fn([fn])
2594type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
2595"#;
2596 let ast = crate::parsing::top_level_parse(input).unwrap();
2597 let actual = ast.recast(&FormatOptions::new(), 0);
2598 assert_eq!(actual, input);
2599 }
2600
2601 #[test]
2602 fn unparse_call_inside_function_args_multiple_lines() {
2603 let input = r#"fn foo() {
2604 toDegrees(
2605 atan(0.5),
2606 foo = 1,
2607 bar = 2,
2608 baz = 3,
2609 qux = 4,
2610 )
2611 return 0
2612}
2613"#;
2614 let ast = crate::parsing::top_level_parse(input).unwrap();
2615 let actual = ast.recast(&FormatOptions::new(), 0);
2616 assert_eq!(actual, input);
2617 }
2618
2619 #[test]
2620 fn unparse_call_inside_function_single_arg_multiple_lines() {
2621 let input = r#"fn foo() {
2622 toDegrees(
2623 [
2624 profile0,
2625 profile1,
2626 profile2,
2627 profile3,
2628 profile4,
2629 profile5
2630 ],
2631 key = 1,
2632 )
2633 return 0
2634}
2635"#;
2636 let ast = crate::parsing::top_level_parse(input).unwrap();
2637 let actual = ast.recast(&FormatOptions::new(), 0);
2638 assert_eq!(actual, input);
2639 }
2640
2641 #[test]
2642 fn recast_objects_with_comments() {
2643 use winnow::Parser;
2644 for (i, (input, expected, reason)) in [(
2645 "\
2646{
2647 a = 1,
2648 // b = 2,
2649 c = 3
2650}",
2651 "\
2652{
2653 a = 1,
2654 // b = 2,
2655 c = 3
2656}",
2657 "preserves comments",
2658 )]
2659 .into_iter()
2660 .enumerate()
2661 {
2662 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2663 crate::parsing::parser::print_tokens(tokens.as_slice());
2664 let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
2665 assert_eq!(
2666 expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2667 expected,
2668 "failed test {i}, which is testing that recasting {reason}"
2669 );
2670 }
2671 }
2672
2673 #[test]
2674 fn recast_array_with_comments() {
2675 use winnow::Parser;
2676 for (i, (input, expected, reason)) in [
2677 (
2678 "\
2679[
2680 1,
2681 2,
2682 3,
2683 4,
2684 5,
2685 6,
2686 7,
2687 8,
2688 9,
2689 10,
2690 11,
2691 12,
2692 13,
2693 14,
2694 15,
2695 16,
2696 17,
2697 18,
2698 19,
2699 20,
2700]",
2701 "\
2702[
2703 1,
2704 2,
2705 3,
2706 4,
2707 5,
2708 6,
2709 7,
2710 8,
2711 9,
2712 10,
2713 11,
2714 12,
2715 13,
2716 14,
2717 15,
2718 16,
2719 17,
2720 18,
2721 19,
2722 20
2723]",
2724 "preserves multi-line arrays",
2725 ),
2726 (
2727 "\
2728[
2729 1,
2730 // 2,
2731 3
2732]",
2733 "\
2734[
2735 1,
2736 // 2,
2737 3
2738]",
2739 "preserves comments",
2740 ),
2741 (
2742 "\
2743[
2744 1,
2745 2,
2746 // 3
2747]",
2748 "\
2749[
2750 1,
2751 2,
2752 // 3
2753]",
2754 "preserves comments at the end of the array",
2755 ),
2756 ]
2757 .into_iter()
2758 .enumerate()
2759 {
2760 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2761 let expr = crate::parsing::parser::array_elem_by_elem
2762 .parse(tokens.as_slice())
2763 .unwrap();
2764 assert_eq!(
2765 expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2766 expected,
2767 "failed test {i}, which is testing that recasting {reason}"
2768 );
2769 }
2770 }
2771
2772 #[test]
2773 fn code_with_comment_and_extra_lines() {
2774 let code = r#"yo = 'c'
2775
2776/* this is
2777a
2778comment */
2779yo = 'bing'
2780"#;
2781 let ast = crate::parsing::top_level_parse(code).unwrap();
2782 let recasted = ast.recast(&FormatOptions::new(), 0);
2783 assert_eq!(recasted, code);
2784 }
2785
2786 #[test]
2787 fn comments_in_a_fn_block() {
2788 let code = r#"fn myFn() {
2789 // this is a comment
2790 yo = { a = { b = { c = '123' } } }
2791
2792 /* block
2793 comment */
2794 key = 'c'
2795 // this is also a comment
2796}
2797"#;
2798 let ast = crate::parsing::top_level_parse(code).unwrap();
2799 let recasted = ast.recast(&FormatOptions::new(), 0);
2800 assert_eq!(recasted, code);
2801 }
2802
2803 #[test]
2804 fn array_range_end_exclusive() {
2805 let code = "myArray = [0..<4]\n";
2806 let ast = crate::parsing::top_level_parse(code).unwrap();
2807 let recasted = ast.recast(&FormatOptions::new(), 0);
2808 assert_eq!(recasted, code);
2809 }
2810}