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