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_arg_shorthand() {
1485 let input = r#"on = XY
1486sketch(on) {
1487 return 0
1488}
1489"#;
1490 let program = crate::parsing::top_level_parse(input).unwrap();
1491 let output = program.recast_top(&Default::default(), 0);
1492 assert_eq!(output, input);
1493 }
1494
1495 #[test]
1496 fn test_recast_sketch_block_with_statements_in_block() {
1497 let input = r#"sketch() {
1498 // Comments inside block.
1499 x = 5
1500 y = 2
1501}
1502"#;
1503 let program = crate::parsing::top_level_parse(input).unwrap();
1504 let output = program.recast_top(&Default::default(), 0);
1505 assert_eq!(output, input);
1506 }
1507
1508 #[test]
1509 fn test_recast_bug_fn_in_fn() {
1510 let some_program_string = r#"// Start point (top left)
1511zoo_x = -20
1512zoo_y = 7
1513// Scale
1514s = 1 // s = 1 -> height of Z is 13.4mm
1515// Depth
1516d = 1
1517
1518fn rect(x, y, w, h) {
1519 startSketchOn(XY)
1520 |> startProfile(at = [x, y])
1521 |> xLine(length = w)
1522 |> yLine(length = h)
1523 |> xLine(length = -w)
1524 |> close()
1525 |> extrude(d)
1526}
1527
1528fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1529 startSketchOn(XY)
1530 |> startProfile(at = [x1, y1])
1531 |> line(endAbsolute = [x2, y2])
1532 |> line(endAbsolute = [x3, y3])
1533 |> line(endAbsolute = [x4, y4])
1534 |> close()
1535 |> extrude(d)
1536}
1537
1538fn crosshair(x, y) {
1539 startSketchOn(XY)
1540 |> startProfile(at = [x, y])
1541 |> yLine(length = 1)
1542 |> yLine(length = -2)
1543 |> yLine(length = 1)
1544 |> xLine(length = 1)
1545 |> xLine(length = -2)
1546}
1547
1548fn z(z_x, z_y) {
1549 z_end_w = s * 8.4
1550 z_end_h = s * 3
1551 z_corner = s * 2
1552 z_w = z_end_w + 2 * z_corner
1553 z_h = z_w * 1.08130081300813
1554 rect(
1555 z_x,
1556 a = z_y,
1557 b = z_end_w,
1558 c = -z_end_h,
1559 )
1560 rect(
1561 z_x + z_w,
1562 a = z_y,
1563 b = -z_corner,
1564 c = -z_corner,
1565 )
1566 rect(
1567 z_x + z_w,
1568 a = z_y - z_h,
1569 b = -z_end_w,
1570 c = z_end_h,
1571 )
1572 rect(
1573 z_x,
1574 a = z_y - z_h,
1575 b = z_corner,
1576 c = z_corner,
1577 )
1578}
1579
1580fn o(c_x, c_y) {
1581 // Outer and inner radii
1582 o_r = s * 6.95
1583 i_r = 0.5652173913043478 * o_r
1584
1585 // Angle offset for diagonal break
1586 a = 7
1587
1588 // Start point for the top sketch
1589 o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
1590 o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
1591
1592 // Start point for the bottom sketch
1593 o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
1594 o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
1595
1596 // End point for the bottom startSketch
1597 o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
1598 o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
1599
1600 // Where is the center?
1601 // crosshair(c_x, c_y)
1602
1603
1604 startSketchOn(XY)
1605 |> startProfile(at = [o_x1, o_y1])
1606 |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
1607 |> angledLine(angle = 45, length = o_r - i_r)
1608 |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
1609 |> close()
1610 |> extrude(d)
1611
1612 startSketchOn(XY)
1613 |> startProfile(at = [o_x2, o_y2])
1614 |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
1615 |> angledLine(angle = 225, length = o_r - i_r)
1616 |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
1617 |> close()
1618 |> extrude(d)
1619}
1620
1621fn zoo(x0, y0) {
1622 z(x = x0, y = y0)
1623 o(x = x0 + s * 20, y = y0 - (s * 6.7))
1624 o(x = x0 + s * 35, y = y0 - (s * 6.7))
1625}
1626
1627zoo(x = zoo_x, y = zoo_y)
1628"#;
1629 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1630
1631 let recasted = program.recast_top(&Default::default(), 0);
1632 assert_eq!(recasted, some_program_string);
1633 }
1634
1635 #[test]
1636 fn test_nested_fns_indent() {
1637 let some_program_string = "\
1638x = 1
1639fn rect(x, y, w, h) {
1640 y = 2
1641 z = 3
1642 startSketchOn(XY)
1643 |> startProfile(at = [x, y])
1644 |> xLine(length = w)
1645 |> yLine(length = h)
1646 |> xLine(length = -w)
1647 |> close()
1648 |> extrude(d)
1649}
1650";
1651 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1652
1653 let recasted = program.recast_top(&Default::default(), 0);
1654 assert_eq!(recasted, some_program_string);
1655 }
1656
1657 #[test]
1658 fn test_recast_bug_extra_parens() {
1659 let some_program_string = r#"// Ball Bearing
1660// 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.
1661
1662// Define constants like ball diameter, inside diameter, overhange length, and thickness
1663sphereDia = 0.5
1664insideDia = 1
1665thickness = 0.25
1666overHangLength = .4
1667
1668// Sketch and revolve the inside bearing piece
1669insideRevolve = startSketchOn(XZ)
1670 |> startProfile(at = [insideDia / 2, 0])
1671 |> line(end = [0, thickness + sphereDia / 2])
1672 |> line(end = [overHangLength, 0])
1673 |> line(end = [0, -thickness])
1674 |> line(end = [-overHangLength + thickness, 0])
1675 |> line(end = [0, -sphereDia])
1676 |> line(end = [overHangLength - thickness, 0])
1677 |> line(end = [0, -thickness])
1678 |> line(end = [-overHangLength, 0])
1679 |> close()
1680 |> revolve(axis = Y)
1681
1682// 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)
1683sphere = startSketchOn(XZ)
1684 |> startProfile(at = [
1685 0.05 + insideDia / 2 + thickness,
1686 0 - 0.05
1687 ])
1688 |> line(end = [sphereDia - 0.1, 0])
1689 |> arc(
1690 angle_start = 0,
1691 angle_end = -180,
1692 radius = sphereDia / 2 - 0.05
1693 )
1694 |> close()
1695 |> revolve(axis = X)
1696 |> patternCircular3d(
1697 axis = [0, 0, 1],
1698 center = [0, 0, 0],
1699 repetitions = 10,
1700 arcDegrees = 360,
1701 rotateDuplicates = true
1702 )
1703
1704// Sketch and revolve the outside bearing
1705outsideRevolve = startSketchOn(XZ)
1706 |> startProfile(at = [
1707 insideDia / 2 + thickness + sphereDia,
1708 0
1709 ]
1710 )
1711 |> line(end = [0, sphereDia / 2])
1712 |> line(end = [-overHangLength + thickness, 0])
1713 |> line(end = [0, thickness])
1714 |> line(end = [overHangLength, 0])
1715 |> line(end = [0, -2 * thickness - sphereDia])
1716 |> line(end = [-overHangLength, 0])
1717 |> line(end = [0, thickness])
1718 |> line(end = [overHangLength - thickness, 0])
1719 |> close()
1720 |> revolve(axis = Y)"#;
1721 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1722
1723 let recasted = program.recast_top(&Default::default(), 0);
1724 assert_eq!(
1725 recasted,
1726 r#"// Ball Bearing
1727// 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.
1728
1729// Define constants like ball diameter, inside diameter, overhange length, and thickness
1730sphereDia = 0.5
1731insideDia = 1
1732thickness = 0.25
1733overHangLength = .4
1734
1735// Sketch and revolve the inside bearing piece
1736insideRevolve = startSketchOn(XZ)
1737 |> startProfile(at = [insideDia / 2, 0])
1738 |> line(end = [0, thickness + sphereDia / 2])
1739 |> line(end = [overHangLength, 0])
1740 |> line(end = [0, -thickness])
1741 |> line(end = [-overHangLength + thickness, 0])
1742 |> line(end = [0, -sphereDia])
1743 |> line(end = [overHangLength - thickness, 0])
1744 |> line(end = [0, -thickness])
1745 |> line(end = [-overHangLength, 0])
1746 |> close()
1747 |> revolve(axis = Y)
1748
1749// 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)
1750sphere = startSketchOn(XZ)
1751 |> startProfile(at = [
1752 0.05 + insideDia / 2 + thickness,
1753 0 - 0.05
1754 ])
1755 |> line(end = [sphereDia - 0.1, 0])
1756 |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
1757 |> close()
1758 |> revolve(axis = X)
1759 |> patternCircular3d(
1760 axis = [0, 0, 1],
1761 center = [0, 0, 0],
1762 repetitions = 10,
1763 arcDegrees = 360,
1764 rotateDuplicates = true,
1765 )
1766
1767// Sketch and revolve the outside bearing
1768outsideRevolve = startSketchOn(XZ)
1769 |> startProfile(at = [
1770 insideDia / 2 + thickness + sphereDia,
1771 0
1772 ])
1773 |> line(end = [0, sphereDia / 2])
1774 |> line(end = [-overHangLength + thickness, 0])
1775 |> line(end = [0, thickness])
1776 |> line(end = [overHangLength, 0])
1777 |> line(end = [0, -2 * thickness - sphereDia])
1778 |> line(end = [-overHangLength, 0])
1779 |> line(end = [0, thickness])
1780 |> line(end = [overHangLength - thickness, 0])
1781 |> close()
1782 |> revolve(axis = Y)
1783"#
1784 );
1785 }
1786
1787 #[test]
1788 fn test_recast_fn_in_object() {
1789 let some_program_string = r#"bing = { yo = 55 }
1790myNestedVar = [{ prop = callExp(bing.yo) }]
1791"#;
1792 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1793
1794 let recasted = program.recast_top(&Default::default(), 0);
1795 assert_eq!(recasted, some_program_string);
1796 }
1797
1798 #[test]
1799 fn test_recast_fn_in_array() {
1800 let some_program_string = r#"bing = { yo = 55 }
1801myNestedVar = [callExp(bing.yo)]
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_ranges() {
1811 let some_program_string = r#"foo = [0..10]
1812ten = 10
1813bar = [0 + 1 .. ten]
1814"#;
1815 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1816
1817 let recasted = program.recast_top(&Default::default(), 0);
1818 assert_eq!(recasted, some_program_string);
1819 }
1820
1821 #[test]
1822 fn test_recast_space_in_fn_call() {
1823 let some_program_string = r#"fn thing (x) {
1824 return x + 1
1825}
1826
1827thing ( 1 )
1828"#;
1829 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1830
1831 let recasted = program.recast_top(&Default::default(), 0);
1832 assert_eq!(
1833 recasted,
1834 r#"fn thing(x) {
1835 return x + 1
1836}
1837
1838thing(1)
1839"#
1840 );
1841 }
1842
1843 #[test]
1844 fn test_recast_typed_fn() {
1845 let some_program_string = r#"fn thing(x: string, y: [bool]): number {
1846 return x + 1
1847}
1848"#;
1849 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1850
1851 let recasted = program.recast_top(&Default::default(), 0);
1852 assert_eq!(recasted, some_program_string);
1853 }
1854
1855 #[test]
1856 fn test_recast_typed_consts() {
1857 let some_program_string = r#"a = 42: number
1858export b = 3.2: number(ft)
1859c = "dsfds": A | B | C
1860d = [1]: [number]
1861e = foo: [number; 3]
1862f = [1, 2, 3]: [number; 1+]
1863f = [1, 2, 3]: [number; 3+]
1864"#;
1865 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1866
1867 let recasted = program.recast_top(&Default::default(), 0);
1868 assert_eq!(recasted, some_program_string);
1869 }
1870
1871 #[test]
1872 fn test_recast_object_fn_in_array_weird_bracket() {
1873 let some_program_string = r#"bing = { yo = 55 }
1874myNestedVar = [
1875 {
1876 prop: line(a = [bing.yo, 21], b = sketch001)
1877}
1878]
1879"#;
1880 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1881
1882 let recasted = program.recast_top(&Default::default(), 0);
1883 let expected = r#"bing = { yo = 55 }
1884myNestedVar = [
1885 {
1886 prop = line(a = [bing.yo, 21], b = sketch001)
1887 }
1888]
1889"#;
1890 assert_eq!(recasted, expected,);
1891 }
1892
1893 #[test]
1894 fn test_recast_empty_file() {
1895 let some_program_string = r#""#;
1896 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1897
1898 let recasted = program.recast_top(&Default::default(), 0);
1899 assert_eq!(recasted, r#""#);
1901 }
1902
1903 #[test]
1904 fn test_recast_empty_file_new_line() {
1905 let some_program_string = r#"
1906"#;
1907 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1908
1909 let recasted = program.recast_top(&Default::default(), 0);
1910 assert_eq!(recasted, r#""#);
1912 }
1913
1914 #[test]
1915 fn test_recast_shebang() {
1916 let some_program_string = r#"#!/usr/local/env zoo kcl
1917part001 = startSketchOn(XY)
1918 |> startProfile(at = [-10, -10])
1919 |> line(end = [20, 0])
1920 |> line(end = [0, 20])
1921 |> line(end = [-20, 0])
1922 |> close()
1923"#;
1924
1925 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1926
1927 let recasted = program.recast_top(&Default::default(), 0);
1928 assert_eq!(
1929 recasted,
1930 r#"#!/usr/local/env zoo kcl
1931
1932part001 = startSketchOn(XY)
1933 |> startProfile(at = [-10, -10])
1934 |> line(end = [20, 0])
1935 |> line(end = [0, 20])
1936 |> line(end = [-20, 0])
1937 |> close()
1938"#
1939 );
1940 }
1941
1942 #[test]
1943 fn test_recast_shebang_new_lines() {
1944 let some_program_string = r#"#!/usr/local/env zoo kcl
1945
1946
1947
1948part001 = startSketchOn(XY)
1949 |> startProfile(at = [-10, -10])
1950 |> line(end = [20, 0])
1951 |> line(end = [0, 20])
1952 |> line(end = [-20, 0])
1953 |> close()
1954"#;
1955
1956 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1957
1958 let recasted = program.recast_top(&Default::default(), 0);
1959 assert_eq!(
1960 recasted,
1961 r#"#!/usr/local/env zoo kcl
1962
1963part001 = startSketchOn(XY)
1964 |> startProfile(at = [-10, -10])
1965 |> line(end = [20, 0])
1966 |> line(end = [0, 20])
1967 |> line(end = [-20, 0])
1968 |> close()
1969"#
1970 );
1971 }
1972
1973 #[test]
1974 fn test_recast_shebang_with_comments() {
1975 let some_program_string = r#"#!/usr/local/env zoo kcl
1976
1977// Yo yo my comments.
1978part001 = startSketchOn(XY)
1979 |> startProfile(at = [-10, -10])
1980 |> line(end = [20, 0])
1981 |> line(end = [0, 20])
1982 |> line(end = [-20, 0])
1983 |> close()
1984"#;
1985
1986 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1987
1988 let recasted = program.recast_top(&Default::default(), 0);
1989 assert_eq!(
1990 recasted,
1991 r#"#!/usr/local/env zoo kcl
1992
1993// Yo yo my comments.
1994part001 = startSketchOn(XY)
1995 |> startProfile(at = [-10, -10])
1996 |> line(end = [20, 0])
1997 |> line(end = [0, 20])
1998 |> line(end = [-20, 0])
1999 |> close()
2000"#
2001 );
2002 }
2003
2004 #[test]
2005 fn test_recast_empty_function_body_with_comments() {
2006 let input = r#"fn myFunc() {
2007 // Yo yo my comments.
2008}
2009"#;
2010
2011 let program = crate::parsing::top_level_parse(input).unwrap();
2012 let output = program.recast_top(&Default::default(), 0);
2013 assert_eq!(output, input);
2014 }
2015
2016 #[test]
2017 fn test_recast_large_file() {
2018 let some_program_string = r#"@settings(units=mm)
2019// define nts
2020radius = 6.0
2021width = 144.0
2022length = 83.0
2023depth = 45.0
2024thk = 5
2025hole_diam = 5
2026// define a rectangular shape func
2027fn rectShape(pos, w, l) {
2028 rr = startSketchOn(XY)
2029 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2030 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2031 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2032 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2033 |> close($edge4)
2034 return rr
2035}
2036// build the body of the focusrite scarlett solo gen 4
2037// only used for visualization
2038scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2039 |> extrude(depth)
2040 |> fillet(
2041 radius = radius,
2042 tags = [
2043 edge2,
2044 edge4,
2045 getOppositeEdge(edge2),
2046 getOppositeEdge(edge4)
2047]
2048 )
2049 // build the bracket sketch around the body
2050fn bracketSketch(w, d, t) {
2051 s = startSketchOn({
2052 plane = {
2053 origin = { x = 0, y = length / 2 + thk, z = 0 },
2054 x_axis = { x = 1, y = 0, z = 0 },
2055 y_axis = { x = 0, y = 0, z = 1 },
2056 z_axis = { x = 0, y = 1, z = 0 }
2057}
2058 })
2059 |> startProfile(at = [-w / 2 - t, d + t])
2060 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2061 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2062 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2063 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2064 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2065 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2066 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2067 |> close($edge8)
2068 return s
2069}
2070// build the body of the bracket
2071bracket_body = bracketSketch(w = width, d = depth, t = thk)
2072 |> extrude(length + 10)
2073 |> fillet(
2074 radius = radius,
2075 tags = [
2076 getNextAdjacentEdge(edge7),
2077 getNextAdjacentEdge(edge2),
2078 getNextAdjacentEdge(edge3),
2079 getNextAdjacentEdge(edge6)
2080]
2081 )
2082 // build the tabs of the mounting bracket (right side)
2083tabs_r = startSketchOn({
2084 plane = {
2085 origin = { x = 0, y = 0, z = depth + thk },
2086 x_axis = { x = 1, y = 0, z = 0 },
2087 y_axis = { x = 0, y = 1, z = 0 },
2088 z_axis = { x = 0, y = 0, z = 1 }
2089}
2090 })
2091 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2092 |> line(end = [10, -5])
2093 |> line(end = [0, -10])
2094 |> line(end = [-10, -5])
2095 |> close()
2096 |> subtract2d(tool = circle(
2097 center = [
2098 width / 2 + thk + hole_diam,
2099 length / 2 - hole_diam
2100 ],
2101 radius = hole_diam / 2
2102 ))
2103 |> extrude(-thk)
2104 |> patternLinear3d(
2105 axis = [0, -1, 0],
2106 repetitions = 1,
2107 distance = length - 10
2108 )
2109 // build the tabs of the mounting bracket (left side)
2110tabs_l = startSketchOn({
2111 plane = {
2112 origin = { x = 0, y = 0, z = depth + thk },
2113 x_axis = { x = 1, y = 0, z = 0 },
2114 y_axis = { x = 0, y = 1, z = 0 },
2115 z_axis = { x = 0, y = 0, z = 1 }
2116}
2117 })
2118 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2119 |> line(end = [-10, -5])
2120 |> line(end = [0, -10])
2121 |> line(end = [10, -5])
2122 |> close()
2123 |> subtract2d(tool = circle(
2124 center = [
2125 -width / 2 - thk - hole_diam,
2126 length / 2 - hole_diam
2127 ],
2128 radius = hole_diam / 2
2129 ))
2130 |> extrude(-thk)
2131 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2132"#;
2133 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2134
2135 let recasted = program.recast_top(&Default::default(), 0);
2136 assert_eq!(
2138 recasted,
2139 r#"@settings(units = mm)
2140
2141// define nts
2142radius = 6.0
2143width = 144.0
2144length = 83.0
2145depth = 45.0
2146thk = 5
2147hole_diam = 5
2148// define a rectangular shape func
2149fn rectShape(pos, w, l) {
2150 rr = startSketchOn(XY)
2151 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2152 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2153 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2154 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2155 |> close($edge4)
2156 return rr
2157}
2158// build the body of the focusrite scarlett solo gen 4
2159// only used for visualization
2160scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2161 |> extrude(depth)
2162 |> fillet(
2163 radius = radius,
2164 tags = [
2165 edge2,
2166 edge4,
2167 getOppositeEdge(edge2),
2168 getOppositeEdge(edge4)
2169 ],
2170 )
2171// build the bracket sketch around the body
2172fn bracketSketch(w, d, t) {
2173 s = startSketchOn({
2174 plane = {
2175 origin = { x = 0, y = length / 2 + thk, z = 0 },
2176 x_axis = { x = 1, y = 0, z = 0 },
2177 y_axis = { x = 0, y = 0, z = 1 },
2178 z_axis = { x = 0, y = 1, z = 0 }
2179 }
2180 })
2181 |> startProfile(at = [-w / 2 - t, d + t])
2182 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2183 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2184 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2185 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2186 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2187 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2188 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2189 |> close($edge8)
2190 return s
2191}
2192// build the body of the bracket
2193bracket_body = bracketSketch(w = width, d = depth, t = thk)
2194 |> extrude(length + 10)
2195 |> fillet(
2196 radius = radius,
2197 tags = [
2198 getNextAdjacentEdge(edge7),
2199 getNextAdjacentEdge(edge2),
2200 getNextAdjacentEdge(edge3),
2201 getNextAdjacentEdge(edge6)
2202 ],
2203 )
2204// build the tabs of the mounting bracket (right side)
2205tabs_r = startSketchOn({
2206 plane = {
2207 origin = { x = 0, y = 0, z = depth + thk },
2208 x_axis = { x = 1, y = 0, z = 0 },
2209 y_axis = { x = 0, y = 1, z = 0 },
2210 z_axis = { x = 0, y = 0, z = 1 }
2211 }
2212})
2213 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2214 |> line(end = [10, -5])
2215 |> line(end = [0, -10])
2216 |> line(end = [-10, -5])
2217 |> close()
2218 |> subtract2d(tool = circle(
2219 center = [
2220 width / 2 + thk + hole_diam,
2221 length / 2 - hole_diam
2222 ],
2223 radius = hole_diam / 2,
2224 ))
2225 |> extrude(-thk)
2226 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2227// build the tabs of the mounting bracket (left side)
2228tabs_l = startSketchOn({
2229 plane = {
2230 origin = { x = 0, y = 0, z = depth + thk },
2231 x_axis = { x = 1, y = 0, z = 0 },
2232 y_axis = { x = 0, y = 1, z = 0 },
2233 z_axis = { x = 0, y = 0, z = 1 }
2234 }
2235})
2236 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2237 |> line(end = [-10, -5])
2238 |> line(end = [0, -10])
2239 |> line(end = [10, -5])
2240 |> close()
2241 |> subtract2d(tool = circle(
2242 center = [
2243 -width / 2 - thk - hole_diam,
2244 length / 2 - hole_diam
2245 ],
2246 radius = hole_diam / 2,
2247 ))
2248 |> extrude(-thk)
2249 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2250"#
2251 );
2252 }
2253
2254 #[test]
2255 fn test_recast_nested_var_declaration_in_fn_body() {
2256 let some_program_string = r#"fn cube(pos, scale) {
2257 sg = startSketchOn(XY)
2258 |> startProfile(at = pos)
2259 |> line(end = [0, scale])
2260 |> line(end = [scale, 0])
2261 |> line(end = [0, -scale])
2262 |> close()
2263 |> extrude(scale)
2264}"#;
2265 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2266
2267 let recasted = program.recast_top(&Default::default(), 0);
2268 assert_eq!(
2269 recasted,
2270 r#"fn cube(pos, scale) {
2271 sg = startSketchOn(XY)
2272 |> startProfile(at = pos)
2273 |> line(end = [0, scale])
2274 |> line(end = [scale, 0])
2275 |> line(end = [0, -scale])
2276 |> close()
2277 |> extrude(scale)
2278}
2279"#
2280 );
2281 }
2282
2283 #[test]
2284 fn test_as() {
2285 let some_program_string = r#"fn cube(pos, scale) {
2286 x = dfsfs + dfsfsd as y
2287
2288 sg = startSketchOn(XY)
2289 |> startProfile(at = pos) as foo
2290 |> line([0, scale])
2291 |> line([scale, 0]) as bar
2292 |> line([0 as baz, -scale] as qux)
2293 |> close()
2294 |> extrude(length = scale)
2295}
2296
2297cube(pos = 0, scale = 0) as cub
2298"#;
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!(recasted, some_program_string,);
2303 }
2304
2305 #[test]
2306 fn test_recast_with_bad_indentation() {
2307 let some_program_string = r#"part001 = startSketchOn(XY)
2308 |> startProfile(at = [0.0, 5.0])
2309 |> line(end = [0.4900857016, -0.0240763666])
2310 |> line(end = [0.6804562304, 0.9087880491])"#;
2311 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2312
2313 let recasted = program.recast_top(&Default::default(), 0);
2314 assert_eq!(
2315 recasted,
2316 r#"part001 = startSketchOn(XY)
2317 |> startProfile(at = [0.0, 5.0])
2318 |> line(end = [0.4900857016, -0.0240763666])
2319 |> line(end = [0.6804562304, 0.9087880491])
2320"#
2321 );
2322 }
2323
2324 #[test]
2325 fn test_recast_with_bad_indentation_and_inline_comment() {
2326 let some_program_string = r#"part001 = startSketchOn(XY)
2327 |> startProfile(at = [0.0, 5.0])
2328 |> line(end = [0.4900857016, -0.0240763666]) // hello world
2329 |> line(end = [0.6804562304, 0.9087880491])"#;
2330 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2331
2332 let recasted = program.recast_top(&Default::default(), 0);
2333 assert_eq!(
2334 recasted,
2335 r#"part001 = startSketchOn(XY)
2336 |> startProfile(at = [0.0, 5.0])
2337 |> line(end = [0.4900857016, -0.0240763666]) // hello world
2338 |> line(end = [0.6804562304, 0.9087880491])
2339"#
2340 );
2341 }
2342 #[test]
2343 fn test_recast_with_bad_indentation_and_line_comment() {
2344 let some_program_string = r#"part001 = startSketchOn(XY)
2345 |> startProfile(at = [0.0, 5.0])
2346 |> line(end = [0.4900857016, -0.0240763666])
2347 // hello world
2348 |> line(end = [0.6804562304, 0.9087880491])"#;
2349 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2350
2351 let recasted = program.recast_top(&Default::default(), 0);
2352 assert_eq!(
2353 recasted,
2354 r#"part001 = startSketchOn(XY)
2355 |> startProfile(at = [0.0, 5.0])
2356 |> line(end = [0.4900857016, -0.0240763666])
2357 // hello world
2358 |> line(end = [0.6804562304, 0.9087880491])
2359"#
2360 );
2361 }
2362
2363 #[test]
2364 fn test_recast_comment_in_a_fn_block() {
2365 let some_program_string = r#"fn myFn() {
2366 // this is a comment
2367 yo = { a = { b = { c = '123' } } } /* block
2368 comment */
2369
2370 key = 'c'
2371 // this is also a comment
2372 return things
2373}"#;
2374 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2375
2376 let recasted = program.recast_top(&Default::default(), 0);
2377 assert_eq!(
2378 recasted,
2379 r#"fn myFn() {
2380 // this is a comment
2381 yo = { a = { b = { c = '123' } } } /* block
2382 comment */
2383
2384 key = 'c'
2385 // this is also a comment
2386 return things
2387}
2388"#
2389 );
2390 }
2391
2392 #[test]
2393 fn test_recast_comment_under_variable() {
2394 let some_program_string = r#"key = 'c'
2395// this is also a comment
2396thing = 'foo'
2397"#;
2398 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2399
2400 let recasted = program.recast_top(&Default::default(), 0);
2401 assert_eq!(
2402 recasted,
2403 r#"key = 'c'
2404// this is also a comment
2405thing = 'foo'
2406"#
2407 );
2408 }
2409
2410 #[test]
2411 fn test_recast_multiline_comment_start_file() {
2412 let some_program_string = r#"// hello world
2413// I am a comment
2414key = 'c'
2415// this is also a comment
2416// hello
2417thing = 'foo'
2418"#;
2419 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2420
2421 let recasted = program.recast_top(&Default::default(), 0);
2422 assert_eq!(
2423 recasted,
2424 r#"// hello world
2425// I am a comment
2426key = 'c'
2427// this is also a comment
2428// hello
2429thing = 'foo'
2430"#
2431 );
2432 }
2433
2434 #[test]
2435 fn test_recast_empty_comment() {
2436 let some_program_string = r#"// hello world
2437//
2438// I am a comment
2439key = 'c'
2440
2441//
2442// I am a comment
2443thing = 'c'
2444
2445foo = 'bar' //
2446"#;
2447 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2448
2449 let recasted = program.recast_top(&Default::default(), 0);
2450 assert_eq!(
2451 recasted,
2452 r#"// hello world
2453//
2454// I am a comment
2455key = 'c'
2456
2457//
2458// I am a comment
2459thing = 'c'
2460
2461foo = 'bar' //
2462"#
2463 );
2464 }
2465
2466 #[test]
2467 fn test_recast_multiline_comment_under_variable() {
2468 let some_program_string = r#"key = 'c'
2469// this is also a comment
2470// hello
2471thing = 'foo'
2472"#;
2473 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2474
2475 let recasted = program.recast_top(&Default::default(), 0);
2476 assert_eq!(
2477 recasted,
2478 r#"key = 'c'
2479// this is also a comment
2480// hello
2481thing = 'foo'
2482"#
2483 );
2484 }
2485
2486 #[test]
2487 fn test_recast_only_line_comments() {
2488 let code = r#"// comment at start
2489"#;
2490 let program = crate::parsing::top_level_parse(code).unwrap();
2491
2492 assert_eq!(program.recast_top(&Default::default(), 0), code);
2493 }
2494
2495 #[test]
2496 fn test_recast_comment_at_start() {
2497 let test_program = r#"
2498/* comment at start */
2499
2500mySk1 = startSketchOn(XY)
2501 |> startProfile(at = [0, 0])"#;
2502 let program = crate::parsing::top_level_parse(test_program).unwrap();
2503
2504 let recasted = program.recast_top(&Default::default(), 0);
2505 assert_eq!(
2506 recasted,
2507 r#"/* comment at start */
2508
2509mySk1 = startSketchOn(XY)
2510 |> startProfile(at = [0, 0])
2511"#
2512 );
2513 }
2514
2515 #[test]
2516 fn test_recast_lots_of_comments() {
2517 let some_program_string = r#"// comment at start
2518mySk1 = startSketchOn(XY)
2519 |> startProfile(at = [0, 0])
2520 |> line(endAbsolute = [1, 1])
2521 // comment here
2522 |> line(endAbsolute = [0, 1], tag = $myTag)
2523 |> line(endAbsolute = [1, 1])
2524 /* and
2525 here
2526 */
2527 // a comment between pipe expression statements
2528 |> rx(90)
2529 // and another with just white space between others below
2530 |> ry(45)
2531 |> rx(45)
2532// one more for good measure"#;
2533 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2534
2535 let recasted = program.recast_top(&Default::default(), 0);
2536 assert_eq!(
2537 recasted,
2538 r#"// comment at start
2539mySk1 = startSketchOn(XY)
2540 |> startProfile(at = [0, 0])
2541 |> line(endAbsolute = [1, 1])
2542 // comment here
2543 |> line(endAbsolute = [0, 1], tag = $myTag)
2544 |> line(endAbsolute = [1, 1])
2545 /* and
2546 here */
2547 // a comment between pipe expression statements
2548 |> rx(90)
2549 // and another with just white space between others below
2550 |> ry(45)
2551 |> rx(45)
2552// one more for good measure
2553"#
2554 );
2555 }
2556
2557 #[test]
2558 fn test_recast_multiline_object() {
2559 let some_program_string = r#"x = {
2560 a = 1000000000,
2561 b = 2000000000,
2562 c = 3000000000,
2563 d = 4000000000,
2564 e = 5000000000
2565}"#;
2566 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2567
2568 let recasted = program.recast_top(&Default::default(), 0);
2569 assert_eq!(recasted.trim(), some_program_string);
2570 }
2571
2572 #[test]
2573 fn test_recast_first_level_object() {
2574 let some_program_string = r#"three = 3
2575
2576yo = {
2577 aStr = 'str',
2578 anum = 2,
2579 identifier = three,
2580 binExp = 4 + 5
2581}
2582yo = [
2583 1,
2584 " 2,",
2585 "three",
2586 4 + 5,
2587 " hey oooooo really long long long"
2588]
2589"#;
2590 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2591
2592 let recasted = program.recast_top(&Default::default(), 0);
2593 assert_eq!(recasted, some_program_string);
2594 }
2595
2596 #[test]
2597 fn test_recast_new_line_before_comment() {
2598 let some_program_string = r#"
2599// this is a comment
2600yo = { a = { b = { c = '123' } } }
2601
2602key = 'c'
2603things = "things"
2604
2605// this is also a comment"#;
2606 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2607
2608 let recasted = program.recast_top(&Default::default(), 0);
2609 let expected = some_program_string.trim();
2610 let actual = recasted.trim();
2612 assert_eq!(actual, expected);
2613 }
2614
2615 #[test]
2616 fn test_recast_comment_tokens_inside_strings() {
2617 let some_program_string = r#"b = {
2618 end = 141,
2619 start = 125,
2620 type_ = "NonCodeNode",
2621 value = "
2622 // a comment
2623 "
2624}"#;
2625 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2626
2627 let recasted = program.recast_top(&Default::default(), 0);
2628 assert_eq!(recasted.trim(), some_program_string.trim());
2629 }
2630
2631 #[test]
2632 fn test_recast_array_new_line_in_pipe() {
2633 let some_program_string = r#"myVar = 3
2634myVar2 = 5
2635myVar3 = 6
2636myAng = 40
2637myAng2 = 134
2638part001 = startSketchOn(XY)
2639 |> startProfile(at = [0, 0])
2640 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2641 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2642 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2643 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2644
2645 let recasted = program.recast_top(&Default::default(), 0);
2646 assert_eq!(recasted.trim(), some_program_string);
2647 }
2648
2649 #[test]
2650 fn test_recast_array_new_line_in_pipe_custom() {
2651 let some_program_string = r#"myVar = 3
2652myVar2 = 5
2653myVar3 = 6
2654myAng = 40
2655myAng2 = 134
2656part001 = startSketchOn(XY)
2657 |> startProfile(at = [0, 0])
2658 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2659 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2660 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2661"#;
2662 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2663
2664 let recasted = program.recast_top(
2665 &FormatOptions {
2666 tab_size: 3,
2667 use_tabs: false,
2668 insert_final_newline: true,
2669 },
2670 0,
2671 );
2672 assert_eq!(recasted, some_program_string);
2673 }
2674
2675 #[test]
2676 fn test_recast_after_rename_std() {
2677 let some_program_string = r#"part001 = startSketchOn(XY)
2678 |> startProfile(at = [0.0000000000, 5.0000000000])
2679 |> line(end = [0.4900857016, -0.0240763666])
2680
2681part002 = "part002"
2682things = [part001, 0.0]
2683blah = 1
2684foo = false
2685baz = {a: 1, part001: "thing"}
2686
2687fn ghi(part001) {
2688 return part001
2689}
2690"#;
2691 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2692 program.rename_symbol("mySuperCoolPart", 6);
2693
2694 let recasted = program.recast_top(&Default::default(), 0);
2695 assert_eq!(
2696 recasted,
2697 r#"mySuperCoolPart = startSketchOn(XY)
2698 |> startProfile(at = [0.0, 5.0])
2699 |> line(end = [0.4900857016, -0.0240763666])
2700
2701part002 = "part002"
2702things = [mySuperCoolPart, 0.0]
2703blah = 1
2704foo = false
2705baz = { a = 1, part001 = "thing" }
2706
2707fn ghi(part001) {
2708 return part001
2709}
2710"#
2711 );
2712 }
2713
2714 #[test]
2715 fn test_recast_after_rename_fn_args() {
2716 let some_program_string = r#"fn ghi(x, y, z) {
2717 return x
2718}"#;
2719 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2720 program.rename_symbol("newName", 7);
2721
2722 let recasted = program.recast_top(&Default::default(), 0);
2723 assert_eq!(
2724 recasted,
2725 r#"fn ghi(newName, y, z) {
2726 return newName
2727}
2728"#
2729 );
2730 }
2731
2732 #[test]
2733 fn test_recast_trailing_comma() {
2734 let some_program_string = r#"startSketchOn(XY)
2735 |> startProfile(at = [0, 0])
2736 |> arc({
2737 radius = 1,
2738 angle_start = 0,
2739 angle_end = 180,
2740 })"#;
2741 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2742
2743 let recasted = program.recast_top(&Default::default(), 0);
2744 assert_eq!(
2745 recasted,
2746 r#"startSketchOn(XY)
2747 |> startProfile(at = [0, 0])
2748 |> arc({
2749 radius = 1,
2750 angle_start = 0,
2751 angle_end = 180
2752 })
2753"#
2754 );
2755 }
2756
2757 #[test]
2758 fn test_recast_array_no_trailing_comma_with_comments() {
2759 let some_program_string = r#"[
2760 1, // one
2761 2, // two
2762 3 // three
2763]"#;
2764 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2765
2766 let recasted = program.recast_top(&Default::default(), 0);
2767 assert_eq!(
2768 recasted,
2769 r#"[
2770 1,
2771 // one
2772 2,
2773 // two
2774 3,
2775 // three
2776]
2777"#
2778 );
2779 }
2780
2781 #[test]
2782 fn test_recast_object_no_trailing_comma_with_comments() {
2783 let some_program_string = r#"{
2784 x=1, // one
2785 y=2, // two
2786 z=3 // three
2787}"#;
2788 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2789
2790 let recasted = program.recast_top(&Default::default(), 0);
2791 assert_eq!(
2794 recasted,
2795 r#"{
2796 x = 1,
2797 // one
2798 y = 2,
2799 // two
2800 z = 3,
2801 // three
2802
2803}
2804"#
2805 );
2806 }
2807
2808 #[test]
2809 fn test_recast_negative_var() {
2810 let some_program_string = r#"w = 20
2811l = 8
2812h = 10
2813
2814firstExtrude = startSketchOn(XY)
2815 |> startProfile(at = [0,0])
2816 |> line(end = [0, l])
2817 |> line(end = [w, 0])
2818 |> line(end = [0, -l])
2819 |> close()
2820 |> extrude(h)
2821"#;
2822 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2823
2824 let recasted = program.recast_top(&Default::default(), 0);
2825 assert_eq!(
2826 recasted,
2827 r#"w = 20
2828l = 8
2829h = 10
2830
2831firstExtrude = startSketchOn(XY)
2832 |> startProfile(at = [0, 0])
2833 |> line(end = [0, l])
2834 |> line(end = [w, 0])
2835 |> line(end = [0, -l])
2836 |> close()
2837 |> extrude(h)
2838"#
2839 );
2840 }
2841
2842 #[test]
2843 fn test_recast_multiline_comment() {
2844 let some_program_string = r#"w = 20
2845l = 8
2846h = 10
2847
2848// This is my comment
2849// It has multiple lines
2850// And it's really long
2851firstExtrude = startSketchOn(XY)
2852 |> startProfile(at = [0,0])
2853 |> line(end = [0, l])
2854 |> line(end = [w, 0])
2855 |> line(end = [0, -l])
2856 |> close()
2857 |> extrude(h)
2858"#;
2859 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2860
2861 let recasted = program.recast_top(&Default::default(), 0);
2862 assert_eq!(
2863 recasted,
2864 r#"w = 20
2865l = 8
2866h = 10
2867
2868// This is my comment
2869// It has multiple lines
2870// And it's really long
2871firstExtrude = startSketchOn(XY)
2872 |> startProfile(at = [0, 0])
2873 |> line(end = [0, l])
2874 |> line(end = [w, 0])
2875 |> line(end = [0, -l])
2876 |> close()
2877 |> extrude(h)
2878"#
2879 );
2880 }
2881
2882 #[test]
2883 fn test_recast_math_start_negative() {
2884 let some_program_string = r#"myVar = -5 + 6"#;
2885 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2886
2887 let recasted = program.recast_top(&Default::default(), 0);
2888 assert_eq!(recasted.trim(), some_program_string);
2889 }
2890
2891 #[test]
2892 fn test_recast_math_negate_parens() {
2893 let some_program_string = r#"wallMountL = 3.82
2894thickness = 0.5
2895
2896startSketchOn(XY)
2897 |> startProfile(at = [0, 0])
2898 |> line(end = [0, -(wallMountL - thickness)])
2899 |> line(end = [0, -(5 - thickness)])
2900 |> line(end = [0, -(5 - 1)])
2901 |> line(end = [0, -(-5 - 1)])"#;
2902 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2903
2904 let recasted = program.recast_top(&Default::default(), 0);
2905 assert_eq!(recasted.trim(), some_program_string);
2906 }
2907
2908 #[test]
2909 fn test_recast_math_nested_parens() {
2910 let some_program_string = r#"distance = 5
2911p = 3: Plane
2912FOS = { a = 3, b = 42 }: Sketch
2913sigmaAllow = 8: number(mm)
2914width = 20
2915thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2916 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2917
2918 let recasted = program.recast_top(&Default::default(), 0);
2919 assert_eq!(recasted.trim(), some_program_string);
2920 }
2921
2922 #[test]
2923 fn no_vardec_keyword() {
2924 let some_program_string = r#"distance = 5"#;
2925 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2926
2927 let recasted = program.recast_top(&Default::default(), 0);
2928 assert_eq!(recasted.trim(), some_program_string);
2929 }
2930
2931 #[test]
2932 fn recast_types() {
2933 let some_program_string = r#"type foo
2934
2935// A comment
2936@(impl = primitive)
2937export type bar(unit, baz)
2938type baz = Foo | Bar
2939type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
2940"#;
2941 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2942 let recasted = program.recast_top(&Default::default(), 0);
2943 assert_eq!(recasted, some_program_string);
2944 }
2945
2946 #[test]
2947 fn recast_nested_fn() {
2948 let some_program_string = r#"fn f() {
2949 return fn() {
2950 return 1
2951}
2952}"#;
2953 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2954 let recasted = program.recast_top(&Default::default(), 0);
2955 let expected = "\
2956fn f() {
2957 return fn() {
2958 return 1
2959 }
2960}";
2961 assert_eq!(recasted.trim(), expected);
2962 }
2963
2964 #[test]
2965 fn recast_literal() {
2966 use winnow::Parser;
2967 for (i, (raw, expected, reason)) in [
2968 (
2969 "5.0",
2970 "5.0",
2971 "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2972 ),
2973 (
2974 "5",
2975 "5",
2976 "integers should stay integral, i.e. don't reformat this to '5.0'",
2977 ),
2978 (
2979 "5.0000000",
2980 "5.0",
2981 "if the number is f64 but not fractional, use its canonical format",
2982 ),
2983 ("5.1", "5.1", "straightforward case works"),
2984 ]
2985 .into_iter()
2986 .enumerate()
2987 {
2988 let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2989 let literal = crate::parsing::parser::unsigned_number_literal
2990 .parse(tokens.as_slice())
2991 .unwrap();
2992 let mut actual = String::new();
2993 literal.recast(&mut actual);
2994 assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
2995 }
2996 }
2997
2998 #[test]
2999 fn recast_objects_no_comments() {
3000 let input = r#"
3001sketch002 = startSketchOn({
3002 plane: {
3003 origin: { x = 1, y = 2, z = 3 },
3004 x_axis = { x = 4, y = 5, z = 6 },
3005 y_axis = { x = 7, y = 8, z = 9 },
3006 z_axis = { x = 10, y = 11, z = 12 }
3007 }
3008 })
3009"#;
3010 let expected = r#"sketch002 = startSketchOn({
3011 plane = {
3012 origin = { x = 1, y = 2, z = 3 },
3013 x_axis = { x = 4, y = 5, z = 6 },
3014 y_axis = { x = 7, y = 8, z = 9 },
3015 z_axis = { x = 10, y = 11, z = 12 }
3016 }
3017})
3018"#;
3019 let ast = crate::parsing::top_level_parse(input).unwrap();
3020 let actual = ast.recast_top(&FormatOptions::new(), 0);
3021 assert_eq!(actual, expected);
3022 }
3023
3024 #[test]
3025 fn unparse_fn_unnamed() {
3026 let input = "\
3027squares_out = reduce(
3028 arr,
3029 n = 0: number,
3030 f = fn(@i, accum) {
3031 return 1
3032 },
3033)
3034";
3035 let ast = crate::parsing::top_level_parse(input).unwrap();
3036 let actual = ast.recast_top(&FormatOptions::new(), 0);
3037 assert_eq!(actual, input);
3038 }
3039
3040 #[test]
3041 fn unparse_fn_named() {
3042 let input = r#"fn f(x) {
3043 return 1
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 unparse_call_inside_function_single_line() {
3053 let input = r#"fn foo() {
3054 toDegrees(atan(0.5), foo = 1)
3055 return 0
3056}
3057"#;
3058 let ast = crate::parsing::top_level_parse(input).unwrap();
3059 let actual = ast.recast_top(&FormatOptions::new(), 0);
3060 assert_eq!(actual, input);
3061 }
3062
3063 #[test]
3064 fn recast_function_types() {
3065 let input = r#"foo = x: fn
3066foo = x: fn(number)
3067fn foo(x: fn(): number): fn {
3068 return 0
3069}
3070fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
3071 return 0
3072}
3073type fn
3074type foo = fn
3075type foo = fn(a: string, b: { f: fn(): any })
3076type foo = fn([fn])
3077type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
3078"#;
3079 let ast = crate::parsing::top_level_parse(input).unwrap();
3080 let actual = ast.recast_top(&FormatOptions::new(), 0);
3081 assert_eq!(actual, input);
3082 }
3083
3084 #[test]
3085 fn unparse_call_inside_function_args_multiple_lines() {
3086 let input = r#"fn foo() {
3087 toDegrees(
3088 atan(0.5),
3089 foo = 1,
3090 bar = 2,
3091 baz = 3,
3092 qux = 4,
3093 )
3094 return 0
3095}
3096"#;
3097 let ast = crate::parsing::top_level_parse(input).unwrap();
3098 let actual = ast.recast_top(&FormatOptions::new(), 0);
3099 assert_eq!(actual, input);
3100 }
3101
3102 #[test]
3103 fn unparse_call_inside_function_single_arg_multiple_lines() {
3104 let input = r#"fn foo() {
3105 toDegrees(
3106 [
3107 profile0,
3108 profile1,
3109 profile2,
3110 profile3,
3111 profile4,
3112 profile5
3113 ],
3114 key = 1,
3115 )
3116 return 0
3117}
3118"#;
3119 let ast = crate::parsing::top_level_parse(input).unwrap();
3120 let actual = ast.recast_top(&FormatOptions::new(), 0);
3121 assert_eq!(actual, input);
3122 }
3123
3124 #[test]
3125 fn recast_objects_with_comments() {
3126 use winnow::Parser;
3127 for (i, (input, expected, reason)) in [(
3128 "\
3129{
3130 a = 1,
3131 // b = 2,
3132 c = 3
3133}",
3134 "\
3135{
3136 a = 1,
3137 // b = 2,
3138 c = 3
3139}",
3140 "preserves comments",
3141 )]
3142 .into_iter()
3143 .enumerate()
3144 {
3145 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3146 crate::parsing::parser::print_tokens(tokens.as_slice());
3147 let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3148 let mut actual = String::new();
3149 expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3150 assert_eq!(
3151 actual, expected,
3152 "failed test {i}, which is testing that recasting {reason}"
3153 );
3154 }
3155 }
3156
3157 #[test]
3158 fn recast_array_with_comments() {
3159 use winnow::Parser;
3160 for (i, (input, expected, reason)) in [
3161 (
3162 "\
3163[
3164 1,
3165 2,
3166 3,
3167 4,
3168 5,
3169 6,
3170 7,
3171 8,
3172 9,
3173 10,
3174 11,
3175 12,
3176 13,
3177 14,
3178 15,
3179 16,
3180 17,
3181 18,
3182 19,
3183 20,
3184]",
3185 "\
3186[
3187 1,
3188 2,
3189 3,
3190 4,
3191 5,
3192 6,
3193 7,
3194 8,
3195 9,
3196 10,
3197 11,
3198 12,
3199 13,
3200 14,
3201 15,
3202 16,
3203 17,
3204 18,
3205 19,
3206 20
3207]",
3208 "preserves multi-line arrays",
3209 ),
3210 (
3211 "\
3212[
3213 1,
3214 // 2,
3215 3
3216]",
3217 "\
3218[
3219 1,
3220 // 2,
3221 3
3222]",
3223 "preserves comments",
3224 ),
3225 (
3226 "\
3227[
3228 1,
3229 2,
3230 // 3
3231]",
3232 "\
3233[
3234 1,
3235 2,
3236 // 3
3237]",
3238 "preserves comments at the end of the array",
3239 ),
3240 ]
3241 .into_iter()
3242 .enumerate()
3243 {
3244 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3245 let expr = crate::parsing::parser::array_elem_by_elem
3246 .parse(tokens.as_slice())
3247 .unwrap();
3248 let mut actual = String::new();
3249 expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3250 assert_eq!(
3251 actual, expected,
3252 "failed test {i}, which is testing that recasting {reason}"
3253 );
3254 }
3255 }
3256
3257 #[test]
3258 fn code_with_comment_and_extra_lines() {
3259 let code = r#"yo = 'c'
3260
3261/* this is
3262a
3263comment */
3264yo = 'bing'
3265"#;
3266 let ast = crate::parsing::top_level_parse(code).unwrap();
3267 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3268 assert_eq!(recasted, code);
3269 }
3270
3271 #[test]
3272 fn comments_in_a_fn_block() {
3273 let code = r#"fn myFn() {
3274 // this is a comment
3275 yo = { a = { b = { c = '123' } } }
3276
3277 /* block
3278 comment */
3279 key = 'c'
3280 // this is also a comment
3281}
3282"#;
3283 let ast = crate::parsing::top_level_parse(code).unwrap();
3284 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3285 assert_eq!(recasted, code);
3286 }
3287
3288 #[test]
3289 fn array_range_end_exclusive() {
3290 let code = "myArray = [0..<4]\n";
3291 let ast = crate::parsing::top_level_parse(code).unwrap();
3292 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3293 assert_eq!(recasted, code);
3294 }
3295
3296 #[test]
3297 fn paren_precedence() {
3298 let code = r#"x = 1 - 2 - 3
3299x = (1 - 2) - 3
3300x = 1 - (2 - 3)
3301x = 1 + 2 + 3
3302x = (1 + 2) + 3
3303x = 1 + (2 + 3)
3304x = 2 * (y % 2)
3305x = (2 * y) % 2
3306x = 2 % (y * 2)
3307x = (2 % y) * 2
3308x = 2 * y % 2
3309"#;
3310
3311 let expected = r#"x = 1 - 2 - 3
3312x = 1 - 2 - 3
3313x = 1 - (2 - 3)
3314x = 1 + 2 + 3
3315x = 1 + 2 + 3
3316x = 1 + 2 + 3
3317x = 2 * (y % 2)
3318x = 2 * y % 2
3319x = 2 % (y * 2)
3320x = 2 % y * 2
3321x = 2 * y % 2
3322"#;
3323 let ast = crate::parsing::top_level_parse(code).unwrap();
3324 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3325 assert_eq!(recasted, expected);
3326 }
3327
3328 #[test]
3329 fn gap_between_body_item_and_documented_fn() {
3330 let code = "\
3331x = 360
3332
3333// Watermelon
3334fn myFn() {
3335}
3336";
3337 let ast = crate::parsing::top_level_parse(code).unwrap();
3338 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3339 let expected = code;
3340 assert_eq!(recasted, expected);
3341 }
3342
3343 #[test]
3344 fn simple_assignment_in_fn() {
3345 let code = "\
3346fn function001() {
3347 extrude002 = extrude()
3348}\n";
3349
3350 let ast = crate::parsing::top_level_parse(code).unwrap();
3351 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3352 let expected = code;
3353 assert_eq!(recasted, expected);
3354 }
3355
3356 #[test]
3357 fn no_weird_extra_lines() {
3358 let code = "\
3361// Initial comment
3362
3363@settings(defaultLengthUnit = mm)
3364
3365x = 1
3366";
3367 let ast = crate::parsing::top_level_parse(code).unwrap();
3368 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3369 let expected = code;
3370 assert_eq!(recasted, expected);
3371 }
3372
3373 #[test]
3374 fn settings_then_code_is_stable() {
3375 let code = "\
3376@settings(defaultLengthUnit = in)
3377
3378import \"cube-inches.kcl\" as cubeIn
3379import \"cube-mm.kcl\" as cubeMm
3380
3381cubeIn
3382cubeMm
3383";
3384 let formatted_once = crate::parsing::top_level_parse(code)
3385 .unwrap()
3386 .recast_top(&FormatOptions::new(), 0);
3387 assert_eq!(formatted_once, code);
3388
3389 let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3390 .unwrap()
3391 .recast_top(&FormatOptions::new(), 0);
3392 assert_eq!(formatted_twice, formatted_once);
3393 }
3394
3395 #[test]
3396 fn settings_then_standalone_comment_is_stable() {
3397 let code = "\
3398@settings(defaultLengthUnit = mm)
3399@settings(defaultAngleUnit = deg)
3400
3401// Cap for gimbal stick
3402
3403x = 1
3404";
3405 let formatted_once = crate::parsing::top_level_parse(code)
3406 .unwrap()
3407 .recast_top(&FormatOptions::new(), 0);
3408 assert_eq!(formatted_once, code);
3409
3410 let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3411 .unwrap()
3412 .recast_top(&FormatOptions::new(), 0);
3413 assert_eq!(formatted_twice, formatted_once);
3414 }
3415
3416 #[test]
3417 fn module_prefix() {
3418 let code = "x = std::sweep::SKETCH_PLANE\n";
3419 let ast = crate::parsing::top_level_parse(code).unwrap();
3420 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3421 let expected = code;
3422 assert_eq!(recasted, expected);
3423 }
3424
3425 #[test]
3426 fn inline_ifs() {
3427 let code = "y = true
3428startSketchOn(XY)
3429 |> startProfile(at = [0, 0])
3430 |> if y {
3431 yLine(length = 1)
3432 } else {
3433 xLine(length = 1)
3434 }
3435";
3436 let ast = crate::parsing::top_level_parse(code).unwrap();
3437 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3438 let expected = code;
3439 assert_eq!(recasted, expected);
3440 }
3441
3442 #[test]
3443 fn indented_binary_expressions() {
3444 let code = "\
3445fn foo() {
3446 1 == 2
3447}
3448";
3449 let ast = crate::parsing::top_level_parse(code).unwrap();
3450 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3451 let expected = code;
3452 assert_eq!(recasted, expected);
3453 }
3454
3455 #[test]
3456 fn indented_assignment() {
3457 let code = "\
3458fn foo() {
3459 x = 1
3460}
3461";
3462 let ast = crate::parsing::top_level_parse(code).unwrap();
3463 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3464 let expected = code;
3465 assert_eq!(recasted, expected);
3466 }
3467
3468 #[test]
3469 fn indented_unary_expression() {
3470 let code = "\
3471fn foo() {
3472 -x
3473}
3474";
3475 let ast = crate::parsing::top_level_parse(code).unwrap();
3476 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3477 let expected = code;
3478 assert_eq!(recasted, expected);
3479 }
3480
3481 #[test]
3482 fn indented_array_expression() {
3483 let code = "\
3484fn foo() {
3485 [1, 2]
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 indented_name_expression() {
3496 let code = "\
3497fn foo() {
3498 x
3499}
3500";
3501 let ast = crate::parsing::top_level_parse(code).unwrap();
3502 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3503 let expected = code;
3504 assert_eq!(recasted, expected);
3505 }
3506
3507 #[test]
3508 fn indented_member_assignment() {
3509 let code = "\
3510brakcetPlane = {
3511 origin = { x = length / 2 },
3512 origin = { x = length / 2 },
3513 origin = { x = length / 2 },
3514 origin = { x = length / 2 },
3515 origin = { x = length / 2 },
3516 origin = { x = length / 2 },
3517 origin = { x = length / 2 },
3518 origin = { x = length / 2 }
3519}
3520";
3521 let ast = crate::parsing::top_level_parse(code).unwrap();
3522 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3523 let expected = code;
3524 assert_eq!(recasted, expected);
3525 }
3526
3527 #[test]
3528 fn badly_formatted_inline_calls() {
3529 let code = "\
3530return union([right, left])
3531 |> subtract(tools = [
3532 translate(axle(), y = pitchStabL + forkBaseL + wheelRGap + wheelR + addedLength),
3533 socket(rakeAngle = rearRake, xyTrans = [0, 12]),
3534 socket(
3535 rakeAngle = frontRake,
3536 xyTrans = [
3537 wheelW / 2 + wheelWGap + forkTineW / 2,
3538 40 + addedLength
3539 ],
3540 )
3541 ])
3542";
3543 let ast = crate::parsing::top_level_parse(code).unwrap();
3544 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3545 let expected = code;
3546 assert_eq!(recasted, expected);
3547 }
3548
3549 #[test]
3550 fn fn_args_prefixed_with_spaces() {
3551 let code = "holeAt(
3552 [cube1, cube2],
3553 plane = XY,
3554 holeBottom = hole::flat(),
3555 holeBody = hole::blind(depth = 2, diameter = 1),
3556 holeType = hole::counterbore(diameter = 1.4, depth = 1),
3557 cutAt = [1, 1],
3558)";
3559 let expected = "holeAt(
3560 [cube1, cube2],
3561 plane = XY,
3562 holeBottom = hole::flat(),
3563 holeBody = hole::blind(depth = 2, diameter = 1),
3564 holeType = hole::counterbore(diameter = 1.4, depth = 1),
3565 cutAt = [1, 1],
3566)
3567";
3568 let ast = crate::parsing::top_level_parse(code).unwrap();
3569 let recasted = ast.recast_top(&FormatOptions::new(), 0);
3570 assert_eq!(recasted, expected);
3571 }
3572
3573 #[test]
3574 fn some_fn_args_still_prefixed() {
3575 let code = "a
3576 |> b()
3577 |> subtract(
3578 tools = startSketchOn(XY)
3579 |> circle(diameter = hubDiameter)
3580 |> extrude(length = hubThickness * 5, symmetric = true),
3581 tolerance,
3582 )
3583";
3584 let ast = crate::parsing::top_level_parse(code).unwrap();
3585 let actual_recasted = ast.recast_top(&FormatOptions::new(), 0);
3586 let expected_recasted = "a
3587 |> b()
3588 |> subtract(
3589 tools = startSketchOn(XY)
3590 |> circle(diameter = hubDiameter)
3591 |> extrude(length = hubThickness * 5, symmetric = true),
3592 tolerance,
3593 )
3594";
3595 assert_eq!(actual_recasted, expected_recasted);
3596 }
3597
3598 #[test]
3599 fn first_in_pipeline_indent() {
3600 let not_clone = "gear::helical(
3603 nTeeth = 12,
3604 module = 1.5,
3605 pressureAngle = 14deg,
3606 helixAngle = 25deg,
3607 gearHeight = 5,
3608)
3609";
3610 let yes_clone = "gear::helical(
3611 nTeeth = 12,
3612 module = 1.5,
3613 pressureAngle = 14deg,
3614 helixAngle = 25deg,
3615 gearHeight = 5,
3616)
3617|> clone()
3618";
3619 let not_clone_recasted = crate::parsing::top_level_parse(not_clone)
3621 .unwrap()
3622 .recast_top(&FormatOptions::new(), 0);
3623 let yes_clone_recasted = crate::parsing::top_level_parse(yes_clone)
3624 .unwrap()
3625 .recast_top(&FormatOptions::new(), 0);
3626 assert!(not_clone_recasted.contains("\n nTeeth"));
3627 assert!(!yes_clone_recasted.contains("\n nTeeth"));
3628 assert!(yes_clone_recasted.contains("\n nTeeth"));
3629 }
3630}