Files
modeling-app/rust/kcl-lib/src/unparser.rs
Jess Frazelle d33d399c31 Repetitive structs removed for import file extensions (#6211)
* get rid of repetitive structs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

get rid of more

Signed-off-by: Jess Frazelle <github@jessfraz.com>

add more

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

await the shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

put it at the root

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix;es

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* kcl-language-server flake

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-10 13:57:12 -07:00

2728 lines
82 KiB
Rust

use std::fmt::Write;
use crate::parsing::{
ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
CallExpression, CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FormatOptions, FunctionExpression,
IfExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter,
PipeExpression, Program, TagDeclarator, TypeDeclaration, UnaryExpression, VariableDeclaration, VariableKind,
},
deprecation,
token::NumericSuffix,
DeprecationKind, PIPE_OPERATOR,
};
impl Program {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
let mut result = self
.shebang
.as_ref()
.map(|sh| format!("{}\n\n", sh.inner.content))
.unwrap_or_default();
for start in &self.non_code_meta.start_nodes {
result.push_str(&start.recast(options, indentation_level));
}
for attr in &self.inner_attrs {
result.push_str(&attr.recast(options, indentation_level));
}
if !self.inner_attrs.is_empty() {
result.push('\n');
}
let result = result; // Remove mutation.
let result = self
.body
.iter()
.map(|body_item| {
let mut result = String::new();
for comment in body_item.get_comments() {
if !comment.is_empty() {
result.push_str(&indentation);
result.push_str(comment);
}
if !result.ends_with("\n\n") && result != "\n" {
result.push('\n');
}
}
for attr in body_item.get_attrs() {
result.push_str(&attr.recast(options, indentation_level));
}
result.push_str(&match body_item.clone() {
BodyItem::ImportStatement(stmt) => stmt.recast(options, indentation_level),
BodyItem::ExpressionStatement(expression_statement) => {
expression_statement
.expression
.recast(options, indentation_level, ExprContext::Other)
}
BodyItem::VariableDeclaration(variable_declaration) => {
variable_declaration.recast(options, indentation_level)
}
BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(),
BodyItem::ReturnStatement(return_statement) => {
format!(
"{}return {}",
indentation,
return_statement
.argument
.recast(options, indentation_level, ExprContext::Other)
.trim_start()
)
}
});
result
})
.enumerate()
.fold(result, |mut output, (index, recast_str)| {
let start_string =
if index == 0 && self.non_code_meta.start_nodes.is_empty() && self.inner_attrs.is_empty() {
// We need to indent.
indentation.to_string()
} else {
// Do nothing, we already applied the indentation elsewhere.
String::new()
};
// determine the value of the end string
// basically if we are inside a nested function we want to end with a new line
let maybe_line_break: String = if index == self.body.len() - 1 && indentation_level == 0 {
String::new()
} else {
"\n".to_string()
};
let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
Some(noncodes) => noncodes
.iter()
.enumerate()
.map(|(i, custom_white_space_or_comment)| {
let formatted = custom_white_space_or_comment.recast(options, indentation_level);
if i == 0 && !formatted.trim().is_empty() {
if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
format!("\n{}", formatted)
} else {
formatted
}
} else {
formatted
}
})
.collect::<String>(),
None => String::new(),
};
let end_string = if custom_white_space_or_comment.is_empty() {
maybe_line_break
} else {
custom_white_space_or_comment
};
let _ = write!(output, "{}{}{}", start_string, recast_str, end_string);
output
})
.trim()
.to_string();
// Insert a final new line if the user wants it.
if options.insert_final_newline && !result.is_empty() {
format!("{}\n", result)
} else {
result
}
}
}
impl NonCodeValue {
fn should_cause_array_newline(&self) -> bool {
match self {
Self::InlineComment { .. } => false,
Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
}
}
}
impl Node<NonCodeNode> {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
match &self.value {
NonCodeValue::InlineComment {
value,
style: CommentStyle::Line,
} => format!(" // {}\n", value),
NonCodeValue::InlineComment {
value,
style: CommentStyle::Block,
} => format!(" /* {} */", value),
NonCodeValue::BlockComment { value, style } => match style {
CommentStyle::Block => format!("{}/* {} */", indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}//\n", indentation)
} else {
format!("{}// {}\n", indentation, value.trim())
}
}
},
NonCodeValue::NewLineBlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
match style {
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}{}//\n", add_start_new_line, indentation)
} else {
format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
}
}
}
}
NonCodeValue::NewLine => "\n\n".to_string(),
}
}
}
impl Node<Annotation> {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
let mut result = String::new();
for comment in &self.pre_comments {
if !comment.is_empty() {
result.push_str(&indentation);
result.push_str(comment);
}
if !comment.ends_with("*/") && !result.ends_with("\n\n") && result != "\n" {
result.push('\n');
}
}
result.push('@');
if let Some(name) = &self.name {
result.push_str(&name.name);
}
if let Some(properties) = &self.properties {
result.push('(');
result.push_str(
&properties
.iter()
.map(|prop| {
format!(
"{} = {}",
prop.key.name,
prop.value
.recast(options, indentation_level + 1, ExprContext::Other)
.trim()
)
})
.collect::<Vec<String>>()
.join(", "),
);
result.push(')');
result.push('\n');
}
result
}
}
impl ImportStatement {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
let vis = if self.visibility == ItemVisibility::Export {
"export "
} else {
""
};
let mut string = format!("{}{}import ", vis, indentation);
match &self.selector {
ImportSelector::List { items } => {
for (i, item) in items.iter().enumerate() {
if i > 0 {
string.push_str(", ");
}
string.push_str(&item.name.name);
if let Some(alias) = &item.alias {
// If the alias is the same, don't output it.
if item.name.name != alias.name {
string.push_str(&format!(" as {}", alias.name));
}
}
}
string.push_str(" from ");
}
ImportSelector::Glob(_) => string.push_str("* from "),
ImportSelector::None { .. } => {}
}
string.push_str(&format!("\"{}\"", self.path));
if let ImportSelector::None { alias: Some(alias) } = &self.selector {
string.push_str(" as ");
string.push_str(&alias.name);
}
string
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum ExprContext {
Pipe,
Decl,
Other,
}
impl Expr {
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String {
let is_decl = matches!(ctxt, ExprContext::Decl);
if is_decl {
// Just because this expression is being bound to a variable, doesn't mean that every child
// expression is being bound. So, reset the expression context if necessary.
// This will still preserve the "::Pipe" context though.
ctxt = ExprContext::Other;
}
match &self {
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
Expr::ArrayRangeExpression(range_exp) => range_exp.recast(options, indentation_level, ctxt),
Expr::ObjectExpression(ref obj_exp) => obj_exp.recast(options, indentation_level, ctxt),
Expr::MemberExpression(mem_exp) => mem_exp.recast(),
Expr::Literal(literal) => literal.recast(),
Expr::FunctionExpression(func_exp) => {
let mut result = if is_decl { String::new() } else { "fn".to_owned() };
result += &func_exp.recast(options, indentation_level);
result
}
Expr::CallExpression(call_exp) => call_exp.recast(options, indentation_level, ctxt),
Expr::CallExpressionKw(call_exp) => call_exp.recast(options, indentation_level, ctxt),
Expr::Name(name) => {
let result = name.to_string();
match deprecation(&result, DeprecationKind::Const) {
Some(suggestion) => suggestion.to_owned(),
None => result,
}
}
Expr::TagDeclarator(tag) => tag.recast(),
Expr::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
Expr::LabelledExpression(e) => {
let mut result = e.expr.recast(options, indentation_level, ctxt);
result += " as ";
result += &e.label.name;
result
}
Expr::AscribedExpression(e) => {
let mut result = e.expr.recast(options, indentation_level, ctxt);
result += ": ";
result += &e.ty.to_string();
result
}
Expr::None(_) => {
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
}
}
}
}
impl BinaryPart {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
match &self {
BinaryPart::Literal(literal) => literal.recast(),
BinaryPart::Name(name) => {
let result = name.to_string();
match deprecation(&result, DeprecationKind::Const) {
Some(suggestion) => suggestion.to_owned(),
None => result,
}
}
BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
BinaryPart::CallExpression(call_expression) => {
call_expression.recast(options, indentation_level, ExprContext::Other)
}
BinaryPart::CallExpressionKw(call_expression) => {
call_expression.recast(options, indentation_level, ExprContext::Other)
}
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
BinaryPart::IfExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
}
}
}
impl CallExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
format!(
"{}{}({})",
if ctxt == ExprContext::Pipe {
"".to_string()
} else {
options.get_indentation(indentation_level)
},
self.callee,
self.arguments
.iter()
.map(|arg| arg.recast(options, indentation_level, ctxt))
.collect::<Vec<String>>()
.join(", ")
)
}
}
impl CallExpressionKw {
fn recast_args(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> Vec<String> {
let mut arg_list = if let Some(first_arg) = &self.unlabeled {
vec![first_arg.recast(options, indentation_level, ctxt)]
} else {
Vec::with_capacity(self.arguments.len())
};
arg_list.extend(
self.arguments
.iter()
.map(|arg| arg.recast(options, indentation_level, ctxt)),
);
arg_list
}
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
let indent = if ctxt == ExprContext::Pipe {
"".to_string()
} else {
options.get_indentation(indentation_level)
};
let name = self.callee.to_string();
if let Some(suggestion) = deprecation(&name, DeprecationKind::Function) {
return format!("{indent}{suggestion}");
}
let arg_list = self.recast_args(options, indentation_level, ctxt);
let args = arg_list.clone().join(", ");
let has_lots_of_args = arg_list.len() >= 4;
let some_arg_is_already_multiline = arg_list.len() > 1 && arg_list.iter().any(|arg| arg.contains('\n'));
let multiline = has_lots_of_args || some_arg_is_already_multiline;
if multiline {
let next_indent = indentation_level + 1;
let inner_indentation = if ctxt == ExprContext::Pipe {
options.get_indentation_offset_pipe(next_indent)
} else {
options.get_indentation(next_indent)
};
let arg_list = self.recast_args(options, next_indent, ctxt);
let mut args = arg_list.join(&format!(",\n{inner_indentation}"));
args.push(',');
let args = args;
let end_indent = if ctxt == ExprContext::Pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {
options.get_indentation(indentation_level)
};
format!("{indent}{name}(\n{inner_indentation}{args}\n{end_indent})")
} else {
format!("{indent}{name}({args})")
}
}
}
impl LabeledArg {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
let label = &self.label.name;
let arg = self.arg.recast(options, indentation_level, ctxt);
format!("{label} = {arg}")
}
}
impl VariableDeclaration {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
let mut output = match self.visibility {
ItemVisibility::Default => String::new(),
ItemVisibility::Export => "export ".to_owned(),
};
let (keyword, eq) = match self.kind {
VariableKind::Fn => ("fn ", ""),
VariableKind::Const => ("", " = "),
};
let _ = write!(
output,
"{}{keyword}{}{eq}{}",
indentation,
self.declaration.id.name,
self.declaration
.init
.recast(options, indentation_level, ExprContext::Decl)
.trim()
);
output
}
}
impl TypeDeclaration {
pub fn recast(&self) -> String {
let vis = match self.visibility {
ItemVisibility::Default => String::new(),
ItemVisibility::Export => "export ".to_owned(),
};
let mut arg_str = String::new();
if let Some(args) = &self.args {
arg_str.push('(');
for a in args {
if arg_str.len() > 1 {
arg_str.push_str(", ");
}
arg_str.push_str(&a.name);
}
arg_str.push(')');
}
if let Some(alias) = &self.alias {
arg_str.push_str(" = ");
arg_str.push_str(&alias.to_string());
}
format!("{}type {}{}", vis, self.name.name, arg_str)
}
}
// Used by TS.
pub fn format_number(value: f64, suffix: NumericSuffix) -> String {
format!("{value}{suffix}")
}
impl Literal {
fn recast(&self) -> String {
match self.value {
LiteralValue::Number { value, suffix } => {
if self.raw.contains('.') && value.fract() == 0.0 {
format!("{value:?}{suffix}")
} else {
self.raw.clone()
}
}
LiteralValue::String(ref s) => {
if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
return suggestion.to_owned();
}
let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
format!("{quote}{s}{quote}")
}
LiteralValue::Bool(_) => self.raw.clone(),
}
}
}
impl TagDeclarator {
pub fn recast(&self) -> String {
// TagDeclarators are always prefixed with a dollar sign.
format!("${}", self.name)
}
}
impl ArrayExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
// Reconstruct the order of items in the array.
// An item can be an element (i.e. an expression for a KCL value),
// or a non-code item (e.g. a comment)
let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
let mut elems = self.elements.iter();
let mut found_line_comment = false;
let mut format_items: Vec<_> = (0..num_items)
.flat_map(|i| {
if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
noncode
.iter()
.map(|nc| {
found_line_comment |= nc.value.should_cause_array_newline();
nc.recast(options, 0)
})
.collect::<Vec<_>>()
} else {
let el = elems.next().unwrap();
let s = format!("{}, ", el.recast(options, 0, ExprContext::Other));
vec![s]
}
})
.collect();
// Format these items into a one-line array.
if let Some(item) = format_items.last_mut() {
if let Some(norm) = item.strip_suffix(", ") {
*item = norm.to_owned();
}
}
let format_items = format_items; // Remove mutability
let flat_recast = format!("[{}]", format_items.join(""));
// We might keep the one-line representation, if it's short enough.
let max_array_length = 40;
let multi_line = flat_recast.len() > max_array_length || found_line_comment;
if !multi_line {
return flat_recast;
}
// Otherwise, we format a multi-line representation.
let inner_indentation = if ctxt == ExprContext::Pipe {
options.get_indentation_offset_pipe(indentation_level + 1)
} else {
options.get_indentation(indentation_level + 1)
};
let formatted_array_lines = format_items
.iter()
.map(|s| {
format!(
"{inner_indentation}{}{}",
if let Some(x) = s.strip_suffix(" ") { x } else { s },
if s.ends_with('\n') { "" } else { "\n" }
)
})
.collect::<Vec<String>>()
.join("")
.to_owned();
let end_indent = if ctxt == ExprContext::Pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {
options.get_indentation(indentation_level)
};
format!("[\n{formatted_array_lines}{end_indent}]")
}
}
/// An expression is syntactically trivial: i.e., a literal, identifier, or similar.
fn expr_is_trivial(expr: &Expr) -> bool {
matches!(
expr,
Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
)
}
impl ArrayRangeExpression {
fn recast(&self, options: &FormatOptions, _: usize, _: ExprContext) -> String {
let s1 = self.start_element.recast(options, 0, ExprContext::Other);
let s2 = self.end_element.recast(options, 0, ExprContext::Other);
// Format these items into a one-line array. Put spaces around the `..` if either expression
// is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
// tightly, but this is a misleading visual representation of the precedence if the range
// components are compound expressions.
if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
format!("[{s1}..{s2}]")
} else {
format!("[{s1} .. {s2}]")
}
// Assume a range expression fits on one line.
}
}
impl ObjectExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
if self
.non_code_meta
.non_code_nodes
.values()
.any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
{
return self.recast_multi_line(options, indentation_level, ctxt);
}
let flat_recast = format!(
"{{ {} }}",
self.properties
.iter()
.map(|prop| {
format!(
"{} = {}",
prop.key.name,
prop.value.recast(options, indentation_level + 1, ctxt).trim()
)
})
.collect::<Vec<String>>()
.join(", ")
);
let max_array_length = 40;
let needs_multiple_lines = flat_recast.len() > max_array_length;
if !needs_multiple_lines {
return flat_recast;
}
self.recast_multi_line(options, indentation_level, ctxt)
}
/// Recast, but always outputs the object with newlines between each property.
fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
let inner_indentation = if ctxt == ExprContext::Pipe {
options.get_indentation_offset_pipe(indentation_level + 1)
} else {
options.get_indentation(indentation_level + 1)
};
let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
let mut props = self.properties.iter();
let format_items: Vec<_> = (0..num_items)
.flat_map(|i| {
if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
} else {
let prop = props.next().unwrap();
// Use a comma unless it's the last item
let comma = if i == num_items - 1 { "" } else { ",\n" };
let s = format!(
"{} = {}{comma}",
prop.key.name,
prop.value.recast(options, indentation_level + 1, ctxt).trim()
);
vec![s]
}
})
.collect();
let end_indent = if ctxt == ExprContext::Pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {
options.get_indentation(indentation_level)
};
format!(
"{{\n{inner_indentation}{}\n{end_indent}}}",
format_items.join(&inner_indentation),
)
}
}
impl MemberExpression {
fn recast(&self) -> String {
let key_str = match &self.property {
LiteralIdentifier::Identifier(identifier) => {
if self.computed {
format!("[{}]", &(*identifier.name))
} else {
format!(".{}", &(*identifier.name))
}
}
LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
};
match &self.object {
MemberObject::MemberExpression(member_exp) => member_exp.recast() + key_str.as_str(),
MemberObject::Identifier(identifier) => identifier.name.to_string() + key_str.as_str(),
}
}
}
impl BinaryExpression {
fn recast(&self, options: &FormatOptions) -> String {
let maybe_wrap_it = |a: String, doit: bool| -> String {
if doit {
format!("({})", a)
} else {
a
}
};
let should_wrap_right = match &self.right {
BinaryPart::BinaryExpression(bin_exp) => {
self.precedence() > bin_exp.precedence()
|| self.operator == BinaryOperator::Sub
|| self.operator == BinaryOperator::Div
}
_ => false,
};
let should_wrap_left = match &self.left {
BinaryPart::BinaryExpression(bin_exp) => self.precedence() > bin_exp.precedence(),
_ => false,
};
format!(
"{} {} {}",
maybe_wrap_it(self.left.recast(options, 0), should_wrap_left),
self.operator,
maybe_wrap_it(self.right.recast(options, 0), should_wrap_right)
)
}
}
impl UnaryExpression {
fn recast(&self, options: &FormatOptions) -> String {
match self.argument {
BinaryPart::Literal(_)
| BinaryPart::Name(_)
| BinaryPart::MemberExpression(_)
| BinaryPart::IfExpression(_)
| BinaryPart::CallExpressionKw(_)
| BinaryPart::CallExpression(_) => {
format!("{}{}", &self.operator, self.argument.recast(options, 0))
}
BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
format!("{}({})", &self.operator, self.argument.recast(options, 0))
}
}
}
}
impl IfExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
// We can calculate how many lines this will take, so let's do it and avoid growing the vec.
// Total lines = starting lines, else-if lines, ending lines.
let n = 2 + (self.else_ifs.len() * 2) + 3;
let mut lines = Vec::with_capacity(n);
let cond = self.cond.recast(options, indentation_level, ctxt);
lines.push((0, format!("if {cond} {{")));
lines.push((1, self.then_val.recast(options, indentation_level + 1)));
for else_if in &self.else_ifs {
let cond = else_if.cond.recast(options, indentation_level, ctxt);
lines.push((0, format!("}} else if {cond} {{")));
lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
}
lines.push((0, "} else {".to_owned()));
lines.push((1, self.final_else.recast(options, indentation_level + 1)));
lines.push((0, "}".to_owned()));
lines
.into_iter()
.map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
.collect::<Vec<_>>()
.join("\n")
}
}
impl Node<PipeExpression> {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let pipe = self
.body
.iter()
.enumerate()
.map(|(index, statement)| {
let indentation = options.get_indentation(indentation_level + 1);
let mut s = statement.recast(options, indentation_level + 1, ExprContext::Pipe);
let non_code_meta = self.non_code_meta.clone();
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
for val in non_code_meta_value {
let formatted = if val.end == self.end {
val.recast(options, indentation_level)
.trim_end_matches('\n')
.to_string()
} else {
val.recast(options, indentation_level + 1)
.trim_end_matches('\n')
.to_string()
};
if let NonCodeValue::BlockComment { .. } = val.value {
s += "\n";
s += &formatted;
} else {
s += &formatted;
}
}
}
if index != self.body.len() - 1 {
s += "\n";
s += &indentation;
s += PIPE_OPERATOR;
s += " ";
}
s
})
.collect::<String>();
format!("{}{}", options.get_indentation(indentation_level), pipe)
}
}
impl FunctionExpression {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
// We don't want to end with a new line inside nested functions.
let mut new_options = options.clone();
new_options.insert_final_newline = false;
let param_list = self
.params
.iter()
.map(|param| param.recast(options, indentation_level))
.collect::<Vec<String>>()
.join(", ");
let tab0 = options.get_indentation(indentation_level);
let tab1 = options.get_indentation(indentation_level + 1);
let return_type = match &self.return_type {
Some(rt) => format!(": {rt}"),
None => String::new(),
};
let body = self.body.recast(&new_options, indentation_level + 1);
format!("({param_list}){return_type} {{\n{tab1}{body}\n{tab0}}}")
}
}
impl Parameter {
pub fn recast(&self, _options: &FormatOptions, _indentation_level: usize) -> String {
let at_sign = if self.labeled { "" } else { "@" };
let identifier = &self.identifier.name;
let question_mark = if self.default_value.is_some() { "?" } else { "" };
let mut result = format!("{at_sign}{identifier}{question_mark}");
if let Some(ty) = &self.type_ {
result += ": ";
result += &ty.to_string();
}
if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
let lit = literal.recast();
result.push_str(&format!(" = {lit}"));
};
result
}
}
/// Collect all the kcl (and other relevant) files in a directory, recursively.
#[cfg(not(target_arch = "wasm32"))]
#[async_recursion::async_recursion]
pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
// Make sure we actually have a directory.
if !dir.is_dir() {
anyhow::bail!("`{}` is not a directory", dir.display());
}
let mut entries = tokio::fs::read_dir(dir).await?;
let mut files = Vec::new();
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_dir() {
files.extend(walk_dir(&path).await?);
} else if path
.extension()
.is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
{
files.push(path);
}
}
Ok(files)
}
/// Recast all the kcl files in a directory, recursively.
#[cfg(not(target_arch = "wasm32"))]
pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
let futures = files
.into_iter()
.filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) // We only care about kcl
// files here.
.map(|file| {
let options = options.clone();
tokio::spawn(async move {
let contents = tokio::fs::read_to_string(&file)
.await
.map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
let report = crate::Report {
kcl_source: contents.to_string(),
error: err.clone(),
filename: file.to_string_lossy().to_string(),
};
let report = miette::Report::new(report);
anyhow::anyhow!("{:?}", report)
})?;
for ce in &ces {
if ce.severity != crate::errors::Severity::Warning {
let report = crate::Report {
kcl_source: contents.to_string(),
error: crate::KclError::Semantic(ce.clone().into()),
filename: file.to_string_lossy().to_string(),
};
let report = miette::Report::new(report);
anyhow::bail!("{:?}", report);
}
}
let Some(program) = program else {
anyhow::bail!("Failed to parse file `{}`", file.display());
};
let recast = program.recast_with_options(&options);
tokio::fs::write(&file, recast)
.await
.map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
Ok::<(), anyhow::Error>(())
})
})
.collect::<Vec<_>>();
// Join all futures and await their completion
let results = futures::future::join_all(futures).await;
// Check if any of the futures failed.
let mut errors = Vec::new();
for result in results {
if let Err(err) = result? {
errors.push(err);
}
}
if !errors.is_empty() {
anyhow::bail!("Failed to recast some files: {:?}", errors);
}
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::{parsing::ast::types::FormatOptions, ModuleId};
#[test]
fn test_recast_annotations_without_body_items() {
let input = r#"@settings(defaultLengthUnit = in)
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_annotations_in_function_body() {
let input = r#"fn myFunc() {
@meta(yes = true)
x = 2
}
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_annotations_in_function_body_without_items() {
let input = r#"fn myFunc() {
@meta(yes = true)
}
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn recast_annotations_with_comments() {
let input = r#"// Start comment
// Comment on attr
@settings(defaultLengthUnit = in)
// Comment on item
foo = 42
// Comment on another item
@(impl = kcl)
bar = 0
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_if_else_if_same() {
let input = r#"b = if false {
3
} else if true {
4
} else {
5
}
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_if_same() {
let input = r#"b = if false {
3
} else {
5
}
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_import() {
let input = r#"import a from "a.kcl"
import a as aaa from "a.kcl"
import a, b from "a.kcl"
import a as aaa, b from "a.kcl"
import a, b as bbb from "a.kcl"
import a as aaa, b as bbb from "a.kcl"
import "a_b.kcl"
import "a-b.kcl" as b
import * from "a.kcl"
export import a as aaa from "a.kcl"
export import a, b from "a.kcl"
export import a as aaa, b from "a.kcl"
export import a, b as bbb from "a.kcl"
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_import_as_same_name() {
let input = r#"import a as a from "a.kcl"
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
let expected = r#"import a from "a.kcl"
"#;
assert_eq!(output, expected);
}
#[test]
fn test_recast_export_fn() {
let input = r#"export fn a() {
return 0
}
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_bug_fn_in_fn() {
let some_program_string = r#"// Start point (top left)
zoo_x = -20
zoo_y = 7
// Scale
s = 1 // s = 1 -> height of Z is 13.4mm
// Depth
d = 1
fn rect(x, y, w, h) {
startSketchOn(XY)
|> startProfileAt([x, y], %)
|> xLine(length = w)
|> yLine(length = h)
|> xLine(length = -w)
|> close()
|> extrude(d, %)
}
fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
startSketchOn(XY)
|> startProfileAt([x1, y1], %)
|> line(endAbsolute = [x2, y2])
|> line(endAbsolute = [x3, y3])
|> line(endAbsolute = [x4, y4])
|> close()
|> extrude(d, %)
}
fn crosshair(x, y) {
startSketchOn(XY)
|> startProfileAt([x, y], %)
|> yLine(length = 1)
|> yLine(length = -2)
|> yLine(length = 1)
|> xLine(length = 1)
|> xLine(length = -2)
}
fn z(z_x, z_y) {
z_end_w = s * 8.4
z_end_h = s * 3
z_corner = s * 2
z_w = z_end_w + 2 * z_corner
z_h = z_w * 1.08130081300813
rect(z_x, z_y, z_end_w, -z_end_h)
rect(z_x + z_w, z_y, -z_corner, -z_corner)
rect(z_x + z_w, z_y - z_h, -z_end_w, z_end_h)
rect(z_x, z_y - z_h, z_corner, z_corner)
quad(z_x, z_y - z_h + z_corner, z_x + z_w - z_corner, z_y, z_x + z_w, z_y - z_corner, z_x + z_corner, z_y - z_h)
}
fn o(c_x, c_y) {
// Outer and inner radii
o_r = s * 6.95
i_r = 0.5652173913043478 * o_r
// Angle offset for diagonal break
a = 7
// Start point for the top sketch
o_x1 = c_x + o_r * cos((45 + a) / 360 * tau())
o_y1 = c_y + o_r * sin((45 + a) / 360 * tau())
// Start point for the bottom sketch
o_x2 = c_x + o_r * cos((225 + a) / 360 * tau())
o_y2 = c_y + o_r * sin((225 + a) / 360 * tau())
// End point for the bottom startSketch
o_x3 = c_x + o_r * cos((45 - a) / 360 * tau())
o_y3 = c_y + o_r * sin((45 - a) / 360 * tau())
// Where is the center?
// crosshair(c_x, c_y)
startSketchOn(XY)
|> startProfileAt([o_x1, o_y1], %)
|> arc({
radius = o_r,
angle_start = 45 + a,
angle_end = 225 - a
}, %)
|> angledLine(angle = 45, length = o_r - i_r)
|> arc({
radius = i_r,
angle_start = 225 - a,
angle_end = 45 + a
}, %)
|> close()
|> extrude(d, %)
startSketchOn(XY)
|> startProfileAt([o_x2, o_y2], %)
|> arc({
radius = o_r,
angle_start = 225 + a,
angle_end = 360 + 45 - a
}, %)
|> angledLine(angle = 225, length = o_r - i_r)
|> arc({
radius = i_r,
angle_start = 45 - a,
angle_end = 225 + a - 360
}, %)
|> close()
|> extrude(d, %)
}
fn zoo(x0, y0) {
z(x0, y0)
o(x0 + s * 20, y0 - (s * 6.7))
o(x0 + s * 35, y0 - (s * 6.7))
}
zoo(zoo_x, zoo_y)
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_bug_extra_parens() {
let some_program_string = r#"// Ball Bearing
// 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.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
sphereDia = 0.5
insideDia = 1
thickness = 0.25
overHangLength = .4
// Sketch and revolve the inside bearing piece
insideRevolve = startSketchOn(XZ)
|> startProfileAt([insideDia / 2, 0], %)
|> line([0, thickness + sphereDia / 2], %)
|> line([overHangLength, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, -sphereDia], %)
|> line([overHangLength - thickness, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength, 0], %)
|> close()
|> revolve({ axis = Y }, %)
// 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)
sphere = startSketchOn(XZ)
|> startProfileAt([
0.05 + insideDia / 2 + thickness,
0 - 0.05
], %)
|> line([sphereDia - 0.1, 0], %)
|> arc({
angle_start = 0,
angle_end = -180,
radius = sphereDia / 2 - 0.05
}, %)
|> close()
|> revolve({ axis = X }, %)
|> patternCircular3d(
axis = [0, 0, 1],
center = [0, 0, 0],
repetitions = 10,
arcDegrees = 360,
rotateDuplicates = true
)
// Sketch and revolve the outside bearing
outsideRevolve = startSketchOn(XZ)
|> startProfileAt([
insideDia / 2 + thickness + sphereDia,
0
], %)
|> line([0, sphereDia / 2], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, thickness], %)
|> line([overHangLength, 0], %)
|> line([0, -2 * thickness - sphereDia], %)
|> line([-overHangLength, 0], %)
|> line([0, thickness], %)
|> line([overHangLength - thickness, 0], %)
|> close()
|> revolve({ axis = Y }, %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// Ball Bearing
// 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.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
sphereDia = 0.5
insideDia = 1
thickness = 0.25
overHangLength = .4
// Sketch and revolve the inside bearing piece
insideRevolve = startSketchOn(XZ)
|> startProfileAt([insideDia / 2, 0], %)
|> line([0, thickness + sphereDia / 2], %)
|> line([overHangLength, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, -sphereDia], %)
|> line([overHangLength - thickness, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength, 0], %)
|> close()
|> revolve({ axis = Y }, %)
// 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)
sphere = startSketchOn(XZ)
|> startProfileAt([
0.05 + insideDia / 2 + thickness,
0 - 0.05
], %)
|> line([sphereDia - 0.1, 0], %)
|> arc({
angle_start = 0,
angle_end = -180,
radius = sphereDia / 2 - 0.05
}, %)
|> close()
|> revolve({ axis = X }, %)
|> patternCircular3d(
axis = [0, 0, 1],
center = [0, 0, 0],
repetitions = 10,
arcDegrees = 360,
rotateDuplicates = true,
)
// Sketch and revolve the outside bearing
outsideRevolve = startSketchOn(XZ)
|> startProfileAt([
insideDia / 2 + thickness + sphereDia,
0
], %)
|> line([0, sphereDia / 2], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, thickness], %)
|> line([overHangLength, 0], %)
|> line([0, -2 * thickness - sphereDia], %)
|> line([-overHangLength, 0], %)
|> line([0, thickness], %)
|> line([overHangLength - thickness, 0], %)
|> close()
|> revolve({ axis = Y }, %)
"#
);
}
#[test]
fn test_recast_fn_in_object() {
let some_program_string = r#"bing = { yo = 55 }
myNestedVar = [{ prop = callExp(bing.yo) }]
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_fn_in_array() {
let some_program_string = r#"bing = { yo = 55 }
myNestedVar = [callExp(bing.yo)]
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_ranges() {
let some_program_string = r#"foo = [0..10]
ten = 10
bar = [0 + 1 .. ten]
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_space_in_fn_call() {
let some_program_string = r#"fn thing = (x) => {
return x + 1
}
thing ( 1 )
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"fn thing(x) {
return x + 1
}
thing(1)
"#
);
}
#[test]
fn test_recast_typed_fn() {
let some_program_string = r#"fn thing(x: string, y: [bool]): number {
return x + 1
}
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_typed_consts() {
let some_program_string = r#"a = 42: number
export b = 3.2: number(ft)
c = "dsfds": A | B | C
d = [1]: [number]
e = foo: [number; 3]
f = [1, 2, 3]: [number; 1+]
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_object_fn_in_array_weird_bracket() {
let some_program_string = r#"bing = { yo = 55 }
myNestedVar = [
{
prop: line([bing.yo, 21], sketch001)
}
]
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"bing = { yo = 55 }
myNestedVar = [
{
prop = line([bing.yo, 21], sketch001)
}
]
"#
);
}
#[test]
fn test_recast_empty_file() {
let some_program_string = r#""#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
// Its VERY important this comes back with zero new lines.
assert_eq!(recasted, r#""#);
}
#[test]
fn test_recast_empty_file_new_line() {
let some_program_string = r#"
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
// Its VERY important this comes back with zero new lines.
assert_eq!(recasted, r#""#);
}
#[test]
fn test_recast_shebang() {
let some_program_string = r#"#!/usr/local/env zoo kcl
part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close()
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"#!/usr/local/env zoo kcl
part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close()
"#
);
}
#[test]
fn test_recast_shebang_new_lines() {
let some_program_string = r#"#!/usr/local/env zoo kcl
part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close()
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"#!/usr/local/env zoo kcl
part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close()
"#
);
}
#[test]
fn test_recast_shebang_with_comments() {
let some_program_string = r#"#!/usr/local/env zoo kcl
// Yo yo my comments.
part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close()
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"#!/usr/local/env zoo kcl
// Yo yo my comments.
part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close()
"#
);
}
#[test]
fn test_recast_empty_function_body_with_comments() {
let input = r#"fn myFunc() {
// Yo yo my comments.
}
"#;
let program = crate::parsing::top_level_parse(input).unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_large_file() {
let some_program_string = r#"@settings(units=mm)
// define nts
radius = 6.0
width = 144.0
length = 83.0
depth = 45.0
thk = 5
hole_diam = 5
// define a rectangular shape func
fn rectShape = (pos, w, l) => {
rr = startSketchOn('xy')
|> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
|> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
|> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
|> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
|> close($edge4)
return rr
}
// build the body of the focusrite scarlett solo gen 4
// only used for visualization
scarlett_body = rectShape([0, 0], width, length)
|> extrude(depth, %)
|> fillet(
radius = radius,
tags = [
edge2,
edge4,
getOppositeEdge(edge2),
getOppositeEdge(edge4)
]
)
// build the bracket sketch around the body
fn bracketSketch = (w, d, t) => {
s = startSketchOn({
plane: {
origin: { x = 0, y = length / 2 + thk, z = 0 },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 0, z = 1 },
z_axis = { x = 0, y = 1, z = 0 }
}
})
|> startProfileAt([-w / 2 - t, d + t], %)
|> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
|> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
|> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
|> line(endAbsolute = [w / 2, d + t], tag = $edge4)
|> line(endAbsolute = [w / 2, 0], tag = $edge5)
|> line(endAbsolute = [-w / 2, 0], tag = $edge6)
|> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
|> close($edge8)
return s
}
// build the body of the bracket
bracket_body = bracketSketch(width, depth, thk)
|> extrude(length + 10, %)
|> fillet(
radius = radius,
tags = [
getNextAdjacentEdge(edge7),
getNextAdjacentEdge(edge2),
getNextAdjacentEdge(edge3),
getNextAdjacentEdge(edge6)
]
)
// build the tabs of the mounting bracket (right side)
tabs_r = startSketchOn({
plane: {
origin: { x = 0, y = 0, z = depth + thk },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 1, z = 0 },
z_axis = { x = 0, y = 0, z = 1 }
}
})
|> startProfileAt([width / 2 + thk, length / 2 + thk], %)
|> line([10, -5], %)
|> line([0, -10], %)
|> line([-10, -5], %)
|> close()
|> hole(circle(
center = [
width / 2 + thk + hole_diam,
length / 2 - hole_diam
],
radius = hole_diam / 2
), %)
|> extrude(-thk, %)
|> patternLinear3d(
axis = [0, -1, 0],
repetitions = 1,
distance = length - 10
)
// build the tabs of the mounting bracket (left side)
tabs_l = startSketchOn({
plane: {
origin = { x = 0, y = 0, z = depth + thk },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 1, z = 0 },
z_axis = { x = 0, y = 0, z = 1 }
}
})
|> startProfileAt([-width / 2 - thk, length / 2 + thk], %)
|> line([-10, -5], %)
|> line([0, -10], %)
|> line([10, -5], %)
|> close()
|> hole(circle(
center = [
-width / 2 - thk - hole_diam,
length / 2 - hole_diam
],
radius = hole_diam / 2
), %)
|> extrude(-thk, %)
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
// Its VERY important this comes back with zero new lines.
assert_eq!(
recasted,
r#"@settings(units = mm)
// define nts
radius = 6.0
width = 144.0
length = 83.0
depth = 45.0
thk = 5
hole_diam = 5
// define a rectangular shape func
fn rectShape(pos, w, l) {
rr = startSketchOn('xy')
|> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
|> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
|> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
|> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
|> close($edge4)
return rr
}
// build the body of the focusrite scarlett solo gen 4
// only used for visualization
scarlett_body = rectShape([0, 0], width, length)
|> extrude(depth, %)
|> fillet(
radius = radius,
tags = [
edge2,
edge4,
getOppositeEdge(edge2),
getOppositeEdge(edge4)
],
)
// build the bracket sketch around the body
fn bracketSketch(w, d, t) {
s = startSketchOn({
plane = {
origin = { x = 0, y = length / 2 + thk, z = 0 },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 0, z = 1 },
z_axis = { x = 0, y = 1, z = 0 }
}
})
|> startProfileAt([-w / 2 - t, d + t], %)
|> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
|> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
|> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
|> line(endAbsolute = [w / 2, d + t], tag = $edge4)
|> line(endAbsolute = [w / 2, 0], tag = $edge5)
|> line(endAbsolute = [-w / 2, 0], tag = $edge6)
|> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
|> close($edge8)
return s
}
// build the body of the bracket
bracket_body = bracketSketch(width, depth, thk)
|> extrude(length + 10, %)
|> fillet(
radius = radius,
tags = [
getNextAdjacentEdge(edge7),
getNextAdjacentEdge(edge2),
getNextAdjacentEdge(edge3),
getNextAdjacentEdge(edge6)
],
)
// build the tabs of the mounting bracket (right side)
tabs_r = startSketchOn({
plane = {
origin = { x = 0, y = 0, z = depth + thk },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 1, z = 0 },
z_axis = { x = 0, y = 0, z = 1 }
}
})
|> startProfileAt([width / 2 + thk, length / 2 + thk], %)
|> line([10, -5], %)
|> line([0, -10], %)
|> line([-10, -5], %)
|> close()
|> hole(circle(
center = [
width / 2 + thk + hole_diam,
length / 2 - hole_diam
],
radius = hole_diam / 2,
), %)
|> extrude(-thk, %)
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
// build the tabs of the mounting bracket (left side)
tabs_l = startSketchOn({
plane = {
origin = { x = 0, y = 0, z = depth + thk },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 1, z = 0 },
z_axis = { x = 0, y = 0, z = 1 }
}
})
|> startProfileAt([-width / 2 - thk, length / 2 + thk], %)
|> line([-10, -5], %)
|> line([0, -10], %)
|> line([10, -5], %)
|> close()
|> hole(circle(
center = [
-width / 2 - thk - hole_diam,
length / 2 - hole_diam
],
radius = hole_diam / 2,
), %)
|> extrude(-thk, %)
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
"#
);
}
#[test]
fn test_recast_nested_var_declaration_in_fn_body() {
let some_program_string = r#"fn cube = (pos, scale) => {
sg = startSketchOn(XY)
|> startProfileAt(pos, %)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
|> close()
|> extrude(scale, %)
}"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"fn cube(pos, scale) {
sg = startSketchOn(XY)
|> startProfileAt(pos, %)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
|> close()
|> extrude(scale, %)
}
"#
);
}
#[test]
fn test_as() {
let some_program_string = r#"fn cube(pos, scale) {
x = dfsfs + dfsfsd as y
sg = startSketchOn(XY)
|> startProfileAt(pos, %) as foo
|> line([0, scale], %)
|> line([scale, 0], %) as bar
|> line([0 as baz, -scale] as qux, %)
|> close()
|> extrude(scale, %)
}
cube(0, 0) as cub
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string,);
}
#[test]
fn test_recast_with_bad_indentation() {
let some_program_string = r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %)
|> line([0.6804562304, 0.9087880491], %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %)
|> line([0.6804562304, 0.9087880491], %)
"#
);
}
#[test]
fn test_recast_with_bad_indentation_and_inline_comment() {
let some_program_string = r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %) // hello world
|> line([0.6804562304, 0.9087880491], %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %) // hello world
|> line([0.6804562304, 0.9087880491], %)
"#
);
}
#[test]
fn test_recast_with_bad_indentation_and_line_comment() {
let some_program_string = r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %)
// hello world
|> line([0.6804562304, 0.9087880491], %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %)
// hello world
|> line([0.6804562304, 0.9087880491], %)
"#
);
}
#[test]
fn test_recast_comment_in_a_fn_block() {
let some_program_string = r#"fn myFn = () => {
// this is a comment
yo = { a = { b = { c = '123' } } } /* block
comment */
key = 'c'
// this is also a comment
return things
}"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"fn myFn() {
// this is a comment
yo = { a = { b = { c = '123' } } } /* block
comment */
key = 'c'
// this is also a comment
return things
}
"#
);
}
#[test]
fn test_recast_comment_under_variable() {
let some_program_string = r#"key = 'c'
// this is also a comment
thing = 'foo'
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"key = 'c'
// this is also a comment
thing = 'foo'
"#
);
}
#[test]
fn test_recast_multiline_comment_start_file() {
let some_program_string = r#"// hello world
// I am a comment
key = 'c'
// this is also a comment
// hello
thing = 'foo'
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// hello world
// I am a comment
key = 'c'
// this is also a comment
// hello
thing = 'foo'
"#
);
}
#[test]
fn test_recast_empty_comment() {
let some_program_string = r#"// hello world
//
// I am a comment
key = 'c'
//
// I am a comment
thing = 'c'
foo = 'bar' //
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// hello world
//
// I am a comment
key = 'c'
//
// I am a comment
thing = 'c'
foo = 'bar' //
"#
);
}
#[test]
fn test_recast_multiline_comment_under_variable() {
let some_program_string = r#"key = 'c'
// this is also a comment
// hello
thing = 'foo'
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"key = 'c'
// this is also a comment
// hello
thing = 'foo'
"#
);
}
#[test]
fn test_recast_only_line_comments() {
let code = r#"// comment at start
"#;
let program = crate::parsing::top_level_parse(code).unwrap();
assert_eq!(program.recast(&Default::default(), 0), code);
}
#[test]
fn test_recast_comment_at_start() {
let test_program = r#"
/* comment at start */
mySk1 = startSketchOn(XY)
|> startProfileAt([0, 0], %)"#;
let program = crate::parsing::top_level_parse(test_program).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"/* comment at start */
mySk1 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
"#
);
}
#[test]
fn test_recast_lots_of_comments() {
let some_program_string = r#"// comment at start
mySk1 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line(endAbsolute = [1, 1])
// comment here
|> line(endAbsolute = [0, 1], tag = $myTag)
|> line(endAbsolute = [1, 1])
/* and
here
*/
// a comment between pipe expression statements
|> rx(90, %)
// and another with just white space between others below
|> ry(45, %)
|> rx(45, %)
// one more for good measure"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// comment at start
mySk1 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line(endAbsolute = [1, 1])
// comment here
|> line(endAbsolute = [0, 1], tag = $myTag)
|> line(endAbsolute = [1, 1])
/* and
here */
// a comment between pipe expression statements
|> rx(90, %)
// and another with just white space between others below
|> ry(45, %)
|> rx(45, %)
// one more for good measure
"#
);
}
#[test]
fn test_recast_multiline_object() {
let some_program_string = r#"part001 = startSketchOn(XY)
|> startProfileAt([-0.01, -0.08], %)
|> line([0.62, 4.15], %, $seg01)
|> line([2.77, -1.24], %)
|> angledLineThatIntersects({
angle = 201,
offset = -1.35,
intersectTag = seg01
}, %)
|> line([-0.42, -1.72], %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
#[test]
fn test_recast_first_level_object() {
let some_program_string = r#"three = 3
yo = {
aStr = 'str',
anum = 2,
identifier = three,
binExp = 4 + 5
}
yo = [
1,
" 2,",
"three",
4 + 5,
" hey oooooo really long long long"
]
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_new_line_before_comment() {
let some_program_string = r#"
// this is a comment
yo = { a = { b = { c = '123' } } }
key = 'c'
things = "things"
// this is also a comment"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
let expected = some_program_string.trim();
// Currently new parser removes an empty line
let actual = recasted.trim();
assert_eq!(actual, expected);
}
#[test]
fn test_recast_comment_tokens_inside_strings() {
let some_program_string = r#"b = {
end = 141,
start = 125,
type_ = "NonCodeNode",
value = "
// a comment
"
}"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string.trim());
}
#[test]
fn test_recast_array_new_line_in_pipe() {
let some_program_string = r#"myVar = 3
myVar2 = 5
myVar3 = 6
myAng = 40
myAng2 = 134
part001 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([1, 3.82], %, $seg01) // ln-should-get-tag
|> angledLine(angle = -angleToMatchLengthX(seg01, myVar, %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLine(angle = -angleToMatchLengthY(seg01, myVar, %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
#[test]
fn test_recast_array_new_line_in_pipe_custom() {
let some_program_string = r#"myVar = 3
myVar2 = 5
myVar3 = 6
myAng = 40
myAng2 = 134
part001 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([1, 3.82], %, $seg01) // ln-should-get-tag
|> angledLine(angle = -angleToMatchLengthX(seg01, myVar, %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLine(angle = -angleToMatchLengthY(seg01, myVar, %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(
&FormatOptions {
tab_size: 3,
use_tabs: false,
insert_final_newline: true,
},
0,
);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_after_rename_std() {
let some_program_string = r#"part001 = startSketchOn(XY)
|> startProfileAt([0.0000000000, 5.0000000000], %)
|> line([0.4900857016, -0.0240763666], %)
part002 = "part002"
things = [part001, 0.0]
blah = 1
foo = false
baz = {a: 1, part001: "thing"}
fn ghi = (part001) => {
return part001
}
"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
program.rename_symbol("mySuperCoolPart", 6);
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"mySuperCoolPart = startSketchOn(XY)
|> startProfileAt([0.0, 5.0], %)
|> line([0.4900857016, -0.0240763666], %)
part002 = "part002"
things = [mySuperCoolPart, 0.0]
blah = 1
foo = false
baz = { a = 1, part001 = "thing" }
fn ghi(part001) {
return part001
}
"#
);
}
#[test]
fn test_recast_after_rename_fn_args() {
let some_program_string = r#"fn ghi = (x, y, z) => {
return x
}"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
program.rename_symbol("newName", 10);
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"fn ghi(newName, y, z) {
return newName
}
"#
);
}
#[test]
fn test_recast_trailing_comma() {
let some_program_string = r#"startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> arc({
radius = 1,
angle_start = 0,
angle_end = 180,
}, %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> arc({
radius = 1,
angle_start = 0,
angle_end = 180
}, %)
"#
);
}
#[test]
fn test_recast_negative_var() {
let some_program_string = r#"w = 20
l = 8
h = 10
firstExtrude = startSketchOn(XY)
|> startProfileAt([0,0], %)
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close()
|> extrude(h, %)
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"w = 20
l = 8
h = 10
firstExtrude = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close()
|> extrude(h, %)
"#
);
}
#[test]
fn test_recast_multiline_comment() {
let some_program_string = r#"w = 20
l = 8
h = 10
// This is my comment
// It has multiple lines
// And it's really long
firstExtrude = startSketchOn(XY)
|> startProfileAt([0,0], %)
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close()
|> extrude(h, %)
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"w = 20
l = 8
h = 10
// This is my comment
// It has multiple lines
// And it's really long
firstExtrude = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close()
|> extrude(h, %)
"#
);
}
#[test]
fn test_recast_math_start_negative() {
let some_program_string = r#"myVar = -5 + 6"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
#[test]
fn test_recast_math_negate_parens() {
let some_program_string = r#"wallMountL = 3.82
thickness = 0.5
startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([0, -(wallMountL - thickness)], %)
|> line([0, -(5 - thickness)], %)
|> line([0, -(5 - 1)], %)
|> line([0, -(-5 - 1)], %)"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
#[test]
fn test_recast_math_nested_parens() {
let some_program_string = r#"distance = 5
p = 3: Plane
FOS = { a = 3, b = 42 }: Sketch
sigmaAllow = 8: number(mm)
width = 20
thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
#[test]
fn no_vardec_keyword() {
let some_program_string = r#"distance = 5"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted.trim(), some_program_string);
}
#[test]
fn recast_types() {
let some_program_string = r#"type foo
// A comment
@(impl = primitive)
export type bar(unit, baz)
type baz = Foo | Bar
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn recast_nested_fn() {
let some_program_string = r#"fn f = () => {
return fn() => {
return 1
}
}"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
let expected = "\
fn f() {
return fn() {
return 1
}
}";
assert_eq!(recasted.trim(), expected);
}
#[test]
fn recast_literal() {
use winnow::Parser;
for (i, (raw, expected, reason)) in [
(
"5.0",
"5.0",
"fractional numbers should stay fractional, i.e. don't reformat this to '5'",
),
(
"5",
"5",
"integers should stay integral, i.e. don't reformat this to '5.0'",
),
(
"5.0000000",
"5.0",
"if the number is f64 but not fractional, use its canonical format",
),
("5.1", "5.1", "straightforward case works"),
]
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
let literal = crate::parsing::parser::unsigned_number_literal
.parse(tokens.as_slice())
.unwrap();
assert_eq!(
literal.recast(),
expected,
"failed test {i}, which is testing that {reason}"
);
}
}
#[test]
fn recast_objects_no_comments() {
let input = r#"
sketch002 = startSketchOn({
plane: {
origin: { x = 1, y = 2, z = 3 },
x_axis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 }
}
})
"#;
let expected = r#"sketch002 = startSketchOn({
plane = {
origin = { x = 1, y = 2, z = 3 },
x_axis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 }
}
})
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, expected);
}
#[test]
fn unparse_fn_unnamed() {
let input = r#"squares_out = reduce(arr, 0: number, fn(i, squares) {
return 1
})
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test]
fn unparse_fn_named() {
let input = r#"fn f(x) {
return 1
}
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test]
fn recast_objects_with_comments() {
use winnow::Parser;
for (i, (input, expected, reason)) in [(
"\
{
a = 1,
// b = 2,
c = 3
}",
"\
{
a = 1,
// b = 2,
c = 3
}",
"preserves comments",
)]
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
crate::parsing::parser::print_tokens(tokens.as_slice());
let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected,
"failed test {i}, which is testing that recasting {reason}"
);
}
}
#[test]
fn recast_array_with_comments() {
use winnow::Parser;
for (i, (input, expected, reason)) in [
(
"\
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
]",
"\
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20
]",
"preserves multi-line arrays",
),
(
"\
[
1,
// 2,
3
]",
"\
[
1,
// 2,
3
]",
"preserves comments",
),
(
"\
[
1,
2,
// 3
]",
"\
[
1,
2,
// 3
]",
"preserves comments at the end of the array",
),
]
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
let expr = crate::parsing::parser::array_elem_by_elem
.parse(tokens.as_slice())
.unwrap();
assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected,
"failed test {i}, which is testing that recasting {reason}"
);
}
}
#[test]
fn code_with_comment_and_extra_lines() {
let code = r#"yo = 'c'
/* this is
a
comment */
yo = 'bing'
"#;
let ast = crate::parsing::top_level_parse(code).unwrap();
let recasted = ast.recast(&FormatOptions::new(), 0);
assert_eq!(recasted, code);
}
#[test]
fn comments_in_a_fn_block() {
let code = r#"fn myFn() {
// this is a comment
yo = { a = { b = { c = '123' } } }
/* block
comment */
key = 'c'
// this is also a comment
}
"#;
let ast = crate::parsing::top_level_parse(code).unwrap();
let recasted = ast.recast(&FormatOptions::new(), 0);
assert_eq!(recasted, code);
}
}