Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feat(rome_js_formatter): Format Member Chain (#3273)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
  • Loading branch information
MichaReiser and ematipico committed Sep 28, 2022
1 parent 187e9a4 commit f604c39
Show file tree
Hide file tree
Showing 58 changed files with 1,334 additions and 4,293 deletions.
Expand Up @@ -544,7 +544,7 @@ fn template_literal_contains_new_line(template: &JsTemplate) -> bool {
/// ```
///
/// Returns `false` because the template isn't on the same line as the '+' token.
fn is_multiline_template_starting_on_same_line(template: &JsTemplate) -> bool {
pub(crate) fn is_multiline_template_starting_on_same_line(template: &JsTemplate) -> bool {
let contains_new_line = template_literal_contains_new_line(template);

let starts_on_same_line = template.syntax().first_token().map_or(false, |token| {
Expand Down
193 changes: 141 additions & 52 deletions crates/rome_js_formatter/src/js/expressions/call_arguments.rs
@@ -1,10 +1,12 @@
use crate::js::expressions::arrow_function_expression::is_multiline_template_starting_on_same_line;
use crate::prelude::*;
use crate::utils::{is_call_like_expression, write_arguments_multi_line};
use rome_formatter::{format_args, write, CstFormatContext};
use rome_js_syntax::{
JsAnyCallArgument, JsAnyExpression, JsAnyFunctionBody, JsAnyLiteralExpression, JsAnyName,
JsAnyStatement, JsArrayExpression, JsArrowFunctionExpression, JsCallArgumentList,
JsCallArguments, JsCallArgumentsFields, JsCallExpression, TsReferenceType,
JsCallArguments, JsCallArgumentsFields, JsCallExpression, JsExpressionStatement,
TsReferenceType,
};
use rome_rowan::{AstSeparatedList, SyntaxResult, SyntaxTokenText};

Expand Down Expand Up @@ -33,6 +35,28 @@ impl FormatNodeRule<JsCallArguments> for FormatJsCallArguments {
);
}

let call_expression = node.parent::<JsCallExpression>();

if is_commonjs_or_amd_call(node, call_expression.as_ref())?
|| is_multiline_template_only_args(node)
{
return write!(
f,
[
l_paren_token.format(),
format_with(|f| {
f.join_with(space())
.entries(
args.format_separated(",")
.with_trailing_separator(TrailingSeparator::Omit),
)
.finish()
}),
r_paren_token.format()
]
);
}

let mut iter = args.iter();
let first_argument = iter.next();
let second_argument = iter.next();
Expand Down Expand Up @@ -97,19 +121,29 @@ impl FormatNodeRule<JsCallArguments> for FormatJsCallArguments {
.map(|e| e.memoized())
.collect();

let an_argument_breaks =
separated
.iter_mut()
.enumerate()
.any(|(index, element)| match element.inspect(f) {
Ok(element) => {
let in_relevant_range = should_group_first_argument && index > 0
|| (should_group_last_argument && index < args.len() - 1);
let mut any_argument_breaks = false;
let mut first_last_breaks = false;

in_relevant_range && element.will_break()
}
Err(_) => false,
});
for (index, argument) in separated.iter_mut().enumerate() {
let breaks = argument.inspect(f)?.will_break();

any_argument_breaks = any_argument_breaks || breaks;

if (should_group_first_argument && index > 0)
|| (should_group_last_argument && index < args.len() - 1)
{
first_last_breaks = first_last_breaks || breaks;
if breaks {
break;
}
}
}

let format_flat_arguments = format_with(|f| {
f.join_with(soft_line_break_or_space())
.entries(separated.iter())
.finish()
});

// We now cache them the delimiters tokens. This is needed because `[rome_formatter::best_fitting]` will try to
// print each version first
Expand All @@ -125,27 +159,18 @@ impl FormatNodeRule<JsCallArguments> for FormatJsCallArguments {
// function, but here we use a different way to print the trailing separator
write!(
f,
[
&l_paren,
&group(&format_with(|f| {
write!(
f,
[
&soft_block_indent(&format_args![
format_with(|f| {
write_arguments_multi_line(separated.iter(), f)
}),
soft_line_break()
]),
&r_paren
]
)
[group(&format_args![
l_paren,
soft_block_indent(&format_with(|f| {
write_arguments_multi_line(separated.iter(), f)
})),
]
r_paren
])
.should_expand(true)]
)
});

if an_argument_breaks {
if first_last_breaks {
return write!(f, [all_arguments_expanded]);
}

Expand All @@ -160,39 +185,26 @@ impl FormatNodeRule<JsCallArguments> for FormatJsCallArguments {
let mut iter = separated.iter();
// SAFETY: check on the existence of at least one argument are done before
let first = iter.next().unwrap();
f.join_with(&space())
.entry(&format_with(|f| {
write!(f, [&format_args![first, expand_parent()]])
}))
.entries(iter)
.finish()?;
f.join_with(&space()).entry(&first).entries(iter).finish()?;
} else {
// special formatting of the last element
let mut iter = separated.iter();
// SAFETY: check on the existence of at least one argument are done before
let last = iter.next_back().unwrap();

f.join_with(&space())
.entries(iter)
.entry(&format_with(|f| {
write!(f, [&format_args![last, expand_parent()]])
}))
.finish()?;
f.join_with(&space()).entries(iter).entry(&last).finish()?;
}
write!(f, [r_paren])
});

if any_argument_breaks {
write!(f, [expand_parent()])?;
}

write!(
f,
[best_fitting![
format_args![
l_paren,
group(&format_with(|f| {
write_arguments_multi_line(separated.iter(), f)
})),
r_paren,
],
edge_arguments_do_not_break,
format_args![l_paren, format_flat_arguments, r_paren],
group(&edge_arguments_do_not_break).should_expand(true),
all_arguments_expanded
]]
)
Expand All @@ -209,7 +221,7 @@ impl FormatNodeRule<JsCallArguments> for FormatJsCallArguments {
write_arguments_multi_line(separated, f)
})),
r_paren,
]),]
])]
)
}
}
Expand Down Expand Up @@ -412,6 +424,83 @@ fn could_group_expression_argument(
Ok(result)
}

/// Tests if this is a call to commonjs [`require`](https://nodejs.org/api/modules.html#requireid)
/// or amd's [`define`](https://github.com/amdjs/amdjs-api/wiki/AMD#define-function-) function.
fn is_commonjs_or_amd_call(
arguments: &JsCallArguments,
call: Option<&JsCallExpression>,
) -> SyntaxResult<bool> {
let call = match call {
Some(call) => call,
None => return Ok(false),
};

let callee = call.callee()?;

Ok(match callee {
JsAnyExpression::JsIdentifierExpression(identifier) => {
let reference = identifier.name()?;

if reference.has_name("require") {
true
} else if reference.has_name("define") {
let in_statement = call.parent::<JsExpressionStatement>().is_some();

if in_statement {
let args = arguments.args();
match args.len() {
1 => true,
2 => matches!(
args.first(),
Some(Ok(JsAnyCallArgument::JsAnyExpression(
JsAnyExpression::JsArrayExpression(_)
)))
),
3 => {
let mut iter = args.iter();
let first = iter.next();
let second = iter.next();
matches!(
(first, second),
(
Some(Ok(JsAnyCallArgument::JsAnyExpression(
JsAnyExpression::JsAnyLiteralExpression(
JsAnyLiteralExpression::JsStringLiteralExpression(_)
)
))),
Some(Ok(JsAnyCallArgument::JsAnyExpression(
JsAnyExpression::JsArrayExpression(_)
)))
)
)
}
_ => false,
}
} else {
false
}
} else {
false
}
}
_ => false,
})
}

/// Returns `true` if `arguments` contains a single [multiline template literal argument that starts on its own ](is_multiline_template_starting_on_same_line).
fn is_multiline_template_only_args(arguments: &JsCallArguments) -> bool {
let args = arguments.args();

match args.first() {
Some(Ok(JsAnyCallArgument::JsAnyExpression(JsAnyExpression::JsTemplate(template))))
if args.len() == 1 =>
{
is_multiline_template_starting_on_same_line(&template)
}
_ => false,
}
}

/// This function is used to check if the code is a hook-like code:
///
/// ```js
Expand Down
49 changes: 45 additions & 4 deletions crates/rome_js_formatter/src/js/expressions/call_expression.rs
@@ -1,17 +1,58 @@
use crate::prelude::*;
use rome_formatter::write;

use crate::parentheses::NeedsParentheses;
use crate::utils::get_member_chain;
use rome_js_syntax::{JsCallExpression, JsSyntaxKind, JsSyntaxNode};
use crate::utils::member_chain::MemberChain;
use rome_js_syntax::{
JsAnyExpression, JsCallExpression, JsCallExpressionFields, JsSyntaxKind, JsSyntaxNode,
};

#[derive(Debug, Clone, Default)]
pub struct FormatJsCallExpression;

impl FormatNodeRule<JsCallExpression> for FormatJsCallExpression {
fn fmt_fields(&self, node: &JsCallExpression, f: &mut JsFormatter) -> FormatResult<()> {
let member_chain = get_member_chain(node, f)?;
let JsCallExpressionFields {
callee,
optional_chain_token,
type_arguments,
arguments,
} = node.as_fields();

member_chain.fmt(f)
let callee = callee?;

if matches!(
callee,
JsAnyExpression::JsStaticMemberExpression(_)
| JsAnyExpression::JsComputedMemberExpression(_)
) && !callee.needs_parentheses()
{
let member_chain = MemberChain::from_call_expression(
node.clone(),
f.comments(),
f.options().tab_width(),
)?;

member_chain.fmt(f)
} else {
let format_inner = format_with(|f| {
write!(
f,
[
callee.format(),
optional_chain_token.format(),
type_arguments.format(),
arguments.format()
]
)
});

if matches!(callee, JsAnyExpression::JsCallExpression(_)) {
write!(f, [group(&format_inner)])
} else {
write!(f, [format_inner])
}
}
}

fn needs_parentheses(&self, item: &JsCallExpression) -> bool {
Expand Down
Expand Up @@ -34,30 +34,44 @@ impl Format<JsFormatContext> for JsAnyComputedMemberLike {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
write!(f, [self.object().format()])?;

match self.member()? {
FormatComputedMemberLookup(self).fmt(f)
}
}

/// Formats the lookup portion (everything except the object) of a computed member like.
pub(crate) struct FormatComputedMemberLookup<'a>(&'a JsAnyComputedMemberLike);

impl<'a> FormatComputedMemberLookup<'a> {
pub(crate) fn new(member_like: &'a JsAnyComputedMemberLike) -> Self {
Self(member_like)
}
}

impl Format<JsFormatContext> for FormatComputedMemberLookup<'_> {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
match self.0.member()? {
JsAnyExpression::JsAnyLiteralExpression(
JsAnyLiteralExpression::JsNumberLiteralExpression(literal),
) => {
write!(
f,
[
self.optional_chain_token().format(),
self.l_brack_token().format(),
self.0.optional_chain_token().format(),
self.0.l_brack_token().format(),
literal.format(),
self.r_brack_token().format()
self.0.r_brack_token().format()
]
)
}
member => {
write![
f,
[group(&format_args![
self.optional_chain_token().format(),
self.l_brack_token().format(),
soft_line_break(),
self.0.optional_chain_token().format(),
self.0.l_brack_token().format(),
soft_block_indent(&member.format()),
self.r_brack_token().format()
]),]
self.0.r_brack_token().format()
])]
]
}
}
Expand Down

0 comments on commit f604c39

Please sign in to comment.