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 label: file.to_string_lossy().to_string(),
1478 };
1479 let report = miette::Report::new(report);
1480 anyhow::anyhow!("{:?}", report)
1481 })?;
1482 for ce in &ces {
1483 if ce.severity != crate::errors::Severity::Warning {
1484 let report = crate::Report {
1485 kcl_source: contents.to_string(),
1486 error: crate::KclError::new_semantic(ce.clone().into()),
1487 filename: file.to_string_lossy().to_string(),
1488 label: file.to_string_lossy().to_string(),
1489 };
1490 let report = miette::Report::new(report);
1491 anyhow::bail!("{:?}", report);
1492 }
1493 }
1494 let Some(program) = program else {
1495 anyhow::bail!("Failed to parse file `{}`", file.display());
1496 };
1497 let recast = program.recast_with_options(&options);
1498 tokio::fs::write(&file, recast)
1499 .await
1500 .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
1501
1502 Ok::<(), anyhow::Error>(())
1503 })
1504 })
1505 .collect::<Vec<_>>();
1506
1507 let results = futures::future::join_all(futures).await;
1509
1510 let mut errors = Vec::new();
1512 for result in results {
1513 if let Err(err) = result? {
1514 errors.push(err);
1515 }
1516 }
1517
1518 if !errors.is_empty() {
1519 anyhow::bail!("Failed to recast some files: {:?}", errors);
1520 }
1521
1522 Ok(())
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527 use pretty_assertions::assert_eq;
1528
1529 use super::*;
1530 use crate::ModuleId;
1531 use crate::parsing::ast::types::FormatOptions;
1532
1533 #[test]
1534 fn test_recast_annotations_without_body_items() {
1535 let input = r#"@settings(defaultLengthUnit = in)
1536"#;
1537 let program = crate::parsing::top_level_parse(input).unwrap();
1538 let output = program.recast_top(&Default::default(), 0);
1539 assert_eq!(output, input);
1540 }
1541
1542 #[test]
1543 fn test_recast_annotations_in_function_body() {
1544 let input = r#"fn myFunc() {
1545 @meta(yes = true)
1546
1547 x = 2
1548}
1549"#;
1550 let program = crate::parsing::top_level_parse(input).unwrap();
1551 let output = program.recast_top(&Default::default(), 0);
1552 assert_eq!(output, input);
1553 }
1554
1555 #[test]
1556 fn test_recast_annotations_in_function_body_without_items() {
1557 let input = "\
1558fn myFunc() {
1559 @meta(yes = true)
1560}
1561";
1562 let program = crate::parsing::top_level_parse(input).unwrap();
1563 let output = program.recast_top(&Default::default(), 0);
1564 assert_eq!(output, input);
1565 }
1566
1567 #[test]
1568 fn recast_annotations_with_comments() {
1569 let input = r#"// Start comment
1570
1571// Comment on attr
1572@settings(defaultLengthUnit = in)
1573
1574// Comment on item
1575foo = 42
1576
1577// Comment on another item
1578@(impl = kcl)
1579bar = 0
1580"#;
1581 let program = crate::parsing::top_level_parse(input).unwrap();
1582 let output = program.recast_top(&Default::default(), 0);
1583 assert_eq!(output, input);
1584 }
1585
1586 #[test]
1587 fn recast_annotations_with_block_comment() {
1588 let input = r#"/* Start comment
1589
1590sdfsdfsdfs */
1591@settings(defaultLengthUnit = in)
1592
1593foo = 42
1594"#;
1595 let program = crate::parsing::top_level_parse(input).unwrap();
1596 let output = program.recast_top(&Default::default(), 0);
1597 assert_eq!(output, input);
1598 }
1599
1600 #[track_caller]
1603 fn assert_recast(input: &str, expected: &str) {
1604 let program = crate::parsing::top_level_parse(input).unwrap();
1605 let output = program.recast_top(&Default::default(), 0);
1606 assert_eq!(output, expected);
1607 let reparsed = crate::parsing::top_level_parse(&output).unwrap();
1608 assert_eq!(reparsed.recast_top(&Default::default(), 0), output);
1609 }
1610
1611 #[test]
1612 fn recast_enum_multi_line_is_stable() {
1613 let input = r#"@settings(experimentalFeatures = allow)
1614
1615type Color {
1616 | Red
1617 | Green
1618 | Blue
1619}
1620"#;
1621 assert_recast(input, input);
1622 }
1623
1624 #[test]
1625 fn recast_enum_expands_single_line() {
1626 let input = r#"@settings(experimentalFeatures = allow)
1627
1628type Color { | Red | Green | Blue }
1629"#;
1630 let expected = r#"@settings(experimentalFeatures = allow)
1631
1632type Color {
1633 | Red
1634 | Green
1635 | Blue
1636}
1637"#;
1638 assert_recast(input, expected);
1639 }
1640
1641 #[test]
1642 fn recast_enum_export() {
1643 let input = r#"@settings(experimentalFeatures = allow)
1644
1645export type Color {
1646 | Red
1647}
1648"#;
1649 assert_recast(input, input);
1650 }
1651
1652 #[test]
1653 fn recast_enum_no_variants() {
1654 let input = r#"@settings(experimentalFeatures = allow)
1655
1656type Empty { | }
1657"#;
1658 assert_recast(input, input);
1659 }
1660
1661 #[test]
1662 fn recast_enum_no_variants_collapses_blank_lines() {
1663 let input = r#"@settings(experimentalFeatures = allow)
1666
1667type Empty {
1668
1669 |
1670
1671}
1672"#;
1673 let expected = r#"@settings(experimentalFeatures = allow)
1674
1675type Empty { | }
1676"#;
1677 assert_recast(input, expected);
1678 }
1679
1680 #[test]
1681 fn recast_enum_no_variants_with_comments() {
1682 let input = r#"@settings(experimentalFeatures = allow)
1683
1684type Empty { /* a */ | /* b */ }
1685"#;
1686 let expected = r#"@settings(experimentalFeatures = allow)
1687
1688type Empty {
1689 /* a */
1690 /* b */
1691 |
1692}
1693"#;
1694 assert_recast(input, expected);
1695 }
1696
1697 #[test]
1698 fn recast_enum_with_comments() {
1699 let input = r#"@settings(experimentalFeatures = allow)
1700
1701type Color {
1702 // before red
1703 | Red // after red
1704 | /* inside green arm */ Green
1705
1706 | Blue
1707 // trailing
1708}
1709"#;
1710 let expected = r#"@settings(experimentalFeatures = allow)
1713
1714type Color {
1715 // before red
1716 | Red // after red
1717 /* inside green arm */
1718 | Green
1719
1720 | Blue
1721 // trailing
1722}
1723"#;
1724 assert_recast(input, expected);
1725 }
1726
1727 #[test]
1728 fn recast_enum_with_comment_above_declaration() {
1729 let input = r#"@settings(experimentalFeatures = allow)
1730
1731// palette
1732type Color {
1733 // before red
1734 | Red
1735}
1736"#;
1737 assert_recast(input, input);
1738 }
1739
1740 #[test]
1741 fn recast_enum_with_outer_annotation() {
1742 let input = r#"@settings(experimentalFeatures = allow)
1743
1744@(impl = kcl)
1745type Color {
1746 | Red
1747}
1748"#;
1749 assert_recast(input, input);
1750 }
1751
1752 #[test]
1753 fn recast_enum_export_annotation_and_comments() {
1754 let input = r#"@settings(experimentalFeatures = allow)
1755
1756// palette
1757@(impl = kcl)
1758export type Color {
1759 | Red // warm
1760}
1761"#;
1762 assert_recast(input, input);
1763 }
1764
1765 #[test]
1766 fn recast_enum_in_function_body() {
1767 let input = r#"@settings(experimentalFeatures = allow)
1768
1769fn palette() {
1770 type Color {
1771 | Red
1772 | Green
1773 }
1774 return 0
1775}
1776"#;
1777 assert_recast(input, input);
1778 }
1779
1780 #[test]
1781 fn test_recast_if_else_if_same() {
1782 let input = r#"b = if false {
1783 3
1784} else if true {
1785 4
1786} else {
1787 5
1788}
1789"#;
1790 let program = crate::parsing::top_level_parse(input).unwrap();
1791 let output = program.recast_top(&Default::default(), 0);
1792 assert_eq!(output, input);
1793 }
1794
1795 #[test]
1796 fn test_recast_if_same() {
1797 let input = r#"b = if false {
1798 3
1799} else {
1800 5
1801}
1802"#;
1803 let program = crate::parsing::top_level_parse(input).unwrap();
1804 let output = program.recast_top(&Default::default(), 0);
1805 assert_eq!(output, input);
1806 }
1807
1808 #[test]
1809 fn test_recast_import() {
1810 let input = r#"import a from "a.kcl"
1811import a as aaa from "a.kcl"
1812import a, b from "a.kcl"
1813import a as aaa, b from "a.kcl"
1814import a, b as bbb from "a.kcl"
1815import a as aaa, b as bbb from "a.kcl"
1816import "a_b.kcl"
1817import "a-b.kcl" as b
1818import * from "a.kcl"
1819export import a as aaa from "a.kcl"
1820export import a, b from "a.kcl"
1821export import a as aaa, b from "a.kcl"
1822export import a, b as bbb from "a.kcl"
1823"#;
1824 let program = crate::parsing::top_level_parse(input).unwrap();
1825 let output = program.recast_top(&Default::default(), 0);
1826 assert_eq!(output, input);
1827 }
1828
1829 #[test]
1830 fn test_recast_import_as_same_name() {
1831 let input = r#"import a as a from "a.kcl"
1832"#;
1833 let program = crate::parsing::top_level_parse(input).unwrap();
1834 let output = program.recast_top(&Default::default(), 0);
1835 let expected = r#"import a from "a.kcl"
1836"#;
1837 assert_eq!(output, expected);
1838 }
1839
1840 #[test]
1841 fn test_recast_export_fn() {
1842 let input = r#"export fn a() {
1843 return 0
1844}
1845"#;
1846 let program = crate::parsing::top_level_parse(input).unwrap();
1847 let output = program.recast_top(&Default::default(), 0);
1848 assert_eq!(output, input);
1849 }
1850
1851 #[test]
1852 fn test_recast_sketch_block_with_no_args() {
1853 let input = r#"sketch() {
1854 return 0
1855}
1856"#;
1857 let program = crate::parsing::top_level_parse(input).unwrap();
1858 let output = program.recast_top(&Default::default(), 0);
1859 assert_eq!(output, input);
1860 }
1861
1862 #[test]
1863 fn test_recast_sketch_block_with_labeled_args() {
1864 let input = r#"sketch(on = XY) {
1865 return 0
1866}
1867"#;
1868 let program = crate::parsing::top_level_parse(input).unwrap();
1869 let output = program.recast_top(&Default::default(), 0);
1870 assert_eq!(output, input);
1871 }
1872
1873 #[test]
1874 fn test_recast_sketch_block_with_arg_shorthand() {
1875 let input = r#"on = XY
1876sketch(on) {
1877 return 0
1878}
1879"#;
1880 let program = crate::parsing::top_level_parse(input).unwrap();
1881 let output = program.recast_top(&Default::default(), 0);
1882 assert_eq!(output, input);
1883 }
1884
1885 #[test]
1886 fn test_recast_sketch_block_with_arg_shorthand_and_comment() {
1887 let input = r#"on = XY
1891sketch(
1892 on,
1893 // plane
1894) {
1895 return 0
1896}
1897"#;
1898 let program = crate::parsing::top_level_parse(input).unwrap();
1899 let output = program.recast_top(&Default::default(), 0);
1900 assert_eq!(output, input);
1901
1902 let program = crate::parsing::top_level_parse(&output).unwrap();
1904 let output2 = program.recast_top(&Default::default(), 0);
1905 assert_eq!(output2, output);
1906 }
1907
1908 #[test]
1909 fn test_recast_sketch_block_with_statements_in_block() {
1910 let input = r#"sketch() {
1911 // Comments inside block.
1912 x = 5
1913 y = 2
1914}
1915"#;
1916 let program = crate::parsing::top_level_parse(input).unwrap();
1917 let output = program.recast_top(&Default::default(), 0);
1918 assert_eq!(output, input);
1919 }
1920
1921 #[test]
1922 fn test_recast_bug_fn_in_fn() {
1923 let some_program_string = r#"// Start point (top left)
1924zoo_x = -20
1925zoo_y = 7
1926// Scale
1927s = 1 // s = 1 -> height of Z is 13.4mm
1928// Depth
1929d = 1
1930
1931fn rect(x, y, w, h) {
1932 startSketchOn(XY)
1933 |> startProfile(at = [x, y])
1934 |> xLine(length = w)
1935 |> yLine(length = h)
1936 |> xLine(length = -w)
1937 |> close()
1938 |> extrude(d)
1939}
1940
1941fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1942 startSketchOn(XY)
1943 |> startProfile(at = [x1, y1])
1944 |> line(endAbsolute = [x2, y2])
1945 |> line(endAbsolute = [x3, y3])
1946 |> line(endAbsolute = [x4, y4])
1947 |> close()
1948 |> extrude(d)
1949}
1950
1951fn crosshair(x, y) {
1952 startSketchOn(XY)
1953 |> startProfile(at = [x, y])
1954 |> yLine(length = 1)
1955 |> yLine(length = -2)
1956 |> yLine(length = 1)
1957 |> xLine(length = 1)
1958 |> xLine(length = -2)
1959}
1960
1961fn z(z_x, z_y) {
1962 z_end_w = s * 8.4
1963 z_end_h = s * 3
1964 z_corner = s * 2
1965 z_w = z_end_w + 2 * z_corner
1966 z_h = z_w * 1.08130081300813
1967 rect(
1968 z_x,
1969 a = z_y,
1970 b = z_end_w,
1971 c = -z_end_h,
1972 )
1973 rect(
1974 z_x + z_w,
1975 a = z_y,
1976 b = -z_corner,
1977 c = -z_corner,
1978 )
1979 rect(
1980 z_x + z_w,
1981 a = z_y - z_h,
1982 b = -z_end_w,
1983 c = z_end_h,
1984 )
1985 rect(
1986 z_x,
1987 a = z_y - z_h,
1988 b = z_corner,
1989 c = z_corner,
1990 )
1991}
1992
1993fn o(c_x, c_y) {
1994 // Outer and inner radii
1995 o_r = s * 6.95
1996 i_r = 0.5652173913043478 * o_r
1997
1998 // Angle offset for diagonal break
1999 a = 7
2000
2001 // Start point for the top sketch
2002 o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
2003 o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
2004
2005 // Start point for the bottom sketch
2006 o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
2007 o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
2008
2009 // End point for the bottom startSketch
2010 o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
2011 o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
2012
2013 // Where is the center?
2014 // crosshair(c_x, c_y)
2015
2016
2017 startSketchOn(XY)
2018 |> startProfile(at = [o_x1, o_y1])
2019 |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
2020 |> angledLine(angle = 45, length = o_r - i_r)
2021 |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
2022 |> close()
2023 |> extrude(d)
2024
2025 startSketchOn(XY)
2026 |> startProfile(at = [o_x2, o_y2])
2027 |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
2028 |> angledLine(angle = 225, length = o_r - i_r)
2029 |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
2030 |> close()
2031 |> extrude(d)
2032}
2033
2034fn zoo(x0, y0) {
2035 z(x = x0, y = y0)
2036 o(x = x0 + s * 20, y = y0 - (s * 6.7))
2037 o(x = x0 + s * 35, y = y0 - (s * 6.7))
2038}
2039
2040zoo(x = zoo_x, y = zoo_y)
2041"#;
2042 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2043
2044 let recasted = program.recast_top(&Default::default(), 0);
2045 assert_eq!(recasted, some_program_string);
2046 }
2047
2048 #[test]
2049 fn test_nested_fns_indent() {
2050 let some_program_string = "\
2051x = 1
2052fn rect(x, y, w, h) {
2053 y = 2
2054 z = 3
2055 startSketchOn(XY)
2056 |> startProfile(at = [x, y])
2057 |> xLine(length = w)
2058 |> yLine(length = h)
2059 |> xLine(length = -w)
2060 |> close()
2061 |> extrude(d)
2062}
2063";
2064 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2065
2066 let recasted = program.recast_top(&Default::default(), 0);
2067 assert_eq!(recasted, some_program_string);
2068 }
2069
2070 #[test]
2071 fn test_recast_bug_extra_parens() {
2072 let some_program_string = r#"// Ball Bearing
2073// 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.
2074
2075// Define constants like ball diameter, inside diameter, overhange length, and thickness
2076sphereDia = 0.5
2077insideDia = 1
2078thickness = 0.25
2079overHangLength = .4
2080
2081// Sketch and revolve the inside bearing piece
2082insideRevolve = startSketchOn(XZ)
2083 |> startProfile(at = [insideDia / 2, 0])
2084 |> line(end = [0, thickness + sphereDia / 2])
2085 |> line(end = [overHangLength, 0])
2086 |> line(end = [0, -thickness])
2087 |> line(end = [-overHangLength + thickness, 0])
2088 |> line(end = [0, -sphereDia])
2089 |> line(end = [overHangLength - thickness, 0])
2090 |> line(end = [0, -thickness])
2091 |> line(end = [-overHangLength, 0])
2092 |> close()
2093 |> revolve(axis = Y)
2094
2095// 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)
2096sphere = startSketchOn(XZ)
2097 |> startProfile(at = [
2098 0.05 + insideDia / 2 + thickness,
2099 0 - 0.05
2100 ])
2101 |> line(end = [sphereDia - 0.1, 0])
2102 |> arc(
2103 angle_start = 0,
2104 angle_end = -180,
2105 radius = sphereDia / 2 - 0.05
2106 )
2107 |> close()
2108 |> revolve(axis = X)
2109 |> patternCircular3d(
2110 axis = [0, 0, 1],
2111 center = [0, 0, 0],
2112 repetitions = 10,
2113 arcDegrees = 360,
2114 rotateDuplicates = true
2115 )
2116
2117// Sketch and revolve the outside bearing
2118outsideRevolve = startSketchOn(XZ)
2119 |> startProfile(at = [
2120 insideDia / 2 + thickness + sphereDia,
2121 0
2122 ]
2123 )
2124 |> line(end = [0, sphereDia / 2])
2125 |> line(end = [-overHangLength + thickness, 0])
2126 |> line(end = [0, thickness])
2127 |> line(end = [overHangLength, 0])
2128 |> line(end = [0, -2 * thickness - sphereDia])
2129 |> line(end = [-overHangLength, 0])
2130 |> line(end = [0, thickness])
2131 |> line(end = [overHangLength - thickness, 0])
2132 |> close()
2133 |> revolve(axis = Y)"#;
2134 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2135
2136 let recasted = program.recast_top(&Default::default(), 0);
2137 assert_eq!(
2138 recasted,
2139 r#"// Ball Bearing
2140// 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.
2141
2142// Define constants like ball diameter, inside diameter, overhange length, and thickness
2143sphereDia = 0.5
2144insideDia = 1
2145thickness = 0.25
2146overHangLength = .4
2147
2148// Sketch and revolve the inside bearing piece
2149insideRevolve = startSketchOn(XZ)
2150 |> startProfile(at = [insideDia / 2, 0])
2151 |> line(end = [0, thickness + sphereDia / 2])
2152 |> line(end = [overHangLength, 0])
2153 |> line(end = [0, -thickness])
2154 |> line(end = [-overHangLength + thickness, 0])
2155 |> line(end = [0, -sphereDia])
2156 |> line(end = [overHangLength - thickness, 0])
2157 |> line(end = [0, -thickness])
2158 |> line(end = [-overHangLength, 0])
2159 |> close()
2160 |> revolve(axis = Y)
2161
2162// 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)
2163sphere = startSketchOn(XZ)
2164 |> startProfile(at = [
2165 0.05 + insideDia / 2 + thickness,
2166 0 - 0.05
2167 ])
2168 |> line(end = [sphereDia - 0.1, 0])
2169 |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
2170 |> close()
2171 |> revolve(axis = X)
2172 |> patternCircular3d(
2173 axis = [0, 0, 1],
2174 center = [0, 0, 0],
2175 repetitions = 10,
2176 arcDegrees = 360,
2177 rotateDuplicates = true,
2178 )
2179
2180// Sketch and revolve the outside bearing
2181outsideRevolve = startSketchOn(XZ)
2182 |> startProfile(at = [
2183 insideDia / 2 + thickness + sphereDia,
2184 0
2185 ])
2186 |> line(end = [0, sphereDia / 2])
2187 |> line(end = [-overHangLength + thickness, 0])
2188 |> line(end = [0, thickness])
2189 |> line(end = [overHangLength, 0])
2190 |> line(end = [0, -2 * thickness - sphereDia])
2191 |> line(end = [-overHangLength, 0])
2192 |> line(end = [0, thickness])
2193 |> line(end = [overHangLength - thickness, 0])
2194 |> close()
2195 |> revolve(axis = Y)
2196"#
2197 );
2198 }
2199
2200 #[test]
2201 fn test_recast_fn_in_object() {
2202 let some_program_string = r#"bing = { yo = 55 }
2203myNestedVar = [{ prop = callExp(bing.yo) }]
2204"#;
2205 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2206
2207 let recasted = program.recast_top(&Default::default(), 0);
2208 assert_eq!(recasted, some_program_string);
2209 }
2210
2211 #[test]
2212 fn test_recast_fn_in_array() {
2213 let some_program_string = r#"bing = { yo = 55 }
2214myNestedVar = [callExp(bing.yo)]
2215"#;
2216 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2217
2218 let recasted = program.recast_top(&Default::default(), 0);
2219 assert_eq!(recasted, some_program_string);
2220 }
2221
2222 #[test]
2223 fn test_recast_ranges() {
2224 let some_program_string = r#"foo = [0..10]
2225ten = 10
2226bar = [0 + 1 .. ten]
2227"#;
2228 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2229
2230 let recasted = program.recast_top(&Default::default(), 0);
2231 assert_eq!(recasted, some_program_string);
2232 }
2233
2234 #[test]
2235 fn test_recast_space_in_fn_call() {
2236 let some_program_string = r#"fn thing (x) {
2237 return x + 1
2238}
2239
2240thing ( 1 )
2241"#;
2242 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2243
2244 let recasted = program.recast_top(&Default::default(), 0);
2245 assert_eq!(
2246 recasted,
2247 r#"fn thing(x) {
2248 return x + 1
2249}
2250
2251thing(1)
2252"#
2253 );
2254 }
2255
2256 #[test]
2257 fn test_recast_typed_fn() {
2258 let some_program_string = r#"fn thing(x: string, y: [bool]): number {
2259 return x + 1
2260}
2261"#;
2262 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2263
2264 let recasted = program.recast_top(&Default::default(), 0);
2265 assert_eq!(recasted, some_program_string);
2266 }
2267
2268 #[test]
2269 fn test_recast_typed_consts() {
2270 let some_program_string = r#"a = 42: number
2271export b = 3.2: number(ft)
2272c = "dsfds": A | B | C
2273d = [1]: [number]
2274e = foo: [number; 3]
2275f = [1, 2, 3]: [number; 1+]
2276f = [1, 2, 3]: [number; 3+]
2277"#;
2278 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2279
2280 let recasted = program.recast_top(&Default::default(), 0);
2281 assert_eq!(recasted, some_program_string);
2282 }
2283
2284 #[test]
2285 fn test_recast_object_fn_in_array_weird_bracket() {
2286 let some_program_string = r#"bing = { yo = 55 }
2287myNestedVar = [
2288 {
2289 prop: line(a = [bing.yo, 21], b = sketch001)
2290}
2291]
2292"#;
2293 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2294
2295 let recasted = program.recast_top(&Default::default(), 0);
2296 let expected = r#"bing = { yo = 55 }
2297myNestedVar = [
2298 {
2299 prop = line(a = [bing.yo, 21], b = sketch001)
2300 }
2301]
2302"#;
2303 assert_eq!(recasted, expected,);
2304 }
2305
2306 #[test]
2307 fn test_recast_empty_file() {
2308 let some_program_string = r#""#;
2309 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2310
2311 let recasted = program.recast_top(&Default::default(), 0);
2312 assert_eq!(recasted, r#""#);
2314 }
2315
2316 #[test]
2317 fn test_recast_empty_file_new_line() {
2318 let some_program_string = r#"
2319"#;
2320 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2321
2322 let recasted = program.recast_top(&Default::default(), 0);
2323 assert_eq!(recasted, r#""#);
2325 }
2326
2327 #[test]
2328 fn test_recast_shebang() {
2329 let some_program_string = r#"#!/usr/local/env zoo kcl
2330part001 = startSketchOn(XY)
2331 |> startProfile(at = [-10, -10])
2332 |> line(end = [20, 0])
2333 |> line(end = [0, 20])
2334 |> line(end = [-20, 0])
2335 |> close()
2336"#;
2337
2338 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2339
2340 let recasted = program.recast_top(&Default::default(), 0);
2341 assert_eq!(
2342 recasted,
2343 r#"#!/usr/local/env zoo kcl
2344
2345part001 = startSketchOn(XY)
2346 |> startProfile(at = [-10, -10])
2347 |> line(end = [20, 0])
2348 |> line(end = [0, 20])
2349 |> line(end = [-20, 0])
2350 |> close()
2351"#
2352 );
2353 }
2354
2355 #[test]
2356 fn test_recast_shebang_new_lines() {
2357 let some_program_string = r#"#!/usr/local/env zoo kcl
2358
2359
2360
2361part001 = startSketchOn(XY)
2362 |> startProfile(at = [-10, -10])
2363 |> line(end = [20, 0])
2364 |> line(end = [0, 20])
2365 |> line(end = [-20, 0])
2366 |> close()
2367"#;
2368
2369 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2370
2371 let recasted = program.recast_top(&Default::default(), 0);
2372 assert_eq!(
2373 recasted,
2374 r#"#!/usr/local/env zoo kcl
2375
2376part001 = startSketchOn(XY)
2377 |> startProfile(at = [-10, -10])
2378 |> line(end = [20, 0])
2379 |> line(end = [0, 20])
2380 |> line(end = [-20, 0])
2381 |> close()
2382"#
2383 );
2384 }
2385
2386 #[test]
2387 fn test_recast_shebang_with_comments() {
2388 let some_program_string = r#"#!/usr/local/env zoo kcl
2389
2390// Yo yo my comments.
2391part001 = startSketchOn(XY)
2392 |> startProfile(at = [-10, -10])
2393 |> line(end = [20, 0])
2394 |> line(end = [0, 20])
2395 |> line(end = [-20, 0])
2396 |> close()
2397"#;
2398
2399 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2400
2401 let recasted = program.recast_top(&Default::default(), 0);
2402 assert_eq!(
2403 recasted,
2404 r#"#!/usr/local/env zoo kcl
2405
2406// Yo yo my comments.
2407part001 = startSketchOn(XY)
2408 |> startProfile(at = [-10, -10])
2409 |> line(end = [20, 0])
2410 |> line(end = [0, 20])
2411 |> line(end = [-20, 0])
2412 |> close()
2413"#
2414 );
2415 }
2416
2417 #[test]
2418 fn test_recast_empty_function_body_with_comments() {
2419 let input = r#"fn myFunc() {
2420 // Yo yo my comments.
2421}
2422"#;
2423
2424 let program = crate::parsing::top_level_parse(input).unwrap();
2425 let output = program.recast_top(&Default::default(), 0);
2426 assert_eq!(output, input);
2427 }
2428
2429 #[test]
2430 fn test_recast_large_file() {
2431 let some_program_string = r#"@settings(units=mm)
2432// define nts
2433radius = 6.0
2434width = 144.0
2435length = 83.0
2436depth = 45.0
2437thk = 5
2438hole_diam = 5
2439// define a rectangular shape func
2440fn rectShape(pos, w, l) {
2441 rr = startSketchOn(XY)
2442 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2443 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2444 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2445 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2446 |> close($edge4)
2447 return rr
2448}
2449// build the body of the focusrite scarlett solo gen 4
2450// only used for visualization
2451scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2452 |> extrude(depth)
2453 |> fillet(
2454 radius = radius,
2455 tags = [
2456 edge2,
2457 edge4,
2458 getOppositeEdge(edge2),
2459 getOppositeEdge(edge4)
2460]
2461 )
2462 // build the bracket sketch around the body
2463fn bracketSketch(w, d, t) {
2464 s = startSketchOn({
2465 plane = {
2466 origin = { x = 0, y = length / 2 + thk, z = 0 },
2467 x_axis = { x = 1, y = 0, z = 0 },
2468 y_axis = { x = 0, y = 0, z = 1 },
2469 z_axis = { x = 0, y = 1, z = 0 }
2470}
2471 })
2472 |> startProfile(at = [-w / 2 - t, d + t])
2473 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2474 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2475 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2476 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2477 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2478 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2479 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2480 |> close($edge8)
2481 return s
2482}
2483// build the body of the bracket
2484bracket_body = bracketSketch(w = width, d = depth, t = thk)
2485 |> extrude(length + 10)
2486 |> fillet(
2487 radius = radius,
2488 tags = [
2489 getNextAdjacentEdge(edge7),
2490 getNextAdjacentEdge(edge2),
2491 getNextAdjacentEdge(edge3),
2492 getNextAdjacentEdge(edge6)
2493]
2494 )
2495 // build the tabs of the mounting bracket (right side)
2496tabs_r = startSketchOn({
2497 plane = {
2498 origin = { x = 0, y = 0, z = depth + thk },
2499 x_axis = { x = 1, y = 0, z = 0 },
2500 y_axis = { x = 0, y = 1, z = 0 },
2501 z_axis = { x = 0, y = 0, z = 1 }
2502}
2503 })
2504 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2505 |> line(end = [10, -5])
2506 |> line(end = [0, -10])
2507 |> line(end = [-10, -5])
2508 |> close()
2509 |> subtract2d(tool = circle(
2510 center = [
2511 width / 2 + thk + hole_diam,
2512 length / 2 - hole_diam
2513 ],
2514 radius = hole_diam / 2
2515 ))
2516 |> extrude(-thk)
2517 |> patternLinear3d(
2518 axis = [0, -1, 0],
2519 repetitions = 1,
2520 distance = length - 10
2521 )
2522 // build the tabs of the mounting bracket (left side)
2523tabs_l = startSketchOn({
2524 plane = {
2525 origin = { x = 0, y = 0, z = depth + thk },
2526 x_axis = { x = 1, y = 0, z = 0 },
2527 y_axis = { x = 0, y = 1, z = 0 },
2528 z_axis = { x = 0, y = 0, z = 1 }
2529}
2530 })
2531 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2532 |> line(end = [-10, -5])
2533 |> line(end = [0, -10])
2534 |> line(end = [10, -5])
2535 |> close()
2536 |> subtract2d(tool = circle(
2537 center = [
2538 -width / 2 - thk - hole_diam,
2539 length / 2 - hole_diam
2540 ],
2541 radius = hole_diam / 2
2542 ))
2543 |> extrude(-thk)
2544 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2545"#;
2546 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2547
2548 let recasted = program.recast_top(&Default::default(), 0);
2549 assert_eq!(
2551 recasted,
2552 r#"@settings(units = mm)
2553
2554// define nts
2555radius = 6.0
2556width = 144.0
2557length = 83.0
2558depth = 45.0
2559thk = 5
2560hole_diam = 5
2561// define a rectangular shape func
2562fn rectShape(pos, w, l) {
2563 rr = startSketchOn(XY)
2564 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2565 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2566 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2567 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2568 |> close($edge4)
2569 return rr
2570}
2571// build the body of the focusrite scarlett solo gen 4
2572// only used for visualization
2573scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2574 |> extrude(depth)
2575 |> fillet(
2576 radius = radius,
2577 tags = [
2578 edge2,
2579 edge4,
2580 getOppositeEdge(edge2),
2581 getOppositeEdge(edge4)
2582 ],
2583 )
2584// build the bracket sketch around the body
2585fn bracketSketch(w, d, t) {
2586 s = startSketchOn({
2587 plane = {
2588 origin = { x = 0, y = length / 2 + thk, z = 0 },
2589 x_axis = { x = 1, y = 0, z = 0 },
2590 y_axis = { x = 0, y = 0, z = 1 },
2591 z_axis = { x = 0, y = 1, z = 0 }
2592 }
2593 })
2594 |> startProfile(at = [-w / 2 - t, d + t])
2595 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2596 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2597 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2598 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2599 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2600 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2601 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2602 |> close($edge8)
2603 return s
2604}
2605// build the body of the bracket
2606bracket_body = bracketSketch(w = width, d = depth, t = thk)
2607 |> extrude(length + 10)
2608 |> fillet(
2609 radius = radius,
2610 tags = [
2611 getNextAdjacentEdge(edge7),
2612 getNextAdjacentEdge(edge2),
2613 getNextAdjacentEdge(edge3),
2614 getNextAdjacentEdge(edge6)
2615 ],
2616 )
2617// build the tabs of the mounting bracket (right side)
2618tabs_r = startSketchOn({
2619 plane = {
2620 origin = { x = 0, y = 0, z = depth + thk },
2621 x_axis = { x = 1, y = 0, z = 0 },
2622 y_axis = { x = 0, y = 1, z = 0 },
2623 z_axis = { x = 0, y = 0, z = 1 }
2624 }
2625})
2626 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2627 |> line(end = [10, -5])
2628 |> line(end = [0, -10])
2629 |> line(end = [-10, -5])
2630 |> close()
2631 |> subtract2d(tool = circle(
2632 center = [
2633 width / 2 + thk + hole_diam,
2634 length / 2 - hole_diam
2635 ],
2636 radius = hole_diam / 2,
2637 ))
2638 |> extrude(-thk)
2639 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2640// build the tabs of the mounting bracket (left side)
2641tabs_l = startSketchOn({
2642 plane = {
2643 origin = { x = 0, y = 0, z = depth + thk },
2644 x_axis = { x = 1, y = 0, z = 0 },
2645 y_axis = { x = 0, y = 1, z = 0 },
2646 z_axis = { x = 0, y = 0, z = 1 }
2647 }
2648})
2649 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2650 |> line(end = [-10, -5])
2651 |> line(end = [0, -10])
2652 |> line(end = [10, -5])
2653 |> close()
2654 |> subtract2d(tool = circle(
2655 center = [
2656 -width / 2 - thk - hole_diam,
2657 length / 2 - hole_diam
2658 ],
2659 radius = hole_diam / 2,
2660 ))
2661 |> extrude(-thk)
2662 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2663"#
2664 );
2665 }
2666
2667 #[test]
2668 fn test_recast_nested_var_declaration_in_fn_body() {
2669 let some_program_string = r#"fn cube(pos, scale) {
2670 sg = startSketchOn(XY)
2671 |> startProfile(at = pos)
2672 |> line(end = [0, scale])
2673 |> line(end = [scale, 0])
2674 |> line(end = [0, -scale])
2675 |> close()
2676 |> extrude(scale)
2677}"#;
2678 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2679
2680 let recasted = program.recast_top(&Default::default(), 0);
2681 assert_eq!(
2682 recasted,
2683 r#"fn cube(pos, scale) {
2684 sg = startSketchOn(XY)
2685 |> startProfile(at = pos)
2686 |> line(end = [0, scale])
2687 |> line(end = [scale, 0])
2688 |> line(end = [0, -scale])
2689 |> close()
2690 |> extrude(scale)
2691}
2692"#
2693 );
2694 }
2695
2696 #[test]
2697 fn test_as() {
2698 let some_program_string = r#"fn cube(pos, scale) {
2699 x = dfsfs + dfsfsd as y
2700
2701 sg = startSketchOn(XY)
2702 |> startProfile(at = pos) as foo
2703 |> line([0, scale])
2704 |> line([scale, 0]) as bar
2705 |> line([0 as baz, -scale] as qux)
2706 |> close()
2707 |> extrude(length = scale)
2708}
2709
2710cube(pos = 0, scale = 0) as cub
2711"#;
2712 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2713
2714 let recasted = program.recast_top(&Default::default(), 0);
2715 assert_eq!(recasted, some_program_string,);
2716 }
2717
2718 #[test]
2719 fn test_recast_with_bad_indentation() {
2720 let some_program_string = r#"part001 = startSketchOn(XY)
2721 |> startProfile(at = [0.0, 5.0])
2722 |> line(end = [0.4900857016, -0.0240763666])
2723 |> line(end = [0.6804562304, 0.9087880491])"#;
2724 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2725
2726 let recasted = program.recast_top(&Default::default(), 0);
2727 assert_eq!(
2728 recasted,
2729 r#"part001 = startSketchOn(XY)
2730 |> startProfile(at = [0.0, 5.0])
2731 |> line(end = [0.4900857016, -0.0240763666])
2732 |> line(end = [0.6804562304, 0.9087880491])
2733"#
2734 );
2735 }
2736
2737 #[test]
2738 fn test_recast_with_bad_indentation_and_inline_comment() {
2739 let some_program_string = r#"part001 = startSketchOn(XY)
2740 |> startProfile(at = [0.0, 5.0])
2741 |> line(end = [0.4900857016, -0.0240763666]) // hello world
2742 |> line(end = [0.6804562304, 0.9087880491])"#;
2743 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2744
2745 let recasted = program.recast_top(&Default::default(), 0);
2746 assert_eq!(
2747 recasted,
2748 r#"part001 = startSketchOn(XY)
2749 |> startProfile(at = [0.0, 5.0])
2750 |> line(end = [0.4900857016, -0.0240763666]) // hello world
2751 |> line(end = [0.6804562304, 0.9087880491])
2752"#
2753 );
2754 }
2755 #[test]
2756 fn test_recast_with_bad_indentation_and_line_comment() {
2757 let some_program_string = r#"part001 = startSketchOn(XY)
2758 |> startProfile(at = [0.0, 5.0])
2759 |> line(end = [0.4900857016, -0.0240763666])
2760 // hello world
2761 |> line(end = [0.6804562304, 0.9087880491])"#;
2762 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2763
2764 let recasted = program.recast_top(&Default::default(), 0);
2765 assert_eq!(
2766 recasted,
2767 r#"part001 = startSketchOn(XY)
2768 |> startProfile(at = [0.0, 5.0])
2769 |> line(end = [0.4900857016, -0.0240763666])
2770 // hello world
2771 |> line(end = [0.6804562304, 0.9087880491])
2772"#
2773 );
2774 }
2775
2776 #[test]
2777 fn test_recast_comment_in_a_fn_block() {
2778 let some_program_string = r#"fn myFn() {
2779 // this is a comment
2780 yo = { a = { b = { c = '123' } } } /* block
2781 comment */
2782
2783 key = 'c'
2784 // this is also a comment
2785 return things
2786}"#;
2787 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2788
2789 let recasted = program.recast_top(&Default::default(), 0);
2790 assert_eq!(
2791 recasted,
2792 r#"fn myFn() {
2793 // this is a comment
2794 yo = { a = { b = { c = '123' } } } /* block
2795 comment */
2796
2797 key = 'c'
2798 // this is also a comment
2799 return things
2800}
2801"#
2802 );
2803 }
2804
2805 #[test]
2806 fn test_recast_comment_under_variable() {
2807 let some_program_string = r#"key = 'c'
2808// this is also a comment
2809thing = 'foo'
2810"#;
2811 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2812
2813 let recasted = program.recast_top(&Default::default(), 0);
2814 assert_eq!(
2815 recasted,
2816 r#"key = 'c'
2817// this is also a comment
2818thing = 'foo'
2819"#
2820 );
2821 }
2822
2823 #[test]
2824 fn test_recast_multiline_comment_start_file() {
2825 let some_program_string = r#"// hello world
2826// I am a comment
2827key = 'c'
2828// this is also a comment
2829// hello
2830thing = 'foo'
2831"#;
2832 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2833
2834 let recasted = program.recast_top(&Default::default(), 0);
2835 assert_eq!(
2836 recasted,
2837 r#"// hello world
2838// I am a comment
2839key = 'c'
2840// this is also a comment
2841// hello
2842thing = 'foo'
2843"#
2844 );
2845 }
2846
2847 #[test]
2848 fn test_recast_empty_comment() {
2849 let some_program_string = r#"// hello world
2850//
2851// I am a comment
2852key = 'c'
2853
2854//
2855// I am a comment
2856thing = 'c'
2857
2858foo = 'bar' //
2859"#;
2860 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2861
2862 let recasted = program.recast_top(&Default::default(), 0);
2863 assert_eq!(
2864 recasted,
2865 r#"// hello world
2866//
2867// I am a comment
2868key = 'c'
2869
2870//
2871// I am a comment
2872thing = 'c'
2873
2874foo = 'bar' //
2875"#
2876 );
2877 }
2878
2879 #[test]
2880 fn test_recast_multiline_comment_under_variable() {
2881 let some_program_string = r#"key = 'c'
2882// this is also a comment
2883// hello
2884thing = 'foo'
2885"#;
2886 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2887
2888 let recasted = program.recast_top(&Default::default(), 0);
2889 assert_eq!(
2890 recasted,
2891 r#"key = 'c'
2892// this is also a comment
2893// hello
2894thing = 'foo'
2895"#
2896 );
2897 }
2898
2899 #[test]
2900 fn test_recast_only_line_comments() {
2901 let code = r#"// comment at start
2902"#;
2903 let program = crate::parsing::top_level_parse(code).unwrap();
2904
2905 assert_eq!(program.recast_top(&Default::default(), 0), code);
2906 }
2907
2908 #[test]
2909 fn test_recast_comment_at_start() {
2910 let test_program = r#"
2911/* comment at start */
2912
2913mySk1 = startSketchOn(XY)
2914 |> startProfile(at = [0, 0])"#;
2915 let program = crate::parsing::top_level_parse(test_program).unwrap();
2916
2917 let recasted = program.recast_top(&Default::default(), 0);
2918 assert_eq!(
2919 recasted,
2920 r#"/* comment at start */
2921
2922mySk1 = startSketchOn(XY)
2923 |> startProfile(at = [0, 0])
2924"#
2925 );
2926 }
2927
2928 #[test]
2929 fn test_recast_lots_of_comments() {
2930 let some_program_string = r#"// comment at start
2931mySk1 = startSketchOn(XY)
2932 |> startProfile(at = [0, 0])
2933 |> line(endAbsolute = [1, 1])
2934 // comment here
2935 |> line(endAbsolute = [0, 1], tag = $myTag)
2936 |> line(endAbsolute = [1, 1])
2937 /* and
2938 here
2939 */
2940 // a comment between pipe expression statements
2941 |> rx(90)
2942 // and another with just white space between others below
2943 |> ry(45)
2944 |> rx(45)
2945// one more for good measure"#;
2946 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2947
2948 let recasted = program.recast_top(&Default::default(), 0);
2949 assert_eq!(
2950 recasted,
2951 r#"// comment at start
2952mySk1 = startSketchOn(XY)
2953 |> startProfile(at = [0, 0])
2954 |> line(endAbsolute = [1, 1])
2955 // comment here
2956 |> line(endAbsolute = [0, 1], tag = $myTag)
2957 |> line(endAbsolute = [1, 1])
2958 /* and
2959 here */
2960 // a comment between pipe expression statements
2961 |> rx(90)
2962 // and another with just white space between others below
2963 |> ry(45)
2964 |> rx(45)
2965// one more for good measure
2966"#
2967 );
2968 }
2969
2970 #[test]
2971 fn test_recast_multiline_object() {
2972 let some_program_string = r#"x = {
2973 a = 1000000000,
2974 b = 2000000000,
2975 c = 3000000000,
2976 d = 4000000000,
2977 e = 5000000000
2978}"#;
2979 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2980
2981 let recasted = program.recast_top(&Default::default(), 0);
2982 assert_eq!(recasted.trim(), some_program_string);
2983 }
2984
2985 #[test]
2986 fn test_recast_first_level_object() {
2987 let some_program_string = r#"three = 3
2988
2989yo = {
2990 aStr = 'str',
2991 anum = 2,
2992 identifier = three,
2993 binExp = 4 + 5
2994}
2995yo = [
2996 1,
2997 " 2,",
2998 "three",
2999 4 + 5,
3000 " hey oooooo really long long long"
3001]
3002"#;
3003 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3004
3005 let recasted = program.recast_top(&Default::default(), 0);
3006 assert_eq!(recasted, some_program_string);
3007 }
3008
3009 #[test]
3010 fn test_recast_new_line_before_comment() {
3011 let some_program_string = r#"
3012// this is a comment
3013yo = { a = { b = { c = '123' } } }
3014
3015key = 'c'
3016things = "things"
3017
3018// this is also a comment"#;
3019 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3020
3021 let recasted = program.recast_top(&Default::default(), 0);
3022 let expected = some_program_string.trim();
3023 let actual = recasted.trim();
3025 assert_eq!(actual, expected);
3026 }
3027
3028 #[test]
3029 fn test_recast_comment_tokens_inside_strings() {
3030 let some_program_string = r#"b = {
3031 end = 141,
3032 start = 125,
3033 type_ = "NonCodeNode",
3034 value = "
3035 // a comment
3036 "
3037}"#;
3038 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3039
3040 let recasted = program.recast_top(&Default::default(), 0);
3041 assert_eq!(recasted.trim(), some_program_string.trim());
3042 }
3043
3044 #[test]
3045 fn test_recast_array_new_line_in_pipe() {
3046 let some_program_string = r#"myVar = 3
3047myVar2 = 5
3048myVar3 = 6
3049myAng = 40
3050myAng2 = 134
3051part001 = startSketchOn(XY)
3052 |> startProfile(at = [0, 0])
3053 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3054 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3055 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
3056 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3057
3058 let recasted = program.recast_top(&Default::default(), 0);
3059 assert_eq!(recasted.trim(), some_program_string);
3060 }
3061
3062 #[test]
3063 fn test_recast_array_new_line_in_pipe_custom() {
3064 let some_program_string = r#"myVar = 3
3065myVar2 = 5
3066myVar3 = 6
3067myAng = 40
3068myAng2 = 134
3069part001 = startSketchOn(XY)
3070 |> startProfile(at = [0, 0])
3071 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3072 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3073 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
3074"#;
3075 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3076
3077 let recasted = program.recast_top(
3078 &FormatOptions {
3079 tab_size: 3,
3080 use_tabs: false,
3081 insert_final_newline: true,
3082 },
3083 0,
3084 );
3085 assert_eq!(recasted, some_program_string);
3086 }
3087
3088 #[test]
3089 fn test_recast_after_rename_std() {
3090 let some_program_string = r#"part001 = startSketchOn(XY)
3091 |> startProfile(at = [0.0000000000, 5.0000000000])
3092 |> line(end = [0.4900857016, -0.0240763666])
3093
3094part002 = "part002"
3095things = [part001, 0.0]
3096blah = 1
3097foo = false
3098baz = {a: 1, part001: "thing"}
3099
3100fn ghi(part001) {
3101 return part001
3102}
3103"#;
3104 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3105 program.rename_symbol("mySuperCoolPart", 6);
3106
3107 let recasted = program.recast_top(&Default::default(), 0);
3108 assert_eq!(
3109 recasted,
3110 r#"mySuperCoolPart = startSketchOn(XY)
3111 |> startProfile(at = [0.0, 5.0])
3112 |> line(end = [0.4900857016, -0.0240763666])
3113
3114part002 = "part002"
3115things = [mySuperCoolPart, 0.0]
3116blah = 1
3117foo = false
3118baz = { a = 1, part001 = "thing" }
3119
3120fn ghi(part001) {
3121 return part001
3122}
3123"#
3124 );
3125 }
3126
3127 #[test]
3128 fn test_recast_after_rename_fn_args() {
3129 let some_program_string = r#"fn ghi(x, y, z) {
3130 return x
3131}"#;
3132 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3133 program.rename_symbol("newName", 7);
3134
3135 let recasted = program.recast_top(&Default::default(), 0);
3136 assert_eq!(
3137 recasted,
3138 r#"fn ghi(newName, y, z) {
3139 return newName
3140}
3141"#
3142 );
3143 }
3144
3145 #[test]
3146 fn test_recast_trailing_comma() {
3147 let some_program_string = r#"startSketchOn(XY)
3148 |> startProfile(at = [0, 0])
3149 |> arc({
3150 radius = 1,
3151 angle_start = 0,
3152 angle_end = 180,
3153 })"#;
3154 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3155
3156 let recasted = program.recast_top(&Default::default(), 0);
3157 assert_eq!(
3158 recasted,
3159 r#"startSketchOn(XY)
3160 |> startProfile(at = [0, 0])
3161 |> arc({
3162 radius = 1,
3163 angle_start = 0,
3164 angle_end = 180
3165 })
3166"#
3167 );
3168 }
3169
3170 #[test]
3171 fn test_recast_array_no_trailing_comma_with_comments() {
3172 let some_program_string = r#"[
3173 1, // one
3174 2, // two
3175 3 // three
3176]"#;
3177 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3178
3179 let recasted = program.recast_top(&Default::default(), 0);
3180 assert_eq!(
3181 recasted,
3182 r#"[
3183 1,
3184 // one
3185 2,
3186 // two
3187 3,
3188 // three
3189]
3190"#
3191 );
3192 }
3193
3194 #[test]
3195 fn test_recast_object_no_trailing_comma_with_comments() {
3196 let some_program_string = r#"{
3197 x=1, // one
3198 y=2, // two
3199 z=3 // three
3200}"#;
3201 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3202
3203 let recasted = program.recast_top(&Default::default(), 0);
3204 assert_eq!(
3207 recasted,
3208 r#"{
3209 x = 1,
3210 // one
3211 y = 2,
3212 // two
3213 z = 3,
3214 // three
3215
3216}
3217"#
3218 );
3219 }
3220
3221 #[test]
3222 fn test_recast_comment_between_call_args() {
3223 let some_program_string = r#"rounded = fillet(
3224 body,
3225 radius = 1mm,
3226 // Keep this comment
3227 tags = [tag1, tag2],
3228)
3229"#;
3230 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3231
3232 let recasted = program.recast_top(&Default::default(), 0);
3233 assert_eq!(recasted, some_program_string);
3234
3235 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3237 let recasted2 = program.recast_top(&Default::default(), 0);
3238 assert_eq!(recasted2, recasted);
3239 }
3240
3241 #[test]
3242 fn test_recast_line_comments_in_call_args_force_multiline() {
3243 let some_program_string = r#"edged = chamfer(
3244 body,
3245 // leading
3246 length = 1mm,
3247 tags = [tag1],
3248 // trailing
3249)
3250"#;
3251 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3252
3253 let recasted = program.recast_top(&Default::default(), 0);
3254 assert_eq!(recasted, some_program_string);
3255
3256 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3258 let recasted2 = program.recast_top(&Default::default(), 0);
3259 assert_eq!(recasted2, recasted);
3260 }
3261
3262 #[test]
3263 fn test_recast_block_comment_in_call_args_stays_inline() {
3264 let some_program_string = r#"rounded = fillet(body, radius = 1mm, /* mid */ tags = [tag1])
3266"#;
3267 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3268
3269 let recasted = program.recast_top(&Default::default(), 0);
3270 assert_eq!(recasted, some_program_string);
3271
3272 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3274 let recasted2 = program.recast_top(&Default::default(), 0);
3275 assert_eq!(recasted2, recasted);
3276 }
3277
3278 #[test]
3279 fn test_recast_block_comment_in_multiline_call_args_stays_inline() {
3280 let some_program_string = r#"rounded = fillet(
3283 body,
3284 radius = 1mm,
3285 /* mid */ tags = [tag1],
3286 tag = $x,
3287)
3288"#;
3289 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3290
3291 let recasted = program.recast_top(&Default::default(), 0);
3292 assert_eq!(recasted, some_program_string);
3293
3294 let program = crate::parsing::top_level_parse(&recasted).unwrap();
3296 let recasted2 = program.recast_top(&Default::default(), 0);
3297 assert_eq!(recasted2, recasted);
3298 }
3299
3300 #[test]
3301 fn test_recast_comment_in_call_args_in_pipe() {
3302 let some_program_string = r#"part = startSketchOn(XY)
3303 |> startProfile(at = [0, 0])
3304 |> fillet(
3305 radius = 1,
3306 // why
3307 tags = [a],
3308 )
3309"#;
3310 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3311
3312 let recasted = program.recast_top(&Default::default(), 0);
3313 assert_eq!(recasted, some_program_string);
3314 }
3315
3316 #[test]
3317 fn test_recast_negative_var() {
3318 let some_program_string = r#"w = 20
3319l = 8
3320h = 10
3321
3322firstExtrude = startSketchOn(XY)
3323 |> startProfile(at = [0,0])
3324 |> line(end = [0, l])
3325 |> line(end = [w, 0])
3326 |> line(end = [0, -l])
3327 |> close()
3328 |> extrude(h)
3329"#;
3330 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3331
3332 let recasted = program.recast_top(&Default::default(), 0);
3333 assert_eq!(
3334 recasted,
3335 r#"w = 20
3336l = 8
3337h = 10
3338
3339firstExtrude = startSketchOn(XY)
3340 |> startProfile(at = [0, 0])
3341 |> line(end = [0, l])
3342 |> line(end = [w, 0])
3343 |> line(end = [0, -l])
3344 |> close()
3345 |> extrude(h)
3346"#
3347 );
3348 }
3349
3350 #[test]
3351 fn test_recast_multiline_comment() {
3352 let some_program_string = r#"w = 20
3353l = 8
3354h = 10
3355
3356// This is my comment
3357// It has multiple lines
3358// And it's really long
3359firstExtrude = startSketchOn(XY)
3360 |> startProfile(at = [0,0])
3361 |> line(end = [0, l])
3362 |> line(end = [w, 0])
3363 |> line(end = [0, -l])
3364 |> close()
3365 |> extrude(h)
3366"#;
3367 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3368
3369 let recasted = program.recast_top(&Default::default(), 0);
3370 assert_eq!(
3371 recasted,
3372 r#"w = 20
3373l = 8
3374h = 10
3375
3376// This is my comment
3377// It has multiple lines
3378// And it's really long
3379firstExtrude = startSketchOn(XY)
3380 |> startProfile(at = [0, 0])
3381 |> line(end = [0, l])
3382 |> line(end = [w, 0])
3383 |> line(end = [0, -l])
3384 |> close()
3385 |> extrude(h)
3386"#
3387 );
3388 }
3389
3390 #[test]
3391 fn test_recast_math_start_negative() {
3392 let some_program_string = r#"myVar = -5 + 6"#;
3393 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3394
3395 let recasted = program.recast_top(&Default::default(), 0);
3396 assert_eq!(recasted.trim(), some_program_string);
3397 }
3398
3399 #[test]
3400 fn test_recast_math_negate_parens() {
3401 let some_program_string = r#"wallMountL = 3.82
3402thickness = 0.5
3403
3404startSketchOn(XY)
3405 |> startProfile(at = [0, 0])
3406 |> line(end = [0, -(wallMountL - thickness)])
3407 |> line(end = [0, -(5 - thickness)])
3408 |> line(end = [0, -(5 - 1)])
3409 |> line(end = [0, -(-5 - 1)])"#;
3410 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3411
3412 let recasted = program.recast_top(&Default::default(), 0);
3413 assert_eq!(recasted.trim(), some_program_string);
3414 }
3415
3416 #[test]
3417 fn test_recast_math_nested_parens() {
3418 let some_program_string = r#"distance = 5
3419p = 3: Plane
3420FOS = { a = 3, b = 42 }: Sketch
3421sigmaAllow = 8: number(mm)
3422width = 20
3423thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
3424 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3425
3426 let recasted = program.recast_top(&Default::default(), 0);
3427 assert_eq!(recasted.trim(), some_program_string);
3428 }
3429
3430 #[test]
3431 fn no_vardec_keyword() {
3432 let some_program_string = r#"distance = 5"#;
3433 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3434
3435 let recasted = program.recast_top(&Default::default(), 0);
3436 assert_eq!(recasted.trim(), some_program_string);
3437 }
3438
3439 #[test]
3440 fn recast_types() {
3441 let some_program_string = r#"type foo
3442
3443// A comment
3444@(impl = primitive)
3445export type bar(unit, baz)
3446type baz = Foo | Bar
3447type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
3448"#;
3449 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3450 let recasted = program.recast_top(&Default::default(), 0);
3451 assert_eq!(recasted, some_program_string);
3452 }
3453
3454 #[test]
3455 fn recast_nested_fn() {
3456 let some_program_string = r#"fn f() {
3457 return fn() {
3458 return 1
3459}
3460}"#;
3461 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3462 let recasted = program.recast_top(&Default::default(), 0);
3463 let expected = "\
3464fn f() {
3465 return fn() {
3466 return 1
3467 }
3468}";
3469 assert_eq!(recasted.trim(), expected);
3470 }
3471
3472 #[test]
3473 fn recast_literal() {
3474 use winnow::Parser;
3475 for (i, (raw, expected, reason)) in [
3476 (
3477 "5.0",
3478 "5.0",
3479 "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
3480 ),
3481 (
3482 "5",
3483 "5",
3484 "integers should stay integral, i.e. don't reformat this to '5.0'",
3485 ),
3486 (
3487 "5.0000000",
3488 "5.0",
3489 "if the number is f64 but not fractional, use its canonical format",
3490 ),
3491 ("5.1", "5.1", "straightforward case works"),
3492 ]
3493 .into_iter()
3494 .enumerate()
3495 {
3496 let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
3497 let literal = crate::parsing::parser::unsigned_number_literal
3498 .parse(tokens.as_slice())
3499 .unwrap();
3500 let mut actual = String::new();
3501 literal.recast(&mut actual);
3502 assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
3503 }
3504 }
3505
3506 #[test]
3507 fn recast_objects_no_comments() {
3508 let input = r#"
3509sketch002 = startSketchOn({
3510 plane: {
3511 origin: { x = 1, y = 2, z = 3 },
3512 x_axis = { x = 4, y = 5, z = 6 },
3513 y_axis = { x = 7, y = 8, z = 9 },
3514 z_axis = { x = 10, y = 11, z = 12 }
3515 }
3516 })
3517"#;
3518 let expected = r#"sketch002 = startSketchOn({
3519 plane = {
3520 origin = { x = 1, y = 2, z = 3 },
3521 x_axis = { x = 4, y = 5, z = 6 },
3522 y_axis = { x = 7, y = 8, z = 9 },
3523 z_axis = { x = 10, y = 11, z = 12 }
3524 }
3525})
3526"#;
3527 let ast = crate::parsing::top_level_parse(input).unwrap();
3528 let actual = ast.recast_top(&FormatOptions::new(), 0);
3529 assert_eq!(actual, expected);
3530 }
3531
3532 #[test]
3533 fn unparse_fn_unnamed() {
3534 let input = "\
3535squares_out = reduce(
3536 arr,
3537 n = 0: number,
3538 f = fn(@i, accum) {
3539 return 1
3540 },
3541)
3542";
3543 let ast = crate::parsing::top_level_parse(input).unwrap();
3544 let actual = ast.recast_top(&FormatOptions::new(), 0);
3545 assert_eq!(actual, input);
3546 }
3547
3548 #[test]
3549 fn unparse_fn_named() {
3550 let input = r#"fn f(x) {
3551 return 1
3552}
3553"#;
3554 let ast = crate::parsing::top_level_parse(input).unwrap();
3555 let actual = ast.recast_top(&FormatOptions::new(), 0);
3556 assert_eq!(actual, input);
3557 }
3558
3559 #[test]
3560 fn unparse_call_inside_function_single_line() {
3561 let input = r#"fn foo() {
3562 toDegrees(atan(0.5), foo = 1)
3563 return 0
3564}
3565"#;
3566 let ast = crate::parsing::top_level_parse(input).unwrap();
3567 let actual = ast.recast_top(&FormatOptions::new(), 0);
3568 assert_eq!(actual, input);
3569 }
3570
3571 #[test]
3572 fn recast_function_types() {
3573 let input = r#"foo = x: fn
3574foo = x: fn(number)
3575fn foo(x: fn(): number): fn {
3576 return 0
3577}
3578fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
3579 return 0
3580}
3581type fn
3582type foo = fn
3583type foo = fn(a: string, b: { f: fn(): any })
3584type foo = fn([fn])
3585type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
3586"#;
3587 let ast = crate::parsing::top_level_parse(input).unwrap();
3588 let actual = ast.recast_top(&FormatOptions::new(), 0);
3589 assert_eq!(actual, input);
3590 }
3591
3592 #[test]
3593 fn unparse_call_inside_function_args_multiple_lines() {
3594 let input = r#"fn foo() {
3595 toDegrees(
3596 atan(0.5),
3597 foo = 1,
3598 bar = 2,
3599 baz = 3,
3600 qux = 4,
3601 )
3602 return 0
3603}
3604"#;
3605 let ast = crate::parsing::top_level_parse(input).unwrap();
3606 let actual = ast.recast_top(&FormatOptions::new(), 0);
3607 assert_eq!(actual, input);
3608 }
3609
3610 #[test]
3611 fn unparse_call_inside_function_single_arg_multiple_lines() {
3612 let input = r#"fn foo() {
3613 toDegrees(
3614 [
3615 profile0,
3616 profile1,
3617 profile2,
3618 profile3,
3619 profile4,
3620 profile5
3621 ],
3622 key = 1,
3623 )
3624 return 0
3625}
3626"#;
3627 let ast = crate::parsing::top_level_parse(input).unwrap();
3628 let actual = ast.recast_top(&FormatOptions::new(), 0);
3629 assert_eq!(actual, input);
3630 }
3631
3632 #[test]
3633 fn recast_objects_with_comments() {
3634 use winnow::Parser;
3635 for (i, (input, expected, reason)) in [(
3636 "\
3637{
3638 a = 1,
3639 // b = 2,
3640 c = 3
3641}",
3642 "\
3643{
3644 a = 1,
3645 // b = 2,
3646 c = 3
3647}",
3648 "preserves comments",
3649 )]
3650 .into_iter()
3651 .enumerate()
3652 {
3653 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3654 crate::parsing::parser::print_tokens(tokens.as_slice());
3655 let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3656 let mut actual = String::new();
3657 expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3658 assert_eq!(
3659 actual, expected,
3660 "failed test {i}, which is testing that recasting {reason}"
3661 );
3662 }
3663 }
3664
3665 #[test]
3666 fn recast_array_with_comments() {
3667 use winnow::Parser;
3668 for (i, (input, expected, reason)) in [
3669 (
3670 "\
3671[
3672 1,
3673 2,
3674 3,
3675 4,
3676 5,
3677 6,
3678 7,
3679 8,
3680 9,
3681 10,
3682 11,
3683 12,
3684 13,
3685 14,
3686 15,
3687 16,
3688 17,
3689 18,
3690 19,
3691 20,
3692]",
3693 "\
3694[
3695 1,
3696 2,
3697 3,
3698 4,
3699 5,
3700 6,
3701 7,
3702 8,
3703 9,
3704 10,
3705 11,
3706 12,
3707 13,
3708 14,
3709 15,
3710 16,
3711 17,
3712 18,
3713 19,
3714 20
3715]",
3716 "preserves multi-line arrays",
3717 ),
3718 (
3719 "\
3720[
3721 1,
3722 // 2,
3723 3
3724]",
3725 "\
3726[
3727 1,
3728 // 2,
3729 3
3730]",
3731 "preserves comments",
3732 ),
3733 (
3734 "\
3735[
3736 1,
3737 2,
3738 // 3
3739]",
3740 "\
3741[
3742 1,
3743 2,
3744 // 3
3745]",
3746 "preserves comments at the end of the array",
3747 ),
3748 ]
3749 .into_iter()
3750 .enumerate()
3751 {
3752 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3753 let expr = crate::parsing::parser::array_elem_by_elem
3754 .parse(tokens.as_slice())
3755 .unwrap();
3756 let mut actual = String::new();
3757 expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3758 assert_eq!(
3759 actual, expected,
3760 "failed test {i}, which is testing that recasting {reason}"
3761 );
3762 }
3763 }
3764
3765 #[test]
3766 fn code_with_comment_and_extra_lines() {
3767 let code = r#"yo = 'c'
3768
3769/* this is
3770a
3771comment */
3772yo = 'bing'
3773"#;
3774 let ast = crate::parsing::top_level_parse(code).unwrap();
3775 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3776 assert_eq!(recasted, code);
3777 }
3778
3779 #[test]
3780 fn comments_in_a_fn_block() {
3781 let code = r#"fn myFn() {
3782 // this is a comment
3783 yo = { a = { b = { c = '123' } } }
3784
3785 /* block
3786 comment */
3787 key = 'c'
3788 // this is also a comment
3789}
3790"#;
3791 let ast = crate::parsing::top_level_parse(code).unwrap();
3792 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3793 assert_eq!(recasted, code);
3794 }
3795
3796 #[test]
3797 fn array_range_end_exclusive() {
3798 let code = "myArray = [0..<4]\n";
3799 let ast = crate::parsing::top_level_parse(code).unwrap();
3800 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3801 assert_eq!(recasted, code);
3802 }
3803
3804 #[test]
3805 fn paren_precedence() {
3806 let code = r#"x = 1 - 2 - 3
3807x = (1 - 2) - 3
3808x = 1 - (2 - 3)
3809x = 1 + 2 + 3
3810x = (1 + 2) + 3
3811x = 1 + (2 + 3)
3812x = 2 * (y % 2)
3813x = (2 * y) % 2
3814x = 2 % (y * 2)
3815x = (2 % y) * 2
3816x = 2 * y % 2
3817"#;
3818
3819 let expected = r#"x = 1 - 2 - 3
3820x = 1 - 2 - 3
3821x = 1 - (2 - 3)
3822x = 1 + 2 + 3
3823x = 1 + 2 + 3
3824x = 1 + 2 + 3
3825x = 2 * (y % 2)
3826x = 2 * y % 2
3827x = 2 % (y * 2)
3828x = 2 % y * 2
3829x = 2 * y % 2
3830"#;
3831 let ast = crate::parsing::top_level_parse(code).unwrap();
3832 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3833 assert_eq!(recasted, expected);
3834 }
3835
3836 #[test]
3837 fn gap_between_body_item_and_documented_fn() {
3838 let code = "\
3839x = 360
3840
3841// Watermelon
3842fn myFn() {
3843}
3844";
3845 let ast = crate::parsing::top_level_parse(code).unwrap();
3846 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3847 let expected = code;
3848 assert_eq!(recasted, expected);
3849 }
3850
3851 #[test]
3852 fn simple_assignment_in_fn() {
3853 let code = "\
3854fn function001() {
3855 extrude002 = extrude()
3856}\n";
3857
3858 let ast = crate::parsing::top_level_parse(code).unwrap();
3859 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3860 let expected = code;
3861 assert_eq!(recasted, expected);
3862 }
3863
3864 #[test]
3865 fn no_weird_extra_lines() {
3866 let code = "\
3869// Initial comment
3870
3871@settings(defaultLengthUnit = mm)
3872
3873x = 1
3874";
3875 let ast = crate::parsing::top_level_parse(code).unwrap();
3876 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3877 let expected = code;
3878 assert_eq!(recasted, expected);
3879 }
3880
3881 #[test]
3882 fn settings_then_code_is_stable() {
3883 let code = "\
3884@settings(defaultLengthUnit = in)
3885
3886import \"cube-inches.kcl\" as cubeIn
3887import \"cube-mm.kcl\" as cubeMm
3888
3889cubeIn
3890cubeMm
3891";
3892 let formatted_once = crate::parsing::top_level_parse(code)
3893 .unwrap()
3894 .recast_top(&FormatOptions::new(), 0);
3895 assert_eq!(formatted_once, code);
3896
3897 let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3898 .unwrap()
3899 .recast_top(&FormatOptions::new(), 0);
3900 assert_eq!(formatted_twice, formatted_once);
3901 }
3902
3903 #[test]
3904 fn settings_then_standalone_comment_is_stable() {
3905 let code = "\
3906@settings(defaultLengthUnit = mm)
3907@settings(defaultAngleUnit = deg)
3908
3909// Cap for gimbal stick
3910
3911x = 1
3912";
3913 let formatted_once = crate::parsing::top_level_parse(code)
3914 .unwrap()
3915 .recast_top(&FormatOptions::new(), 0);
3916 assert_eq!(formatted_once, code);
3917
3918 let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3919 .unwrap()
3920 .recast_top(&FormatOptions::new(), 0);
3921 assert_eq!(formatted_twice, formatted_once);
3922 }
3923
3924 #[test]
3925 fn module_prefix() {
3926 let code = "x = std::sweep::SKETCH_PLANE\n";
3927 let ast = crate::parsing::top_level_parse(code).unwrap();
3928 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3929 let expected = code;
3930 assert_eq!(recasted, expected);
3931 }
3932
3933 #[test]
3934 fn inline_ifs() {
3935 let code = "y = true
3936startSketchOn(XY)
3937 |> startProfile(at = [0, 0])
3938 |> if y {
3939 yLine(length = 1)
3940 } else {
3941 xLine(length = 1)
3942 }
3943";
3944 let ast = crate::parsing::top_level_parse(code).unwrap();
3945 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3946 let expected = code;
3947 assert_eq!(recasted, expected);
3948 }
3949
3950 #[test]
3951 fn indented_binary_expressions() {
3952 let code = "\
3953fn foo() {
3954 1 == 2
3955}
3956";
3957 let ast = crate::parsing::top_level_parse(code).unwrap();
3958 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3959 let expected = code;
3960 assert_eq!(recasted, expected);
3961 }
3962
3963 #[test]
3964 fn indented_assignment() {
3965 let code = "\
3966fn foo() {
3967 x = 1
3968}
3969";
3970 let ast = crate::parsing::top_level_parse(code).unwrap();
3971 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3972 let expected = code;
3973 assert_eq!(recasted, expected);
3974 }
3975
3976 #[test]
3977 fn indented_unary_expression() {
3978 let code = "\
3979fn foo() {
3980 -x
3981}
3982";
3983 let ast = crate::parsing::top_level_parse(code).unwrap();
3984 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3985 let expected = code;
3986 assert_eq!(recasted, expected);
3987 }
3988
3989 #[test]
3990 fn indented_array_expression() {
3991 let code = "\
3992fn foo() {
3993 [1, 2]
3994}
3995";
3996 let ast = crate::parsing::top_level_parse(code).unwrap();
3997 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3998 let expected = code;
3999 assert_eq!(recasted, expected);
4000 }
4001
4002 #[test]
4003 fn indented_name_expression() {
4004 let code = "\
4005fn foo() {
4006 x
4007}
4008";
4009 let ast = crate::parsing::top_level_parse(code).unwrap();
4010 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4011 let expected = code;
4012 assert_eq!(recasted, expected);
4013 }
4014
4015 #[test]
4016 fn indented_member_assignment() {
4017 let code = "\
4018brakcetPlane = {
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 origin = { x = length / 2 },
4026 origin = { x = length / 2 }
4027}
4028";
4029 let ast = crate::parsing::top_level_parse(code).unwrap();
4030 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4031 let expected = code;
4032 assert_eq!(recasted, expected);
4033 }
4034
4035 #[test]
4036 fn badly_formatted_inline_calls() {
4037 let code = "\
4038return union([right, left])
4039 |> subtract(tools = [
4040 translate(axle(), y = pitchStabL + forkBaseL + wheelRGap + wheelR + addedLength),
4041 socket(rakeAngle = rearRake, xyTrans = [0, 12]),
4042 socket(
4043 rakeAngle = frontRake,
4044 xyTrans = [
4045 wheelW / 2 + wheelWGap + forkTineW / 2,
4046 40 + addedLength
4047 ],
4048 )
4049 ])
4050";
4051 let ast = crate::parsing::top_level_parse(code).unwrap();
4052 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4053 let expected = code;
4054 assert_eq!(recasted, expected);
4055 }
4056
4057 #[test]
4058 fn fn_args_prefixed_with_spaces() {
4059 let code = "holeAt(
4060 [cube1, cube2],
4061 plane = XY,
4062 holeBottom = hole::flat(),
4063 holeBody = hole::blind(depth = 2, diameter = 1),
4064 holeType = hole::counterbore(diameter = 1.4, depth = 1),
4065 cutAt = [1, 1],
4066)";
4067 let expected = "holeAt(
4068 [cube1, cube2],
4069 plane = XY,
4070 holeBottom = hole::flat(),
4071 holeBody = hole::blind(depth = 2, diameter = 1),
4072 holeType = hole::counterbore(diameter = 1.4, depth = 1),
4073 cutAt = [1, 1],
4074)
4075";
4076 let ast = crate::parsing::top_level_parse(code).unwrap();
4077 let recasted = ast.recast_top(&FormatOptions::new(), 0);
4078 assert_eq!(recasted, expected);
4079 }
4080
4081 #[test]
4082 fn some_fn_args_still_prefixed() {
4083 let code = "a
4084 |> b()
4085 |> subtract(
4086 tools = startSketchOn(XY)
4087 |> circle(diameter = hubDiameter)
4088 |> extrude(length = hubThickness * 5, symmetric = true),
4089 tolerance,
4090 )
4091";
4092 let ast = crate::parsing::top_level_parse(code).unwrap();
4093 let actual_recasted = ast.recast_top(&FormatOptions::new(), 0);
4094 let expected_recasted = "a
4095 |> b()
4096 |> subtract(
4097 tools = startSketchOn(XY)
4098 |> circle(diameter = hubDiameter)
4099 |> extrude(length = hubThickness * 5, symmetric = true),
4100 tolerance,
4101 )
4102";
4103 assert_eq!(actual_recasted, expected_recasted);
4104 }
4105
4106 #[test]
4107 fn first_in_pipeline_indent() {
4108 let not_clone = "gear::helical(
4111 nTeeth = 12,
4112 module = 1.5,
4113 pressureAngle = 14deg,
4114 helixAngle = 25deg,
4115 gearHeight = 5,
4116)
4117";
4118 let yes_clone = "gear::helical(
4119 nTeeth = 12,
4120 module = 1.5,
4121 pressureAngle = 14deg,
4122 helixAngle = 25deg,
4123 gearHeight = 5,
4124)
4125|> clone()
4126";
4127 let not_clone_recasted = crate::parsing::top_level_parse(not_clone)
4129 .unwrap()
4130 .recast_top(&FormatOptions::new(), 0);
4131 let yes_clone_recasted = crate::parsing::top_level_parse(yes_clone)
4132 .unwrap()
4133 .recast_top(&FormatOptions::new(), 0);
4134 assert!(not_clone_recasted.contains("\n nTeeth"));
4135 assert!(!yes_clone_recasted.contains("\n nTeeth"));
4136 assert!(yes_clone_recasted.contains("\n nTeeth"));
4137 }
4138}