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