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