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