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