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