mod chain_member;
mod groups;
mod simple_argument;
use crate::context::TabWidth;
use crate::prelude::*;
use crate::utils::is_long_curried_call;
use crate::utils::member_chain::chain_member::{CallExpressionPosition, ChainMember};
use crate::utils::member_chain::groups::{
MemberChainGroup, MemberChainGroupsBuilder, TailChainGroups,
};
use crate::utils::member_chain::simple_argument::SimpleArgument;
use crate::JsLabels;
use biome_formatter::{write, Buffer};
use biome_js_syntax::{
AnyJsCallArgument, AnyJsExpression, AnyJsLiteralExpression, JsCallExpression,
JsIdentifierExpression, JsSyntaxKind, JsSyntaxNode, JsSyntaxToken, JsThisExpression,
};
use biome_rowan::{AstNode, SyntaxResult};
use std::iter::FusedIterator;
#[derive(Debug, Clone)]
pub(crate) struct MemberChain {
root: JsCallExpression,
head: MemberChainGroup,
tail: TailChainGroups,
}
impl MemberChain {
pub(crate) fn from_call_expression(
call_expression: JsCallExpression,
comments: &JsComments,
tab_width: TabWidth,
) -> SyntaxResult<MemberChain> {
let parent = call_expression.syntax().parent();
let mut chain_members =
ChainMembersIterator::new(call_expression.clone().into(), comments).collect::<Vec<_>>();
chain_members.reverse();
let (head_group, remaining_members) =
split_members_into_head_and_remaining_groups(chain_members);
let tail_groups = compute_remaining_groups(remaining_members, comments);
let mut member_chain = MemberChain {
head: head_group,
tail: tail_groups,
root: call_expression,
};
member_chain.maybe_merge_with_first_group(comments, tab_width, parent.as_ref());
Ok(member_chain)
}
fn maybe_merge_with_first_group(
&mut self,
comments: &JsComments,
tab_width: TabWidth,
parent: Option<&JsSyntaxNode>,
) {
if self.should_merge_tail_with_head(parent, tab_width, comments) {
let group = self.tail.pop_first().unwrap();
self.head.extend_members(group.into_members());
}
}
fn should_merge_tail_with_head(
&self,
parent: Option<&JsSyntaxNode>,
tab_width: TabWidth,
comments: &JsComments,
) -> bool {
let first_group = match self.tail.first() {
None => {
return false;
}
Some(first_group) => first_group,
};
let has_comments = first_group
.members()
.first()
.map_or(false, |member| comments.has_comments(member.syntax()));
if has_comments {
return false;
}
let has_computed_property = first_group
.members()
.first()
.map_or(false, |item| item.is_computed_expression());
if self.head.members().len() == 1 {
let only_member = &self.head.members()[0];
let in_expression_statement = parent.map_or(false, |parent| {
parent.kind() == JsSyntaxKind::JS_EXPRESSION_STATEMENT
});
match only_member {
ChainMember::Node(node) => {
if JsThisExpression::can_cast(node.kind()) {
true
} else if let Some(identifier) = JsIdentifierExpression::cast_ref(node) {
let is_factory = identifier
.name()
.and_then(|name| name.value_token())
.as_ref()
.map_or(false, is_factory);
has_computed_property ||
is_factory ||
(in_expression_statement
&& has_short_name(&identifier, tab_width))
} else {
false
}
}
_ => false,
}
} else if let Some(ChainMember::StaticMember { expression }) = self.head.members().last() {
let member = expression.member().ok();
let is_factory = member
.as_ref()
.and_then(|member| member.as_js_name())
.and_then(|name| name.value_token().ok())
.as_ref()
.map_or(false, is_factory);
has_computed_property || is_factory
} else {
false
}
}
fn groups_should_break(&self, f: &mut JsFormatter) -> FormatResult<bool> {
let comments = f.comments();
let node_has_comments =
self.head.has_comments(comments) || self.tail.has_comments(comments);
if node_has_comments {
return Ok(true);
}
let mut call_expressions = self
.members()
.filter_map(|member| match member {
ChainMember::CallExpression { expression, .. } => Some(expression),
_ => None,
})
.peekable();
let mut calls_count = 0u32;
let mut any_has_function_like_argument = false;
let mut any_complex_args = false;
while let Some(call) = call_expressions.next() {
calls_count += 1;
if call_expressions.peek().is_some() {
any_has_function_like_argument =
any_has_function_like_argument || has_arrow_or_function_expression_arg(call)
}
any_complex_args = any_complex_args || !has_simple_arguments(call);
}
if calls_count > 2 && any_complex_args {
return Ok(true);
}
if self.last_call_breaks(f)? && any_has_function_like_argument {
return Ok(true);
}
if !self.tail.is_empty() && self.head.will_break(f)? {
return Ok(true);
}
if self.tail.any_except_last_will_break(f)? {
return Ok(true);
}
Ok(false)
}
fn last_call_breaks(&self, f: &mut JsFormatter) -> FormatResult<bool> {
let last_group = self.last_group();
if let Some(ChainMember::CallExpression { .. }) = last_group.members().last() {
last_group.will_break(f)
} else {
Ok(false)
}
}
fn last_group(&self) -> &MemberChainGroup {
self.tail.last().unwrap_or(&self.head)
}
fn members(&self) -> impl Iterator<Item = &ChainMember> + DoubleEndedIterator {
self.head.members().iter().chain(self.tail.members())
}
fn has_comments(&self, comments: &JsComments) -> bool {
let mut members = self.members();
if let Some(first) = members.next() {
if comments.has_trailing_comments(first.syntax()) {
return true;
}
}
members.next_back();
for member in members {
if comments.has_leading_comments(member.syntax())
|| comments.has_trailing_comments(member.syntax())
{
return true;
}
}
false
}
}
impl Format<JsFormatContext> for MemberChain {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
let has_comments = self.has_comments(f.comments());
let format_one_line = format_with(|f| {
let mut joiner = f.join();
joiner.entry(&self.head);
joiner.entries(self.tail.iter());
joiner.finish()
});
if self.tail.len() <= 1 && !has_comments {
return if is_long_curried_call(Some(&self.root)) {
write!(f, [format_one_line])
} else {
write!(f, [group(&format_one_line)])
};
}
let has_empty_line = match self.tail.members().next() {
Some(member) => member.needs_empty_line_before(),
None => false,
};
let format_tail = format_with(|f| {
if !has_empty_line {
write!(f, [hard_line_break()])?;
}
f.join_with(hard_line_break())
.entries(self.tail.iter())
.finish()
});
let format_expanded = format_with(|f| write!(f, [self.head, indent(&group(&format_tail))]));
let format_content = format_with(|f| {
if self.groups_should_break(f)? {
write!(f, [group(&format_expanded)])
} else {
if has_empty_line || self.last_group().will_break(f)? {
write!(f, [expand_parent()])?;
}
write!(f, [best_fitting!(format_one_line, format_expanded)])
}
});
write!(
f,
[labelled(
LabelId::of(JsLabels::MemberChain),
&format_content
)]
)
}
}
fn split_members_into_head_and_remaining_groups(
mut members: Vec<ChainMember>,
) -> (MemberChainGroup, Vec<ChainMember>) {
let non_call_or_array_member_access_start = members
.iter()
.enumerate()
.skip(1)
.find_map(|(index, member)| match member {
ChainMember::CallExpression { .. }
| ChainMember::TsNonNullAssertionExpression { .. } => None,
ChainMember::ComputedMember { expression } => {
if matches!(
expression.member(),
Ok(AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsNumberLiteralExpression(_),
))
) {
None
} else {
Some(index)
}
}
_ => Some(index),
})
.unwrap_or(members.len());
let first_group_end_index = if !members
.first()
.map_or(false, |member| member.is_call_expression())
{
let rest = &members[non_call_or_array_member_access_start..];
let member_end = rest
.iter()
.enumerate()
.find_map(|(index, member)| match member {
ChainMember::StaticMember { .. } | ChainMember::ComputedMember { .. } => {
let next_is_member = matches!(
rest.get(index + 1),
Some(ChainMember::ComputedMember { .. } | ChainMember::StaticMember { .. })
);
(!next_is_member).then_some(index)
}
_ => Some(index),
})
.unwrap_or(rest.len());
non_call_or_array_member_access_start + member_end
} else {
non_call_or_array_member_access_start
};
let remaining = members.split_off(first_group_end_index);
(MemberChainGroup::from(members), remaining)
}
fn compute_remaining_groups(members: Vec<ChainMember>, comments: &JsComments) -> TailChainGroups {
let mut has_seen_call_expression = false;
let mut groups_builder = MemberChainGroupsBuilder::default();
for member in members {
let has_trailing_comments = comments.has_trailing_comments(member.syntax());
match member {
ChainMember::ComputedMember { .. } if is_computed_array_member_access(&member) => {
groups_builder.start_or_continue_group(member);
}
ChainMember::StaticMember { .. } | ChainMember::ComputedMember { .. } => {
if has_seen_call_expression {
groups_builder.close_group();
groups_builder.start_group(member);
has_seen_call_expression = false;
} else {
groups_builder.start_or_continue_group(member);
}
}
ChainMember::CallExpression { .. } => {
groups_builder.start_or_continue_group(member);
has_seen_call_expression = true;
}
ChainMember::TsNonNullAssertionExpression { .. } => {
groups_builder.start_or_continue_group(member);
}
ChainMember::Node(_) if member.is_call_like_expression() => {
groups_builder.start_or_continue_group(member);
has_seen_call_expression = true;
}
ChainMember::Node(_) => groups_builder.continue_group(member),
}
if has_trailing_comments {
groups_builder.close_group();
has_seen_call_expression = false;
}
}
groups_builder.finish()
}
fn is_computed_array_member_access(member: &ChainMember) -> bool {
if let ChainMember::ComputedMember { expression } = member {
matches!(
expression.member(),
Ok(AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsNumberLiteralExpression(_)
))
)
} else {
false
}
}
fn has_arrow_or_function_expression_arg(call: &JsCallExpression) -> bool {
call.arguments().map_or(false, |arguments| {
arguments.args().iter().any(|argument| {
matches!(
argument,
Ok(AnyJsCallArgument::AnyJsExpression(
AnyJsExpression::JsArrowFunctionExpression(_)
| AnyJsExpression::JsFunctionExpression(_)
))
)
})
})
}
fn has_simple_arguments(call: &JsCallExpression) -> bool {
call.arguments().map_or(false, |arguments| {
arguments.args().iter().all(|argument| {
argument.map_or(false, |argument| SimpleArgument::new(argument).is_simple())
})
})
}
fn is_factory(token: &JsSyntaxToken) -> bool {
let text = token.text_trimmed();
let mut chars = text.chars();
match text.chars().next() {
Some('_' | '$') => chars.all(|c| matches!(c, '_' | '$')),
Some(c) => c.is_uppercase(),
_ => false,
}
}
pub fn is_member_call_chain(
expression: JsCallExpression,
comments: &JsComments,
tab_width: TabWidth,
) -> SyntaxResult<bool> {
let chain = MemberChain::from_call_expression(expression, comments, tab_width)?;
Ok(chain.tail.is_member_call_chain(comments))
}
fn has_short_name(identifier: &JsIdentifierExpression, tab_width: TabWidth) -> bool {
identifier
.name()
.and_then(|name| name.value_token())
.map_or(false, |name| {
name.text_trimmed().len() <= u8::from(tab_width) as usize
})
}
struct ChainMembersIterator<'a> {
next: Option<AnyJsExpression>,
comments: &'a JsComments,
root: bool,
}
impl<'a> ChainMembersIterator<'a> {
fn new(root: AnyJsExpression, comments: &'a JsComments) -> Self {
Self {
next: Some(root),
comments,
root: true,
}
}
}
impl Iterator for ChainMembersIterator<'_> {
type Item = ChainMember;
fn next(&mut self) -> Option<Self::Item> {
use AnyJsExpression::*;
let expression = self.next.take()?;
if self.comments.is_suppressed(expression.syntax()) {
return Some(ChainMember::Node(expression.into_syntax()));
}
let member = match expression {
JsCallExpression(call_expression) => {
let callee = call_expression.callee().ok();
let is_chain = matches!(
callee,
Some(
JsStaticMemberExpression(_)
| JsComputedMemberExpression(_)
| JsCallExpression(_)
)
);
if is_chain {
self.next = callee;
}
let position = if self.root {
CallExpressionPosition::End
} else if !is_chain {
CallExpressionPosition::Start
} else {
CallExpressionPosition::Middle
};
ChainMember::CallExpression {
expression: call_expression,
position,
}
}
JsStaticMemberExpression(static_member) => {
self.next = static_member.object().ok();
ChainMember::StaticMember {
expression: static_member,
}
}
JsComputedMemberExpression(computed_expression) => {
self.next = computed_expression.object().ok();
ChainMember::ComputedMember {
expression: computed_expression,
}
}
TsNonNullAssertionExpression(expression) => {
self.next = expression.expression().ok();
ChainMember::TsNonNullAssertionExpression { expression }
}
expression => ChainMember::Node(expression.into_syntax()),
};
self.root = false;
Some(member)
}
}
impl FusedIterator for ChainMembersIterator<'_> {}