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