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