Skip to content

Commit

Permalink
feat(prettier): print CallExpression arguments correctly (#1631)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing authored Dec 10, 2023
1 parent e2d5763 commit 0c19991
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 30 deletions.
9 changes: 9 additions & 0 deletions crates/oxc_ast/src/ast/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ impl<'a> Expression<'a> {
matches!(self, Expression::FunctionExpression(_) | Expression::ArrowExpression(_))
}

pub fn is_call_expression(&self) -> bool {
matches!(self, Expression::CallExpression(_))
}

pub fn is_call_like_expression(&self) -> bool {
self.is_call_expression()
&& matches!(self, Expression::NewExpression(_) | Expression::ImportExpression(_))
}

pub fn is_binaryish(&self) -> bool {
matches!(self, Expression::BinaryExpression(_) | Expression::LogicalExpression(_))
}
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_prettier/src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ pub trait DocBuilder<'a> {
fn vec<T>(&self) -> Vec<'a, T> {
Vec::new_in(self.allocator())
}
fn vec_single<T>(&self, value: T) -> Vec<'a, T> {
let mut vec = Vec::with_capacity_in(1, self.allocator());
vec.push(value);
vec
}

#[inline]
fn str(&self, s: &str) -> Doc<'a> {
Expand Down
335 changes: 324 additions & 11 deletions crates/oxc_prettier/src/format/call_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ use super::misc;
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_span::{GetSpan, Span};
use oxc_syntax::operator::UnaryOperator;

use crate::{
array, conditional_group,
doc::{Doc, DocBuilder, Group},
if_break, line, softline, ss, Format, Prettier,
group_break, hardline, if_break, indent, line, softline, ss,
utils::will_break,
Format, Prettier,
};

pub(super) enum CallExpressionLike<'a, 'b> {
Expand Down Expand Up @@ -88,31 +92,140 @@ fn print_call_expression_arguments<'a>(
return Doc::Array(parts);
}

let mut parts_inner = p.vec();
#[allow(clippy::cast_sign_loss)]
let get_printed_arguments = |p: &mut Prettier<'a>, skip_index: isize| {
let mut printed_arguments = p.vec();
let mut len = arguments.len();
let arguments: Box<dyn Iterator<Item = (usize, &Argument)>> = match skip_index {
_ if skip_index > 0 => {
len -= skip_index as usize;
Box::new(arguments.iter().skip(skip_index as usize).enumerate())
}
_ if skip_index < 0 => {
len -= (-skip_index) as usize;
Box::new(
arguments.iter().take(arguments.len() - (-skip_index) as usize).enumerate(),
)
}
_ => Box::new(arguments.iter().enumerate()),
};

for (i, element) in arguments.iter().enumerate() {
let doc = element.format(p);
parts_inner.push(doc);
for (i, element) in arguments {
let doc = element.format(p);
let mut arg = p.vec();
arg.push(doc);

if i < arguments.len() - 1 {
parts_inner.push(ss!(","));
parts_inner.push(line!());
if i < len - 1 {
arg.push(ss!(","));
if p.is_next_line_empty(element.span()) {
arg.extend(hardline!());
arg.extend(hardline!());
} else {
arg.push(line!());
}
}
printed_arguments.push(Doc::Array(arg));
}
printed_arguments
};

let all_args_broken_out = |p: &mut Prettier<'a>| {
let mut parts = p.vec();
parts.push(ss!("("));
parts.push(indent!(
p,
line!(),
Doc::Array(get_printed_arguments(p, 0)),
if p.should_print_all_comma() { ss!(",") } else { ss!("") }
));
parts.push(line!());
parts.push(ss!(")"));
Doc::Group(Group::new(parts, true))
};

if should_expand_first_arg(arguments) {
p.args.expand_first_arg = true;
let mut first_doc = arguments[0].format(p);
p.args.expand_first_arg = false;

if will_break(&mut first_doc) {
let last_doc = get_printed_arguments(p, 1).pop().unwrap();
return array![
p,
Doc::BreakParent,
conditional_group!(
p,
array!(p, ss!("("), group_break!(p, first_doc), ss!(", "), last_doc, ss!(")")),
all_args_broken_out(p)
)
];
}
}

if should_expand_last_arg(arguments) {
let mut printed_arguments = get_printed_arguments(p, -1);
if printed_arguments.iter_mut().any(will_break) {
return all_args_broken_out(p);
}

let get_last_doc = |p: &mut Prettier<'a>| {
p.args.expand_last_arg = true;
let last_doc = arguments.last().unwrap().format(p);
p.args.expand_last_arg = false;
last_doc
};

let mut last_doc = get_last_doc(p);

if will_break(&mut last_doc) {
return array![
p,
Doc::BreakParent,
conditional_group!(
p,
array!(
p,
ss!("("),
Doc::Array(printed_arguments),
group_break!(p, last_doc),
ss!(")")
),
all_args_broken_out(p)
),
];
}

return conditional_group!(
p,
array!(p, ss!("("), Doc::Array(printed_arguments), last_doc, ss!(")")),
array!(
p,
ss!("("),
Doc::Array(get_printed_arguments(p, -1)),
group_break!(p, get_last_doc(p)),
ss!(")")
),
all_args_broken_out(p)
);
}

let mut printed_arguments = get_printed_arguments(p, 0);

if should_break {
parts_inner.insert(0, softline!());
parts.push(Doc::Indent(parts_inner));
printed_arguments.insert(0, softline!());
parts.push(Doc::Indent(printed_arguments));
parts.push(if_break!(p, ",", "", None));
parts.push(softline!());
} else {
parts.extend(parts_inner);
parts.extend(printed_arguments);
}
parts.push(ss!(")"));

let should_break = should_break
&& arguments.iter().any(|arg| {
misc::has_new_line_in_range(p.source_text, arg.span().start, arg.span().end)
});

Doc::Group(Group::new(parts, should_break))
}

Expand Down Expand Up @@ -145,3 +258,203 @@ fn is_commons_js_or_amd_call<'a>(
}
false
}

/// * Reference https://github.com/prettier/prettier/blob/main/src/language-js/print/call-arguments.js#L247-L272
fn should_expand_first_arg<'a>(arguments: &Vec<'a, Argument<'a>>) -> bool {
if arguments.len() != 2 {
return false;
}

let Argument::Expression(first_arg) = &arguments[0] else { return false };
let Argument::Expression(second_arg) = &arguments[1] else { return false };

let first_check = match first_arg {
Expression::FunctionExpression(_) => true,
Expression::ArrowExpression(arrow) => !arrow.expression,
_ => false,
};

first_check
&& !matches!(
second_arg,
Expression::FunctionExpression(_)
| Expression::ArrowExpression(_)
| Expression::ConditionalExpression(_)
)
&& is_hopefully_short_call_argument(second_arg)
&& !could_expand_arg(second_arg, false)
}

fn should_expand_last_arg(args: &Vec<'_, Argument<'_>>) -> bool {
let Some(Argument::Expression(last_arg)) = args.last() else { return false };

let penultimate_arg = if args.len() >= 2 { Some(&args[args.len() - 2]) } else { None };

could_expand_arg(last_arg, false)
&& (penultimate_arg.is_none() || matches!(last_arg, arg))
&& (args.len() != 2
|| !matches!(
penultimate_arg,
Some(Argument::Expression(Expression::ArrowExpression(_)))
)
|| !matches!(last_arg, Expression::ArrayExpression(_)))
}

fn is_hopefully_short_call_argument(node: &Expression) -> bool {
if let Expression::ParenthesizedExpression(expr) = node {
return is_hopefully_short_call_argument(&expr.expression);
}

if node.is_call_like_expression() {
return !match node {
Expression::CallExpression(call) => call.arguments.len() > 1,
Expression::NewExpression(call) => call.arguments.len() > 1,
Expression::ImportExpression(call) => call.arguments.len() > 0,
_ => false,
};
}

if let Expression::BinaryExpression(expr) = node {
return is_simple_call_argument(&expr.left, 1) && is_simple_call_argument(&expr.right, 1);
}

matches!(node, Expression::RegExpLiteral(_)) || is_simple_call_argument(node, 2)
}

fn is_simple_call_argument(node: &Expression, depth: usize) -> bool {
if let Expression::RegExpLiteral(literal) = node {
return literal.regex.pattern.len() <= 5;
}

if node.is_literal() || is_string_word_type(node) {
return true;
}

let is_child_simple = |node: &Expression| {
if depth <= 1 {
return false;
}
is_simple_call_argument(node, depth - 1)
};

if let Expression::TemplateLiteral(literal) = node {
return literal.quasis.iter().all(|element| !element.value.raw.contains('\n'))
&& literal.expressions.iter().all(|expr| is_child_simple(expr));
}

if let Expression::ObjectExpression(expr) = node {
return expr.properties.iter().all(|p| {
if let ObjectPropertyKind::ObjectProperty(property) = p {
!property.computed && (property.shorthand || is_child_simple(&property.value))
} else {
false
}
});
}

if let Expression::ArrayExpression(expr) = node {
return expr.elements.iter().all(
|x| matches!(x, ArrayExpressionElement::Expression(expr) if is_child_simple(expr)),
);
}

if node.is_call_expression() {
if let Expression::ImportExpression(expr) = node {
return expr.arguments.len() <= depth && expr.arguments.iter().all(is_child_simple);
} else if let Expression::CallExpression(expr) = node {
if is_simple_call_argument(&expr.callee, depth) {
return expr.arguments.len() <= depth
&& expr.arguments.iter().all(|arg| {
if let Argument::Expression(expr) = arg {
is_child_simple(expr)
} else {
false
}
});
}
} else if let Expression::NewExpression(expr) = node {
if is_simple_call_argument(&expr.callee, depth) {
return expr.arguments.len() <= depth
&& expr.arguments.iter().all(|arg| {
if let Argument::Expression(expr) = arg {
is_child_simple(expr)
} else {
false
}
});
}
}
return false;
}

let check_member_expression = |expr: &MemberExpression<'_>| {
if let MemberExpression::StaticMemberExpression(expr) = expr {
return is_simple_call_argument(&expr.object, depth);
}
if let MemberExpression::ComputedMemberExpression(expr) = expr {
return is_simple_call_argument(&expr.object, depth)
&& is_simple_call_argument(&expr.expression, depth);
}
if let MemberExpression::PrivateFieldExpression(expr) = expr {
return is_simple_call_argument(&expr.object, depth);
}
false
};

if let Expression::MemberExpression(expr) = node {
return check_member_expression(expr);
}

if let Expression::UnaryExpression(expr) = node {
return matches!(
expr.operator,
UnaryOperator::LogicalNot
| UnaryOperator::UnaryNegation
| UnaryOperator::UnaryPlus
| UnaryOperator::BitwiseNot
) && is_simple_call_argument(&expr.argument, depth);
}

if let Expression::UpdateExpression(expr) = node {
return match &expr.argument {
SimpleAssignmentTarget::AssignmentTargetIdentifier(target) => true,
SimpleAssignmentTarget::MemberAssignmentTarget(target) => {
check_member_expression(target)
}
_ => return false,
};
}

false
}

fn could_expand_arg(arg: &Expression, arrow_chain_recursion: bool) -> bool {
match arg {
Expression::ObjectExpression(expr) => expr.properties.len() > 0,
Expression::ArrayExpression(expr) => expr.elements.len() > 0,
Expression::BinaryExpression(expr) => could_expand_arg(&expr.left, arrow_chain_recursion),
Expression::FunctionExpression(_) => true,
Expression::ArrowExpression(expr) => {
if !expr.expression {
return true;
}
let Statement::ExpressionStatement(statement) = &expr.body.statements[0] else {
return false;
};

match &statement.expression {
Expression::ArrayExpression(expr) => could_expand_arg(&statement.expression, true),
Expression::ObjectExpression(_) => true,
Expression::CallExpression(_) | Expression::ConditionalExpression(_) => {
!arrow_chain_recursion
}
_ => false,
}
}
_ => false,
}
}

fn is_string_word_type(node: &Expression) -> bool {
matches!(node, Expression::Identifier(_) | Expression::ThisExpression(_) | Expression::Super(_))
}
Loading

0 comments on commit 0c19991

Please sign in to comment.