1use std::fmt::Write;
2
3use crate::KclError;
4use crate::ModuleId;
5use crate::parsing::DeprecationKind;
6use crate::parsing::PIPE_OPERATOR;
7use crate::parsing::ast::types::Annotation;
8use crate::parsing::ast::types::ArrayExpression;
9use crate::parsing::ast::types::ArrayRangeExpression;
10use crate::parsing::ast::types::AscribedExpression;
11use crate::parsing::ast::types::Associativity;
12use crate::parsing::ast::types::BinaryExpression;
13use crate::parsing::ast::types::BinaryOperator;
14use crate::parsing::ast::types::BinaryPart;
15use crate::parsing::ast::types::Block;
16use crate::parsing::ast::types::BodyItem;
17use crate::parsing::ast::types::CallExpressionKw;
18use crate::parsing::ast::types::CommentStyle;
19use crate::parsing::ast::types::DefaultParamVal;
20use crate::parsing::ast::types::EnumDeclaration;
21use crate::parsing::ast::types::Expr;
22use crate::parsing::ast::types::FormatOptions;
23use crate::parsing::ast::types::FunctionExpression;
24use crate::parsing::ast::types::Identifier;
25use crate::parsing::ast::types::IfExpression;
26use crate::parsing::ast::types::ImportSelector;
27use crate::parsing::ast::types::ImportStatement;
28use crate::parsing::ast::types::ItemVisibility;
29use crate::parsing::ast::types::LabeledArg;
30use crate::parsing::ast::types::Literal;
31use crate::parsing::ast::types::LiteralValue;
32use crate::parsing::ast::types::MemberExpression;
33use crate::parsing::ast::types::Name;
34use crate::parsing::ast::types::Node;
35use crate::parsing::ast::types::NodeList;
36use crate::parsing::ast::types::NonCodeMeta;
37use crate::parsing::ast::types::NonCodeNode;
38use crate::parsing::ast::types::NonCodeValue;
39use crate::parsing::ast::types::NumericLiteral;
40use crate::parsing::ast::types::ObjectExpression;
41use crate::parsing::ast::types::Parameter;
42use crate::parsing::ast::types::PipeExpression;
43use crate::parsing::ast::types::Program;
44use crate::parsing::ast::types::SketchBlock;
45use crate::parsing::ast::types::SketchVar;
46use crate::parsing::ast::types::TagDeclarator;
47use crate::parsing::ast::types::TypeDeclaration;
48use crate::parsing::ast::types::TypeDeclarationDefinition;
49use crate::parsing::ast::types::UnaryExpression;
50use crate::parsing::ast::types::VariableDeclaration;
51use crate::parsing::ast::types::VariableKind;
52use crate::parsing::deprecation;
53
54#[allow(dead_code)]
55pub fn fmt(input: &str) -> Result<String, KclError> {
56 let program = crate::parsing::parse_str(input, ModuleId::default()).parse_errs_as_err()?;
57 Ok(program.recast_top(&Default::default(), 0))
58}
59
60impl Program {
61 pub fn recast_top(&self, options: &FormatOptions, indentation_level: usize) -> String {
62 let mut buf = String::with_capacity(1024);
63 self.recast(&mut buf, options, indentation_level);
64 buf
65 }
66
67 pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
68 if let Some(sh) = self.shebang.as_ref() {
69 write!(buf, "{}\n\n", sh.inner.content).no_fail();
70 }
71
72 recast_body(
73 &self.body,
74 &self.non_code_meta,
75 &self.inner_attrs,
76 buf,
77 options,
78 indentation_level,
79 );
80 }
81}
82
83fn recast_body(
84 items: &[BodyItem],
85 non_code_meta: &NonCodeMeta,
86 inner_attrs: &NodeList<Annotation>,
87 buf: &mut String,
88 options: &FormatOptions,
89 indentation_level: usize,
90) {
91 let indentation = options.get_indentation(indentation_level);
92
93 let has_non_newline_start_node = non_code_meta
94 .start_nodes
95 .iter()
96 .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
97 if has_non_newline_start_node {
98 let mut pending_newline = false;
99 for start_node in &non_code_meta.start_nodes {
100 match start_node.value {
101 NonCodeValue::NewLine => pending_newline = true,
102 _ => {
103 if pending_newline {
104 if buf.ends_with('\n') {
106 buf.push('\n');
107 } else {
108 buf.push_str("\n\n");
109 }
110 pending_newline = false;
111 }
112 let noncode_recast = start_node.recast(options, indentation_level);
113 buf.push_str(&noncode_recast);
114 }
115 }
116 }
117 if pending_newline {
119 if buf.ends_with('\n') {
120 buf.push('\n');
121 } else {
122 buf.push_str("\n\n");
123 }
124 }
125 }
126
127 for attr in inner_attrs {
128 options.write_indentation(buf, indentation_level);
129 attr.recast(buf, options, indentation_level);
130 }
131 if !inner_attrs.is_empty() {
132 buf.push('\n');
133 }
134
135 let body_item_lines = items.iter().map(|body_item| {
136 let mut result = String::with_capacity(256);
137 for comment in body_item.get_comments() {
138 if !comment.is_empty() {
139 result.push_str(&indentation);
140 result.push_str(comment);
141 }
142 if comment.is_empty() && !result.ends_with("\n") {
143 result.push('\n');
144 }
145 if !result.ends_with("\n\n") && result != "\n" {
146 result.push('\n');
147 }
148 }
149 for attr in body_item.get_attrs() {
150 attr.recast(&mut result, options, indentation_level);
151 }
152 match body_item {
153 BodyItem::ImportStatement(stmt) => {
154 result.push_str(&stmt.recast(options, indentation_level));
155 }
156 BodyItem::ExpressionStatement(expression_statement) => {
157 let mut tmp_buf = String::new();
158 expression_statement
159 .expression
160 .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
161 options.write_indentation(&mut result, indentation_level);
162 result.push_str(tmp_buf.trim_start());
163 }
164 BodyItem::VariableDeclaration(variable_declaration) => {
165 variable_declaration.recast(&mut result, options, indentation_level);
166 }
167 BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(&mut result, options, indentation_level),
168 BodyItem::ReturnStatement(return_statement) => {
169 write!(&mut result, "{indentation}return ").no_fail();
170 let mut tmp_buf = String::with_capacity(256);
171 return_statement
172 .argument
173 .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
174 write!(&mut result, "{}", tmp_buf.trim_start()).no_fail();
175 }
176 };
177 result
178 });
179 for (index, recast_str) in body_item_lines.enumerate() {
180 write!(buf, "{recast_str}").no_fail();
181
182 let needs_line_break = !(index == items.len() - 1 && indentation_level == 0);
185
186 let custom_white_space_or_comment = non_code_meta.non_code_nodes.get(&index).map(|noncodes| {
187 noncodes.iter().enumerate().map(|(i, custom_white_space_or_comment)| {
188 let formatted = custom_white_space_or_comment.recast(options, indentation_level);
189 if i == 0 && !formatted.trim().is_empty() {
190 if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
191 format!("\n{formatted}")
192 } else {
193 formatted
194 }
195 } else {
196 formatted
197 }
198 })
199 });
200
201 if let Some(custom) = custom_white_space_or_comment {
202 for to_write in custom {
203 write!(buf, "{to_write}").no_fail();
204 }
205 } else if needs_line_break {
206 buf.push('\n')
207 }
208 }
209 trim_end(buf);
210
211 if options.insert_final_newline && !buf.is_empty() {
213 buf.push('\n');
214 }
215}
216
217impl NonCodeValue {
218 fn should_cause_array_newline(&self) -> bool {
219 match self {
220 Self::InlineComment { .. } => false,
221 Self::BlockComment { .. } | Self::NewLine => true,
222 }
223 }
224}
225
226impl Node<NonCodeNode> {
227 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
228 let indentation = options.get_indentation(indentation_level);
229 match &self.value {
230 NonCodeValue::InlineComment {
231 value,
232 style: CommentStyle::Line,
233 } => format!(" // {value}\n"),
234 NonCodeValue::InlineComment {
235 value,
236 style: CommentStyle::Block,
237 } => format!(" /* {value} */"),
238 NonCodeValue::BlockComment { value, style } => match style {
239 CommentStyle::Block => format!("{indentation}/* {value} */"),
240 CommentStyle::Line => {
241 if value.trim().is_empty() {
242 format!("{indentation}//\n")
243 } else {
244 format!("{}// {}\n", indentation, value.trim())
245 }
246 }
247 },
248 NonCodeValue::NewLine => "\n\n".to_string(),
249 }
250 }
251}
252
253impl Node<Annotation> {
254 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
255 let indentation = options.get_indentation(indentation_level);
256 let mut result = String::new();
257 for comment in &self.pre_comments {
258 if !comment.is_empty() {
259 result.push_str(&indentation);
260 result.push_str(comment);
261 }
262 if !result.ends_with("\n\n") && result != "\n" {
263 result.push('\n');
264 }
265 }
266 result.push('@');
267 if let Some(name) = &self.name {
268 result.push_str(&name.name);
269 }
270 if let Some(properties) = &self.properties {
271 result.push('(');
272 result.push_str(
273 &properties
274 .iter()
275 .map(|prop| {
276 let mut temp = format!("{} = ", prop.key.name);
277 prop.value
278 .recast(&mut temp, options, indentation_level + 1, ExprContext::Other);
279 temp.trim().to_owned()
280 })
281 .collect::<Vec<String>>()
282 .join(", "),
283 );
284 result.push(')');
285 result.push('\n');
286 }
287
288 buf.push_str(&result)
289 }
290}
291
292impl ImportStatement {
293 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
294 let indentation = options.get_indentation(indentation_level);
295 let vis = if self.visibility == ItemVisibility::Export {
296 "export "
297 } else {
298 ""
299 };
300 let mut string = format!("{vis}{indentation}import ");
301 match &self.selector {
302 ImportSelector::List { items } => {
303 for (i, item) in items.iter().enumerate() {
304 if i > 0 {
305 string.push_str(", ");
306 }
307 string.push_str(&item.name.name);
308 if let Some(alias) = &item.alias {
309 if item.name.name != alias.name {
311 string.push_str(&format!(" as {}", alias.name));
312 }
313 }
314 }
315 string.push_str(" from ");
316 }
317 ImportSelector::Glob(_) => string.push_str("* from "),
318 ImportSelector::None { .. } => {}
319 }
320 string.push_str(&format!("\"{}\"", self.path));
321
322 if let ImportSelector::None { alias: Some(alias) } = &self.selector {
323 string.push_str(" as ");
324 string.push_str(&alias.name);
325 }
326 string
327 }
328}
329
330#[derive(Copy, Clone, Debug, Eq, PartialEq)]
331pub(crate) enum ExprContext {
332 Pipe,
333 PipeHead,
336 FnDecl,
337 PipeCallArg,
339 CallArg,
341 Other,
342}
343
344impl ExprContext {
345 fn in_pipe(self) -> bool {
346 matches!(self, ExprContext::Pipe | ExprContext::PipeCallArg)
347 }
348
349 fn needs_leading_indent(self) -> bool {
350 !matches!(
351 self,
352 ExprContext::PipeHead | ExprContext::CallArg | ExprContext::PipeCallArg
353 )
354 }
355
356 fn call_arg_context(self) -> ExprContext {
357 if self.in_pipe() {
358 ExprContext::PipeCallArg
359 } else {
360 ExprContext::CallArg
361 }
362 }
363}
364
365impl Expr {
366 pub(crate) fn recast(
367 &self,
368 buf: &mut String,
369 options: &FormatOptions,
370 indentation_level: usize,
371 mut ctxt: ExprContext,
372 ) {
373 let is_decl = matches!(ctxt, ExprContext::FnDecl);
374 if is_decl {
375 ctxt = ExprContext::Other;
379 }
380 match &self {
381 Expr::BinaryExpression(bin_exp) => bin_exp.recast(buf, options, indentation_level, ctxt),
382 Expr::ArrayExpression(array_exp) => array_exp.recast(buf, options, indentation_level, ctxt),
383 Expr::ArrayRangeExpression(range_exp) => range_exp.recast(buf, options, indentation_level, ctxt),
384 Expr::ObjectExpression(obj_exp) => obj_exp.recast(buf, options, indentation_level, ctxt),
385 Expr::MemberExpression(mem_exp) => mem_exp.recast(buf, options, indentation_level, ctxt),
386 Expr::Literal(literal) => {
387 literal.recast(buf);
388 }
389 Expr::FunctionExpression(func_exp) => {
390 if !is_decl {
391 buf.push_str("fn");
392 if let Some(name) = &func_exp.name {
393 buf.push(' ');
394 buf.push_str(&name.name);
395 }
396 }
397 func_exp.recast(buf, options, indentation_level);
398 }
399 Expr::CallExpressionKw(call_exp) => call_exp.recast(buf, options, indentation_level, ctxt),
400 Expr::Name(name) => {
401 let result = &name.inner.name.inner.name;
402 match deprecation(result, DeprecationKind::Const) {
403 Some(suggestion) => buf.push_str(suggestion),
404 None => {
405 for prefix in &name.path {
406 buf.push_str(&prefix.name);
407 buf.push(':');
408 buf.push(':');
409 }
410 buf.push_str(result);
411 }
412 }
413 }
414 Expr::TagDeclarator(tag) => tag.recast(buf),
415 Expr::PipeExpression(pipe_exp) => {
416 pipe_exp.recast(buf, options, indentation_level, !is_decl && ctxt.needs_leading_indent())
417 }
418 Expr::UnaryExpression(unary_exp) => unary_exp.recast(buf, options, indentation_level, ctxt),
419 Expr::IfExpression(e) => e.recast(buf, options, indentation_level, ctxt),
420 Expr::PipeSubstitution(_) => buf.push_str(crate::parsing::PIPE_SUBSTITUTION_OPERATOR),
421 Expr::LabelledExpression(e) => {
422 e.expr.recast(buf, options, indentation_level, ctxt);
423 buf.push_str(" as ");
424 buf.push_str(&e.label.name);
425 }
426 Expr::AscribedExpression(e) => e.recast(buf, options, indentation_level, ctxt),
427 Expr::SketchBlock(e) => e.recast(buf, options, indentation_level, ctxt),
428 Expr::SketchVar(e) => e.recast(buf),
429 Expr::None(_) => {
430 unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
431 }
432 }
433 }
434}
435
436impl AscribedExpression {
437 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
438 if matches!(
439 self.expr,
440 Expr::BinaryExpression(..) | Expr::PipeExpression(..) | Expr::UnaryExpression(..)
441 ) {
442 buf.push('(');
443 self.expr.recast(buf, options, indentation_level, ctxt);
444 buf.push(')');
445 } else {
446 self.expr.recast(buf, options, indentation_level, ctxt);
447 }
448 buf.push_str(": ");
449 write!(buf, "{}", self.ty).no_fail();
450 }
451}
452
453impl BinaryPart {
454 pub(crate) fn recast(
455 &self,
456 buf: &mut String,
457 options: &FormatOptions,
458 indentation_level: usize,
459 ctxt: ExprContext,
460 ) {
461 match &self {
462 BinaryPart::Literal(literal) => {
463 literal.recast(buf);
464 }
465 BinaryPart::Name(name) => match deprecation(&name.inner.name.inner.name, DeprecationKind::Const) {
466 Some(suggestion) => write!(buf, "{suggestion}").no_fail(),
467 None => name.write_to(buf).no_fail(),
468 },
469 BinaryPart::BinaryExpression(binary_expression) => {
470 binary_expression.recast(buf, options, indentation_level, ctxt)
471 }
472 BinaryPart::CallExpressionKw(call_expression) => {
473 call_expression.recast(buf, options, indentation_level, ExprContext::Other)
474 }
475 BinaryPart::UnaryExpression(unary_expression) => {
476 unary_expression.recast(buf, options, indentation_level, ctxt)
477 }
478 BinaryPart::MemberExpression(member_expression) => {
479 member_expression.recast(buf, options, indentation_level, ctxt)
480 }
481 BinaryPart::ArrayExpression(e) => e.recast(buf, options, indentation_level, ctxt),
482 BinaryPart::ArrayRangeExpression(e) => e.recast(buf, options, indentation_level, ctxt),
483 BinaryPart::ObjectExpression(e) => e.recast(buf, options, indentation_level, ctxt),
484 BinaryPart::IfExpression(e) => e.recast(buf, options, indentation_level, ExprContext::Other),
485 BinaryPart::AscribedExpression(e) => e.recast(buf, options, indentation_level, ExprContext::Other),
486 BinaryPart::SketchVar(e) => e.recast(buf),
487 }
488 }
489}
490
491impl CallExpressionKw {
492 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
493 recast_call(
494 &self.callee,
495 self.unlabeled.as_ref(),
496 &self.arguments,
497 &self.non_code_meta,
498 buf,
499 options,
500 indentation_level,
501 ctxt,
502 );
503 }
504}
505
506fn recast_args(
507 unlabeled: Option<&Expr>,
508 arguments: &[LabeledArg],
509 options: &FormatOptions,
510 indentation_level: usize,
511 ctxt: ExprContext,
512) -> Vec<String> {
513 let arg_ctxt = ctxt.call_arg_context();
514 let mut arg_list = if let Some(first_arg) = unlabeled {
515 let mut first = String::with_capacity(256);
516 first_arg.recast(&mut first, options, indentation_level, arg_ctxt);
517 vec![first.trim().to_owned()]
518 } else {
519 Vec::with_capacity(arguments.len())
520 };
521 arg_list.extend(arguments.iter().map(|arg| {
522 let mut buf = String::with_capacity(256);
523 arg.recast(&mut buf, options, indentation_level, arg_ctxt);
524 buf
525 }));
526 arg_list
527}
528
529#[allow(clippy::too_many_arguments)]
530fn recast_call(
531 callee: &Name,
532 unlabeled: Option<&Expr>,
533 arguments: &[LabeledArg],
534 non_code_meta: &NonCodeMeta,
535 buf: &mut String,
536 options: &FormatOptions,
537 indentation_level: usize,
538 ctxt: ExprContext,
539) {
540 let smart_indent_level = if ctxt.in_pipe() { 0 } else { indentation_level };
541 let name = callee;
542
543 if let Some(suggestion) = deprecation(&name.name.inner.name, DeprecationKind::Function) {
544 options.write_indentation(buf, smart_indent_level);
545 return write!(buf, "{suggestion}").no_fail();
546 }
547
548 struct FormatItem {
551 text: String,
552 is_arg: bool,
553 }
554
555 let build_items = |arg_indent: usize| -> Vec<FormatItem> {
562 let arg_list = recast_args(unlabeled, arguments, options, arg_indent, ctxt);
563 let mut arg_iter = arg_list.into_iter();
564 let mut items = Vec::with_capacity(arguments.len() + non_code_meta.non_code_nodes_len() + 1);
565 if unlabeled.is_some()
566 && let Some(first_arg) = arg_iter.next()
567 {
568 items.push(FormatItem {
569 text: first_arg,
570 is_arg: true,
571 });
572 }
573 let num_items = arguments.len() + non_code_meta.non_code_nodes_len();
574 let num_slots = non_code_meta
575 .non_code_nodes
576 .keys()
577 .max()
578 .map_or(num_items, |max| num_items.max(max + 1));
579 let mut pending_block_comments = String::new();
582 for i in 0..num_slots {
583 if let Some(noncode) = non_code_meta.non_code_nodes.get(&i) {
584 for nc in noncode {
585 match &nc.value {
586 NonCodeValue::BlockComment {
587 style: CommentStyle::Block,
588 ..
589 }
590 | NonCodeValue::InlineComment {
591 style: CommentStyle::Block,
592 ..
593 } => {
594 pending_block_comments.push_str(nc.recast(options, 0).trim());
595 pending_block_comments.push(' ');
596 }
597 _ => {
598 let mut text = std::mem::take(&mut pending_block_comments);
601 text.push_str(nc.recast(options, 0).trim_end_matches('\n'));
602 items.push(FormatItem {
603 text: text.trim().to_owned(),
604 is_arg: false,
605 });
606 }
607 }
608 }
609 } else if let Some(arg) = arg_iter.next() {
610 let mut text = std::mem::take(&mut pending_block_comments);
611 text.push_str(&arg);
612 items.push(FormatItem { text, is_arg: true });
613 }
614 }
615 items.extend(arg_iter.map(|arg| FormatItem {
616 text: arg,
617 is_arg: true,
618 }));
619 if !pending_block_comments.is_empty() {
621 items.push(FormatItem {
622 text: pending_block_comments.trim_end().to_owned(),
623 is_arg: false,
624 });
625 }
626 items
627 };
628
629 let items = build_items(indentation_level);
630 let has_lots_of_args = items.iter().filter(|item| item.is_arg).count() >= 4;
631 let has_own_line_comment = items.iter().any(|item| !item.is_arg);
633 let some_arg_is_already_multiline = items.len() > 1 && items.iter().any(|item| item.text.contains('\n'));
634 let multiline = has_lots_of_args || some_arg_is_already_multiline || has_own_line_comment;
635 if multiline {
636 let next_indent = indentation_level + 1;
637 let inner_indentation = if ctxt.in_pipe() {
638 options.get_indentation_offset_pipe(next_indent)
639 } else {
640 options.get_indentation(next_indent)
641 };
642 let items = build_items(next_indent);
643 let end_indent = if ctxt.in_pipe() {
644 options.get_indentation_offset_pipe(indentation_level)
645 } else {
646 options.get_indentation(indentation_level)
647 };
648 if ctxt.needs_leading_indent() {
649 options.write_indentation(buf, smart_indent_level);
650 }
651 name.write_to(buf).no_fail();
652 buf.push('(');
653 buf.push('\n');
654 for item in items {
655 if item.is_arg {
656 writeln!(buf, "{inner_indentation}{},", item.text).no_fail();
657 } else if item.text.is_empty() {
658 buf.push('\n');
660 } else {
661 writeln!(buf, "{inner_indentation}{}", item.text).no_fail();
662 }
663 }
664 write!(buf, "{end_indent}").no_fail();
665 buf.push(')');
666 } else {
667 if ctxt.needs_leading_indent() {
668 options.write_indentation(buf, smart_indent_level);
669 }
670 name.write_to(buf).no_fail();
671 buf.push('(');
672 let args = items
673 .iter()
674 .map(|item| item.text.as_str())
675 .collect::<Vec<_>>()
676 .join(", ");
677 write!(buf, "{args}").no_fail();
678 buf.push(')');
679 }
680}
681
682impl LabeledArg {
683 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
684 if let Some(l) = &self.label {
685 buf.push_str(&l.name);
686 buf.push_str(" = ");
687 }
688 self.arg.recast(buf, options, indentation_level, ctxt);
689 }
690}
691
692impl VariableDeclaration {
693 pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
694 options.write_indentation(buf, indentation_level);
695 match self.visibility {
696 ItemVisibility::Default => {}
697 ItemVisibility::Export => buf.push_str("export "),
698 };
699
700 let (keyword, eq, ctxt) = match self.kind {
701 VariableKind::Fn => ("fn ", "", ExprContext::FnDecl),
702 VariableKind::Const => ("", " = ", ExprContext::Other),
703 };
704 buf.push_str(keyword);
705 buf.push_str(&self.declaration.id.name);
706 buf.push_str(eq);
707
708 let mut tmp_buf = String::new();
714 self.declaration
715 .init
716 .recast(&mut tmp_buf, options, indentation_level, ctxt);
717 buf.push_str(tmp_buf.trim_start());
718 }
719}
720
721impl TypeDeclaration {
722 pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
723 options.write_indentation(buf, indentation_level);
724 match self.visibility {
725 ItemVisibility::Default => {}
726 ItemVisibility::Export => buf.push_str("export "),
727 };
728 buf.push_str("type ");
729 buf.push_str(&self.name.name);
730
731 if let Some(args) = &self.args {
732 buf.push('(');
733 for (i, a) in args.iter().enumerate() {
734 buf.push_str(&a.name);
735 if i < args.len() - 1 {
736 buf.push_str(", ");
737 }
738 }
739 buf.push(')');
740 }
741 match &self.definition {
742 TypeDeclarationDefinition::Bare => {}
743 TypeDeclarationDefinition::Alias { ty } => {
744 buf.push_str(" = ");
745 write!(buf, "{ty}").no_fail();
746 }
747 TypeDeclarationDefinition::Enum(e) => e.recast(buf, options, indentation_level),
748 }
749 }
750}
751
752impl EnumDeclaration {
753 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
754 let has_start_comment = self
759 .non_code_meta
760 .start_nodes
761 .iter()
762 .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
763
764 if self.variants.is_empty() && !has_start_comment {
767 buf.push_str(" { | }");
768 return;
769 }
770
771 let body_level = indentation_level + 1;
772 let body_indentation = options.get_indentation(body_level);
773 buf.push_str(" {\n");
774
775 if has_start_comment {
778 let mut pending_newline = false;
779 for start_node in &self.non_code_meta.start_nodes {
780 if matches!(start_node.value, NonCodeValue::NewLine) {
781 pending_newline = true;
782 continue;
783 }
784 if pending_newline {
785 buf.push('\n');
786 pending_newline = false;
787 }
788 buf.push_str(&start_node.recast(options, body_level));
789 if !buf.ends_with('\n') {
790 buf.push('\n');
791 }
792 }
793 if pending_newline {
794 buf.push('\n');
795 }
796 }
797
798 if self.variants.is_empty() {
799 buf.push_str(&body_indentation);
800 buf.push_str("|\n");
801 }
802
803 for (index, variant) in self.variants.iter().enumerate() {
804 let mut arm = String::new();
807 for comment in &variant.pre_comments {
808 if !comment.is_empty() {
809 arm.push_str(&body_indentation);
810 arm.push_str(comment);
811 }
812 if comment.is_empty() && !arm.ends_with('\n') {
813 arm.push('\n');
814 }
815 if !arm.ends_with("\n\n") && arm != "\n" {
816 arm.push('\n');
817 }
818 }
819 arm.push_str(&body_indentation);
820 arm.push_str("| ");
821 arm.push_str(&variant.name.name);
822 buf.push_str(&arm);
823
824 if let Some(noncodes) = self.non_code_meta.non_code_nodes.get(&index) {
827 for (i, noncode) in noncodes.iter().enumerate() {
828 let formatted = noncode.recast(options, body_level);
829 if i == 0
830 && !formatted.trim().is_empty()
831 && matches!(noncode.value, NonCodeValue::BlockComment { .. })
832 {
833 buf.push('\n');
834 }
835 buf.push_str(&formatted);
836 }
837 if !buf.ends_with('\n') {
838 buf.push('\n');
839 }
840 } else {
841 buf.push('\n');
842 }
843 }
844
845 options.write_indentation(buf, indentation_level);
846 buf.push('}');
847 }
848}
849
850fn write<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Display) {
851 f.write_fmt(format_args!("{s}"))
852 .expect("writing to a string should always succeed")
853}
854
855fn write_dbg<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Debug) {
856 f.write_fmt(format_args!("{s:?}"))
857 .expect("writing to a string should always succeed")
858}
859
860impl NumericLiteral {
861 fn recast(&self, buf: &mut String) {
862 if self.raw.contains('.') && self.value.fract() == 0.0 {
863 write_dbg(buf, self.value);
864 write(buf, self.suffix);
865 } else {
866 write(buf, &self.raw);
867 }
868 }
869}
870
871impl Literal {
872 fn recast(&self, buf: &mut String) {
873 match self.value {
874 LiteralValue::Number { value, suffix } => {
875 if self.raw.contains('.') && value.fract() == 0.0 {
876 write_dbg(buf, value);
877 write(buf, suffix);
878 } else {
879 write(buf, &self.raw);
880 }
881 }
882 LiteralValue::String(ref s) => {
883 if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
884 return write!(buf, "{suggestion}").unwrap();
885 }
886 let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
887 write(buf, quote);
888 write(buf, s);
889 write(buf, quote);
890 }
891 LiteralValue::Bool(_) => {
892 write(buf, &self.raw);
893 }
894 }
895 }
896}
897
898impl TagDeclarator {
899 pub fn recast(&self, buf: &mut String) {
900 buf.push('$');
902 buf.push_str(&self.name);
903 }
904}
905
906impl ArrayExpression {
907 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
908 fn indent_multiline_item(item: &str, indent: &str) -> String {
909 if !item.contains('\n') {
910 return item.to_owned();
911 }
912 let mut out = String::with_capacity(item.len() + indent.len() * 2);
913 let mut first = true;
914 for segment in item.split_inclusive('\n') {
915 if first {
916 out.push_str(segment);
917 first = false;
918 continue;
919 }
920 out.push_str(indent);
921 out.push_str(segment);
922 }
923 out
924 }
925
926 let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
930 let mut elems = self.elements.iter();
931 let mut found_line_comment = false;
932 let mut format_items: Vec<_> = Vec::with_capacity(num_items);
933 for i in 0..num_items {
934 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
935 format_items.extend(noncode.iter().map(|nc| {
936 found_line_comment |= nc.value.should_cause_array_newline();
937 nc.recast(options, 0)
938 }));
939 } else {
940 let el = elems.next().unwrap();
941 let mut s = String::with_capacity(256);
942 el.recast(&mut s, options, 0, ExprContext::Other);
943 s.push_str(", ");
944 format_items.push(s);
945 }
946 }
947
948 if let Some(item) = format_items.last_mut()
950 && let Some(norm) = item.strip_suffix(", ")
951 {
952 *item = norm.to_owned();
953 }
954 let mut flat_recast = String::with_capacity(256);
955 flat_recast.push('[');
956 for fi in &format_items {
957 flat_recast.push_str(fi)
958 }
959 flat_recast.push(']');
960
961 let max_array_length = 40;
963 let multi_line = flat_recast.len() > max_array_length || found_line_comment;
964 if !multi_line {
965 buf.push_str(&flat_recast);
966 return;
967 }
968
969 buf.push_str("[\n");
971 let inner_indentation = if ctxt.in_pipe() {
972 options.get_indentation_offset_pipe(indentation_level + 1)
973 } else {
974 options.get_indentation(indentation_level + 1)
975 };
976 for format_item in format_items {
977 let item = if let Some(x) = format_item.strip_suffix(" ") {
978 x
979 } else {
980 &format_item
981 };
982 let item = indent_multiline_item(item, &inner_indentation);
983 buf.push_str(&inner_indentation);
984 buf.push_str(&item);
985 if !format_item.ends_with('\n') {
986 buf.push('\n')
987 }
988 }
989 let end_indent = if ctxt.in_pipe() {
990 options.get_indentation_offset_pipe(indentation_level)
991 } else {
992 options.get_indentation(indentation_level)
993 };
994 buf.push_str(&end_indent);
995 buf.push(']');
996 }
997}
998
999fn expr_is_trivial(expr: &Expr) -> bool {
1001 matches!(
1002 expr,
1003 Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
1004 )
1005}
1006
1007trait CannotActuallyFail {
1008 fn no_fail(self);
1009}
1010
1011impl CannotActuallyFail for std::fmt::Result {
1012 fn no_fail(self) {
1013 self.expect("writing to a string cannot fail, there's no IO happening")
1014 }
1015}
1016
1017impl ArrayRangeExpression {
1018 fn recast(&self, buf: &mut String, options: &FormatOptions, _: usize, _: ExprContext) {
1019 buf.push('[');
1020 self.start_element.recast(buf, options, 0, ExprContext::Other);
1021
1022 let range_op = if self.end_inclusive { ".." } else { "..<" };
1023 let no_spaces = expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element);
1028 if no_spaces {
1029 write!(buf, "{range_op}").no_fail()
1030 } else {
1031 write!(buf, " {range_op} ").no_fail()
1032 }
1033 self.end_element.recast(buf, options, 0, ExprContext::Other);
1034 buf.push(']');
1035 }
1037}
1038
1039fn trim_end(buf: &mut String) {
1040 buf.truncate(buf.trim_end().len())
1041}
1042
1043impl ObjectExpression {
1044 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1045 if self
1046 .non_code_meta
1047 .non_code_nodes
1048 .values()
1049 .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
1050 {
1051 return self.recast_multi_line(buf, options, indentation_level, ctxt);
1052 }
1053 let mut flat_recast_buf = String::new();
1054 flat_recast_buf.push_str("{ ");
1055 for (i, prop) in self.properties.iter().enumerate() {
1056 let obj_key = &prop.key.name;
1057 write!(flat_recast_buf, "{obj_key} = ").no_fail();
1058 prop.value
1059 .recast(&mut flat_recast_buf, options, indentation_level, ctxt);
1060 if i < self.properties.len() - 1 {
1061 flat_recast_buf.push_str(", ");
1062 }
1063 }
1064 flat_recast_buf.push_str(" }");
1065 let max_array_length = 40;
1066 let needs_multiple_lines = flat_recast_buf.len() > max_array_length;
1067 if !needs_multiple_lines {
1068 buf.push_str(&flat_recast_buf);
1069 } else {
1070 self.recast_multi_line(buf, options, indentation_level, ctxt);
1071 }
1072 }
1073
1074 fn recast_multi_line(
1076 &self,
1077 buf: &mut String,
1078 options: &FormatOptions,
1079 indentation_level: usize,
1080 ctxt: ExprContext,
1081 ) {
1082 let inner_indentation = if ctxt.in_pipe() {
1083 options.get_indentation_offset_pipe(indentation_level + 1)
1084 } else {
1085 options.get_indentation(indentation_level + 1)
1086 };
1087 let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
1088 let mut props = self.properties.iter();
1089 let format_items: Vec<_> = (0..num_items)
1090 .flat_map(|i| {
1091 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
1092 noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
1093 } else {
1094 let prop = props.next().unwrap();
1095 let comma = if i == num_items - 1 { "" } else { ",\n" };
1097 let mut s = String::new();
1098 prop.value.recast(&mut s, options, indentation_level + 1, ctxt);
1099 vec![format!("{} = {}{comma}", prop.key.name, s.trim())]
1101 }
1102 })
1103 .collect();
1104 let end_indent = if ctxt.in_pipe() {
1105 options.get_indentation_offset_pipe(indentation_level)
1106 } else {
1107 options.get_indentation(indentation_level)
1108 };
1109 write!(
1110 buf,
1111 "{{\n{inner_indentation}{}\n{end_indent}}}",
1112 format_items.join(&inner_indentation),
1113 )
1114 .no_fail();
1115 }
1116}
1117
1118impl MemberExpression {
1119 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1120 self.object.recast(buf, options, indentation_level, ctxt);
1122 if self.computed {
1124 buf.push('[');
1125 self.property.recast(buf, options, indentation_level, ctxt);
1126 buf.push(']');
1127 } else {
1128 buf.push('.');
1129 self.property.recast(buf, options, indentation_level, ctxt);
1130 };
1131 }
1132}
1133
1134impl BinaryExpression {
1135 fn recast(&self, buf: &mut String, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) {
1136 let maybe_wrap_it = |a: String, doit: bool| -> String { if doit { format!("({a})") } else { a } };
1137
1138 let should_wrap_left = match &self.left {
1141 BinaryPart::BinaryExpression(bin_exp) => {
1142 self.precedence() > bin_exp.precedence()
1143 || ((self.precedence() == bin_exp.precedence())
1144 && (!(self.operator.associative() && self.operator == bin_exp.operator)
1145 && self.operator.associativity() == Associativity::Right))
1146 }
1147 _ => false,
1148 };
1149
1150 let should_wrap_right = match &self.right {
1151 BinaryPart::BinaryExpression(bin_exp) => {
1152 self.precedence() > bin_exp.precedence()
1153 || self.operator == BinaryOperator::Sub
1155 || self.operator == BinaryOperator::Div
1156 || ((self.precedence() == bin_exp.precedence())
1157 && (!(self.operator.associative() && self.operator == bin_exp.operator)
1158 && self.operator.associativity() == Associativity::Left))
1159 }
1160 _ => false,
1161 };
1162
1163 let mut left = String::new();
1164 self.left.recast(&mut left, options, 0, ctxt);
1165 let mut right = String::new();
1166 self.right.recast(&mut right, options, 0, ctxt);
1167 write!(
1168 buf,
1169 "{} {} {}",
1170 maybe_wrap_it(left, should_wrap_left),
1171 self.operator,
1172 maybe_wrap_it(right, should_wrap_right)
1173 )
1174 .no_fail();
1175 }
1176}
1177
1178impl UnaryExpression {
1179 fn recast(&self, buf: &mut String, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) {
1180 match self.argument {
1181 BinaryPart::Literal(_)
1182 | BinaryPart::Name(_)
1183 | BinaryPart::MemberExpression(_)
1184 | BinaryPart::ArrayExpression(_)
1185 | BinaryPart::ArrayRangeExpression(_)
1186 | BinaryPart::ObjectExpression(_)
1187 | BinaryPart::IfExpression(_)
1188 | BinaryPart::AscribedExpression(_)
1189 | BinaryPart::CallExpressionKw(_) => {
1190 write!(buf, "{}", self.operator).no_fail();
1191 self.argument.recast(buf, options, 0, ctxt)
1192 }
1193 BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) | BinaryPart::SketchVar(_) => {
1194 write!(buf, "{}", self.operator).no_fail();
1195 buf.push('(');
1196 self.argument.recast(buf, options, 0, ctxt);
1197 buf.push(')');
1198 }
1199 }
1200 }
1201}
1202
1203impl IfExpression {
1204 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1205 let n = 2 + (self.else_ifs.len() * 2) + 3;
1208 let mut lines = Vec::with_capacity(n);
1209
1210 let cond = {
1211 let mut tmp_buf = String::new();
1212 self.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
1213 tmp_buf
1214 };
1215 lines.push((0, format!("if {cond} {{")));
1216 lines.push((1, {
1217 let mut tmp_buf = String::new();
1218 self.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
1219 tmp_buf
1220 }));
1221 for else_if in &self.else_ifs {
1222 let cond = {
1223 let mut tmp_buf = String::new();
1224 else_if.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
1225 tmp_buf
1226 };
1227 lines.push((0, format!("}} else if {cond} {{")));
1228 lines.push((1, {
1229 let mut tmp_buf = String::new();
1230 else_if.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
1231 tmp_buf
1232 }));
1233 }
1234 lines.push((0, "} else {".to_owned()));
1235 lines.push((1, {
1236 let mut tmp_buf = String::new();
1237 self.final_else.recast(&mut tmp_buf, options, indentation_level + 1);
1238 tmp_buf
1239 }));
1240 lines.push((0, "}".to_owned()));
1241 let out = lines
1242 .into_iter()
1243 .enumerate()
1244 .map(|(idx, (ind, line))| {
1245 let indentation = if ctxt.in_pipe() && idx == 0 {
1246 String::new()
1247 } else {
1248 options.get_indentation(indentation_level + ind)
1249 };
1250 format!("{indentation}{}", line.trim())
1251 })
1252 .collect::<Vec<_>>()
1253 .join("\n");
1254 buf.push_str(&out);
1255 }
1256}
1257
1258impl Node<PipeExpression> {
1259 fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, preceding_indent: bool) {
1260 if preceding_indent {
1261 options.write_indentation(buf, indentation_level);
1262 }
1263 for (index, statement) in self.body.iter().enumerate() {
1264 let (statement_indentation, statement_ctxt) = if index == 0 {
1265 (indentation_level, ExprContext::PipeHead)
1266 } else {
1267 (indentation_level + 1, ExprContext::Pipe)
1268 };
1269 statement.recast(buf, options, statement_indentation, statement_ctxt);
1270 let non_code_meta = &self.non_code_meta;
1271 if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
1272 for val in non_code_meta_value {
1273 if let NonCodeValue::NewLine = val.value {
1274 buf.push('\n');
1275 continue;
1276 }
1277 let formatted = if val.end == self.end {
1279 val.recast(options, indentation_level)
1280 .trim_end_matches('\n')
1281 .to_string()
1282 } else {
1283 val.recast(options, indentation_level + 1)
1284 .trim_end_matches('\n')
1285 .to_string()
1286 };
1287 if let NonCodeValue::BlockComment { .. } = val.value
1288 && !buf.ends_with('\n')
1289 {
1290 buf.push('\n');
1291 }
1292 buf.push_str(&formatted);
1293 }
1294 }
1295
1296 if index != self.body.len() - 1 {
1297 buf.push('\n');
1298 options.write_indentation(buf, indentation_level + 1);
1299 buf.push_str(PIPE_OPERATOR);
1300 buf.push(' ');
1301 }
1302 }
1303 }
1304}
1305
1306impl FunctionExpression {
1307 pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1308 let mut new_options = options.clone();
1310 new_options.insert_final_newline = false;
1311
1312 buf.push('(');
1313 for (i, param) in self.params.iter().enumerate() {
1314 param.recast(buf, options, indentation_level);
1315 if i < self.params.len() - 1 {
1316 buf.push_str(", ");
1317 }
1318 }
1319 buf.push(')');
1320 if let Some(return_type) = &self.return_type {
1321 write!(buf, ": {return_type}").no_fail();
1322 }
1323 writeln!(buf, " {{").no_fail();
1324 self.body.recast(buf, &new_options, indentation_level + 1);
1325 buf.push('\n');
1326 options.write_indentation(buf, indentation_level);
1327 buf.push('}');
1328 }
1329}
1330
1331impl Parameter {
1332 pub fn recast(&self, buf: &mut String, _options: &FormatOptions, _indentation_level: usize) {
1333 if !self.labeled {
1334 buf.push('@');
1335 }
1336 buf.push_str(&self.identifier.name);
1337 if self.default_value.is_some() {
1338 buf.push('?');
1339 };
1340 if let Some(ty) = &self.param_type {
1341 buf.push_str(": ");
1342 write!(buf, "{ty}").no_fail();
1343 }
1344 if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
1345 buf.push_str(" = ");
1346 literal.recast(buf);
1347 };
1348 }
1349}
1350
1351impl SketchBlock {
1352 pub(crate) fn recast(
1353 &self,
1354 buf: &mut String,
1355 options: &FormatOptions,
1356 indentation_level: usize,
1357 ctxt: ExprContext,
1358 ) {
1359 let name = Name {
1360 name: Node {
1361 inner: Identifier {
1362 name: SketchBlock::CALLEE_NAME.to_owned(),
1363 digest: None,
1364 },
1365 start: Default::default(),
1366 end: Default::default(),
1367 module_id: Default::default(),
1368 node_path: None,
1369 outer_attrs: Default::default(),
1370 pre_comments: Default::default(),
1371 comment_start: Default::default(),
1372 },
1373 path: Vec::new(),
1374 abs_path: false,
1375 digest: None,
1376 };
1377 recast_call(
1378 &name,
1379 None,
1380 &self.arguments,
1381 &self.non_code_meta,
1382 buf,
1383 options,
1384 indentation_level,
1385 ctxt,
1386 );
1387
1388 let mut new_options = options.clone();
1390 new_options.insert_final_newline = false;
1391
1392 writeln!(buf, " {{").no_fail();
1393 self.body.recast(buf, &new_options, indentation_level + 1);
1394 buf.push('\n');
1395 options.write_indentation(buf, indentation_level);
1396 buf.push('}');
1397 }
1398}
1399
1400impl Block {
1401 pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1402 recast_body(
1403 &self.items,
1404 &self.non_code_meta,
1405 &self.inner_attrs,
1406 buf,
1407 options,
1408 indentation_level,
1409 );
1410 }
1411}
1412
1413impl SketchVar {
1414 fn recast(&self, buf: &mut String) {
1415 if let Some(initial) = &self.initial {
1416 write!(buf, "var ").no_fail();
1417 initial.recast(buf);
1418 } else {
1419 write!(buf, "var").no_fail();
1420 }
1421 }
1422}
1423
1424#[cfg(not(target_arch = "wasm32"))]
1426#[async_recursion::async_recursion]
1427pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
1428 if !dir.is_dir() {
1430 anyhow::bail!("`{}` is not a directory", dir.display());
1431 }
1432
1433 let mut entries = tokio::fs::read_dir(dir).await?;
1434
1435 let mut files = Vec::new();
1436 while let Some(entry) = entries.next_entry().await? {
1437 let path = entry.path();
1438
1439 if path.is_dir() {
1440 files.extend(walk_dir(&path).await?);
1441 } else if path
1442 .extension()
1443 .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_lowercase()))
1444 {
1445 files.push(path);
1446 }
1447 }
1448
1449 Ok(files)
1450}
1451
1452#[cfg(not(target_arch = "wasm32"))]
1454pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
1455 let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
1456 crate::KclError::new_internal(crate::errors::KclErrorDetails::new(
1457 format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
1458 vec![crate::SourceRange::default()],
1459 ))
1460 })?;
1461
1462 let futures = files
1463 .into_iter()
1464 .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) .map(|file| {
1467 let options = options.clone();
1468 tokio::spawn(async move {
1469 let contents = tokio::fs::read_to_string(&file)
1470 .await
1471 .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
1472 let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
1473 let report = crate::Report {
1474 kcl_source: contents.to_string(),
1475 error: err,
1476 filename: file.to_string_lossy().to_string(),
1477 };
1478 let report = miette::Report::new(report);
1479 anyhow::anyhow!("{:?}", report)
1480 })?;
1481 for ce in &ces {
1482 if ce.severity != crate::errors::Severity::Warning {
1483 let report = crate::Report {
1484 kcl_source: contents.to_string(),
1485 error: crate::KclError::new_semantic(ce.clone().into()),
1486 filename: file.to_string_lossy().to_string(),
1487 };
1488 let report = miette::Report::new(report);
1489 anyhow::bail!("{:?}", report);
1490 }
1491 }
1492 let Some(program) = program else {
1493 anyhow::bail!("Failed to parse file `{}`", file.display());
1494 };
1495 let recast = program.recast_with_options(&options);
1496 tokio::fs::write(&file, recast)
1497 .await
1498 .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
1499
1500 Ok::<(), anyhow::Error>(())
1501 })
1502 })
1503 .collect::<Vec<_>>();
1504
1505 let results = futures::future::join_all(futures).await;
1507
1508 let mut errors = Vec::new();
1510 for result in results {
1511 if let Err(err) = result? {
1512 errors.push(err);
1513 }
1514 }
1515
1516 if !errors.is_empty() {
1517 anyhow::bail!("Failed to recast some files: {:?}", errors);
1518 }
1519
1520 Ok(())
1521}
1522
1523#[cfg(test)]
1524mod tests {
1525 use pretty_assertions::assert_eq;
1526
1527 use super::*;
1528 use crate::ModuleId;
1529 use crate::parsing::ast::types::FormatOptions;
1530
1531 #[test]
1532 fn test_recast_annotations_without_body_items() {
1533 let input = r#"@settings(defaultLengthUnit = in)
1534"#;
1535 let program = crate::parsing::top_level_parse(input).unwrap();
1536 let output = program.recast_top(&Default::default(), 0);
1537 assert_eq!(output, input);
1538 }
1539
1540 #[test]
1541 fn test_recast_annotations_in_function_body() {
1542 let input = r#"fn myFunc() {
1543 @meta(yes = true)
1544
1545 x = 2
1546}
1547"#;
1548 let program = crate::parsing::top_level_parse(input).unwrap();
1549 let output = program.recast_top(&Default::default(), 0);
1550 assert_eq!(output, input);
1551 }
1552
1553 #[test]
1554 fn test_recast_annotations_in_function_body_without_items() {
1555 let input = "\
1556fn myFunc() {
1557 @meta(yes = true)
1558}
1559";
1560 let program = crate::parsing::top_level_parse(input).unwrap();
1561 let output = program.recast_top(&Default::default(), 0);
1562 assert_eq!(output, input);
1563 }
1564
1565 #[test]
1566 fn recast_annotations_with_comments() {
1567 let input = r#"// Start comment
1568
1569// Comment on attr
1570@settings(defaultLengthUnit = in)
1571
1572// Comment on item
1573foo = 42
1574
1575// Comment on another item
1576@(impl = kcl)
1577bar = 0
1578"#;
1579 let program = crate::parsing::top_level_parse(input).unwrap();
1580 let output = program.recast_top(&Default::default(), 0);
1581 assert_eq!(output, input);
1582 }
1583
1584 #[test]
1585 fn recast_annotations_with_block_comment() {
1586 let input = r#"/* Start comment
1587
1588sdfsdfsdfs */
1589@settings(defaultLengthUnit = in)
1590
1591foo = 42
1592"#;
1593 let program = crate::parsing::top_level_parse(input).unwrap();
1594 let output = program.recast_top(&Default::default(), 0);
1595 assert_eq!(output, input);
1596 }
1597
1598 #[track_caller]
1601 fn assert_recast(input: &str, expected: &str) {
1602 let program = crate::parsing::top_level_parse(input).unwrap();
1603 let output = program.recast_top(&Default::default(), 0);
1604 assert_eq!(output, expected);
1605 let reparsed = crate::parsing::top_level_parse(&output).unwrap();
1606 assert_eq!(reparsed.recast_top(&Default::default(), 0), output);
1607 }
1608
1609 #[test]
1610 fn recast_enum_multi_line_is_stable() {
1611 let input = r#"@settings(experimentalFeatures = allow)
1612
1613type Color {
1614 | Red
1615 | Green
1616 | Blue
1617}
1618"#;
1619 assert_recast(input, input);
1620 }
1621
1622 #[test]
1623 fn recast_enum_expands_single_line() {
1624 let input = r#"@settings(experimentalFeatures = allow)
1625
1626type Color { | Red | Green | Blue }
1627"#;
1628 let expected = r#"@settings(experimentalFeatures = allow)
1629
1630type Color {
1631 | Red
1632 | Green
1633 | Blue
1634}
1635"#;
1636 assert_recast(input, expected);
1637 }
1638
1639 #[test]
1640 fn recast_enum_export() {
1641 let input = r#"@settings(experimentalFeatures = allow)
1642
1643export type Color {
1644 | Red
1645}
1646"#;
1647 assert_recast(input, input);
1648 }
1649
1650 #[test]
1651 fn recast_enum_no_variants() {
1652 let input = r#"@settings(experimentalFeatures = allow)
1653
1654type Empty { | }
1655"#;
1656 assert_recast(input, input);
1657 }
1658
1659 #[test]
1660 fn recast_enum_no_variants_collapses_blank_lines() {
1661 let input = r#"@settings(experimentalFeatures = allow)
1664
1665type Empty {
1666
1667 |
1668
1669}
1670"#;
1671 let expected = r#"@settings(experimentalFeatures = allow)
1672
1673type Empty { | }
1674"#;
1675 assert_recast(input, expected);
1676 }
1677
1678 #[test]
1679 fn recast_enum_no_variants_with_comments() {
1680 let input = r#"@settings(experimentalFeatures = allow)
1681
1682type Empty { /* a */ | /* b */ }
1683"#;
1684 let expected = r#"@settings(experimentalFeatures = allow)
1685
1686type Empty {
1687 /* a */
1688 /* b */
1689 |
1690}
1691"#;
1692 assert_recast(input, expected);
1693 }
1694
1695 #[test]
1696 fn recast_enum_with_comments() {
1697 let input = r#"@settings(experimentalFeatures = allow)
1698
1699type Color {
1700 // before red
1701 | Red // after red
1702 | /* inside green arm */ Green
1703
1704 | Blue
1705 // trailing
1706}
1707"#;
1708 let expected = r#"@settings(experimentalFeatures = allow)
1711
1712type Color {
1713 // before red
1714 | Red // after red
1715 /* inside green arm */
1716 | Green
1717
1718 | Blue
1719 // trailing
1720}
1721"#;
1722 assert_recast(input, expected);
1723 }
1724
1725 #[test]
1726 fn recast_enum_with_comment_above_declaration() {
1727 let input = r#"@settings(experimentalFeatures = allow)
1728
1729// palette
1730type Color {
1731 // before red
1732 | Red
1733}
1734"#;
1735 assert_recast(input, input);
1736 }
1737
1738 #[test]
1739 fn recast_enum_with_outer_annotation() {
1740 let input = r#"@settings(experimentalFeatures = allow)
1741
1742@(impl = kcl)
1743type Color {
1744 | Red
1745}
1746"#;
1747 assert_recast(input, input);
1748 }
1749
1750 #[test]
1751 fn recast_enum_export_annotation_and_comments() {
1752 let input = r#"@settings(experimentalFeatures = allow)
1753
1754// palette
1755@(impl = kcl)
1756export type Color {
1757 | Red // warm
1758}
1759"#;
1760 assert_recast(input, input);
1761 }
1762
1763 #[test]
1764 fn recast_enum_in_function_body() {
1765 let input = r#"@settings(experimentalFeatures = allow)
1766
1767fn palette() {
1768 type Color {
1769 | Red
1770 | Green
1771 }
1772 return 0
1773}
1774"#;
1775 assert_recast(input, input);
1776 }
1777
1778 #[test]
1779 fn test_recast_if_else_if_same() {
1780 let input = r#"b = if false {
1781 3
1782} else if true {
1783 4
1784} else {
1785 5
1786}
1787"#;
1788 let program = crate::parsing::top_level_parse(input).unwrap();
1789 let output = program.recast_top(&Default::default(), 0);
1790 assert_eq!(output, input);
1791 }
1792
1793 #[test]
1794 fn test_recast_if_same() {
1795 let input = r#"b = if false {
1796 3
1797} else {
1798 5
1799}
1800"#;
1801 let program = crate::parsing::top_level_parse(input).unwrap();
1802 let output = program.recast_top(&Default::default(), 0);
1803 assert_eq!(output, input);
1804 }
1805
1806 #[test]
1807 fn test_recast_import() {
1808 let input = r#"import a from "a.kcl"
1809import a as aaa from "a.kcl"
1810import a, b from "a.kcl"
1811import a as aaa, b from "a.kcl"
1812import a, b as bbb from "a.kcl"
1813import a as aaa, b as bbb from "a.kcl"
1814import "a_b.kcl"
1815import "a-b.kcl" as b
1816import * from "a.kcl"
1817export import a as aaa from "a.kcl"
1818export import a, b from "a.kcl"
1819export import a as aaa, b from "a.kcl"
1820export import a, b as bbb from "a.kcl"
1821"#;
1822 let program = crate::parsing::top_level_parse(input).unwrap();
1823 let output = program.recast_top(&Default::default(), 0);
1824 assert_eq!(output, input);
1825 }
1826
1827 #[test]
1828 fn test_recast_import_as_same_name() {
1829 let input = r#"import a as a from "a.kcl"
1830"#;
1831 let program = crate::parsing::top_level_parse(input).unwrap();
1832 let output = program.recast_top(&Default::default(), 0);
1833 let expected = r#"import a from "a.kcl"
1834"#;
1835 assert_eq!(output, expected);
1836 }
1837
1838 #[test]
1839 fn test_recast_export_fn() {
1840 let input = r#"export fn a() {
1841 return 0
1842}
1843"#;
1844 let program = crate::parsing::top_level_parse(input).unwrap();
1845 let output = program.recast_top(&Default::default(), 0);
1846 assert_eq!(output, input);
1847 }
1848
1849 #[test]
1850 fn test_recast_sketch_block_with_no_args() {
1851 let input = r#"sketch() {
1852 return 0
1853}
1854"#;
1855 let program = crate::parsing::top_level_parse(input).unwrap();
1856 let output = program.recast_top(&Default::default(), 0);
1857 assert_eq!(output, input);
1858 }
1859
1860 #[test]
1861 fn test_recast_sketch_block_with_labeled_args() {
1862 let input = r#"sketch(on = XY) {
1863 return 0
1864}
1865"#;
1866 let program = crate::parsing::top_level_parse(input).unwrap();
1867 let output = program.recast_top(&Default::default(), 0);
1868 assert_eq!(output, input);
1869 }
1870
1871 #[test]
1872 fn test_recast_sketch_block_with_arg_shorthand() {
1873 let input = r#"on = XY
1874sketch(on) {
1875 return 0
1876}
1877"#;
1878 let program = crate::parsing::top_level_parse(input).unwrap();
1879 let output = program.recast_top(&Default::default(), 0);
1880 assert_eq!(output, input);
1881 }
1882
1883 #[test]
1884 fn test_recast_sketch_block_with_arg_shorthand_and_comment() {
1885 let input = r#"on = XY
1889sketch(
1890 on,
1891 // plane
1892) {
1893 return 0
1894}
1895"#;
1896 let program = crate::parsing::top_level_parse(input).unwrap();
1897 let output = program.recast_top(&Default::default(), 0);
1898 assert_eq!(output, input);
1899
1900 let program = crate::parsing::top_level_parse(&output).unwrap();
1902 let output2 = program.recast_top(&Default::default(), 0);
1903 assert_eq!(output2, output);
1904 }
1905
1906 #[test]
1907 fn test_recast_sketch_block_with_statements_in_block() {
1908 let input = r#"sketch() {
1909 // Comments inside block.
1910 x = 5
1911 y = 2
1912}
1913"#;
1914 let program = crate::parsing::top_level_parse(input).unwrap();
1915 let output = program.recast_top(&Default::default(), 0);
1916 assert_eq!(output, input);
1917 }
1918
1919 #[test]
1920 fn test_recast_bug_fn_in_fn() {
1921 let some_program_string = r#"// Start point (top left)
1922zoo_x = -20
1923zoo_y = 7
1924// Scale
1925s = 1 // s = 1 -> height of Z is 13.4mm
1926// Depth
1927d = 1
1928
1929fn rect(x, y, w, h) {
1930 startSketchOn(XY)
1931 |> startProfile(at = [x, y])
1932 |> xLine(length = w)
1933 |> yLine(length = h)
1934 |> xLine(length = -w)
1935 |> close()
1936 |> extrude(d)
1937}
1938
1939fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1940 startSketchOn(XY)
1941 |> startProfile(at = [x1, y1])
1942 |> line(endAbsolute = [x2, y2])
1943 |> line(endAbsolute = [x3, y3])
1944 |> line(endAbsolute = [x4, y4])
1945 |> close()
1946 |> extrude(d)
1947}
1948
1949fn crosshair(x, y) {
1950 startSketchOn(XY)
1951 |> startProfile(at = [x, y])
1952 |> yLine(length = 1)
1953 |> yLine(length = -2)
1954 |> yLine(length = 1)
1955 |> xLine(length = 1)
1956 |> xLine(length = -2)
1957}
1958
1959fn z(z_x, z_y) {
1960 z_end_w = s * 8.4
1961 z_end_h = s * 3
1962 z_corner = s * 2
1963 z_w = z_end_w + 2 * z_corner
1964 z_h = z_w * 1.08130081300813
1965 rect(
1966 z_x,
1967 a = z_y,
1968 b = z_end_w,
1969 c = -z_end_h,
1970 )
1971 rect(
1972 z_x + z_w,
1973 a = z_y,
1974 b = -z_corner,
1975 c = -z_corner,
1976 )
1977 rect(
1978 z_x + z_w,
1979 a = z_y - z_h,
1980 b = -z_end_w,
1981 c = z_end_h,
1982 )
1983 rect(
1984 z_x,
1985 a = z_y - z_h,
1986 b = z_corner,
1987 c = z_corner,
1988 )
1989}
1990
1991fn o(c_x, c_y) {
1992 // Outer and inner radii
1993 o_r = s * 6.95
1994 i_r = 0.5652173913043478 * o_r
1995
1996 // Angle offset for diagonal break
1997 a = 7
1998
1999 // Start point for the top sketch
2000 o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
2001 o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
2002
2003 // Start point for the bottom sketch
2004 o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
2005 o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
2006
2007 // End point for the bottom startSketch
2008 o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
2009 o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
2010
2011 // Where is the center?
2012 // crosshair(c_x, c_y)
2013
2014
2015 startSketchOn(XY)
2016 |> startProfile(at = [o_x1, o_y1])
2017 |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
2018 |> angledLine(angle = 45, length = o_r - i_r)
2019 |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
2020 |> close()
2021 |> extrude(d)
2022
2023 startSketchOn(XY)
2024 |> startProfile(at = [o_x2, o_y2])
2025 |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
2026 |> angledLine(angle = 225, length = o_r - i_r)
2027 |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
2028 |> close()
2029 |> extrude(d)
2030}
2031
2032fn zoo(x0, y0) {
2033 z(x = x0, y = y0)
2034 o(x = x0 + s * 20, y = y0 - (s * 6.7))
2035 o(x = x0 + s * 35, y = y0 - (s * 6.7))
2036}
2037
2038zoo(x = zoo_x, y = zoo_y)
2039"#;
2040 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2041
2042 let recasted = program.recast_top(&Default::default(), 0);
2043 assert_eq!(recasted, some_program_string);
2044 }
2045
2046 #[test]
2047 fn test_nested_fns_indent() {
2048 let some_program_string = "\
2049x = 1
2050fn rect(x, y, w, h) {
2051 y = 2
2052 z = 3
2053 startSketchOn(XY)
2054 |> startProfile(at = [x, y])
2055 |> xLine(length = w)
2056 |> yLine(length = h)
2057 |> xLine(length = -w)
2058 |> close()
2059 |> extrude(d)
2060}
2061";
2062 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2063
2064 let recasted = program.recast_top(&Default::default(), 0);
2065 assert_eq!(recasted, some_program_string);
2066 }
2067
2068 #[test]
2069 fn test_recast_bug_extra_parens() {
2070 let some_program_string = r#"// Ball Bearing
2071// 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.
2072
2073// Define constants like ball diameter, inside diameter, overhange length, and thickness
2074sphereDia = 0.5
2075insideDia = 1
2076thickness = 0.25
2077overHangLength = .4
2078
2079// Sketch and revolve the inside bearing piece
2080insideRevolve = startSketchOn(XZ)
2081 |> startProfile(at = [insideDia / 2, 0])
2082 |> line(end = [0, thickness + sphereDia / 2])
2083 |> line(end = [overHangLength, 0])
2084 |> line(end = [0, -thickness])
2085 |> line(end = [-overHangLength + thickness, 0])
2086 |> line(end = [0, -sphereDia])
2087 |> line(end = [overHangLength - thickness, 0])
2088 |> line(end = [0, -thickness])
2089 |> line(end = [-overHangLength, 0])
2090 |> close()
2091 |> revolve(axis = Y)
2092
2093// 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)
2094sphere = startSketchOn(XZ)
2095 |> startProfile(at = [
2096 0.05 + insideDia / 2 + thickness,
2097 0 - 0.05
2098 ])
2099 |> line(end = [sphereDia - 0.1, 0])
2100 |> arc(
2101 angle_start = 0,
2102 angle_end = -180,
2103 radius = sphereDia / 2 - 0.05
2104 )
2105 |> close()
2106 |> revolve(axis = X)
2107 |> patternCircular3d(
2108 axis = [0, 0, 1],
2109 center = [0, 0, 0],
2110 repetitions = 10,
2111 arcDegrees = 360,
2112 rotateDuplicates = true
2113 )
2114
2115// Sketch and revolve the outside bearing
2116outsideRevolve = startSketchOn(XZ)
2117 |> startProfile(at = [
2118 insideDia / 2 + thickness + sphereDia,
2119 0
2120 ]
2121 )
2122 |> line(end = [0, sphereDia / 2])
2123 |> line(end = [-overHangLength + thickness, 0])
2124 |> line(end = [0, thickness])
2125 |> line(end = [overHangLength, 0])
2126 |> line(end = [0, -2 * thickness - sphereDia])
2127 |> line(end = [-overHangLength, 0])
2128 |> line(end = [0, thickness])
2129 |> line(end = [overHangLength - thickness, 0])
2130 |> close()
2131 |> revolve(axis = Y)"#;
2132 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2133
2134 let recasted = program.recast_top(&Default::default(), 0);
2135 assert_eq!(
2136 recasted,
2137 r#"// Ball Bearing
2138// 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.
2139
2140// Define constants like ball diameter, inside diameter, overhange length, and thickness
2141sphereDia = 0.5
2142insideDia = 1
2143thickness = 0.25
2144overHangLength = .4
2145
2146// Sketch and revolve the inside bearing piece
2147insideRevolve = startSketchOn(XZ)
2148 |> startProfile(at = [insideDia / 2, 0])
2149 |> line(end = [0, thickness + sphereDia / 2])
2150 |> line(end = [overHangLength, 0])
2151 |> line(end = [0, -thickness])
2152 |> line(end = [-overHangLength + thickness, 0])
2153 |> line(end = [0, -sphereDia])
2154 |> line(end = [overHangLength - thickness, 0])
2155 |> line(end = [0, -thickness])
2156 |> line(end = [-overHangLength, 0])
2157 |> close()
2158 |> revolve(axis = Y)
2159
2160// 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)
2161sphere = startSketchOn(XZ)
2162 |> startProfile(at = [
2163 0.05 + insideDia / 2 + thickness,
2164 0 - 0.05
2165 ])
2166 |> line(end = [sphereDia - 0.1, 0])
2167 |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
2168 |> close()
2169 |> revolve(axis = X)
2170 |> patternCircular3d(
2171 axis = [0, 0, 1],
2172 center = [0, 0, 0],
2173 repetitions = 10,
2174 arcDegrees = 360,
2175 rotateDuplicates = true,
2176 )
2177
2178// Sketch and revolve the outside bearing
2179outsideRevolve = startSketchOn(XZ)
2180 |> startProfile(at = [
2181 insideDia / 2 + thickness + sphereDia,
2182 0
2183 ])
2184 |> line(end = [0, sphereDia / 2])
2185 |> line(end = [-overHangLength + thickness, 0])
2186 |> line(end = [0, thickness])
2187 |> line(end = [overHangLength, 0])
2188 |> line(end = [0, -2 * thickness - sphereDia])
2189 |> line(end = [-overHangLength, 0])
2190 |> line(end = [0, thickness])
2191 |> line(end = [overHangLength - thickness, 0])
2192 |> close()
2193 |> revolve(axis = Y)
2194"#
2195 );
2196 }
2197
2198 #[test]
2199 fn test_recast_fn_in_object() {
2200 let some_program_string = r#"bing = { yo = 55 }
2201myNestedVar = [{ prop = callExp(bing.yo) }]
2202"#;
2203 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2204
2205 let recasted = program.recast_top(&Default::default(), 0);
2206 assert_eq!(recasted, some_program_string);
2207 }
2208
2209 #[test]
2210 fn test_recast_fn_in_array() {
2211 let some_program_string = r#"bing = { yo = 55 }
2212myNestedVar = [callExp(bing.yo)]
2213"#;
2214 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2215
2216 let recasted = program.recast_top(&Default::default(), 0);
2217 assert_eq!(recasted, some_program_string);
2218 }
2219
2220 #[test]
2221 fn test_recast_ranges() {
2222 let some_program_string = r#"foo = [0..10]
2223ten = 10
2224bar = [0 + 1 .. ten]
2225"#;
2226 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2227
2228 let recasted = program.recast_top(&Default::default(), 0);
2229 assert_eq!(recasted, some_program_string);
2230 }
2231
2232 #[test]
2233 fn test_recast_space_in_fn_call() {
2234 let some_program_string = r#"fn thing (x) {
2235 return x + 1
2236}
2237
2238thing ( 1 )
2239"#;
2240 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2241
2242 let recasted = program.recast_top(&Default::default(), 0);
2243 assert_eq!(
2244 recasted,
2245 r#"fn thing(x) {
2246 return x + 1
2247}
2248
2249thing(1)
2250"#
2251 );
2252 }
2253
2254 #[test]
2255 fn test_recast_typed_fn() {
2256 let some_program_string = r#"fn thing(x: string, y: [bool]): number {
2257 return x + 1
2258}
2259"#;
2260 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2261
2262 let recasted = program.recast_top(&Default::default(), 0);
2263 assert_eq!(recasted, some_program_string);
2264 }
2265
2266 #[test]
2267 fn test_recast_typed_consts() {
2268 let some_program_string = r#"a = 42: number
2269export b = 3.2: number(ft)
2270c = "dsfds": A | B | C
2271d = [1]: [number]
2272e = foo: [number; 3]
2273f = [1, 2, 3]: [number; 1+]
2274f = [1, 2, 3]: [number; 3+]
2275"#;
2276 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2277
2278 let recasted = program.recast_top(&Default::default(), 0);
2279 assert_eq!(recasted, some_program_string);
2280 }
2281
2282 #[test]
2283 fn test_recast_object_fn_in_array_weird_bracket() {
2284 let some_program_string = r#"bing = { yo = 55 }
2285myNestedVar = [
2286 {
2287 prop: line(a = [bing.yo, 21], b = sketch001)
2288}
2289]
2290"#;
2291 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2292
2293 let recasted = program.recast_top(&Default::default(), 0);
2294 let expected = r#"bing = { yo = 55 }
2295myNestedVar = [
2296 {
2297 prop = line(a = [bing.yo, 21], b = sketch001)
2298 }
2299]
2300"#;
2301 assert_eq!(recasted, expected,);
2302 }
2303
2304 #[test]
2305 fn test_recast_empty_file() {
2306 let some_program_string = r#""#;
2307 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2308
2309 let recasted = program.recast_top(&Default::default(), 0);
2310 assert_eq!(recasted, r#""#);
2312 }
2313
2314 #[test]
2315 fn test_recast_empty_file_new_line() {
2316 let some_program_string = r#"
2317"#;
2318 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2319
2320 let recasted = program.recast_top(&Default::default(), 0);
2321 assert_eq!(recasted, r#""#);
2323 }
2324
2325 #[test]
2326 fn test_recast_shebang() {
2327 let some_program_string = r#"#!/usr/local/env zoo kcl
2328part001 = startSketchOn(XY)
2329 |> startProfile(at = [-10, -10])
2330 |> line(end = [20, 0])
2331 |> line(end = [0, 20])
2332 |> line(end = [-20, 0])
2333 |> close()
2334"#;
2335
2336 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2337
2338 let recasted = program.recast_top(&Default::default(), 0);
2339 assert_eq!(
2340 recasted,
2341 r#"#!/usr/local/env zoo kcl
2342
2343part001 = startSketchOn(XY)
2344 |> startProfile(at = [-10, -10])
2345 |> line(end = [20, 0])
2346 |> line(end = [0, 20])
2347 |> line(end = [-20, 0])
2348 |> close()
2349"#
2350 );
2351 }
2352
2353 #[test]
2354 fn test_recast_shebang_new_lines() {
2355 let some_program_string = r#"#!/usr/local/env zoo kcl
2356
2357
2358
2359part001 = startSketchOn(XY)
2360 |> startProfile(at = [-10, -10])
2361 |> line(end = [20, 0])
2362 |> line(end = [0, 20])
2363 |> line(end = [-20, 0])
2364 |> close()
2365"#;
2366
2367 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2368
2369 let recasted = program.recast_top(&Default::default(), 0);
2370 assert_eq!(
2371 recasted,
2372 r#"#!/usr/local/env zoo kcl
2373
2374part001 = startSketchOn(XY)
2375 |> startProfile(at = [-10, -10])
2376 |> line(end = [20, 0])
2377 |> line(end = [0, 20])
2378 |> line(end = [-20, 0])
2379 |> close()
2380"#
2381 );
2382 }
2383
2384 #[test]
2385 fn test_recast_shebang_with_comments() {
2386 let some_program_string = r#"#!/usr/local/env zoo kcl
2387
2388// Yo yo my comments.
2389part001 = startSketchOn(XY)
2390 |> startProfile(at = [-10, -10])
2391 |> line(end = [20, 0])
2392 |> line(end = [0, 20])
2393 |> line(end = [-20, 0])
2394 |> close()
2395"#;
2396
2397 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2398
2399 let recasted = program.recast_top(&Default::default(), 0);
2400 assert_eq!(
2401 recasted,
2402 r#"#!/usr/local/env zoo kcl
2403
2404// Yo yo my comments.
2405part001 = startSketchOn(XY)
2406 |> startProfile(at = [-10, -10])
2407 |> line(end = [20, 0])
2408 |> line(end = [0, 20])
2409 |> line(end = [-20, 0])
2410 |> close()
2411"#
2412 );
2413 }
2414
2415 #[test]
2416 fn test_recast_empty_function_body_with_comments() {
2417 let input = r#"fn myFunc() {
2418 // Yo yo my comments.
2419}
2420"#;
2421
2422 let program = crate::parsing::top_level_parse(input).unwrap();
2423 let output = program.recast_top(&Default::default(), 0);
2424 assert_eq!(output, input);
2425 }
2426
2427 #[test]
2428 fn test_recast_large_file() {
2429 let some_program_string = r#"@settings(units=mm)
2430// define nts
2431radius = 6.0
2432width = 144.0
2433length = 83.0
2434depth = 45.0
2435thk = 5
2436hole_diam = 5
2437// define a rectangular shape func
2438fn rectShape(pos, w, l) {
2439 rr = startSketchOn(XY)
2440 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2441 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2442 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2443 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2444 |> close($edge4)
2445 return rr
2446}
2447// build the body of the focusrite scarlett solo gen 4
2448// only used for visualization
2449scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2450 |> extrude(depth)
2451 |> fillet(
2452 radius = radius,
2453 tags = [
2454 edge2,
2455 edge4,
2456 getOppositeEdge(edge2),
2457 getOppositeEdge(edge4)
2458]
2459 )
2460 // build the bracket sketch around the body
2461fn bracketSketch(w, d, t) {
2462 s = startSketchOn({
2463 plane = {
2464 origin = { x = 0, y = length / 2 + thk, z = 0 },
2465 x_axis = { x = 1, y = 0, z = 0 },
2466 y_axis = { x = 0, y = 0, z = 1 },
2467 z_axis = { x = 0, y = 1, z = 0 }
2468}
2469 })
2470 |> startProfile(at = [-w / 2 - t, d + t])
2471 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2472 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2473 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2474 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2475 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2476 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2477 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2478 |> close($edge8)
2479 return s
2480}
2481// build the body of the bracket
2482bracket_body = bracketSketch(w = width, d = depth, t = thk)
2483 |> extrude(length + 10)
2484 |> fillet(
2485 radius = radius,
2486 tags = [
2487 getNextAdjacentEdge(edge7),
2488 getNextAdjacentEdge(edge2),
2489 getNextAdjacentEdge(edge3),
2490 getNextAdjacentEdge(edge6)
2491]
2492 )
2493 // build the tabs of the mounting bracket (right side)
2494tabs_r = startSketchOn({
2495 plane = {
2496 origin = { x = 0, y = 0, z = depth + thk },
2497 x_axis = { x = 1, y = 0, z = 0 },
2498 y_axis = { x = 0, y = 1, z = 0 },
2499 z_axis = { x = 0, y = 0, z = 1 }
2500}
2501 })
2502 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2503 |> line(end = [10, -5])
2504 |> line(end = [0, -10])
2505 |> line(end = [-10, -5])
2506 |> close()
2507 |> subtract2d(tool = circle(
2508 center = [
2509 width / 2 + thk + hole_diam,
2510 length / 2 - hole_diam
2511 ],
2512 radius = hole_diam / 2
2513 ))
2514 |> extrude(-thk)
2515 |> patternLinear3d(
2516 axis = [0, -1, 0],
2517 repetitions = 1,
2518 distance = length - 10
2519 )
2520 // build the tabs of the mounting bracket (left side)
2521tabs_l = startSketchOn({
2522 plane = {
2523 origin = { x = 0, y = 0, z = depth + thk },
2524 x_axis = { x = 1, y = 0, z = 0 },
2525 y_axis = { x = 0, y = 1, z = 0 },
2526 z_axis = { x = 0, y = 0, z = 1 }
2527}
2528 })
2529 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2530 |> line(end = [-10, -5])
2531 |> line(end = [0, -10])
2532 |> line(end = [10, -5])
2533 |> close()
2534 |> subtract2d(tool = circle(
2535 center = [
2536 -width / 2 - thk - hole_diam,
2537 length / 2 - hole_diam
2538 ],
2539 radius = hole_diam / 2
2540 ))
2541 |> extrude(-thk)
2542 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2543"#;
2544 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2545
2546 let recasted = program.recast_top(&Default::default(), 0);
2547 assert_eq!(
2549 recasted,
2550 r#"@settings(units = mm)
2551
2552// define nts
2553radius = 6.0
2554width = 144.0
2555length = 83.0
2556depth = 45.0
2557thk = 5
2558hole_diam = 5
2559// define a rectangular shape func
2560fn rectShape(pos, w, l) {
2561 rr = startSketchOn(XY)
2562 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2563 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2564 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2565 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2566 |> close($edge4)
2567 return rr
2568}
2569// build the body of the focusrite scarlett solo gen 4
2570// only used for visualization
2571scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2572 |> extrude(depth)
2573 |> fillet(
2574 radius = radius,
2575 tags = [
2576 edge2,
2577 edge4,
2578 getOppositeEdge(edge2),
2579 getOppositeEdge(edge4)
2580 ],
2581 )
2582// build the bracket sketch around the body
2583fn bracketSketch(w, d, t) {
2584 s = startSketchOn({
2585 plane = {
2586 origin = { x = 0, y = length / 2 + thk, z = 0 },
2587 x_axis = { x = 1, y = 0, z = 0 },
2588 y_axis = { x = 0, y = 0, z = 1 },
2589 z_axis = { x = 0, y = 1, z = 0 }
2590 }
2591 })
2592 |> startProfile(at = [-w / 2 - t, d + t])
2593 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2594 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2595 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2596 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2597 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2598 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2599 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2600 |> close($edge8)
2601 return s
2602}
2603// build the body of the bracket
2604bracket_body = bracketSketch(w = width, d = depth, t = thk)
2605 |> extrude(length + 10)
2606 |> fillet(
2607 radius = radius,
2608 tags = [
2609 getNextAdjacentEdge(edge7),
2610 getNextAdjacentEdge(edge2),
2611 getNextAdjacentEdge(edge3),
2612 getNextAdjacentEdge(edge6)
2613 ],
2614 )
2615// build the tabs of the mounting bracket (right side)
2616tabs_r = startSketchOn({
2617 plane = {
2618 origin = { x = 0, y = 0, z = depth + thk },
2619 x_axis = { x = 1, y = 0, z = 0 },
2620 y_axis = { x = 0, y = 1, z = 0 },
2621 z_axis = { x = 0, y = 0, z = 1 }
2622 }
2623})
2624 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2625 |> line(end = [10, -5])
2626 |> line(end = [0, -10])
2627 |> line(end = [-10, -5])
2628 |> close()
2629 |> subtract2d(tool = circle(
2630 center = [
2631 width / 2 + thk + hole_diam,
2632 length / 2 - hole_diam
2633 ],
2634 radius = hole_diam / 2,
2635 ))
2636 |> extrude(-thk)
2637 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2638// build the tabs of the mounting bracket (left side)
2639tabs_l = startSketchOn({
2640 plane = {
2641 origin = { x = 0, y = 0, z = depth + thk },
2642 x_axis = { x = 1, y = 0, z = 0 },
2643 y_axis = { x = 0, y = 1, z = 0 },
2644 z_axis = { x = 0, y = 0, z = 1 }
2645 }
2646})
2647 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2648 |> line(end = [-10, -5])
2649 |> line(end = [0, -10])
2650 |> line(end = [10, -5])
2651 |> close()
2652 |> subtract2d(tool = circle(
2653 center = [
2654 -width / 2 - thk - hole_diam,
2655 length / 2 - hole_diam
2656 ],
2657 radius = hole_diam / 2,
2658 ))
2659 |> extrude(-thk)
2660 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2661"#
2662 );
2663 }
2664
2665 #[test]
2666 fn test_recast_nested_var_declaration_in_fn_body() {
2667 let some_program_string = r#"fn cube(pos, scale) {
2668 sg = startSketchOn(XY)
2669 |> startProfile(at = pos)
2670 |> line(end = [0, scale])
2671 |> line(end = [scale, 0])
2672 |> line(end = [0, -scale])
2673 |> close()
2674 |> extrude(scale)
2675}"#;
2676 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2677
2678 let recasted = program.recast_top(&Default::default(), 0);
2679 assert_eq!(
2680 recasted,
2681 r#"fn cube(pos, scale) {
2682 sg = startSketchOn(XY)
2683 |> startProfile(at = pos)
2684 |> line(end = [0, scale])
2685 |> line(end = [scale, 0])
2686 |> line(end = [0, -scale])
2687 |> close()
2688 |> extrude(scale)
2689}
2690"#
2691 );
2692 }
2693
2694 #[test]
2695 fn test_as() {
2696 let some_program_string = r#"fn cube(pos, scale) {
2697 x = dfsfs + dfsfsd as y
2698
2699 sg = startSketchOn(XY)
2700 |> startProfile(at = pos) as foo
2701 |> line([0, scale])
2702 |> line([scale, 0]) as bar
2703 |> line([0 as baz, -scale] as qux)
2704 |> close()
2705 |> extrude(length = scale)
2706}
2707
2708cube(pos = 0, scale = 0) as cub
2709"#;
2710 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2711
2712 let recasted = program.recast_top(&Default::default(), 0);
2713 assert_eq!(recasted, some_program_string,);
2714 }
2715
2716 #[test]
2717 fn test_recast_with_bad_indentation() {
2718 let some_program_string = r#"part001 = startSketchOn(XY)
2719 |> startProfile(at = [0.0, 5.0])
2720 |> line(end = [0.4900857016, -0.0240763666])
2721 |> line(end = [0.6804562304, 0.9087880491])"#;
2722 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2723
2724 let recasted = program.recast_top(&Default::default(), 0);
2725 assert_eq!(
2726 recasted,
2727 r#"part001 = startSketchOn(XY)
2728 |> startProfile(at = [0.0, 5.0])
2729 |> line(end = [0.4900857016, -0.0240763666])
2730 |> line(end = [0.6804562304, 0.9087880491])
2731"#
2732 );
2733 }
2734
2735 #[test]
2736 fn test_recast_with_bad_indentation_and_inline_comment() {
2737 let some_program_string = r#"part001 = startSketchOn(XY)
2738 |> startProfile(at = [0.0, 5.0])
2739 |> line(end = [0.4900857016, -0.0240763666]) // hello world
2740 |> line(end = [0.6804562304, 0.9087880491])"#;
2741 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2742
2743 let recasted = program.recast_top(&Default::default(), 0);
2744 assert_eq!(
2745 recasted,
2746 r#"part001 = startSketchOn(XY)
2747 |> startProfile(at = [0.0, 5.0])
2748 |> line(end = [0.4900857016, -0.0240763666]) // hello world
2749 |> line(end = [0.6804562304, 0.9087880491])
2750"#
2751 );
2752 }
2753 #[test]
2754 fn test_recast_with_bad_indentation_and_line_comment() {
2755 let some_program_string = r#"part001 = startSketchOn(XY)
2756 |> startProfile(at = [0.0, 5.0])
2757 |> line(end = [0.4900857016, -0.0240763666])
2758 // hello world
2759 |> line(end = [0.6804562304, 0.9087880491])"#;
2760 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2761
2762 let recasted = program.recast_top(&Default::default(), 0);
2763 assert_eq!(
2764 recasted,
2765 r#"part001 = startSketchOn(XY)
2766 |> startProfile(at = [0.0, 5.0])
2767 |> line(end = [0.4900857016, -0.0240763666])
2768 // hello world
2769 |> line(end = [0.6804562304, 0.9087880491])
2770"#
2771 );
2772 }
2773
2774 #[test]
2775 fn test_recast_comment_in_a_fn_block() {
2776 let some_program_string = r#"fn myFn() {
2777 // this is a comment
2778 yo = { a = { b = { c = '123' } } } /* block
2779 comment */
2780
2781 key = 'c'
2782 // this is also a comment
2783 return things
2784}"#;
2785 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2786
2787 let recasted = program.recast_top(&Default::default(), 0);
2788 assert_eq!(
2789 recasted,
2790 r#"fn myFn() {
2791 // this is a comment
2792 yo = { a = { b = { c = '123' } } } /* block
2793 comment */
2794
2795 key = 'c'
2796 // this is also a comment
2797 return things
2798}
2799"#
2800 );
2801 }
2802
2803 #[test]
2804 fn test_recast_comment_under_variable() {
2805 let some_program_string = r#"key = 'c'
2806// this is also a comment
2807thing = 'foo'
2808"#;
2809 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2810
2811 let recasted = program.recast_top(&Default::default(), 0);
2812 assert_eq!(
2813 recasted,
2814 r#"key = 'c'
2815// this is also a comment
2816thing = 'foo'
2817"#
2818 );
2819 }
2820
2821 #[test]
2822 fn test_recast_multiline_comment_start_file() {
2823 let some_program_string = r#"// hello world
2824// I am a comment
2825key = 'c'
2826// this is also a comment
2827// hello
2828thing = 'foo'
2829"#;
2830 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2831
2832 let recasted = program.recast_top(&Default::default(), 0);
2833 assert_eq!(
2834 recasted,
2835 r#"// hello world
2836// I am a comment
2837key = 'c'
2838// this is also a comment
2839// hello
2840thing = 'foo'
2841"#
2842 );
2843 }
2844
2845 #[test]
2846 fn test_recast_empty_comment() {
2847 let some_program_string = r#"// hello world
2848//
2849// I am a comment
2850key = 'c'
2851
2852//
2853// I am a comment
2854thing = 'c'
2855
2856foo = 'bar' //
2857"#;
2858 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2859
2860 let recasted = program.recast_top(&Default::default(), 0);
2861 assert_eq!(
2862 recasted,
2863 r#"// hello world
2864//
2865// I am a comment
2866key = 'c'
2867
2868//
2869// I am a comment
2870thing = 'c'
2871
2872foo = 'bar' //
2873"#
2874 );
2875 }
2876
2877 #[test]
2878 fn test_recast_multiline_comment_under_variable() {
2879 let some_program_string = r#"key = 'c'
2880// this is also a comment
2881// hello
2882thing = 'foo'
2883"#;
2884 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2885
2886 let recasted = program.recast_top(&Default::default(), 0);
2887 assert_eq!(
2888 recasted,
2889 r#"key = 'c'
2890// this is also a comment
2891// hello
2892thing = 'foo'
2893"#
2894 );
2895 }
2896
2897 #[test]
2898 fn test_recast_only_line_comments() {
2899 let code = r#"// comment at start
2900"#;
2901 let program = crate::parsing::top_level_parse(code).unwrap();
2902
2903 assert_eq!(program.recast_top(&Default::default(), 0), code);
2904 }
2905
2906 #[test]
2907 fn test_recast_comment_at_start() {
2908 let test_program = r#"
2909/* comment at start */
2910
2911mySk1 = startSketchOn(XY)
2912 |> startProfile(at = [0, 0])"#;
2913 let program = crate::parsing::top_level_parse(test_program).unwrap();
2914
2915 let recasted = program.recast_top(&Default::default(), 0);
2916 assert_eq!(
2917 recasted,
2918 r#"/* comment at start */
2919
2920mySk1 = startSketchOn(XY)
2921 |> startProfile(at = [0, 0])
2922"#
2923 );
2924 }
2925
2926 #[test]
2927 fn test_recast_lots_of_comments() {
2928 let some_program_string = r#"// comment at start
2929mySk1 = startSketchOn(XY)
2930 |> startProfile(at = [0, 0])
2931 |> line(endAbsolute = [1, 1])
2932 // comment here
2933 |> line(endAbsolute = [0, 1], tag = $myTag)
2934 |> line(endAbsolute = [1, 1])
2935 /* and
2936 here
2937 */
2938 // a comment between pipe expression statements
2939 |> rx(90)
2940 // and another with just white space between others below
2941 |> ry(45)
2942 |> rx(45)
2943// one more for good measure"#;
2944 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2945
2946 let recasted = program.recast_top(&Default::default(), 0);
2947 assert_eq!(
2948 recasted,
2949 r#"// comment at start
2950mySk1 = startSketchOn(XY)
2951 |> startProfile(at = [0, 0])
2952 |> line(endAbsolute = [1, 1])
2953 // comment here
2954 |> line(endAbsolute = [0, 1], tag = $myTag)
2955 |> line(endAbsolute = [1, 1])
2956 /* and
2957 here */
2958 // a comment between pipe expression statements
2959 |> rx(90)
2960 // and another with just white space between others below
2961 |> ry(45)
2962 |> rx(45)
2963// one more for good measure
2964"#
2965 );
2966 }
2967
2968 #[test]
2969 fn test_recast_multiline_object() {
2970 let some_program_string = r#"x = {
2971 a = 1000000000,
2972 b = 2000000000,
2973 c = 3000000000,
2974 d = 4000000000,
2975 e = 5000000000
2976}"#;
2977 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2978
2979 let recasted = program.recast_top(&Default::default(), 0);
2980 assert_eq!(recasted.trim(), some_program_string);
2981 }
2982
2983 #[test]
2984 fn test_recast_first_level_object() {
2985 let some_program_string = r#"three = 3
2986
2987yo = {
2988 aStr = 'str',
2989 anum = 2,
2990 identifier = three,
2991 binExp = 4 + 5
2992}
2993yo = [
2994 1,
2995 " 2,",
2996 "three",
2997 4 + 5,
2998 " hey oooooo really long long long"
2999]
3000"#;
3001 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3002
3003 let recasted = program.recast_top(&Default::default(), 0);
3004 assert_eq!(recasted, some_program_string);
3005 }
3006
3007 #[test]
3008 fn test_recast_new_line_before_comment() {
3009 let some_program_string = r#"
3010// this is a comment
3011yo = { a = { b = { c = '123' } } }
3012
3013key = 'c'
3014things = "things"
3015
3016// this is also a comment"#;
3017 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3018
3019 let recasted = program.recast_top(&Default::default(), 0);
3020 let expected = some_program_string.trim();
3021 let actual = recasted.trim();
3023 assert_eq!(actual, expected);
3024 }
3025
3026 #[test]
3027 fn test_recast_comment_tokens_inside_strings() {
3028 let some_program_string = r#"b = {
3029 end = 141,
3030 start = 125,
3031 type_ = "NonCodeNode",
3032 value = "
3033 // a comment
3034 "
3035}"#;
3036 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3037
3038 let recasted = program.recast_top(&Default::default(), 0);
3039 assert_eq!(recasted.trim(), some_program_string.trim());
3040 }
3041
3042 #[test]
3043 fn test_recast_array_new_line_in_pipe() {
3044 let some_program_string = r#"myVar = 3
3045myVar2 = 5
3046myVar3 = 6
3047myAng = 40
3048myAng2 = 134
3049part001 = startSketchOn(XY)
3050 |> startProfile(at = [0, 0])
3051 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3052 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3053 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
3054 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3055
3056 let recasted = program.recast_top(&Default::default(), 0);
3057 assert_eq!(recasted.trim(), some_program_string);
3058 }
3059
3060 #[test]
3061 fn test_recast_array_new_line_in_pipe_custom() {
3062 let some_program_string = r#"myVar = 3
3063myVar2 = 5
3064myVar3 = 6
3065myAng = 40
3066myAng2 = 134
3067part001 = startSketchOn(XY)
3068 |> startProfile(at = [0, 0])
3069 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3070 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3071 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
3072"#;
3073 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3074
3075 let recasted = program.recast_top(
3076 &FormatOptions {
3077 tab_size: 3,
3078 use_tabs: false,
3079 insert_final_newline: true,
3080 },
3081 0,
3082 );
3083 assert_eq!(recasted, some_program_string);
3084 }
3085
3086 #[test]
3087 fn test_recast_after_rename_std() {
3088 let some_program_string = r#"part001 = startSketchOn(XY)
3089 |> startProfile(at = [0.0000000000, 5.0000000000])
3090 |> line(end = [0.4900857016, -0.0240763666])
3091
3092part002 = "part002"
3093things = [part001, 0.0]
3094blah = 1
3095foo = false
3096baz = {a: 1, part001: "thing"}
3097
3098fn ghi(part001) {
3099 return part001
3100}
3101"#;
3102 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3103 program.rename_symbol("mySuperCoolPart", 6);
3104
3105 let recasted = program.recast_top(&Default::default(), 0);
3106 assert_eq!(
3107 recasted,
3108 r#"mySuperCoolPart = startSketchOn(XY)
3109 |> startProfile(at = [0.0, 5.0])
3110 |> line(end = [0.4900857016, -0.0240763666])
3111
3112part002 = "part002"
3113things = [mySuperCoolPart, 0.0]
3114blah = 1
3115foo = false
3116baz = { a = 1, part001 = "thing" }
3117
3118fn ghi(part001) {
3119 return part001
3120}
3121"#
3122 );
3123 }
3124
3125 #[test]
3126 fn test_recast_after_rename_fn_args() {
3127 let some_program_string = r#"fn ghi(x, y, z) {
3128 return x
3129}"#;
3130 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3131 program.rename_symbol("newName", 7);
3132
3133 let recasted = program.recast_top(&Default::default(), 0);
3134 assert_eq!(
3135 recasted,
3136 r#"fn ghi(newName, y, z) {
3137 return newName
3138}
3139"#
3140 );
3141 }
3142
3143 #[test]
3144 fn test_recast_trailing_comma() {
3145 let some_program_string = r#"startSketchOn(XY)
3146 |> startProfile(at = [0, 0])
3147 |> arc({
3148 radius = 1,
3149 angle_start = 0,
3150 angle_end = 180,
3151 })"#;
3152 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3153
3154 let recasted = program.recast_top(&Default::default(), 0);
3155 assert_eq!(
3156 recasted,
3157 r#"startSketchOn(XY)
3158 |> startProfile(at = [0, 0])
3159 |> arc({
3160 radius = 1,
3161 angle_start = 0,
3162 angle_end = 180
3163 })
3164"#
3165 );
3166 }
3167
3168 #[test]
3169 fn test_recast_array_no_trailing_comma_with_comments() {
3170 let some_program_string = r#"[
3171 1, // one
3172 2, // two
3173 3 // three
3174]"#;
3175 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3176
3177 let recasted = program.recast_top(&Default::default(), 0);
3178 assert_eq!(
3179 recasted,
3180 r#"[
3181 1,
3182 // one
3183 2,
3184 // two
3185 3,
3186 // three
3187]
3188"#
3189 );
3190 }
3191
3192 #[test]
3193 fn test_recast_object_no_trailing_comma_with_comments() {
3194 let some_program_string = r#"{
3195 x=1, // one
3196 y=2, // two
3197 z=3 // three
3198}"#;
3199 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3200
3201 let recasted = program.recast_top(&Default::default(), 0);
3202 assert_eq!(
3205 recasted,
3206 r#"{
3207 x = 1,
3208 // one
3209 y = 2,
3210 // two
3211 z = 3,
3212 // three
3213
3214}
3215"#
3216 );
3217 }
3218
3219 #[test]
3220 fn test_recast_comment_between_call_args() {
3221 let some_program_string = r#"rounded = fillet(
3222 body,
3223 radius = 1mm,
3224 // Keep this comment
3225 tags = [tag1, tag2],
3226)
3227"#;
3228 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3229
3230 let recasted = program.recast_top(&Default::default(), 0);
3231 assert_eq!(recasted, some_program_string);
3232
3233 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3235 let recasted2 = program.recast_top(&Default::default(), 0);
3236 assert_eq!(recasted2, recasted);
3237 }
3238
3239 #[test]
3240 fn test_recast_line_comments_in_call_args_force_multiline() {
3241 let some_program_string = r#"edged = chamfer(
3242 body,
3243 // leading
3244 length = 1mm,
3245 tags = [tag1],
3246 // trailing
3247)
3248"#;
3249 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3250
3251 let recasted = program.recast_top(&Default::default(), 0);
3252 assert_eq!(recasted, some_program_string);
3253
3254 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3256 let recasted2 = program.recast_top(&Default::default(), 0);
3257 assert_eq!(recasted2, recasted);
3258 }
3259
3260 #[test]
3261 fn test_recast_block_comment_in_call_args_stays_inline() {
3262 let some_program_string = r#"rounded = fillet(body, radius = 1mm, /* mid */ tags = [tag1])
3264"#;
3265 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3266
3267 let recasted = program.recast_top(&Default::default(), 0);
3268 assert_eq!(recasted, some_program_string);
3269
3270 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3272 let recasted2 = program.recast_top(&Default::default(), 0);
3273 assert_eq!(recasted2, recasted);
3274 }
3275
3276 #[test]
3277 fn test_recast_block_comment_in_multiline_call_args_stays_inline() {
3278 let some_program_string = r#"rounded = fillet(
3281 body,
3282 radius = 1mm,
3283 /* mid */ tags = [tag1],
3284 tag = $x,
3285)
3286"#;
3287 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3288
3289 let recasted = program.recast_top(&Default::default(), 0);
3290 assert_eq!(recasted, some_program_string);
3291
3292 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3294 let recasted2 = program.recast_top(&Default::default(), 0);
3295 assert_eq!(recasted2, recasted);
3296 }
3297
3298 #[test]
3299 fn test_recast_comment_in_call_args_in_pipe() {
3300 let some_program_string = r#"part = startSketchOn(XY)
3301 |> startProfile(at = [0, 0])
3302 |> fillet(
3303 radius = 1,
3304 // why
3305 tags = [a],
3306 )
3307"#;
3308 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3309
3310 let recasted = program.recast_top(&Default::default(), 0);
3311 assert_eq!(recasted, some_program_string);
3312 }
3313
3314 #[test]
3315 fn test_recast_negative_var() {
3316 let some_program_string = r#"w = 20
3317l = 8
3318h = 10
3319
3320firstExtrude = startSketchOn(XY)
3321 |> startProfile(at = [0,0])
3322 |> line(end = [0, l])
3323 |> line(end = [w, 0])
3324 |> line(end = [0, -l])
3325 |> close()
3326 |> extrude(h)
3327"#;
3328 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3329
3330 let recasted = program.recast_top(&Default::default(), 0);
3331 assert_eq!(
3332 recasted,
3333 r#"w = 20
3334l = 8
3335h = 10
3336
3337firstExtrude = startSketchOn(XY)
3338 |> startProfile(at = [0, 0])
3339 |> line(end = [0, l])
3340 |> line(end = [w, 0])
3341 |> line(end = [0, -l])
3342 |> close()
3343 |> extrude(h)
3344"#
3345 );
3346 }
3347
3348 #[test]
3349 fn test_recast_multiline_comment() {
3350 let some_program_string = r#"w = 20
3351l = 8
3352h = 10
3353
3354// This is my comment
3355// It has multiple lines
3356// And it's really long
3357firstExtrude = startSketchOn(XY)
3358 |> startProfile(at = [0,0])
3359 |> line(end = [0, l])
3360 |> line(end = [w, 0])
3361 |> line(end = [0, -l])
3362 |> close()
3363 |> extrude(h)
3364"#;
3365 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3366
3367 let recasted = program.recast_top(&Default::default(), 0);
3368 assert_eq!(
3369 recasted,
3370 r#"w = 20
3371l = 8
3372h = 10
3373
3374// This is my comment
3375// It has multiple lines
3376// And it's really long
3377firstExtrude = startSketchOn(XY)
3378 |> startProfile(at = [0, 0])
3379 |> line(end = [0, l])
3380 |> line(end = [w, 0])
3381 |> line(end = [0, -l])
3382 |> close()
3383 |> extrude(h)
3384"#
3385 );
3386 }
3387
3388 #[test]
3389 fn test_recast_math_start_negative() {
3390 let some_program_string = r#"myVar = -5 + 6"#;
3391 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3392
3393 let recasted = program.recast_top(&Default::default(), 0);
3394 assert_eq!(recasted.trim(), some_program_string);
3395 }
3396
3397 #[test]
3398 fn test_recast_math_negate_parens() {
3399 let some_program_string = r#"wallMountL = 3.82
3400thickness = 0.5
3401
3402startSketchOn(XY)
3403 |> startProfile(at = [0, 0])
3404 |> line(end = [0, -(wallMountL - thickness)])
3405 |> line(end = [0, -(5 - thickness)])
3406 |> line(end = [0, -(5 - 1)])
3407 |> line(end = [0, -(-5 - 1)])"#;
3408 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3409
3410 let recasted = program.recast_top(&Default::default(), 0);
3411 assert_eq!(recasted.trim(), some_program_string);
3412 }
3413
3414 #[test]
3415 fn test_recast_math_nested_parens() {
3416 let some_program_string = r#"distance = 5
3417p = 3: Plane
3418FOS = { a = 3, b = 42 }: Sketch
3419sigmaAllow = 8: number(mm)
3420width = 20
3421thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
3422 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3423
3424 let recasted = program.recast_top(&Default::default(), 0);
3425 assert_eq!(recasted.trim(), some_program_string);
3426 }
3427
3428 #[test]
3429 fn no_vardec_keyword() {
3430 let some_program_string = r#"distance = 5"#;
3431 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3432
3433 let recasted = program.recast_top(&Default::default(), 0);
3434 assert_eq!(recasted.trim(), some_program_string);
3435 }
3436
3437 #[test]
3438 fn recast_types() {
3439 let some_program_string = r#"type foo
3440
3441// A comment
3442@(impl = primitive)
3443export type bar(unit, baz)
3444type baz = Foo | Bar
3445type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
3446"#;
3447 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3448 let recasted = program.recast_top(&Default::default(), 0);
3449 assert_eq!(recasted, some_program_string);
3450 }
3451
3452 #[test]
3453 fn recast_nested_fn() {
3454 let some_program_string = r#"fn f() {
3455 return fn() {
3456 return 1
3457}
3458}"#;
3459 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3460 let recasted = program.recast_top(&Default::default(), 0);
3461 let expected = "\
3462fn f() {
3463 return fn() {
3464 return 1
3465 }
3466}";
3467 assert_eq!(recasted.trim(), expected);
3468 }
3469
3470 #[test]
3471 fn recast_literal() {
3472 use winnow::Parser;
3473 for (i, (raw, expected, reason)) in [
3474 (
3475 "5.0",
3476 "5.0",
3477 "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
3478 ),
3479 (
3480 "5",
3481 "5",
3482 "integers should stay integral, i.e. don't reformat this to '5.0'",
3483 ),
3484 (
3485 "5.0000000",
3486 "5.0",
3487 "if the number is f64 but not fractional, use its canonical format",
3488 ),
3489 ("5.1", "5.1", "straightforward case works"),
3490 ]
3491 .into_iter()
3492 .enumerate()
3493 {
3494 let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
3495 let literal = crate::parsing::parser::unsigned_number_literal
3496 .parse(tokens.as_slice())
3497 .unwrap();
3498 let mut actual = String::new();
3499 literal.recast(&mut actual);
3500 assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
3501 }
3502 }
3503
3504 #[test]
3505 fn recast_objects_no_comments() {
3506 let input = r#"
3507sketch002 = startSketchOn({
3508 plane: {
3509 origin: { x = 1, y = 2, z = 3 },
3510 x_axis = { x = 4, y = 5, z = 6 },
3511 y_axis = { x = 7, y = 8, z = 9 },
3512 z_axis = { x = 10, y = 11, z = 12 }
3513 }
3514 })
3515"#;
3516 let expected = r#"sketch002 = startSketchOn({
3517 plane = {
3518 origin = { x = 1, y = 2, z = 3 },
3519 x_axis = { x = 4, y = 5, z = 6 },
3520 y_axis = { x = 7, y = 8, z = 9 },
3521 z_axis = { x = 10, y = 11, z = 12 }
3522 }
3523})
3524"#;
3525 let ast = crate::parsing::top_level_parse(input).unwrap();
3526 let actual = ast.recast_top(&FormatOptions::new(), 0);
3527 assert_eq!(actual, expected);
3528 }
3529
3530 #[test]
3531 fn unparse_fn_unnamed() {
3532 let input = "\
3533squares_out = reduce(
3534 arr,
3535 n = 0: number,
3536 f = fn(@i, accum) {
3537 return 1
3538 },
3539)
3540";
3541 let ast = crate::parsing::top_level_parse(input).unwrap();
3542 let actual = ast.recast_top(&FormatOptions::new(), 0);
3543 assert_eq!(actual, input);
3544 }
3545
3546 #[test]
3547 fn unparse_fn_named() {
3548 let input = r#"fn f(x) {
3549 return 1
3550}
3551"#;
3552 let ast = crate::parsing::top_level_parse(input).unwrap();
3553 let actual = ast.recast_top(&FormatOptions::new(), 0);
3554 assert_eq!(actual, input);
3555 }
3556
3557 #[test]
3558 fn unparse_call_inside_function_single_line() {
3559 let input = r#"fn foo() {
3560 toDegrees(atan(0.5), foo = 1)
3561 return 0
3562}
3563"#;
3564 let ast = crate::parsing::top_level_parse(input).unwrap();
3565 let actual = ast.recast_top(&FormatOptions::new(), 0);
3566 assert_eq!(actual, input);
3567 }
3568
3569 #[test]
3570 fn recast_function_types() {
3571 let input = r#"foo = x: fn
3572foo = x: fn(number)
3573fn foo(x: fn(): number): fn {
3574 return 0
3575}
3576fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
3577 return 0
3578}
3579type fn
3580type foo = fn
3581type foo = fn(a: string, b: { f: fn(): any })
3582type foo = fn([fn])
3583type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
3584"#;
3585 let ast = crate::parsing::top_level_parse(input).unwrap();
3586 let actual = ast.recast_top(&FormatOptions::new(), 0);
3587 assert_eq!(actual, input);
3588 }
3589
3590 #[test]
3591 fn unparse_call_inside_function_args_multiple_lines() {
3592 let input = r#"fn foo() {
3593 toDegrees(
3594 atan(0.5),
3595 foo = 1,
3596 bar = 2,
3597 baz = 3,
3598 qux = 4,
3599 )
3600 return 0
3601}
3602"#;
3603 let ast = crate::parsing::top_level_parse(input).unwrap();
3604 let actual = ast.recast_top(&FormatOptions::new(), 0);
3605 assert_eq!(actual, input);
3606 }
3607
3608 #[test]
3609 fn unparse_call_inside_function_single_arg_multiple_lines() {
3610 let input = r#"fn foo() {
3611 toDegrees(
3612 [
3613 profile0,
3614 profile1,
3615 profile2,
3616 profile3,
3617 profile4,
3618 profile5
3619 ],
3620 key = 1,
3621 )
3622 return 0
3623}
3624"#;
3625 let ast = crate::parsing::top_level_parse(input).unwrap();
3626 let actual = ast.recast_top(&FormatOptions::new(), 0);
3627 assert_eq!(actual, input);
3628 }
3629
3630 #[test]
3631 fn recast_objects_with_comments() {
3632 use winnow::Parser;
3633 for (i, (input, expected, reason)) in [(
3634 "\
3635{
3636 a = 1,
3637 // b = 2,
3638 c = 3
3639}",
3640 "\
3641{
3642 a = 1,
3643 // b = 2,
3644 c = 3
3645}",
3646 "preserves comments",
3647 )]
3648 .into_iter()
3649 .enumerate()
3650 {
3651 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3652 crate::parsing::parser::print_tokens(tokens.as_slice());
3653 let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3654 let mut actual = String::new();
3655 expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3656 assert_eq!(
3657 actual, expected,
3658 "failed test {i}, which is testing that recasting {reason}"
3659 );
3660 }
3661 }
3662
3663 #[test]
3664 fn recast_array_with_comments() {
3665 use winnow::Parser;
3666 for (i, (input, expected, reason)) in [
3667 (
3668 "\
3669[
3670 1,
3671 2,
3672 3,
3673 4,
3674 5,
3675 6,
3676 7,
3677 8,
3678 9,
3679 10,
3680 11,
3681 12,
3682 13,
3683 14,
3684 15,
3685 16,
3686 17,
3687 18,
3688 19,
3689 20,
3690]",
3691 "\
3692[
3693 1,
3694 2,
3695 3,
3696 4,
3697 5,
3698 6,
3699 7,
3700 8,
3701 9,
3702 10,
3703 11,
3704 12,
3705 13,
3706 14,
3707 15,
3708 16,
3709 17,
3710 18,
3711 19,
3712 20
3713]",
3714 "preserves multi-line arrays",
3715 ),
3716 (
3717 "\
3718[
3719 1,
3720 // 2,
3721 3
3722]",
3723 "\
3724[
3725 1,
3726 // 2,
3727 3
3728]",
3729 "preserves comments",
3730 ),
3731 (
3732 "\
3733[
3734 1,
3735 2,
3736 // 3
3737]",
3738 "\
3739[
3740 1,
3741 2,
3742 // 3
3743]",
3744 "preserves comments at the end of the array",
3745 ),
3746 ]
3747 .into_iter()
3748 .enumerate()
3749 {
3750 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3751 let expr = crate::parsing::parser::array_elem_by_elem
3752 .parse(tokens.as_slice())
3753 .unwrap();
3754 let mut actual = String::new();
3755 expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3756 assert_eq!(
3757 actual, expected,
3758 "failed test {i}, which is testing that recasting {reason}"
3759 );
3760 }
3761 }
3762
3763 #[test]
3764 fn code_with_comment_and_extra_lines() {
3765 let code = r#"yo = 'c'
3766
3767/* this is
3768a
3769comment */
3770yo = 'bing'
3771"#;
3772 let ast = crate::parsing::top_level_parse(code).unwrap();
3773 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3774 assert_eq!(recasted, code);
3775 }
3776
3777 #[test]
3778 fn comments_in_a_fn_block() {
3779 let code = r#"fn myFn() {
3780 // this is a comment
3781 yo = { a = { b = { c = '123' } } }
3782
3783 /* block
3784 comment */
3785 key = 'c'
3786 // this is also a comment
3787}
3788"#;
3789 let ast = crate::parsing::top_level_parse(code).unwrap();
3790 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3791 assert_eq!(recasted, code);
3792 }
3793
3794 #[test]
3795 fn array_range_end_exclusive() {
3796 let code = "myArray = [0..<4]\n";
3797 let ast = crate::parsing::top_level_parse(code).unwrap();
3798 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3799 assert_eq!(recasted, code);
3800 }
3801
3802 #[test]
3803 fn paren_precedence() {
3804 let code = r#"x = 1 - 2 - 3
3805x = (1 - 2) - 3
3806x = 1 - (2 - 3)
3807x = 1 + 2 + 3
3808x = (1 + 2) + 3
3809x = 1 + (2 + 3)
3810x = 2 * (y % 2)
3811x = (2 * y) % 2
3812x = 2 % (y * 2)
3813x = (2 % y) * 2
3814x = 2 * y % 2
3815"#;
3816
3817 let expected = r#"x = 1 - 2 - 3
3818x = 1 - 2 - 3
3819x = 1 - (2 - 3)
3820x = 1 + 2 + 3
3821x = 1 + 2 + 3
3822x = 1 + 2 + 3
3823x = 2 * (y % 2)
3824x = 2 * y % 2
3825x = 2 % (y * 2)
3826x = 2 % y * 2
3827x = 2 * y % 2
3828"#;
3829 let ast = crate::parsing::top_level_parse(code).unwrap();
3830 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3831 assert_eq!(recasted, expected);
3832 }
3833
3834 #[test]
3835 fn gap_between_body_item_and_documented_fn() {
3836 let code = "\
3837x = 360
3838
3839// Watermelon
3840fn myFn() {
3841}
3842";
3843 let ast = crate::parsing::top_level_parse(code).unwrap();
3844 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3845 let expected = code;
3846 assert_eq!(recasted, expected);
3847 }
3848
3849 #[test]
3850 fn simple_assignment_in_fn() {
3851 let code = "\
3852fn function001() {
3853 extrude002 = extrude()
3854}\n";
3855
3856 let ast = crate::parsing::top_level_parse(code).unwrap();
3857 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3858 let expected = code;
3859 assert_eq!(recasted, expected);
3860 }
3861
3862 #[test]
3863 fn no_weird_extra_lines() {
3864 let code = "\
3867// Initial comment
3868
3869@settings(defaultLengthUnit = mm)
3870
3871x = 1
3872";
3873 let ast = crate::parsing::top_level_parse(code).unwrap();
3874 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3875 let expected = code;
3876 assert_eq!(recasted, expected);
3877 }
3878
3879 #[test]
3880 fn settings_then_code_is_stable() {
3881 let code = "\
3882@settings(defaultLengthUnit = in)
3883
3884import \"cube-inches.kcl\" as cubeIn
3885import \"cube-mm.kcl\" as cubeMm
3886
3887cubeIn
3888cubeMm
3889";
3890 let formatted_once = crate::parsing::top_level_parse(code)
3891 .unwrap()
3892 .recast_top(&FormatOptions::new(), 0);
3893 assert_eq!(formatted_once, code);
3894
3895 let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3896 .unwrap()
3897 .recast_top(&FormatOptions::new(), 0);
3898 assert_eq!(formatted_twice, formatted_once);
3899 }
3900
3901 #[test]
3902 fn settings_then_standalone_comment_is_stable() {
3903 let code = "\
3904@settings(defaultLengthUnit = mm)
3905@settings(defaultAngleUnit = deg)
3906
3907// Cap for gimbal stick
3908
3909x = 1
3910";
3911 let formatted_once = crate::parsing::top_level_parse(code)
3912 .unwrap()
3913 .recast_top(&FormatOptions::new(), 0);
3914 assert_eq!(formatted_once, code);
3915
3916 let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3917 .unwrap()
3918 .recast_top(&FormatOptions::new(), 0);
3919 assert_eq!(formatted_twice, formatted_once);
3920 }
3921
3922 #[test]
3923 fn module_prefix() {
3924 let code = "x = std::sweep::SKETCH_PLANE\n";
3925 let ast = crate::parsing::top_level_parse(code).unwrap();
3926 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3927 let expected = code;
3928 assert_eq!(recasted, expected);
3929 }
3930
3931 #[test]
3932 fn inline_ifs() {
3933 let code = "y = true
3934startSketchOn(XY)
3935 |> startProfile(at = [0, 0])
3936 |> if y {
3937 yLine(length = 1)
3938 } else {
3939 xLine(length = 1)
3940 }
3941";
3942 let ast = crate::parsing::top_level_parse(code).unwrap();
3943 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3944 let expected = code;
3945 assert_eq!(recasted, expected);
3946 }
3947
3948 #[test]
3949 fn indented_binary_expressions() {
3950 let code = "\
3951fn foo() {
3952 1 == 2
3953}
3954";
3955 let ast = crate::parsing::top_level_parse(code).unwrap();
3956 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3957 let expected = code;
3958 assert_eq!(recasted, expected);
3959 }
3960
3961 #[test]
3962 fn indented_assignment() {
3963 let code = "\
3964fn foo() {
3965 x = 1
3966}
3967";
3968 let ast = crate::parsing::top_level_parse(code).unwrap();
3969 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3970 let expected = code;
3971 assert_eq!(recasted, expected);
3972 }
3973
3974 #[test]
3975 fn indented_unary_expression() {
3976 let code = "\
3977fn foo() {
3978 -x
3979}
3980";
3981 let ast = crate::parsing::top_level_parse(code).unwrap();
3982 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3983 let expected = code;
3984 assert_eq!(recasted, expected);
3985 }
3986
3987 #[test]
3988 fn indented_array_expression() {
3989 let code = "\
3990fn foo() {
3991 [1, 2]
3992}
3993";
3994 let ast = crate::parsing::top_level_parse(code).unwrap();
3995 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3996 let expected = code;
3997 assert_eq!(recasted, expected);
3998 }
3999
4000 #[test]
4001 fn indented_name_expression() {
4002 let code = "\
4003fn foo() {
4004 x
4005}
4006";
4007 let ast = crate::parsing::top_level_parse(code).unwrap();
4008 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4009 let expected = code;
4010 assert_eq!(recasted, expected);
4011 }
4012
4013 #[test]
4014 fn indented_member_assignment() {
4015 let code = "\
4016brakcetPlane = {
4017 origin = { x = length / 2 },
4018 origin = { x = length / 2 },
4019 origin = { x = length / 2 },
4020 origin = { x = length / 2 },
4021 origin = { x = length / 2 },
4022 origin = { x = length / 2 },
4023 origin = { x = length / 2 },
4024 origin = { x = length / 2 }
4025}
4026";
4027 let ast = crate::parsing::top_level_parse(code).unwrap();
4028 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4029 let expected = code;
4030 assert_eq!(recasted, expected);
4031 }
4032
4033 #[test]
4034 fn badly_formatted_inline_calls() {
4035 let code = "\
4036return union([right, left])
4037 |> subtract(tools = [
4038 translate(axle(), y = pitchStabL + forkBaseL + wheelRGap + wheelR + addedLength),
4039 socket(rakeAngle = rearRake, xyTrans = [0, 12]),
4040 socket(
4041 rakeAngle = frontRake,
4042 xyTrans = [
4043 wheelW / 2 + wheelWGap + forkTineW / 2,
4044 40 + addedLength
4045 ],
4046 )
4047 ])
4048";
4049 let ast = crate::parsing::top_level_parse(code).unwrap();
4050 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4051 let expected = code;
4052 assert_eq!(recasted, expected);
4053 }
4054
4055 #[test]
4056 fn fn_args_prefixed_with_spaces() {
4057 let code = "holeAt(
4058 [cube1, cube2],
4059 plane = XY,
4060 holeBottom = hole::flat(),
4061 holeBody = hole::blind(depth = 2, diameter = 1),
4062 holeType = hole::counterbore(diameter = 1.4, depth = 1),
4063 cutAt = [1, 1],
4064)";
4065 let expected = "holeAt(
4066 [cube1, cube2],
4067 plane = XY,
4068 holeBottom = hole::flat(),
4069 holeBody = hole::blind(depth = 2, diameter = 1),
4070 holeType = hole::counterbore(diameter = 1.4, depth = 1),
4071 cutAt = [1, 1],
4072)
4073";
4074 let ast = crate::parsing::top_level_parse(code).unwrap();
4075 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4076 assert_eq!(recasted, expected);
4077 }
4078
4079 #[test]
4080 fn some_fn_args_still_prefixed() {
4081 let code = "a
4082 |> b()
4083 |> subtract(
4084 tools = startSketchOn(XY)
4085 |> circle(diameter = hubDiameter)
4086 |> extrude(length = hubThickness * 5, symmetric = true),
4087 tolerance,
4088 )
4089";
4090 let ast = crate::parsing::top_level_parse(code).unwrap();
4091 let actual_recasted = ast.recast_top(&FormatOptions::new(), 0);
4092 let expected_recasted = "a
4093 |> b()
4094 |> subtract(
4095 tools = startSketchOn(XY)
4096 |> circle(diameter = hubDiameter)
4097 |> extrude(length = hubThickness * 5, symmetric = true),
4098 tolerance,
4099 )
4100";
4101 assert_eq!(actual_recasted, expected_recasted);
4102 }
4103
4104 #[test]
4105 fn first_in_pipeline_indent() {
4106 let not_clone = "gear::helical(
4109 nTeeth = 12,
4110 module = 1.5,
4111 pressureAngle = 14deg,
4112 helixAngle = 25deg,
4113 gearHeight = 5,
4114)
4115";
4116 let yes_clone = "gear::helical(
4117 nTeeth = 12,
4118 module = 1.5,
4119 pressureAngle = 14deg,
4120 helixAngle = 25deg,
4121 gearHeight = 5,
4122)
4123|> clone()
4124";
4125 let not_clone_recasted = crate::parsing::top_level_parse(not_clone)
4127 .unwrap()
4128 .recast_top(&FormatOptions::new(), 0);
4129 let yes_clone_recasted = crate::parsing::top_level_parse(yes_clone)
4130 .unwrap()
4131 .recast_top(&FormatOptions::new(), 0);
4132 assert!(not_clone_recasted.contains("\n nTeeth"));
4133 assert!(!yes_clone_recasted.contains("\n nTeeth"));
4134 assert!(yes_clone_recasted.contains("\n nTeeth"));
4135 }
4136}