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