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