1use std::fmt::Write;
2
3use crate::{
4 KclError, ModuleId,
5 parsing::{
6 DeprecationKind, PIPE_OPERATOR,
7 ast::types::{
8 Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, Associativity, BinaryExpression,
9 BinaryOperator, BinaryPart, BodyItem, CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FormatOptions,
10 FunctionExpression, IfExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal,
11 LiteralIdentifier, LiteralValue, MemberExpression, Node, NonCodeNode, NonCodeValue, ObjectExpression,
12 Parameter, PipeExpression, Program, TagDeclarator, TypeDeclaration, UnaryExpression, VariableDeclaration,
13 VariableKind,
14 },
15 deprecation,
16 },
17};
18
19#[allow(dead_code)]
20pub fn fmt(input: &str) -> Result<String, KclError> {
21 let program = crate::parsing::parse_str(input, ModuleId::default()).parse_errs_as_err()?;
22 Ok(program.recast(&Default::default(), 0))
23}
24
25impl Program {
26 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
27 let indentation = options.get_indentation(indentation_level);
28
29 let mut result = self
30 .shebang
31 .as_ref()
32 .map(|sh| format!("{}\n\n", sh.inner.content))
33 .unwrap_or_default();
34
35 for start in &self.non_code_meta.start_nodes {
36 result.push_str(&start.recast(options, indentation_level));
37 }
38 for attr in &self.inner_attrs {
39 result.push_str(&attr.recast(options, indentation_level));
40 }
41 if !self.inner_attrs.is_empty() {
42 result.push('\n');
43 }
44
45 let result = result; let result = self
47 .body
48 .iter()
49 .map(|body_item| {
50 let mut result = String::new();
51 for comment in body_item.get_comments() {
52 if !comment.is_empty() {
53 result.push_str(&indentation);
54 result.push_str(comment);
55 }
56 if !result.ends_with("\n\n") && result != "\n" {
57 result.push('\n');
58 }
59 }
60 for attr in body_item.get_attrs() {
61 result.push_str(&attr.recast(options, indentation_level));
62 }
63 result.push_str(&match body_item.clone() {
64 BodyItem::ImportStatement(stmt) => stmt.recast(options, indentation_level),
65 BodyItem::ExpressionStatement(expression_statement) => {
66 expression_statement
67 .expression
68 .recast(options, indentation_level, ExprContext::Other)
69 }
70 BodyItem::VariableDeclaration(variable_declaration) => {
71 variable_declaration.recast(options, indentation_level)
72 }
73 BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(),
74 BodyItem::ReturnStatement(return_statement) => {
75 format!(
76 "{}return {}",
77 indentation,
78 return_statement
79 .argument
80 .recast(options, indentation_level, ExprContext::Other)
81 .trim_start()
82 )
83 }
84 });
85 result
86 })
87 .enumerate()
88 .fold(result, |mut output, (index, recast_str)| {
89 let start_string =
90 if index == 0 && self.non_code_meta.start_nodes.is_empty() && self.inner_attrs.is_empty() {
91 indentation.to_string()
93 } else {
94 String::new()
96 };
97
98 let maybe_line_break: String = if index == self.body.len() - 1 && indentation_level == 0 {
101 String::new()
102 } else {
103 "\n".to_string()
104 };
105
106 let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
107 Some(noncodes) => noncodes
108 .iter()
109 .enumerate()
110 .map(|(i, custom_white_space_or_comment)| {
111 let formatted = custom_white_space_or_comment.recast(options, indentation_level);
112 if i == 0 && !formatted.trim().is_empty() {
113 if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
114 format!("\n{formatted}")
115 } else {
116 formatted
117 }
118 } else {
119 formatted
120 }
121 })
122 .collect::<String>(),
123 None => String::new(),
124 };
125 let end_string = if custom_white_space_or_comment.is_empty() {
126 maybe_line_break
127 } else {
128 custom_white_space_or_comment
129 };
130
131 let _ = write!(output, "{start_string}{recast_str}{end_string}");
132 output
133 })
134 .trim()
135 .to_string();
136
137 if options.insert_final_newline && !result.is_empty() {
139 format!("{result}\n")
140 } else {
141 result
142 }
143 }
144}
145
146impl NonCodeValue {
147 fn should_cause_array_newline(&self) -> bool {
148 match self {
149 Self::InlineComment { .. } => false,
150 Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
151 }
152 }
153}
154
155impl Node<NonCodeNode> {
156 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
157 let indentation = options.get_indentation(indentation_level);
158 match &self.value {
159 NonCodeValue::InlineComment {
160 value,
161 style: CommentStyle::Line,
162 } => format!(" // {value}\n"),
163 NonCodeValue::InlineComment {
164 value,
165 style: CommentStyle::Block,
166 } => format!(" /* {value} */"),
167 NonCodeValue::BlockComment { value, style } => match style {
168 CommentStyle::Block => format!("{indentation}/* {value} */"),
169 CommentStyle::Line => {
170 if value.trim().is_empty() {
171 format!("{indentation}//\n")
172 } else {
173 format!("{}// {}\n", indentation, value.trim())
174 }
175 }
176 },
177 NonCodeValue::NewLineBlockComment { value, style } => {
178 let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
179 match style {
180 CommentStyle::Block => format!("{add_start_new_line}{indentation}/* {value} */\n"),
181 CommentStyle::Line => {
182 if value.trim().is_empty() {
183 format!("{add_start_new_line}{indentation}//\n")
184 } else {
185 format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
186 }
187 }
188 }
189 }
190 NonCodeValue::NewLine => "\n\n".to_string(),
191 }
192 }
193}
194
195impl Node<Annotation> {
196 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
197 let indentation = options.get_indentation(indentation_level);
198 let mut result = String::new();
199 for comment in &self.pre_comments {
200 if !comment.is_empty() {
201 result.push_str(&indentation);
202 result.push_str(comment);
203 }
204 if !result.ends_with("\n\n") && result != "\n" {
205 result.push('\n');
206 }
207 }
208 result.push('@');
209 if let Some(name) = &self.name {
210 result.push_str(&name.name);
211 }
212 if let Some(properties) = &self.properties {
213 result.push('(');
214 result.push_str(
215 &properties
216 .iter()
217 .map(|prop| {
218 format!(
219 "{} = {}",
220 prop.key.name,
221 prop.value
222 .recast(options, indentation_level + 1, ExprContext::Other)
223 .trim()
224 )
225 })
226 .collect::<Vec<String>>()
227 .join(", "),
228 );
229 result.push(')');
230 result.push('\n');
231 }
232
233 result
234 }
235}
236
237impl ImportStatement {
238 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
239 let indentation = options.get_indentation(indentation_level);
240 let vis = if self.visibility == ItemVisibility::Export {
241 "export "
242 } else {
243 ""
244 };
245 let mut string = format!("{vis}{indentation}import ");
246 match &self.selector {
247 ImportSelector::List { items } => {
248 for (i, item) in items.iter().enumerate() {
249 if i > 0 {
250 string.push_str(", ");
251 }
252 string.push_str(&item.name.name);
253 if let Some(alias) = &item.alias {
254 if item.name.name != alias.name {
256 string.push_str(&format!(" as {}", alias.name));
257 }
258 }
259 }
260 string.push_str(" from ");
261 }
262 ImportSelector::Glob(_) => string.push_str("* from "),
263 ImportSelector::None { .. } => {}
264 }
265 string.push_str(&format!("\"{}\"", self.path));
266
267 if let ImportSelector::None { alias: Some(alias) } = &self.selector {
268 string.push_str(" as ");
269 string.push_str(&alias.name);
270 }
271 string
272 }
273}
274
275#[derive(Copy, Clone, Debug, Eq, PartialEq)]
276pub(crate) enum ExprContext {
277 Pipe,
278 Decl,
279 Other,
280}
281
282impl Expr {
283 pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String {
284 let is_decl = matches!(ctxt, ExprContext::Decl);
285 if is_decl {
286 ctxt = ExprContext::Other;
290 }
291 match &self {
292 Expr::BinaryExpression(bin_exp) => bin_exp.recast(options, indentation_level, ctxt),
293 Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
294 Expr::ArrayRangeExpression(range_exp) => range_exp.recast(options, indentation_level, ctxt),
295 Expr::ObjectExpression(obj_exp) => obj_exp.recast(options, indentation_level, ctxt),
296 Expr::MemberExpression(mem_exp) => mem_exp.recast(options, indentation_level, ctxt),
297 Expr::Literal(literal) => literal.recast(),
298 Expr::FunctionExpression(func_exp) => {
299 let mut result = if is_decl { String::new() } else { "fn".to_owned() };
300 result += &func_exp.recast(options, indentation_level);
301 result
302 }
303 Expr::CallExpressionKw(call_exp) => call_exp.recast(options, indentation_level, ctxt),
304 Expr::Name(name) => {
305 let result = name.to_string();
306 match deprecation(&result, DeprecationKind::Const) {
307 Some(suggestion) => suggestion.to_owned(),
308 None => result,
309 }
310 }
311 Expr::TagDeclarator(tag) => tag.recast(),
312 Expr::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
313 Expr::UnaryExpression(unary_exp) => unary_exp.recast(options, indentation_level, ctxt),
314 Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
315 Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
316 Expr::LabelledExpression(e) => {
317 let mut result = e.expr.recast(options, indentation_level, ctxt);
318 result += " as ";
319 result += &e.label.name;
320 result
321 }
322 Expr::AscribedExpression(e) => e.recast(options, indentation_level, ctxt),
323 Expr::None(_) => {
324 unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
325 }
326 }
327 }
328}
329
330impl AscribedExpression {
331 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
332 let mut result = self.expr.recast(options, indentation_level, ctxt);
333 if matches!(
334 self.expr,
335 Expr::BinaryExpression(..) | Expr::PipeExpression(..) | Expr::UnaryExpression(..)
336 ) {
337 result = format!("({result})");
338 }
339 result += ": ";
340 result += &self.ty.to_string();
341 result
342 }
343}
344
345impl BinaryPart {
346 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
347 match &self {
348 BinaryPart::Literal(literal) => literal.recast(),
349 BinaryPart::Name(name) => {
350 let result = name.to_string();
351 match deprecation(&result, DeprecationKind::Const) {
352 Some(suggestion) => suggestion.to_owned(),
353 None => result,
354 }
355 }
356 BinaryPart::BinaryExpression(binary_expression) => {
357 binary_expression.recast(options, indentation_level, ctxt)
358 }
359 BinaryPart::CallExpressionKw(call_expression) => {
360 call_expression.recast(options, indentation_level, ExprContext::Other)
361 }
362 BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options, indentation_level, ctxt),
363 BinaryPart::MemberExpression(member_expression) => {
364 member_expression.recast(options, indentation_level, ctxt)
365 }
366 BinaryPart::IfExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
367 BinaryPart::AscribedExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
368 }
369 }
370}
371
372impl CallExpressionKw {
373 fn recast_args(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> Vec<String> {
374 let mut arg_list = if let Some(first_arg) = &self.unlabeled {
375 vec![first_arg.recast(options, indentation_level, ctxt).trim().to_owned()]
376 } else {
377 Vec::with_capacity(self.arguments.len())
378 };
379 arg_list.extend(
380 self.arguments
381 .iter()
382 .map(|arg| arg.recast(options, indentation_level, ctxt)),
383 );
384 arg_list
385 }
386 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
387 let indent = if ctxt == ExprContext::Pipe {
388 "".to_string()
389 } else {
390 options.get_indentation(indentation_level)
391 };
392 let name = self.callee.to_string();
393
394 if let Some(suggestion) = deprecation(&name, DeprecationKind::Function) {
395 return format!("{indent}{suggestion}");
396 }
397
398 let arg_list = self.recast_args(options, indentation_level, ctxt);
399 let args = arg_list.clone().join(", ");
400 let has_lots_of_args = arg_list.len() >= 4;
401 let some_arg_is_already_multiline = arg_list.len() > 1 && arg_list.iter().any(|arg| arg.contains('\n'));
402 let multiline = has_lots_of_args || some_arg_is_already_multiline;
403 if multiline {
404 let next_indent = indentation_level + 1;
405 let inner_indentation = if ctxt == ExprContext::Pipe {
406 options.get_indentation_offset_pipe(next_indent)
407 } else {
408 options.get_indentation(next_indent)
409 };
410 let arg_list = self.recast_args(options, next_indent, ctxt);
411 let mut args = arg_list.join(&format!(",\n{inner_indentation}"));
412 args.push(',');
413 let args = args;
414 let end_indent = if ctxt == ExprContext::Pipe {
415 options.get_indentation_offset_pipe(indentation_level)
416 } else {
417 options.get_indentation(indentation_level)
418 };
419 format!("{indent}{name}(\n{inner_indentation}{args}\n{end_indent})")
420 } else {
421 format!("{indent}{name}({args})")
422 }
423 }
424}
425
426impl LabeledArg {
427 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
428 let mut result = String::new();
429 if let Some(l) = &self.label {
430 result.push_str(&l.name);
431 result.push_str(" = ");
432 }
433 result.push_str(&self.arg.recast(options, indentation_level, ctxt));
434 result
435 }
436}
437
438impl VariableDeclaration {
439 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
440 let indentation = options.get_indentation(indentation_level);
441 let mut output = match self.visibility {
442 ItemVisibility::Default => String::new(),
443 ItemVisibility::Export => "export ".to_owned(),
444 };
445
446 let (keyword, eq) = match self.kind {
447 VariableKind::Fn => ("fn ", ""),
448 VariableKind::Const => ("", " = "),
449 };
450 let _ = write!(
451 output,
452 "{}{keyword}{}{eq}{}",
453 indentation,
454 self.declaration.id.name,
455 self.declaration
456 .init
457 .recast(options, indentation_level, ExprContext::Decl)
458 .trim()
459 );
460 output
461 }
462}
463
464impl TypeDeclaration {
465 pub fn recast(&self) -> String {
466 let vis = match self.visibility {
467 ItemVisibility::Default => String::new(),
468 ItemVisibility::Export => "export ".to_owned(),
469 };
470
471 let mut arg_str = String::new();
472 if let Some(args) = &self.args {
473 arg_str.push('(');
474 for a in args {
475 if arg_str.len() > 1 {
476 arg_str.push_str(", ");
477 }
478 arg_str.push_str(&a.name);
479 }
480 arg_str.push(')');
481 }
482 if let Some(alias) = &self.alias {
483 arg_str.push_str(" = ");
484 arg_str.push_str(&alias.to_string());
485 }
486 format!("{}type {}{}", vis, self.name.name, arg_str)
487 }
488}
489
490impl Literal {
491 fn recast(&self) -> String {
492 match self.value {
493 LiteralValue::Number { value, suffix } => {
494 if self.raw.contains('.') && value.fract() == 0.0 {
495 format!("{value:?}{suffix}")
496 } else {
497 self.raw.clone()
498 }
499 }
500 LiteralValue::String(ref s) => {
501 if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
502 return suggestion.to_owned();
503 }
504 let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
505 format!("{quote}{s}{quote}")
506 }
507 LiteralValue::Bool(_) => self.raw.clone(),
508 }
509 }
510}
511
512impl TagDeclarator {
513 pub fn recast(&self) -> String {
514 format!("${}", self.name)
516 }
517}
518
519impl ArrayExpression {
520 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
521 let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
525 let mut elems = self.elements.iter();
526 let mut found_line_comment = false;
527 let mut format_items: Vec<_> = (0..num_items)
528 .flat_map(|i| {
529 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
530 noncode
531 .iter()
532 .map(|nc| {
533 found_line_comment |= nc.value.should_cause_array_newline();
534 nc.recast(options, 0)
535 })
536 .collect::<Vec<_>>()
537 } else {
538 let el = elems.next().unwrap();
539 let s = format!("{}, ", el.recast(options, 0, ExprContext::Other));
540 vec![s]
541 }
542 })
543 .collect();
544
545 if let Some(item) = format_items.last_mut() {
547 if let Some(norm) = item.strip_suffix(", ") {
548 *item = norm.to_owned();
549 }
550 }
551 let format_items = format_items; let flat_recast = format!("[{}]", format_items.join(""));
553
554 let max_array_length = 40;
556 let multi_line = flat_recast.len() > max_array_length || found_line_comment;
557 if !multi_line {
558 return flat_recast;
559 }
560
561 let inner_indentation = if ctxt == ExprContext::Pipe {
563 options.get_indentation_offset_pipe(indentation_level + 1)
564 } else {
565 options.get_indentation(indentation_level + 1)
566 };
567 let formatted_array_lines = format_items
568 .iter()
569 .map(|s| {
570 format!(
571 "{inner_indentation}{}{}",
572 if let Some(x) = s.strip_suffix(" ") { x } else { s },
573 if s.ends_with('\n') { "" } else { "\n" }
574 )
575 })
576 .collect::<Vec<String>>()
577 .join("")
578 .to_owned();
579 let end_indent = if ctxt == ExprContext::Pipe {
580 options.get_indentation_offset_pipe(indentation_level)
581 } else {
582 options.get_indentation(indentation_level)
583 };
584 format!("[\n{formatted_array_lines}{end_indent}]")
585 }
586}
587
588fn expr_is_trivial(expr: &Expr) -> bool {
590 matches!(
591 expr,
592 Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
593 )
594}
595
596impl ArrayRangeExpression {
597 fn recast(&self, options: &FormatOptions, _: usize, _: ExprContext) -> String {
598 let s1 = self.start_element.recast(options, 0, ExprContext::Other);
599 let s2 = self.end_element.recast(options, 0, ExprContext::Other);
600
601 let range_op = if self.end_inclusive { ".." } else { "..<" };
602
603 if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
608 format!("[{s1}{range_op}{s2}]")
609 } else {
610 format!("[{s1} {range_op} {s2}]")
611 }
612
613 }
615}
616
617impl ObjectExpression {
618 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
619 if self
620 .non_code_meta
621 .non_code_nodes
622 .values()
623 .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
624 {
625 return self.recast_multi_line(options, indentation_level, ctxt);
626 }
627 let flat_recast = format!(
628 "{{ {} }}",
629 self.properties
630 .iter()
631 .map(|prop| {
632 format!(
633 "{} = {}",
634 prop.key.name,
635 prop.value.recast(options, indentation_level + 1, ctxt).trim()
636 )
637 })
638 .collect::<Vec<String>>()
639 .join(", ")
640 );
641 let max_array_length = 40;
642 let needs_multiple_lines = flat_recast.len() > max_array_length;
643 if !needs_multiple_lines {
644 return flat_recast;
645 }
646 self.recast_multi_line(options, indentation_level, ctxt)
647 }
648
649 fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
651 let inner_indentation = if ctxt == ExprContext::Pipe {
652 options.get_indentation_offset_pipe(indentation_level + 1)
653 } else {
654 options.get_indentation(indentation_level + 1)
655 };
656 let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
657 let mut props = self.properties.iter();
658 let format_items: Vec<_> = (0..num_items)
659 .flat_map(|i| {
660 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
661 noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
662 } else {
663 let prop = props.next().unwrap();
664 let comma = if i == num_items - 1 { "" } else { ",\n" };
666 let s = format!(
667 "{} = {}{comma}",
668 prop.key.name,
669 prop.value.recast(options, indentation_level + 1, ctxt).trim()
670 );
671 vec![s]
672 }
673 })
674 .collect();
675 let end_indent = if ctxt == ExprContext::Pipe {
676 options.get_indentation_offset_pipe(indentation_level)
677 } else {
678 options.get_indentation(indentation_level)
679 };
680 format!(
681 "{{\n{inner_indentation}{}\n{end_indent}}}",
682 format_items.join(&inner_indentation),
683 )
684 }
685}
686
687impl MemberExpression {
688 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
689 let key_str = match &self.property {
690 LiteralIdentifier::Identifier(identifier) => {
691 if self.computed {
692 format!("[{}]", &(*identifier.name))
693 } else {
694 format!(".{}", &(*identifier.name))
695 }
696 }
697 LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
698 };
699
700 self.object.recast(options, indentation_level, ctxt) + key_str.as_str()
701 }
702}
703
704impl BinaryExpression {
705 fn recast(&self, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) -> String {
706 let maybe_wrap_it = |a: String, doit: bool| -> String { if doit { format!("({a})") } else { a } };
707
708 let should_wrap_left = match &self.left {
711 BinaryPart::BinaryExpression(bin_exp) => {
712 self.precedence() > bin_exp.precedence()
713 || ((self.precedence() == bin_exp.precedence())
714 && (!(self.operator.associative() && self.operator == bin_exp.operator)
715 && self.operator.associativity() == Associativity::Right))
716 }
717 _ => false,
718 };
719
720 let should_wrap_right = match &self.right {
721 BinaryPart::BinaryExpression(bin_exp) => {
722 self.precedence() > bin_exp.precedence()
723 || self.operator == BinaryOperator::Sub
725 || self.operator == BinaryOperator::Div
726 || ((self.precedence() == bin_exp.precedence())
727 && (!(self.operator.associative() && self.operator == bin_exp.operator)
728 && self.operator.associativity() == Associativity::Left))
729 }
730 _ => false,
731 };
732
733 format!(
734 "{} {} {}",
735 maybe_wrap_it(self.left.recast(options, 0, ctxt), should_wrap_left),
736 self.operator,
737 maybe_wrap_it(self.right.recast(options, 0, ctxt), should_wrap_right)
738 )
739 }
740}
741
742impl UnaryExpression {
743 fn recast(&self, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) -> String {
744 match self.argument {
745 BinaryPart::Literal(_)
746 | BinaryPart::Name(_)
747 | BinaryPart::MemberExpression(_)
748 | BinaryPart::IfExpression(_)
749 | BinaryPart::AscribedExpression(_)
750 | BinaryPart::CallExpressionKw(_) => {
751 format!("{}{}", &self.operator, self.argument.recast(options, 0, ctxt))
752 }
753 BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
754 format!("{}({})", &self.operator, self.argument.recast(options, 0, ctxt))
755 }
756 }
757 }
758}
759
760impl IfExpression {
761 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
762 let n = 2 + (self.else_ifs.len() * 2) + 3;
765 let mut lines = Vec::with_capacity(n);
766
767 let cond = self.cond.recast(options, indentation_level, ctxt);
768 lines.push((0, format!("if {cond} {{")));
769 lines.push((1, self.then_val.recast(options, indentation_level + 1)));
770 for else_if in &self.else_ifs {
771 let cond = else_if.cond.recast(options, indentation_level, ctxt);
772 lines.push((0, format!("}} else if {cond} {{")));
773 lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
774 }
775 lines.push((0, "} else {".to_owned()));
776 lines.push((1, self.final_else.recast(options, indentation_level + 1)));
777 lines.push((0, "}".to_owned()));
778 lines
779 .into_iter()
780 .map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
781 .collect::<Vec<_>>()
782 .join("\n")
783 }
784}
785
786impl Node<PipeExpression> {
787 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
788 let pipe = self
789 .body
790 .iter()
791 .enumerate()
792 .map(|(index, statement)| {
793 let indentation = options.get_indentation(indentation_level + 1);
794 let mut s = statement.recast(options, indentation_level + 1, ExprContext::Pipe);
795 let non_code_meta = self.non_code_meta.clone();
796 if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
797 for val in non_code_meta_value {
798 let formatted = if val.end == self.end {
799 val.recast(options, indentation_level)
800 .trim_end_matches('\n')
801 .to_string()
802 } else {
803 val.recast(options, indentation_level + 1)
804 .trim_end_matches('\n')
805 .to_string()
806 };
807 if let NonCodeValue::BlockComment { .. } = val.value {
808 s += "\n";
809 s += &formatted;
810 } else {
811 s += &formatted;
812 }
813 }
814 }
815
816 if index != self.body.len() - 1 {
817 s += "\n";
818 s += &indentation;
819 s += PIPE_OPERATOR;
820 s += " ";
821 }
822 s
823 })
824 .collect::<String>();
825 format!("{}{}", options.get_indentation(indentation_level), pipe)
826 }
827}
828
829impl FunctionExpression {
830 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
831 let mut new_options = options.clone();
833 new_options.insert_final_newline = false;
834 let param_list = self
835 .params
836 .iter()
837 .map(|param| param.recast(options, indentation_level))
838 .collect::<Vec<String>>()
839 .join(", ");
840 let tab0 = options.get_indentation(indentation_level);
841 let tab1 = options.get_indentation(indentation_level + 1);
842 let return_type = match &self.return_type {
843 Some(rt) => format!(": {rt}"),
844 None => String::new(),
845 };
846 let body = self.body.recast(&new_options, indentation_level + 1);
847
848 format!("({param_list}){return_type} {{\n{tab1}{body}\n{tab0}}}")
849 }
850}
851
852impl Parameter {
853 pub fn recast(&self, _options: &FormatOptions, _indentation_level: usize) -> String {
854 let at_sign = if self.labeled { "" } else { "@" };
855 let identifier = &self.identifier.name;
856 let question_mark = if self.default_value.is_some() { "?" } else { "" };
857 let mut result = format!("{at_sign}{identifier}{question_mark}");
858 if let Some(ty) = &self.type_ {
859 result += ": ";
860 result += &ty.to_string();
861 }
862 if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
863 let lit = literal.recast();
864 result.push_str(&format!(" = {lit}"));
865 };
866
867 result
868 }
869}
870
871#[cfg(not(target_arch = "wasm32"))]
873#[async_recursion::async_recursion]
874pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
875 if !dir.is_dir() {
877 anyhow::bail!("`{}` is not a directory", dir.display());
878 }
879
880 let mut entries = tokio::fs::read_dir(dir).await?;
881
882 let mut files = Vec::new();
883 while let Some(entry) = entries.next_entry().await? {
884 let path = entry.path();
885
886 if path.is_dir() {
887 files.extend(walk_dir(&path).await?);
888 } else if path
889 .extension()
890 .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
891 {
892 files.push(path);
893 }
894 }
895
896 Ok(files)
897}
898
899#[cfg(not(target_arch = "wasm32"))]
901pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
902 let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
903 crate::KclError::new_internal(crate::errors::KclErrorDetails::new(
904 format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
905 vec![crate::SourceRange::default()],
906 ))
907 })?;
908
909 let futures = files
910 .into_iter()
911 .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) .map(|file| {
914 let options = options.clone();
915 tokio::spawn(async move {
916 let contents = tokio::fs::read_to_string(&file)
917 .await
918 .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
919 let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
920 let report = crate::Report {
921 kcl_source: contents.to_string(),
922 error: err.clone(),
923 filename: file.to_string_lossy().to_string(),
924 };
925 let report = miette::Report::new(report);
926 anyhow::anyhow!("{:?}", report)
927 })?;
928 for ce in &ces {
929 if ce.severity != crate::errors::Severity::Warning {
930 let report = crate::Report {
931 kcl_source: contents.to_string(),
932 error: crate::KclError::new_semantic(ce.clone().into()),
933 filename: file.to_string_lossy().to_string(),
934 };
935 let report = miette::Report::new(report);
936 anyhow::bail!("{:?}", report);
937 }
938 }
939 let Some(program) = program else {
940 anyhow::bail!("Failed to parse file `{}`", file.display());
941 };
942 let recast = program.recast_with_options(&options);
943 tokio::fs::write(&file, recast)
944 .await
945 .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
946
947 Ok::<(), anyhow::Error>(())
948 })
949 })
950 .collect::<Vec<_>>();
951
952 let results = futures::future::join_all(futures).await;
954
955 let mut errors = Vec::new();
957 for result in results {
958 if let Err(err) = result? {
959 errors.push(err);
960 }
961 }
962
963 if !errors.is_empty() {
964 anyhow::bail!("Failed to recast some files: {:?}", errors);
965 }
966
967 Ok(())
968}
969
970#[cfg(test)]
971mod tests {
972 use pretty_assertions::assert_eq;
973
974 use super::*;
975 use crate::{ModuleId, parsing::ast::types::FormatOptions};
976
977 #[test]
978 fn test_recast_annotations_without_body_items() {
979 let input = r#"@settings(defaultLengthUnit = in)
980"#;
981 let program = crate::parsing::top_level_parse(input).unwrap();
982 let output = program.recast(&Default::default(), 0);
983 assert_eq!(output, input);
984 }
985
986 #[test]
987 fn test_recast_annotations_in_function_body() {
988 let input = r#"fn myFunc() {
989 @meta(yes = true)
990
991 x = 2
992}
993"#;
994 let program = crate::parsing::top_level_parse(input).unwrap();
995 let output = program.recast(&Default::default(), 0);
996 assert_eq!(output, input);
997 }
998
999 #[test]
1000 fn test_recast_annotations_in_function_body_without_items() {
1001 let input = r#"fn myFunc() {
1002 @meta(yes = true)
1003}
1004"#;
1005 let program = crate::parsing::top_level_parse(input).unwrap();
1006 let output = program.recast(&Default::default(), 0);
1007 assert_eq!(output, input);
1008 }
1009
1010 #[test]
1011 fn recast_annotations_with_comments() {
1012 let input = r#"// Start comment
1013
1014// Comment on attr
1015@settings(defaultLengthUnit = in)
1016
1017// Comment on item
1018foo = 42
1019
1020// Comment on another item
1021@(impl = kcl)
1022bar = 0
1023"#;
1024 let program = crate::parsing::top_level_parse(input).unwrap();
1025 let output = program.recast(&Default::default(), 0);
1026 assert_eq!(output, input);
1027 }
1028
1029 #[test]
1030 fn recast_annotations_with_block_comment() {
1031 let input = r#"/* Start comment
1032
1033sdfsdfsdfs */
1034@settings(defaultLengthUnit = in)
1035
1036foo = 42
1037"#;
1038 let program = crate::parsing::top_level_parse(input).unwrap();
1039 let output = program.recast(&Default::default(), 0);
1040 assert_eq!(output, input);
1041 }
1042
1043 #[test]
1044 fn test_recast_if_else_if_same() {
1045 let input = r#"b = if false {
1046 3
1047} else if true {
1048 4
1049} else {
1050 5
1051}
1052"#;
1053 let program = crate::parsing::top_level_parse(input).unwrap();
1054 let output = program.recast(&Default::default(), 0);
1055 assert_eq!(output, input);
1056 }
1057
1058 #[test]
1059 fn test_recast_if_same() {
1060 let input = r#"b = if false {
1061 3
1062} else {
1063 5
1064}
1065"#;
1066 let program = crate::parsing::top_level_parse(input).unwrap();
1067 let output = program.recast(&Default::default(), 0);
1068 assert_eq!(output, input);
1069 }
1070
1071 #[test]
1072 fn test_recast_import() {
1073 let input = r#"import a from "a.kcl"
1074import a as aaa from "a.kcl"
1075import a, b from "a.kcl"
1076import a as aaa, b from "a.kcl"
1077import a, b as bbb from "a.kcl"
1078import a as aaa, b as bbb from "a.kcl"
1079import "a_b.kcl"
1080import "a-b.kcl" as b
1081import * from "a.kcl"
1082export import a as aaa from "a.kcl"
1083export import a, b from "a.kcl"
1084export import a as aaa, b from "a.kcl"
1085export import a, b as bbb from "a.kcl"
1086"#;
1087 let program = crate::parsing::top_level_parse(input).unwrap();
1088 let output = program.recast(&Default::default(), 0);
1089 assert_eq!(output, input);
1090 }
1091
1092 #[test]
1093 fn test_recast_import_as_same_name() {
1094 let input = r#"import a as a from "a.kcl"
1095"#;
1096 let program = crate::parsing::top_level_parse(input).unwrap();
1097 let output = program.recast(&Default::default(), 0);
1098 let expected = r#"import a from "a.kcl"
1099"#;
1100 assert_eq!(output, expected);
1101 }
1102
1103 #[test]
1104 fn test_recast_export_fn() {
1105 let input = r#"export fn a() {
1106 return 0
1107}
1108"#;
1109 let program = crate::parsing::top_level_parse(input).unwrap();
1110 let output = program.recast(&Default::default(), 0);
1111 assert_eq!(output, input);
1112 }
1113
1114 #[test]
1115 fn test_recast_bug_fn_in_fn() {
1116 let some_program_string = r#"// Start point (top left)
1117zoo_x = -20
1118zoo_y = 7
1119// Scale
1120s = 1 // s = 1 -> height of Z is 13.4mm
1121// Depth
1122d = 1
1123
1124fn rect(x, y, w, h) {
1125 startSketchOn(XY)
1126 |> startProfile(at = [x, y])
1127 |> xLine(length = w)
1128 |> yLine(length = h)
1129 |> xLine(length = -w)
1130 |> close()
1131 |> extrude(d)
1132}
1133
1134fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1135 startSketchOn(XY)
1136 |> startProfile(at = [x1, y1])
1137 |> line(endAbsolute = [x2, y2])
1138 |> line(endAbsolute = [x3, y3])
1139 |> line(endAbsolute = [x4, y4])
1140 |> close()
1141 |> extrude(d)
1142}
1143
1144fn crosshair(x, y) {
1145 startSketchOn(XY)
1146 |> startProfile(at = [x, y])
1147 |> yLine(length = 1)
1148 |> yLine(length = -2)
1149 |> yLine(length = 1)
1150 |> xLine(length = 1)
1151 |> xLine(length = -2)
1152}
1153
1154fn z(z_x, z_y) {
1155 z_end_w = s * 8.4
1156 z_end_h = s * 3
1157 z_corner = s * 2
1158 z_w = z_end_w + 2 * z_corner
1159 z_h = z_w * 1.08130081300813
1160 rect(
1161 z_x,
1162 a = z_y,
1163 b = z_end_w,
1164 c = -z_end_h,
1165 )
1166 rect(
1167 z_x + z_w,
1168 a = z_y,
1169 b = -z_corner,
1170 c = -z_corner,
1171 )
1172 rect(
1173 z_x + z_w,
1174 a = z_y - z_h,
1175 b = -z_end_w,
1176 c = z_end_h,
1177 )
1178 rect(
1179 z_x,
1180 a = z_y - z_h,
1181 b = z_corner,
1182 c = z_corner,
1183 )
1184}
1185
1186fn o(c_x, c_y) {
1187 // Outer and inner radii
1188 o_r = s * 6.95
1189 i_r = 0.5652173913043478 * o_r
1190
1191 // Angle offset for diagonal break
1192 a = 7
1193
1194 // Start point for the top sketch
1195 o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
1196 o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
1197
1198 // Start point for the bottom sketch
1199 o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
1200 o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
1201
1202 // End point for the bottom startSketch
1203 o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
1204 o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
1205
1206 // Where is the center?
1207 // crosshair(c_x, c_y)
1208
1209
1210 startSketchOn(XY)
1211 |> startProfile(at = [o_x1, o_y1])
1212 |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
1213 |> angledLine(angle = 45, length = o_r - i_r)
1214 |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
1215 |> close()
1216 |> extrude(d)
1217
1218 startSketchOn(XY)
1219 |> startProfile(at = [o_x2, o_y2])
1220 |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
1221 |> angledLine(angle = 225, length = o_r - i_r)
1222 |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
1223 |> close()
1224 |> extrude(d)
1225}
1226
1227fn zoo(x0, y0) {
1228 z(x = x0, y = y0)
1229 o(x = x0 + s * 20, y = y0 - (s * 6.7))
1230 o(x = x0 + s * 35, y = y0 - (s * 6.7))
1231}
1232
1233zoo(x = zoo_x, y = zoo_y)
1234"#;
1235 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1236
1237 let recasted = program.recast(&Default::default(), 0);
1238 assert_eq!(recasted, some_program_string);
1239 }
1240
1241 #[test]
1242 fn test_recast_bug_extra_parens() {
1243 let some_program_string = r#"// Ball Bearing
1244// 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.
1245
1246// Define constants like ball diameter, inside diameter, overhange length, and thickness
1247sphereDia = 0.5
1248insideDia = 1
1249thickness = 0.25
1250overHangLength = .4
1251
1252// Sketch and revolve the inside bearing piece
1253insideRevolve = startSketchOn(XZ)
1254 |> startProfile(at = [insideDia / 2, 0])
1255 |> line(end = [0, thickness + sphereDia / 2])
1256 |> line(end = [overHangLength, 0])
1257 |> line(end = [0, -thickness])
1258 |> line(end = [-overHangLength + thickness, 0])
1259 |> line(end = [0, -sphereDia])
1260 |> line(end = [overHangLength - thickness, 0])
1261 |> line(end = [0, -thickness])
1262 |> line(end = [-overHangLength, 0])
1263 |> close()
1264 |> revolve(axis = Y)
1265
1266// 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)
1267sphere = startSketchOn(XZ)
1268 |> startProfile(at = [
1269 0.05 + insideDia / 2 + thickness,
1270 0 - 0.05
1271 ])
1272 |> line(end = [sphereDia - 0.1, 0])
1273 |> arc(
1274 angle_start = 0,
1275 angle_end = -180,
1276 radius = sphereDia / 2 - 0.05
1277 )
1278 |> close()
1279 |> revolve(axis = X)
1280 |> patternCircular3d(
1281 axis = [0, 0, 1],
1282 center = [0, 0, 0],
1283 repetitions = 10,
1284 arcDegrees = 360,
1285 rotateDuplicates = true
1286 )
1287
1288// Sketch and revolve the outside bearing
1289outsideRevolve = startSketchOn(XZ)
1290 |> startProfile(at = [
1291 insideDia / 2 + thickness + sphereDia,
1292 0
1293 ]
1294 )
1295 |> line(end = [0, sphereDia / 2])
1296 |> line(end = [-overHangLength + thickness, 0])
1297 |> line(end = [0, thickness])
1298 |> line(end = [overHangLength, 0])
1299 |> line(end = [0, -2 * thickness - sphereDia])
1300 |> line(end = [-overHangLength, 0])
1301 |> line(end = [0, thickness])
1302 |> line(end = [overHangLength - thickness, 0])
1303 |> close()
1304 |> revolve(axis = Y)"#;
1305 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1306
1307 let recasted = program.recast(&Default::default(), 0);
1308 assert_eq!(
1309 recasted,
1310 r#"// Ball Bearing
1311// 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.
1312
1313// Define constants like ball diameter, inside diameter, overhange length, and thickness
1314sphereDia = 0.5
1315insideDia = 1
1316thickness = 0.25
1317overHangLength = .4
1318
1319// Sketch and revolve the inside bearing piece
1320insideRevolve = startSketchOn(XZ)
1321 |> startProfile(at = [insideDia / 2, 0])
1322 |> line(end = [0, thickness + sphereDia / 2])
1323 |> line(end = [overHangLength, 0])
1324 |> line(end = [0, -thickness])
1325 |> line(end = [-overHangLength + thickness, 0])
1326 |> line(end = [0, -sphereDia])
1327 |> line(end = [overHangLength - thickness, 0])
1328 |> line(end = [0, -thickness])
1329 |> line(end = [-overHangLength, 0])
1330 |> close()
1331 |> revolve(axis = Y)
1332
1333// 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)
1334sphere = startSketchOn(XZ)
1335 |> startProfile(at = [
1336 0.05 + insideDia / 2 + thickness,
1337 0 - 0.05
1338 ])
1339 |> line(end = [sphereDia - 0.1, 0])
1340 |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
1341 |> close()
1342 |> revolve(axis = X)
1343 |> patternCircular3d(
1344 axis = [0, 0, 1],
1345 center = [0, 0, 0],
1346 repetitions = 10,
1347 arcDegrees = 360,
1348 rotateDuplicates = true,
1349 )
1350
1351// Sketch and revolve the outside bearing
1352outsideRevolve = startSketchOn(XZ)
1353 |> startProfile(at = [
1354 insideDia / 2 + thickness + sphereDia,
1355 0
1356 ])
1357 |> line(end = [0, sphereDia / 2])
1358 |> line(end = [-overHangLength + thickness, 0])
1359 |> line(end = [0, thickness])
1360 |> line(end = [overHangLength, 0])
1361 |> line(end = [0, -2 * thickness - sphereDia])
1362 |> line(end = [-overHangLength, 0])
1363 |> line(end = [0, thickness])
1364 |> line(end = [overHangLength - thickness, 0])
1365 |> close()
1366 |> revolve(axis = Y)
1367"#
1368 );
1369 }
1370
1371 #[test]
1372 fn test_recast_fn_in_object() {
1373 let some_program_string = r#"bing = { yo = 55 }
1374myNestedVar = [{ prop = callExp(bing.yo) }]
1375"#;
1376 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1377
1378 let recasted = program.recast(&Default::default(), 0);
1379 assert_eq!(recasted, some_program_string);
1380 }
1381
1382 #[test]
1383 fn test_recast_fn_in_array() {
1384 let some_program_string = r#"bing = { yo = 55 }
1385myNestedVar = [callExp(bing.yo)]
1386"#;
1387 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1388
1389 let recasted = program.recast(&Default::default(), 0);
1390 assert_eq!(recasted, some_program_string);
1391 }
1392
1393 #[test]
1394 fn test_recast_ranges() {
1395 let some_program_string = r#"foo = [0..10]
1396ten = 10
1397bar = [0 + 1 .. ten]
1398"#;
1399 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1400
1401 let recasted = program.recast(&Default::default(), 0);
1402 assert_eq!(recasted, some_program_string);
1403 }
1404
1405 #[test]
1406 fn test_recast_space_in_fn_call() {
1407 let some_program_string = r#"fn thing (x) {
1408 return x + 1
1409}
1410
1411thing ( 1 )
1412"#;
1413 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1414
1415 let recasted = program.recast(&Default::default(), 0);
1416 assert_eq!(
1417 recasted,
1418 r#"fn thing(x) {
1419 return x + 1
1420}
1421
1422thing(1)
1423"#
1424 );
1425 }
1426
1427 #[test]
1428 fn test_recast_typed_fn() {
1429 let some_program_string = r#"fn thing(x: string, y: [bool]): number {
1430 return x + 1
1431}
1432"#;
1433 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1434
1435 let recasted = program.recast(&Default::default(), 0);
1436 assert_eq!(recasted, some_program_string);
1437 }
1438
1439 #[test]
1440 fn test_recast_typed_consts() {
1441 let some_program_string = r#"a = 42: number
1442export b = 3.2: number(ft)
1443c = "dsfds": A | B | C
1444d = [1]: [number]
1445e = foo: [number; 3]
1446f = [1, 2, 3]: [number; 1+]
1447f = [1, 2, 3]: [number; 3+]
1448"#;
1449 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1450
1451 let recasted = program.recast(&Default::default(), 0);
1452 assert_eq!(recasted, some_program_string);
1453 }
1454
1455 #[test]
1456 fn test_recast_object_fn_in_array_weird_bracket() {
1457 let some_program_string = r#"bing = { yo = 55 }
1458myNestedVar = [
1459 {
1460 prop: line(a = [bing.yo, 21], b = sketch001)
1461}
1462]
1463"#;
1464 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1465
1466 let recasted = program.recast(&Default::default(), 0);
1467 assert_eq!(
1468 recasted,
1469 r#"bing = { yo = 55 }
1470myNestedVar = [
1471 {
1472 prop = line(a = [bing.yo, 21], b = sketch001)
1473}
1474]
1475"#
1476 );
1477 }
1478
1479 #[test]
1480 fn test_recast_empty_file() {
1481 let some_program_string = r#""#;
1482 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1483
1484 let recasted = program.recast(&Default::default(), 0);
1485 assert_eq!(recasted, r#""#);
1487 }
1488
1489 #[test]
1490 fn test_recast_empty_file_new_line() {
1491 let some_program_string = r#"
1492"#;
1493 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1494
1495 let recasted = program.recast(&Default::default(), 0);
1496 assert_eq!(recasted, r#""#);
1498 }
1499
1500 #[test]
1501 fn test_recast_shebang() {
1502 let some_program_string = r#"#!/usr/local/env zoo kcl
1503part001 = startSketchOn(XY)
1504 |> startProfile(at = [-10, -10])
1505 |> line(end = [20, 0])
1506 |> line(end = [0, 20])
1507 |> line(end = [-20, 0])
1508 |> close()
1509"#;
1510
1511 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1512
1513 let recasted = program.recast(&Default::default(), 0);
1514 assert_eq!(
1515 recasted,
1516 r#"#!/usr/local/env zoo kcl
1517
1518part001 = startSketchOn(XY)
1519 |> startProfile(at = [-10, -10])
1520 |> line(end = [20, 0])
1521 |> line(end = [0, 20])
1522 |> line(end = [-20, 0])
1523 |> close()
1524"#
1525 );
1526 }
1527
1528 #[test]
1529 fn test_recast_shebang_new_lines() {
1530 let some_program_string = r#"#!/usr/local/env zoo kcl
1531
1532
1533
1534part001 = startSketchOn(XY)
1535 |> startProfile(at = [-10, -10])
1536 |> line(end = [20, 0])
1537 |> line(end = [0, 20])
1538 |> line(end = [-20, 0])
1539 |> close()
1540"#;
1541
1542 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1543
1544 let recasted = program.recast(&Default::default(), 0);
1545 assert_eq!(
1546 recasted,
1547 r#"#!/usr/local/env zoo kcl
1548
1549part001 = startSketchOn(XY)
1550 |> startProfile(at = [-10, -10])
1551 |> line(end = [20, 0])
1552 |> line(end = [0, 20])
1553 |> line(end = [-20, 0])
1554 |> close()
1555"#
1556 );
1557 }
1558
1559 #[test]
1560 fn test_recast_shebang_with_comments() {
1561 let some_program_string = r#"#!/usr/local/env zoo kcl
1562
1563// Yo yo my comments.
1564part001 = startSketchOn(XY)
1565 |> startProfile(at = [-10, -10])
1566 |> line(end = [20, 0])
1567 |> line(end = [0, 20])
1568 |> line(end = [-20, 0])
1569 |> close()
1570"#;
1571
1572 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1573
1574 let recasted = program.recast(&Default::default(), 0);
1575 assert_eq!(
1576 recasted,
1577 r#"#!/usr/local/env zoo kcl
1578
1579// Yo yo my comments.
1580part001 = startSketchOn(XY)
1581 |> startProfile(at = [-10, -10])
1582 |> line(end = [20, 0])
1583 |> line(end = [0, 20])
1584 |> line(end = [-20, 0])
1585 |> close()
1586"#
1587 );
1588 }
1589
1590 #[test]
1591 fn test_recast_empty_function_body_with_comments() {
1592 let input = r#"fn myFunc() {
1593 // Yo yo my comments.
1594}
1595"#;
1596
1597 let program = crate::parsing::top_level_parse(input).unwrap();
1598 let output = program.recast(&Default::default(), 0);
1599 assert_eq!(output, input);
1600 }
1601
1602 #[test]
1603 fn test_recast_large_file() {
1604 let some_program_string = r#"@settings(units=mm)
1605// define nts
1606radius = 6.0
1607width = 144.0
1608length = 83.0
1609depth = 45.0
1610thk = 5
1611hole_diam = 5
1612// define a rectangular shape func
1613fn rectShape(pos, w, l) {
1614 rr = startSketchOn(XY)
1615 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1616 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1617 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1618 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1619 |> close($edge4)
1620 return rr
1621}
1622// build the body of the focusrite scarlett solo gen 4
1623// only used for visualization
1624scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1625 |> extrude(depth)
1626 |> fillet(
1627 radius = radius,
1628 tags = [
1629 edge2,
1630 edge4,
1631 getOppositeEdge(edge2),
1632 getOppositeEdge(edge4)
1633]
1634 )
1635 // build the bracket sketch around the body
1636fn bracketSketch(w, d, t) {
1637 s = startSketchOn({
1638 plane = {
1639 origin = { x = 0, y = length / 2 + thk, z = 0 },
1640 x_axis = { x = 1, y = 0, z = 0 },
1641 y_axis = { x = 0, y = 0, z = 1 },
1642 z_axis = { x = 0, y = 1, z = 0 }
1643}
1644 })
1645 |> startProfile(at = [-w / 2 - t, d + t])
1646 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1647 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1648 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1649 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1650 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1651 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1652 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1653 |> close($edge8)
1654 return s
1655}
1656// build the body of the bracket
1657bracket_body = bracketSketch(w = width, d = depth, t = thk)
1658 |> extrude(length + 10)
1659 |> fillet(
1660 radius = radius,
1661 tags = [
1662 getNextAdjacentEdge(edge7),
1663 getNextAdjacentEdge(edge2),
1664 getNextAdjacentEdge(edge3),
1665 getNextAdjacentEdge(edge6)
1666]
1667 )
1668 // build the tabs of the mounting bracket (right side)
1669tabs_r = startSketchOn({
1670 plane = {
1671 origin = { x = 0, y = 0, z = depth + thk },
1672 x_axis = { x = 1, y = 0, z = 0 },
1673 y_axis = { x = 0, y = 1, z = 0 },
1674 z_axis = { x = 0, y = 0, z = 1 }
1675}
1676 })
1677 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1678 |> line(end = [10, -5])
1679 |> line(end = [0, -10])
1680 |> line(end = [-10, -5])
1681 |> close()
1682 |> subtract2d(tool = circle(
1683 center = [
1684 width / 2 + thk + hole_diam,
1685 length / 2 - hole_diam
1686 ],
1687 radius = hole_diam / 2
1688 ))
1689 |> extrude(-thk)
1690 |> patternLinear3d(
1691 axis = [0, -1, 0],
1692 repetitions = 1,
1693 distance = length - 10
1694 )
1695 // build the tabs of the mounting bracket (left side)
1696tabs_l = startSketchOn({
1697 plane = {
1698 origin = { x = 0, y = 0, z = depth + thk },
1699 x_axis = { x = 1, y = 0, z = 0 },
1700 y_axis = { x = 0, y = 1, z = 0 },
1701 z_axis = { x = 0, y = 0, z = 1 }
1702}
1703 })
1704 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1705 |> line(end = [-10, -5])
1706 |> line(end = [0, -10])
1707 |> line(end = [10, -5])
1708 |> close()
1709 |> subtract2d(tool = circle(
1710 center = [
1711 -width / 2 - thk - hole_diam,
1712 length / 2 - hole_diam
1713 ],
1714 radius = hole_diam / 2
1715 ))
1716 |> extrude(-thk)
1717 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1718"#;
1719 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1720
1721 let recasted = program.recast(&Default::default(), 0);
1722 assert_eq!(
1724 recasted,
1725 r#"@settings(units = mm)
1726
1727// define nts
1728radius = 6.0
1729width = 144.0
1730length = 83.0
1731depth = 45.0
1732thk = 5
1733hole_diam = 5
1734// define a rectangular shape func
1735fn rectShape(pos, w, l) {
1736 rr = startSketchOn(XY)
1737 |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1738 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1739 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1740 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1741 |> close($edge4)
1742 return rr
1743}
1744// build the body of the focusrite scarlett solo gen 4
1745// only used for visualization
1746scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1747 |> extrude(depth)
1748 |> fillet(
1749 radius = radius,
1750 tags = [
1751 edge2,
1752 edge4,
1753 getOppositeEdge(edge2),
1754 getOppositeEdge(edge4)
1755 ],
1756 )
1757// build the bracket sketch around the body
1758fn bracketSketch(w, d, t) {
1759 s = startSketchOn({
1760 plane = {
1761 origin = { x = 0, y = length / 2 + thk, z = 0 },
1762 x_axis = { x = 1, y = 0, z = 0 },
1763 y_axis = { x = 0, y = 0, z = 1 },
1764 z_axis = { x = 0, y = 1, z = 0 }
1765 }
1766 })
1767 |> startProfile(at = [-w / 2 - t, d + t])
1768 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1769 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1770 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1771 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1772 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1773 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1774 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1775 |> close($edge8)
1776 return s
1777}
1778// build the body of the bracket
1779bracket_body = bracketSketch(w = width, d = depth, t = thk)
1780 |> extrude(length + 10)
1781 |> fillet(
1782 radius = radius,
1783 tags = [
1784 getNextAdjacentEdge(edge7),
1785 getNextAdjacentEdge(edge2),
1786 getNextAdjacentEdge(edge3),
1787 getNextAdjacentEdge(edge6)
1788 ],
1789 )
1790// build the tabs of the mounting bracket (right side)
1791tabs_r = startSketchOn({
1792 plane = {
1793 origin = { x = 0, y = 0, z = depth + thk },
1794 x_axis = { x = 1, y = 0, z = 0 },
1795 y_axis = { x = 0, y = 1, z = 0 },
1796 z_axis = { x = 0, y = 0, z = 1 }
1797 }
1798 })
1799 |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1800 |> line(end = [10, -5])
1801 |> line(end = [0, -10])
1802 |> line(end = [-10, -5])
1803 |> close()
1804 |> subtract2d(tool = circle(
1805 center = [
1806 width / 2 + thk + hole_diam,
1807 length / 2 - hole_diam
1808 ],
1809 radius = hole_diam / 2,
1810 ))
1811 |> extrude(-thk)
1812 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1813// build the tabs of the mounting bracket (left side)
1814tabs_l = startSketchOn({
1815 plane = {
1816 origin = { x = 0, y = 0, z = depth + thk },
1817 x_axis = { x = 1, y = 0, z = 0 },
1818 y_axis = { x = 0, y = 1, z = 0 },
1819 z_axis = { x = 0, y = 0, z = 1 }
1820 }
1821 })
1822 |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
1823 |> line(end = [-10, -5])
1824 |> line(end = [0, -10])
1825 |> line(end = [10, -5])
1826 |> close()
1827 |> subtract2d(tool = circle(
1828 center = [
1829 -width / 2 - thk - hole_diam,
1830 length / 2 - hole_diam
1831 ],
1832 radius = hole_diam / 2,
1833 ))
1834 |> extrude(-thk)
1835 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
1836"#
1837 );
1838 }
1839
1840 #[test]
1841 fn test_recast_nested_var_declaration_in_fn_body() {
1842 let some_program_string = r#"fn cube(pos, scale) {
1843 sg = startSketchOn(XY)
1844 |> startProfile(at = pos)
1845 |> line(end = [0, scale])
1846 |> line(end = [scale, 0])
1847 |> line(end = [0, -scale])
1848 |> close()
1849 |> extrude(scale)
1850}"#;
1851 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1852
1853 let recasted = program.recast(&Default::default(), 0);
1854 assert_eq!(
1855 recasted,
1856 r#"fn cube(pos, scale) {
1857 sg = startSketchOn(XY)
1858 |> startProfile(at = pos)
1859 |> line(end = [0, scale])
1860 |> line(end = [scale, 0])
1861 |> line(end = [0, -scale])
1862 |> close()
1863 |> extrude(scale)
1864}
1865"#
1866 );
1867 }
1868
1869 #[test]
1870 fn test_as() {
1871 let some_program_string = r#"fn cube(pos, scale) {
1872 x = dfsfs + dfsfsd as y
1873
1874 sg = startSketchOn(XY)
1875 |> startProfile(at = pos) as foo
1876 |> line([0, scale])
1877 |> line([scale, 0]) as bar
1878 |> line([0 as baz, -scale] as qux)
1879 |> close()
1880 |> extrude(length = scale)
1881}
1882
1883cube(pos = 0, scale = 0) as cub
1884"#;
1885 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1886
1887 let recasted = program.recast(&Default::default(), 0);
1888 assert_eq!(recasted, some_program_string,);
1889 }
1890
1891 #[test]
1892 fn test_recast_with_bad_indentation() {
1893 let some_program_string = r#"part001 = startSketchOn(XY)
1894 |> startProfile(at = [0.0, 5.0])
1895 |> line(end = [0.4900857016, -0.0240763666])
1896 |> line(end = [0.6804562304, 0.9087880491])"#;
1897 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1898
1899 let recasted = program.recast(&Default::default(), 0);
1900 assert_eq!(
1901 recasted,
1902 r#"part001 = startSketchOn(XY)
1903 |> startProfile(at = [0.0, 5.0])
1904 |> line(end = [0.4900857016, -0.0240763666])
1905 |> line(end = [0.6804562304, 0.9087880491])
1906"#
1907 );
1908 }
1909
1910 #[test]
1911 fn test_recast_with_bad_indentation_and_inline_comment() {
1912 let some_program_string = r#"part001 = startSketchOn(XY)
1913 |> startProfile(at = [0.0, 5.0])
1914 |> line(end = [0.4900857016, -0.0240763666]) // hello world
1915 |> line(end = [0.6804562304, 0.9087880491])"#;
1916 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1917
1918 let recasted = program.recast(&Default::default(), 0);
1919 assert_eq!(
1920 recasted,
1921 r#"part001 = startSketchOn(XY)
1922 |> startProfile(at = [0.0, 5.0])
1923 |> line(end = [0.4900857016, -0.0240763666]) // hello world
1924 |> line(end = [0.6804562304, 0.9087880491])
1925"#
1926 );
1927 }
1928 #[test]
1929 fn test_recast_with_bad_indentation_and_line_comment() {
1930 let some_program_string = r#"part001 = startSketchOn(XY)
1931 |> startProfile(at = [0.0, 5.0])
1932 |> line(end = [0.4900857016, -0.0240763666])
1933 // hello world
1934 |> line(end = [0.6804562304, 0.9087880491])"#;
1935 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1936
1937 let recasted = program.recast(&Default::default(), 0);
1938 assert_eq!(
1939 recasted,
1940 r#"part001 = startSketchOn(XY)
1941 |> startProfile(at = [0.0, 5.0])
1942 |> line(end = [0.4900857016, -0.0240763666])
1943 // hello world
1944 |> line(end = [0.6804562304, 0.9087880491])
1945"#
1946 );
1947 }
1948
1949 #[test]
1950 fn test_recast_comment_in_a_fn_block() {
1951 let some_program_string = r#"fn myFn() {
1952 // this is a comment
1953 yo = { a = { b = { c = '123' } } } /* block
1954 comment */
1955
1956 key = 'c'
1957 // this is also a comment
1958 return things
1959}"#;
1960 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1961
1962 let recasted = program.recast(&Default::default(), 0);
1963 assert_eq!(
1964 recasted,
1965 r#"fn myFn() {
1966 // this is a comment
1967 yo = { a = { b = { c = '123' } } } /* block
1968 comment */
1969
1970 key = 'c'
1971 // this is also a comment
1972 return things
1973}
1974"#
1975 );
1976 }
1977
1978 #[test]
1979 fn test_recast_comment_under_variable() {
1980 let some_program_string = r#"key = 'c'
1981// this is also a comment
1982thing = 'foo'
1983"#;
1984 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1985
1986 let recasted = program.recast(&Default::default(), 0);
1987 assert_eq!(
1988 recasted,
1989 r#"key = 'c'
1990// this is also a comment
1991thing = 'foo'
1992"#
1993 );
1994 }
1995
1996 #[test]
1997 fn test_recast_multiline_comment_start_file() {
1998 let some_program_string = r#"// hello world
1999// I am a comment
2000key = 'c'
2001// this is also a comment
2002// hello
2003thing = 'foo'
2004"#;
2005 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2006
2007 let recasted = program.recast(&Default::default(), 0);
2008 assert_eq!(
2009 recasted,
2010 r#"// hello world
2011// I am a comment
2012key = 'c'
2013// this is also a comment
2014// hello
2015thing = 'foo'
2016"#
2017 );
2018 }
2019
2020 #[test]
2021 fn test_recast_empty_comment() {
2022 let some_program_string = r#"// hello world
2023//
2024// I am a comment
2025key = 'c'
2026
2027//
2028// I am a comment
2029thing = 'c'
2030
2031foo = 'bar' //
2032"#;
2033 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2034
2035 let recasted = program.recast(&Default::default(), 0);
2036 assert_eq!(
2037 recasted,
2038 r#"// hello world
2039//
2040// I am a comment
2041key = 'c'
2042
2043//
2044// I am a comment
2045thing = 'c'
2046
2047foo = 'bar' //
2048"#
2049 );
2050 }
2051
2052 #[test]
2053 fn test_recast_multiline_comment_under_variable() {
2054 let some_program_string = r#"key = 'c'
2055// this is also a comment
2056// hello
2057thing = 'foo'
2058"#;
2059 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2060
2061 let recasted = program.recast(&Default::default(), 0);
2062 assert_eq!(
2063 recasted,
2064 r#"key = 'c'
2065// this is also a comment
2066// hello
2067thing = 'foo'
2068"#
2069 );
2070 }
2071
2072 #[test]
2073 fn test_recast_only_line_comments() {
2074 let code = r#"// comment at start
2075"#;
2076 let program = crate::parsing::top_level_parse(code).unwrap();
2077
2078 assert_eq!(program.recast(&Default::default(), 0), code);
2079 }
2080
2081 #[test]
2082 fn test_recast_comment_at_start() {
2083 let test_program = r#"
2084/* comment at start */
2085
2086mySk1 = startSketchOn(XY)
2087 |> startProfile(at = [0, 0])"#;
2088 let program = crate::parsing::top_level_parse(test_program).unwrap();
2089
2090 let recasted = program.recast(&Default::default(), 0);
2091 assert_eq!(
2092 recasted,
2093 r#"/* comment at start */
2094
2095mySk1 = startSketchOn(XY)
2096 |> startProfile(at = [0, 0])
2097"#
2098 );
2099 }
2100
2101 #[test]
2102 fn test_recast_lots_of_comments() {
2103 let some_program_string = r#"// comment at start
2104mySk1 = startSketchOn(XY)
2105 |> startProfile(at = [0, 0])
2106 |> line(endAbsolute = [1, 1])
2107 // comment here
2108 |> line(endAbsolute = [0, 1], tag = $myTag)
2109 |> line(endAbsolute = [1, 1])
2110 /* and
2111 here
2112 */
2113 // a comment between pipe expression statements
2114 |> rx(90)
2115 // and another with just white space between others below
2116 |> ry(45)
2117 |> rx(45)
2118// one more for good measure"#;
2119 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2120
2121 let recasted = program.recast(&Default::default(), 0);
2122 assert_eq!(
2123 recasted,
2124 r#"// comment at start
2125mySk1 = startSketchOn(XY)
2126 |> startProfile(at = [0, 0])
2127 |> line(endAbsolute = [1, 1])
2128 // comment here
2129 |> line(endAbsolute = [0, 1], tag = $myTag)
2130 |> line(endAbsolute = [1, 1])
2131 /* and
2132 here */
2133 // a comment between pipe expression statements
2134 |> rx(90)
2135 // and another with just white space between others below
2136 |> ry(45)
2137 |> rx(45)
2138// one more for good measure
2139"#
2140 );
2141 }
2142
2143 #[test]
2144 fn test_recast_multiline_object() {
2145 let some_program_string = r#"x = {
2146 a = 1000000000,
2147 b = 2000000000,
2148 c = 3000000000,
2149 d = 4000000000,
2150 e = 5000000000
2151}"#;
2152 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2153
2154 let recasted = program.recast(&Default::default(), 0);
2155 assert_eq!(recasted.trim(), some_program_string);
2156 }
2157
2158 #[test]
2159 fn test_recast_first_level_object() {
2160 let some_program_string = r#"three = 3
2161
2162yo = {
2163 aStr = 'str',
2164 anum = 2,
2165 identifier = three,
2166 binExp = 4 + 5
2167}
2168yo = [
2169 1,
2170 " 2,",
2171 "three",
2172 4 + 5,
2173 " hey oooooo really long long long"
2174]
2175"#;
2176 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2177
2178 let recasted = program.recast(&Default::default(), 0);
2179 assert_eq!(recasted, some_program_string);
2180 }
2181
2182 #[test]
2183 fn test_recast_new_line_before_comment() {
2184 let some_program_string = r#"
2185// this is a comment
2186yo = { a = { b = { c = '123' } } }
2187
2188key = 'c'
2189things = "things"
2190
2191// this is also a comment"#;
2192 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2193
2194 let recasted = program.recast(&Default::default(), 0);
2195 let expected = some_program_string.trim();
2196 let actual = recasted.trim();
2198 assert_eq!(actual, expected);
2199 }
2200
2201 #[test]
2202 fn test_recast_comment_tokens_inside_strings() {
2203 let some_program_string = r#"b = {
2204 end = 141,
2205 start = 125,
2206 type_ = "NonCodeNode",
2207 value = "
2208 // a comment
2209 "
2210}"#;
2211 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2212
2213 let recasted = program.recast(&Default::default(), 0);
2214 assert_eq!(recasted.trim(), some_program_string.trim());
2215 }
2216
2217 #[test]
2218 fn test_recast_array_new_line_in_pipe() {
2219 let some_program_string = r#"myVar = 3
2220myVar2 = 5
2221myVar3 = 6
2222myAng = 40
2223myAng2 = 134
2224part001 = startSketchOn(XY)
2225 |> startProfile(at = [0, 0])
2226 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2227 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2228 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2229 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2230
2231 let recasted = program.recast(&Default::default(), 0);
2232 assert_eq!(recasted.trim(), some_program_string);
2233 }
2234
2235 #[test]
2236 fn test_recast_array_new_line_in_pipe_custom() {
2237 let some_program_string = r#"myVar = 3
2238myVar2 = 5
2239myVar3 = 6
2240myAng = 40
2241myAng2 = 134
2242part001 = startSketchOn(XY)
2243 |> startProfile(at = [0, 0])
2244 |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2245 |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2246 |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2247"#;
2248 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2249
2250 let recasted = program.recast(
2251 &FormatOptions {
2252 tab_size: 3,
2253 use_tabs: false,
2254 insert_final_newline: true,
2255 },
2256 0,
2257 );
2258 assert_eq!(recasted, some_program_string);
2259 }
2260
2261 #[test]
2262 fn test_recast_after_rename_std() {
2263 let some_program_string = r#"part001 = startSketchOn(XY)
2264 |> startProfile(at = [0.0000000000, 5.0000000000])
2265 |> line(end = [0.4900857016, -0.0240763666])
2266
2267part002 = "part002"
2268things = [part001, 0.0]
2269blah = 1
2270foo = false
2271baz = {a: 1, part001: "thing"}
2272
2273fn ghi(part001) {
2274 return part001
2275}
2276"#;
2277 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2278 program.rename_symbol("mySuperCoolPart", 6);
2279
2280 let recasted = program.recast(&Default::default(), 0);
2281 assert_eq!(
2282 recasted,
2283 r#"mySuperCoolPart = startSketchOn(XY)
2284 |> startProfile(at = [0.0, 5.0])
2285 |> line(end = [0.4900857016, -0.0240763666])
2286
2287part002 = "part002"
2288things = [mySuperCoolPart, 0.0]
2289blah = 1
2290foo = false
2291baz = { a = 1, part001 = "thing" }
2292
2293fn ghi(part001) {
2294 return part001
2295}
2296"#
2297 );
2298 }
2299
2300 #[test]
2301 fn test_recast_after_rename_fn_args() {
2302 let some_program_string = r#"fn ghi(x, y, z) {
2303 return x
2304}"#;
2305 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2306 program.rename_symbol("newName", 7);
2307
2308 let recasted = program.recast(&Default::default(), 0);
2309 assert_eq!(
2310 recasted,
2311 r#"fn ghi(newName, y, z) {
2312 return newName
2313}
2314"#
2315 );
2316 }
2317
2318 #[test]
2319 fn test_recast_trailing_comma() {
2320 let some_program_string = r#"startSketchOn(XY)
2321 |> startProfile(at = [0, 0])
2322 |> arc({
2323 radius = 1,
2324 angle_start = 0,
2325 angle_end = 180,
2326 })"#;
2327 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2328
2329 let recasted = program.recast(&Default::default(), 0);
2330 assert_eq!(
2331 recasted,
2332 r#"startSketchOn(XY)
2333 |> startProfile(at = [0, 0])
2334 |> arc({
2335 radius = 1,
2336 angle_start = 0,
2337 angle_end = 180
2338 })
2339"#
2340 );
2341 }
2342
2343 #[test]
2344 fn test_recast_negative_var() {
2345 let some_program_string = r#"w = 20
2346l = 8
2347h = 10
2348
2349firstExtrude = startSketchOn(XY)
2350 |> startProfile(at = [0,0])
2351 |> line(end = [0, l])
2352 |> line(end = [w, 0])
2353 |> line(end = [0, -l])
2354 |> close()
2355 |> extrude(h)
2356"#;
2357 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2358
2359 let recasted = program.recast(&Default::default(), 0);
2360 assert_eq!(
2361 recasted,
2362 r#"w = 20
2363l = 8
2364h = 10
2365
2366firstExtrude = startSketchOn(XY)
2367 |> startProfile(at = [0, 0])
2368 |> line(end = [0, l])
2369 |> line(end = [w, 0])
2370 |> line(end = [0, -l])
2371 |> close()
2372 |> extrude(h)
2373"#
2374 );
2375 }
2376
2377 #[test]
2378 fn test_recast_multiline_comment() {
2379 let some_program_string = r#"w = 20
2380l = 8
2381h = 10
2382
2383// This is my comment
2384// It has multiple lines
2385// And it's really long
2386firstExtrude = startSketchOn(XY)
2387 |> startProfile(at = [0,0])
2388 |> line(end = [0, l])
2389 |> line(end = [w, 0])
2390 |> line(end = [0, -l])
2391 |> close()
2392 |> extrude(h)
2393"#;
2394 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2395
2396 let recasted = program.recast(&Default::default(), 0);
2397 assert_eq!(
2398 recasted,
2399 r#"w = 20
2400l = 8
2401h = 10
2402
2403// This is my comment
2404// It has multiple lines
2405// And it's really long
2406firstExtrude = startSketchOn(XY)
2407 |> startProfile(at = [0, 0])
2408 |> line(end = [0, l])
2409 |> line(end = [w, 0])
2410 |> line(end = [0, -l])
2411 |> close()
2412 |> extrude(h)
2413"#
2414 );
2415 }
2416
2417 #[test]
2418 fn test_recast_math_start_negative() {
2419 let some_program_string = r#"myVar = -5 + 6"#;
2420 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2421
2422 let recasted = program.recast(&Default::default(), 0);
2423 assert_eq!(recasted.trim(), some_program_string);
2424 }
2425
2426 #[test]
2427 fn test_recast_math_negate_parens() {
2428 let some_program_string = r#"wallMountL = 3.82
2429thickness = 0.5
2430
2431startSketchOn(XY)
2432 |> startProfile(at = [0, 0])
2433 |> line(end = [0, -(wallMountL - thickness)])
2434 |> line(end = [0, -(5 - thickness)])
2435 |> line(end = [0, -(5 - 1)])
2436 |> line(end = [0, -(-5 - 1)])"#;
2437 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2438
2439 let recasted = program.recast(&Default::default(), 0);
2440 assert_eq!(recasted.trim(), some_program_string);
2441 }
2442
2443 #[test]
2444 fn test_recast_math_nested_parens() {
2445 let some_program_string = r#"distance = 5
2446p = 3: Plane
2447FOS = { a = 3, b = 42 }: Sketch
2448sigmaAllow = 8: number(mm)
2449width = 20
2450thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2451 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2452
2453 let recasted = program.recast(&Default::default(), 0);
2454 assert_eq!(recasted.trim(), some_program_string);
2455 }
2456
2457 #[test]
2458 fn no_vardec_keyword() {
2459 let some_program_string = r#"distance = 5"#;
2460 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2461
2462 let recasted = program.recast(&Default::default(), 0);
2463 assert_eq!(recasted.trim(), some_program_string);
2464 }
2465
2466 #[test]
2467 fn recast_types() {
2468 let some_program_string = r#"type foo
2469
2470// A comment
2471@(impl = primitive)
2472export type bar(unit, baz)
2473type baz = Foo | Bar
2474type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
2475"#;
2476 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2477 let recasted = program.recast(&Default::default(), 0);
2478 assert_eq!(recasted, some_program_string);
2479 }
2480
2481 #[test]
2482 fn recast_nested_fn() {
2483 let some_program_string = r#"fn f() {
2484 return fn() {
2485 return 1
2486}
2487}"#;
2488 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2489 let recasted = program.recast(&Default::default(), 0);
2490 let expected = "\
2491fn f() {
2492 return fn() {
2493 return 1
2494 }
2495}";
2496 assert_eq!(recasted.trim(), expected);
2497 }
2498
2499 #[test]
2500 fn recast_literal() {
2501 use winnow::Parser;
2502 for (i, (raw, expected, reason)) in [
2503 (
2504 "5.0",
2505 "5.0",
2506 "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2507 ),
2508 (
2509 "5",
2510 "5",
2511 "integers should stay integral, i.e. don't reformat this to '5.0'",
2512 ),
2513 (
2514 "5.0000000",
2515 "5.0",
2516 "if the number is f64 but not fractional, use its canonical format",
2517 ),
2518 ("5.1", "5.1", "straightforward case works"),
2519 ]
2520 .into_iter()
2521 .enumerate()
2522 {
2523 let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2524 let literal = crate::parsing::parser::unsigned_number_literal
2525 .parse(tokens.as_slice())
2526 .unwrap();
2527 assert_eq!(
2528 literal.recast(),
2529 expected,
2530 "failed test {i}, which is testing that {reason}"
2531 );
2532 }
2533 }
2534
2535 #[test]
2536 fn recast_objects_no_comments() {
2537 let input = r#"
2538sketch002 = startSketchOn({
2539 plane: {
2540 origin: { x = 1, y = 2, z = 3 },
2541 x_axis = { x = 4, y = 5, z = 6 },
2542 y_axis = { x = 7, y = 8, z = 9 },
2543 z_axis = { x = 10, y = 11, z = 12 }
2544 }
2545 })
2546"#;
2547 let expected = r#"sketch002 = startSketchOn({
2548 plane = {
2549 origin = { x = 1, y = 2, z = 3 },
2550 x_axis = { x = 4, y = 5, z = 6 },
2551 y_axis = { x = 7, y = 8, z = 9 },
2552 z_axis = { x = 10, y = 11, z = 12 }
2553 }
2554})
2555"#;
2556 let ast = crate::parsing::top_level_parse(input).unwrap();
2557 let actual = ast.recast(&FormatOptions::new(), 0);
2558 assert_eq!(actual, expected);
2559 }
2560
2561 #[test]
2562 fn unparse_fn_unnamed() {
2563 let input = r#"squares_out = reduce(
2564 arr,
2565 n = 0: number,
2566 f = fn(@i, accum) {
2567 return 1
2568 },
2569)
2570"#;
2571 let ast = crate::parsing::top_level_parse(input).unwrap();
2572 let actual = ast.recast(&FormatOptions::new(), 0);
2573 assert_eq!(actual, input);
2574 }
2575
2576 #[test]
2577 fn unparse_fn_named() {
2578 let input = r#"fn f(x) {
2579 return 1
2580}
2581"#;
2582 let ast = crate::parsing::top_level_parse(input).unwrap();
2583 let actual = ast.recast(&FormatOptions::new(), 0);
2584 assert_eq!(actual, input);
2585 }
2586
2587 #[test]
2588 fn unparse_call_inside_function_single_line() {
2589 let input = r#"fn foo() {
2590 toDegrees(atan(0.5), foo = 1)
2591 return 0
2592}
2593"#;
2594 let ast = crate::parsing::top_level_parse(input).unwrap();
2595 let actual = ast.recast(&FormatOptions::new(), 0);
2596 assert_eq!(actual, input);
2597 }
2598
2599 #[test]
2600 fn recast_function_types() {
2601 let input = r#"foo = x: fn
2602foo = x: fn(number)
2603fn foo(x: fn(): number): fn {
2604 return 0
2605}
2606fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
2607 return 0
2608}
2609type fn
2610type foo = fn
2611type foo = fn(a: string, b: { f: fn(): any })
2612type foo = fn([fn])
2613type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
2614"#;
2615 let ast = crate::parsing::top_level_parse(input).unwrap();
2616 let actual = ast.recast(&FormatOptions::new(), 0);
2617 assert_eq!(actual, input);
2618 }
2619
2620 #[test]
2621 fn unparse_call_inside_function_args_multiple_lines() {
2622 let input = r#"fn foo() {
2623 toDegrees(
2624 atan(0.5),
2625 foo = 1,
2626 bar = 2,
2627 baz = 3,
2628 qux = 4,
2629 )
2630 return 0
2631}
2632"#;
2633 let ast = crate::parsing::top_level_parse(input).unwrap();
2634 let actual = ast.recast(&FormatOptions::new(), 0);
2635 assert_eq!(actual, input);
2636 }
2637
2638 #[test]
2639 fn unparse_call_inside_function_single_arg_multiple_lines() {
2640 let input = r#"fn foo() {
2641 toDegrees(
2642 [
2643 profile0,
2644 profile1,
2645 profile2,
2646 profile3,
2647 profile4,
2648 profile5
2649 ],
2650 key = 1,
2651 )
2652 return 0
2653}
2654"#;
2655 let ast = crate::parsing::top_level_parse(input).unwrap();
2656 let actual = ast.recast(&FormatOptions::new(), 0);
2657 assert_eq!(actual, input);
2658 }
2659
2660 #[test]
2661 fn recast_objects_with_comments() {
2662 use winnow::Parser;
2663 for (i, (input, expected, reason)) in [(
2664 "\
2665{
2666 a = 1,
2667 // b = 2,
2668 c = 3
2669}",
2670 "\
2671{
2672 a = 1,
2673 // b = 2,
2674 c = 3
2675}",
2676 "preserves comments",
2677 )]
2678 .into_iter()
2679 .enumerate()
2680 {
2681 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2682 crate::parsing::parser::print_tokens(tokens.as_slice());
2683 let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
2684 assert_eq!(
2685 expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2686 expected,
2687 "failed test {i}, which is testing that recasting {reason}"
2688 );
2689 }
2690 }
2691
2692 #[test]
2693 fn recast_array_with_comments() {
2694 use winnow::Parser;
2695 for (i, (input, expected, reason)) in [
2696 (
2697 "\
2698[
2699 1,
2700 2,
2701 3,
2702 4,
2703 5,
2704 6,
2705 7,
2706 8,
2707 9,
2708 10,
2709 11,
2710 12,
2711 13,
2712 14,
2713 15,
2714 16,
2715 17,
2716 18,
2717 19,
2718 20,
2719]",
2720 "\
2721[
2722 1,
2723 2,
2724 3,
2725 4,
2726 5,
2727 6,
2728 7,
2729 8,
2730 9,
2731 10,
2732 11,
2733 12,
2734 13,
2735 14,
2736 15,
2737 16,
2738 17,
2739 18,
2740 19,
2741 20
2742]",
2743 "preserves multi-line arrays",
2744 ),
2745 (
2746 "\
2747[
2748 1,
2749 // 2,
2750 3
2751]",
2752 "\
2753[
2754 1,
2755 // 2,
2756 3
2757]",
2758 "preserves comments",
2759 ),
2760 (
2761 "\
2762[
2763 1,
2764 2,
2765 // 3
2766]",
2767 "\
2768[
2769 1,
2770 2,
2771 // 3
2772]",
2773 "preserves comments at the end of the array",
2774 ),
2775 ]
2776 .into_iter()
2777 .enumerate()
2778 {
2779 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2780 let expr = crate::parsing::parser::array_elem_by_elem
2781 .parse(tokens.as_slice())
2782 .unwrap();
2783 assert_eq!(
2784 expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2785 expected,
2786 "failed test {i}, which is testing that recasting {reason}"
2787 );
2788 }
2789 }
2790
2791 #[test]
2792 fn code_with_comment_and_extra_lines() {
2793 let code = r#"yo = 'c'
2794
2795/* this is
2796a
2797comment */
2798yo = 'bing'
2799"#;
2800 let ast = crate::parsing::top_level_parse(code).unwrap();
2801 let recasted = ast.recast(&FormatOptions::new(), 0);
2802 assert_eq!(recasted, code);
2803 }
2804
2805 #[test]
2806 fn comments_in_a_fn_block() {
2807 let code = r#"fn myFn() {
2808 // this is a comment
2809 yo = { a = { b = { c = '123' } } }
2810
2811 /* block
2812 comment */
2813 key = 'c'
2814 // this is also a comment
2815}
2816"#;
2817 let ast = crate::parsing::top_level_parse(code).unwrap();
2818 let recasted = ast.recast(&FormatOptions::new(), 0);
2819 assert_eq!(recasted, code);
2820 }
2821
2822 #[test]
2823 fn array_range_end_exclusive() {
2824 let code = "myArray = [0..<4]\n";
2825 let ast = crate::parsing::top_level_parse(code).unwrap();
2826 let recasted = ast.recast(&FormatOptions::new(), 0);
2827 assert_eq!(recasted, code);
2828 }
2829
2830 #[test]
2831 fn paren_precedence() {
2832 let code = r#"x = 1 - 2 - 3
2833x = (1 - 2) - 3
2834x = 1 - (2 - 3)
2835x = 1 + 2 + 3
2836x = (1 + 2) + 3
2837x = 1 + (2 + 3)
2838x = 2 * (y % 2)
2839x = (2 * y) % 2
2840x = 2 % (y * 2)
2841x = (2 % y) * 2
2842x = 2 * y % 2
2843"#;
2844
2845 let expected = r#"x = 1 - 2 - 3
2846x = 1 - 2 - 3
2847x = 1 - (2 - 3)
2848x = 1 + 2 + 3
2849x = 1 + 2 + 3
2850x = 1 + 2 + 3
2851x = 2 * (y % 2)
2852x = 2 * y % 2
2853x = 2 % (y * 2)
2854x = 2 % y * 2
2855x = 2 * y % 2
2856"#;
2857 let ast = crate::parsing::top_level_parse(code).unwrap();
2858 let recasted = ast.recast(&FormatOptions::new(), 0);
2859 assert_eq!(recasted, expected);
2860 }
2861}