mod arg_comments;
pub(in crate::printer) mod arg_predicates;
mod arg_wrapping;
mod call_formatting;
mod chain_args;
mod expand_last;
mod import_expr;
mod module_paths;
mod new_expression;
mod test_patterns;
pub(crate) use arg_comments::{
PartitionedComments, has_stripped_paren_gap, skip_stripped_open_paren,
};
pub(in crate::printer) use arg_wrapping::{
AssumedHugKind, AssumedParamHug, sole_template_on_its_own_line,
};
pub(in crate::printer) use import_expr::{ImportOptionsArg, build_import_args_comment_layout};
use super::chain::{self, ChainCall, call_callee_paren_leading_start};
use super::{ArrowChainContext, ParenContext, Printer, SecondTypeArgs, is_curried_arrow_chain};
use crate::ast;
use crate::printer::expressions::functions::callback_signature_refuses_hug;
use arg_comments::{any_arg_empty_line, any_comment_forces_expansion, last_arg_has_comments};
use arg_predicates::is_block_function;
use arg_wrapping::{
build_args_split_last, is_sole_multiline_template_arg, multiline_template_hug_applies,
};
use std::ops::ControlFlow;
use tsv_lang::Span;
use tsv_lang::doc::arena::DocId;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct CalleeGap {
pub(super) owned_pair: bool,
pub(super) trailing_gap: Option<(u32, u32)>,
pub(super) start: u32,
pub(super) optional: CallOptional,
pub(super) paren_split: Option<(u32, u32)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CallOptional {
Absent,
Fused,
Unsplit,
Split { start: u32, end: u32 },
}
impl CallOptional {
pub(super) fn fused(self) -> bool {
matches!(self, Self::Fused)
}
fn arg_side_start(self) -> Option<u32> {
match self {
Self::Split { end, .. } => Some(end + OPTIONAL_TOKEN_LEN),
Self::Absent | Self::Fused | Self::Unsplit => None,
}
}
}
const OPTIONAL_TOKEN_LEN: u32 = "?.".len() as u32;
impl CalleeGap {
pub(super) fn paren_open(&self, call: &ast::CallExpression<'_>) -> u32 {
self.paren_split
.map_or_else(|| args_side_start(call, self.start), |(_, paren)| paren)
}
}
pub(super) fn callee_gap(
printer: &Printer<'_>,
call: &ast::CallExpression<'_>,
span: Span,
) -> CalleeGap {
if !call.optional && call_callee_paren_leading_start(call, span).is_none() {
let start = call.callee.span().end;
let gap_start = args_side_start(call, start);
if call
.arguments
.first()
.is_none_or(|arg| printer.gap_known_comment_free(gap_start, arg.span().start))
{
let plain = CalleeGap {
owned_pair: false,
trailing_gap: None,
start,
optional: CallOptional::Absent,
paren_split: None,
};
debug_assert_eq!(
plain,
callee_gap_wide(printer, call, span),
"the plain call's gap is the derivation's answer"
);
return plain;
}
}
callee_gap_wide(printer, call, span)
}
#[inline(never)]
fn callee_gap_wide(printer: &Printer<'_>, call: &ast::CallExpression<'_>, span: Span) -> CalleeGap {
let callee_end = call.callee.span().end;
let owned_pair = call_callee_paren_leading_start(call, span).is_some();
let trailing_gap = printer.owned_pair_trailing_gap(callee_end, owned_pair);
let start = Printer::gap_start_after_owned_pair(callee_end, trailing_gap);
let optional = call_optional(printer, call, span, start);
let paren_open = args_side_start(call, optional.arg_side_start().unwrap_or(start));
CalleeGap {
paren_split: call_paren_split(printer, call, paren_open),
owned_pair,
trailing_gap,
start: optional.arg_side_start().unwrap_or(start),
optional,
}
}
fn args_side_start(call: &ast::CallExpression<'_>, start_after_optional: u32) -> u32 {
call.type_arguments
.as_ref()
.map_or(start_after_optional, |ta| ta.span.end)
}
fn call_paren_split(
printer: &Printer<'_>,
call: &ast::CallExpression<'_>,
gap_start: u32,
) -> Option<(u32, u32)> {
if call.optional {
return None;
}
paren_split_for(printer, gap_start, call.arguments)
}
#[inline]
pub(super) fn paren_split_for(
printer: &Printer<'_>,
gap_start: u32,
arguments: &[&ast::Expression<'_>],
) -> Option<(u32, u32)> {
let first_arg_start = arguments.first()?.span().start;
if printer.gap_known_comment_free(gap_start, first_arg_start) {
debug_assert!(
split_paren_wide(printer, gap_start, first_arg_start, arguments).is_none(),
"a comment-free callee gap split at its `(`"
);
return None;
}
split_paren_wide(printer, gap_start, first_arg_start, arguments).map(|paren| (gap_start, paren))
}
#[inline(never)]
pub(super) fn paren_split_for_outlined(
printer: &Printer<'_>,
gap_start: u32,
arguments: &[&ast::Expression<'_>],
) -> Option<(u32, u32)> {
paren_split_for(printer, gap_start, arguments)
}
#[inline(never)]
fn split_paren_wide(
printer: &Printer<'_>,
gap_start: u32,
first_arg_start: u32,
arguments: &[&ast::Expression<'_>],
) -> Option<u32> {
let paren = printer.find_char_outside_comments(gap_start, first_arg_start, b'(')?;
if !printer.has_line_comments_between(gap_start, paren) {
return None;
}
if split_strands_directive(printer, gap_start, paren, arguments) {
return None;
}
Some(paren)
}
pub(super) fn hang_args_under_split(
printer: &Printer<'_>,
head: DocId,
split: (u32, u32),
args: DocId,
) -> DocId {
let (start, paren) = split;
printer
.d()
.concat(&[head, printer.build_line_split_gap_doc(start, paren, args)])
}
fn split_strands_directive(
printer: &Printer<'_>,
gap_start: u32,
arg_side_start: u32,
arguments: &[&ast::Expression<'_>],
) -> bool {
!arguments.is_empty()
&& printer.args_frozen_span(gap_start, arguments, 0).is_some()
&& printer
.args_frozen_span(arg_side_start, arguments, 0)
.is_none()
}
fn call_optional(
printer: &Printer<'_>,
call: &ast::CallExpression<'_>,
span: Span,
start: u32,
) -> CallOptional {
if !call.optional {
return CallOptional::Absent;
}
if call.arguments.is_empty() && call.type_arguments.is_none() {
return CallOptional::Fused;
}
let end = call.type_arguments.as_ref().map_or_else(
|| {
call.arguments
.first()
.map_or(span.end, |arg| arg.span().start)
},
|ta| ta.span.start,
);
let Some(question) = printer.find_char_outside_comments(start, end, b'?') else {
return CallOptional::Unsplit;
};
if split_strands_directive(
printer,
start,
question + OPTIONAL_TOKEN_LEN,
call.arguments,
) {
return CallOptional::Unsplit;
}
CallOptional::Split {
start,
end: question,
}
}
pub(super) fn optional_callee_gap_doc(printer: &Printer<'_>, gap: CalleeGap) -> Option<DocId> {
let d = printer.d();
match gap.optional {
CallOptional::Absent | CallOptional::Fused => None,
CallOptional::Unsplit => Some(d.text("?.")),
CallOptional::Split { start, end } => {
Some(printer.build_line_split_gap_doc(start, end, d.text("?.")))
}
}
}
pub(super) fn call_paren_open(
printer: &Printer<'_>,
call: &ast::CallExpression<'_>,
span: Span,
) -> u32 {
callee_gap(printer, call, span).paren_open(call)
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub(in crate::printer) fn chain_has_calls(mut expr: &ast::Expression<'_>) -> bool {
loop {
expr = match &expr.kind {
ast::ExpressionKind::CallExpression(_) => return true,
ast::ExpressionKind::MemberExpression(member) => member.object,
ast::ExpressionKind::TSNonNullExpression(non_null) => non_null.expression,
ast::ExpressionKind::AwaitExpression(await_expr) => await_expr.argument,
ast::ExpressionKind::YieldExpression(yield_expr) => match yield_expr.argument {
Some(argument) => argument,
None => return false,
},
_ => return false,
};
}
}
pub(super) enum CalleeParens<'a> {
Welded(&'a ast::Expression<'a>),
Cast(&'a ast::Expression<'a>),
Binary(&'a ast::BinaryExpression<'a>, Span),
}
impl<'a> CalleeParens<'a> {
pub(super) fn of(
printer: &Printer<'_>,
callee: &'a ast::Expression<'a>,
context: ParenContext,
second_type_args: Option<SecondTypeArgs>,
) -> Option<Self> {
let pair = printer.needs_parens(callee, context)
|| second_type_args
.is_some_and(|follow| printer.instantiation_keeps_pair(callee, follow))
|| (matches!(context, ParenContext::NewCallee)
&& printer.new_callee_holds_bare_call(callee));
if !pair {
return None;
}
Some(match &callee.kind {
ast::ExpressionKind::TSAsExpression(_)
| ast::ExpressionKind::TSSatisfiesExpression(_) => Self::Cast(callee),
ast::ExpressionKind::BinaryExpression(binary) => Self::Binary(binary, callee.span),
_ => Self::Welded(callee),
})
}
pub(super) fn build_body_doc(
&self,
printer: &Printer<'_>,
pair_is_comment_free: bool,
) -> DocId {
match self {
Self::Binary(binary, span) => printer.build_binary_chain_doc_ungrouped(binary, *span),
Self::Welded(callee) if pair_is_comment_free && is_curried_arrow_chain(callee) => {
printer.build_with_arrow_chain_context(ArrowChainContext::Callee, || {
printer.build_expression_doc(callee)
})
}
Self::Welded(callee) | Self::Cast(callee) => printer.build_expression_doc(callee),
}
}
pub(super) fn build_doc(&self, printer: &Printer<'_>, body: DocId) -> DocId {
match self {
Self::Welded(_) => printer.arena().parens(body),
Self::Cast(_) | Self::Binary(..) => printer.build_expanding_parens_doc(body),
}
}
}
pub(in crate::printer) fn arrow_body_hugs_call_parens(body: &ast::Expression<'_>) -> bool {
arg_predicates::arrow_body_is_call_through_non_null(body)
|| arg_predicates::is_ternary_arrow_body(body)
}
pub(in crate::printer) fn is_memberish(expr: &ast::Expression<'_>) -> bool {
matches!(
expr.kind,
ast::ExpressionKind::MemberExpression(_) | ast::ExpressionKind::TSNonNullExpression(_)
)
}
fn curried_run_roots_in_member_chain(callee: &ast::CallExpression<'_>) -> bool {
let mut call = callee;
while let ast::ExpressionKind::CallExpression(inner) = &call.callee.kind {
call = inner;
}
is_memberish(call.callee)
}
fn callee_is_curried_member_chain_call(callee: &ast::Expression<'_>) -> bool {
let mut callee = callee;
while let ast::ExpressionKind::TSNonNullExpression(non_null) = &callee.kind {
callee = non_null.expression;
}
matches!(
&callee.kind,
ast::ExpressionKind::CallExpression(inner) if curried_run_roots_in_member_chain(inner)
)
}
#[expect(clippy::inline_always)]
#[inline(always)]
fn spine_step<'e>(
expr: &'e ast::Expression<'e>,
) -> ControlFlow<Option<(&'e ast::CallExpression<'e>, Span)>, &'e ast::Expression<'e>> {
tsv_lang::fanout_probe::predicate();
match &expr.kind {
ast::ExpressionKind::MemberExpression(member) => ControlFlow::Continue(member.object),
ast::ExpressionKind::TSNonNullExpression(non_null) => {
ControlFlow::Continue(non_null.expression)
}
ast::ExpressionKind::CallExpression(call) => ControlFlow::Break(Some((call, expr.span))),
_ => ControlFlow::Break(None),
}
}
fn spine_first_call<'e>(
expr: &'e ast::Expression<'e>,
) -> Option<(&'e ast::CallExpression<'e>, Span)> {
let mut expr = expr;
loop {
match spine_step(expr) {
ControlFlow::Continue(next) => expr = next,
ControlFlow::Break(found) => return found,
}
}
}
pub(crate) fn spine_first_call_within<'e>(
expr: &'e ast::Expression<'e>,
links: u32,
) -> Result<Option<&'e ast::CallExpression<'e>>, &'e ast::Expression<'e>> {
let mut expr = expr;
let mut links = links;
loop {
match spine_step(expr) {
ControlFlow::Continue(next) => expr = next,
ControlFlow::Break(found) => return Ok(found.map(|(call, _)| call)),
}
links -= 1;
if links == 0 {
return Err(expr);
}
}
}
pub(crate) fn call_may_print_whole(call: &ast::CallExpression<'_>) -> bool {
callee_is_curried_member_chain_call(call.callee)
|| is_sole_multiline_template_arg(call.arguments)
}
fn callee_is_long_curried(call: &ast::CallExpression<'_>) -> bool {
matches!(
&call.callee.kind,
ast::ExpressionKind::CallExpression(inner)
if !call.arguments.is_empty() && inner.arguments.len() > call.arguments.len()
)
}
impl<'a> Printer<'a> {
#[expect(clippy::inline_always)]
#[inline(always)]
pub(in crate::printer) fn call_takes_template_bypass(
&self,
call: &ast::CallExpression<'_>,
span: Span,
) -> bool {
is_sole_multiline_template_arg(call.arguments)
&& self.call_takes_template_bypass_beyond_shape(call, span)
}
#[inline(never)]
fn call_takes_template_bypass_beyond_shape(
&self,
call: &ast::CallExpression<'_>,
span: Span,
) -> bool {
let paren_open = call_paren_open(self, call, span);
multiline_template_hug_applies(self, call.arguments, paren_open)
&& !self.has_line_comments_between(span.start, paren_open)
}
fn spine_first_call_prints_whole(&self, expr: &ast::Expression<'_>) -> bool {
spine_first_call(expr).is_some_and(|(call, span)| {
callee_is_curried_member_chain_call(call.callee)
|| self.call_takes_template_bypass(call, span)
})
}
pub(in crate::printer) fn build_whole_call_operand_doc(
&self,
operand: &ast::Expression<'_>,
span: Span,
parent_optional: bool,
) -> Option<DocId> {
if !self.spine_first_call_prints_whole(operand)
|| self.has_comments_on_page_between(span.start, span.end)
{
return None;
}
Some(
if chain::child_stops_optional_chain(span.start, parent_optional, operand)
|| self.region_keeps_shell(operand)
{
self.d().parens(self.build_expression_doc(operand))
} else {
self.build_with_inline_every_lookup_forwarded(span, operand, || {
self.build_expression_doc(operand)
})
},
)
}
#[inline(never)]
fn build_whole_call_member_doc(
&self,
member: &ast::MemberExpression<'_>,
span: Span,
) -> Option<DocId> {
let object_doc = self.build_whole_call_operand_doc(member.object, span, member.optional)?;
Some(chain::build_lookup_after_operand_doc(
member, span, object_doc, self,
))
}
#[inline(never)]
fn build_curried_member_chain_call_doc(
&self,
call: &ast::CallExpression<'_>,
span: Span,
) -> DocId {
self.is_expression_statement.set(false);
let doc = self.build_call_doc_with_wrapping(call, span);
self.d().group(doc)
}
pub(super) fn build_call_doc_with_wrapping(
&self,
call: &ast::CallExpression<'_>,
span: Span,
) -> DocId {
call_formatting::build_call_doc_with_wrapping(self, call, span)
}
pub(super) fn build_call_doc(&self, call: &ast::CallExpression<'_>, span: Span) -> DocId {
if callee_is_curried_member_chain_call(call.callee) {
return self.build_curried_member_chain_call_doc(call, span);
}
if let ast::ExpressionKind::CallExpression(inner) = &call.callee.kind {
let inner_has_multiline_arg = inner.arguments.iter().any(|arg| {
matches!(
arg.kind,
ast::ExpressionKind::ArrayExpression(_)
| ast::ExpressionKind::ObjectExpression(_)
)
});
let any_arg_empty_line = any_arg_empty_line(call.arguments, self);
let paren_open = call.callee.span().end;
let call_has_comments = self.has_comments_on_page_between(paren_open, span.end);
if call.type_arguments.is_none()
&& !call.optional
&& call.arguments.len() >= 2
&& call
.arguments
.last()
.copied()
.is_some_and(is_block_function)
&& !any_arg_empty_line
&& !(call_has_comments
&& any_comment_forces_expansion(call, span, self, paren_open))
&& !(call_has_comments
&& last_arg_has_comments(call.arguments, self, span.end, paren_open))
&& !inner_has_multiline_arg
&& !callee_is_long_curried(call)
&& !call.arguments.last().is_some_and(|arg| {
callback_signature_refuses_hug(self, arg, call.arguments.len())
})
{
let d = self.d();
let callee_doc = self.build_expression_doc(call.callee);
let assumed = call
.arguments
.last()
.and_then(|last| AssumedParamHug::new(self, last));
let (head_parts, last_arg_doc, all_args_broken) =
AssumedParamHug::sharing(assumed.as_ref(), self, || {
build_args_split_last(call.arguments, self, paren_open, call_has_comments)
});
let last_index = call.arguments.len() - 1;
let assumed = assumed.map_or_else(Default::default, |hug| {
hug.build(self, call.arguments.len() > 1, || {
arg_wrapping::build_joined_argument_doc(
self,
paren_open,
call.arguments,
last_index,
)
})
});
return arg_wrapping::ArgOpener::callee(d, callee_doc)
.inline_assumed_or_expand_all(
d,
&head_parts,
last_arg_doc,
assumed,
all_args_broken,
);
}
}
let is_true_chain = chain_has_calls(call.callee);
let callee_is_memberish = is_memberish(call.callee);
if is_true_chain || callee_is_memberish {
if test_patterns::test_call_flat_layout_applies(call, span, self) {
return self.build_call_doc_with_wrapping(call, span);
}
if self.call_takes_template_bypass(call, span) {
self.is_expression_statement.set(false);
return self.build_call_doc_with_wrapping(call, span);
}
let mut nodes = chain::ChainNodeVec::new();
let long_curried = chain::linearize_chain_from_call_into(
call,
span,
self.linearize_input(),
&mut nodes,
);
if self.long_curried_target.get() == Some(span)
&& let Some(chain::ChainNode::Call { facts, .. }) = nodes.last_mut()
{
facts.ungroup_args = true;
}
chain::build_linearized_chain_doc(
&mut nodes,
long_curried,
call.callee,
span.start,
span,
self,
)
} else {
self.build_call_doc_with_wrapping(call, span)
}
}
pub(super) fn build_member_doc(&self, member: &ast::MemberExpression<'_>, span: Span) -> DocId {
if self.member_gap_frozen(
self.member_gap_start(member.object),
member.property.span().start,
) {
return self.build_frozen_opaque_node_doc(span);
}
if matches!(
member.object.kind,
ast::ExpressionKind::CallExpression(_)
| ast::ExpressionKind::MemberExpression(_)
| ast::ExpressionKind::TSNonNullExpression(_)
) && let Some(doc) = self.build_whole_call_member_doc(member, span)
{
return doc;
}
let mut nodes = chain::ChainNodeVec::new();
let long_curried = chain::linearize_chain_from_member_into(
member,
span,
self.linearize_input(),
&mut nodes,
);
chain::build_linearized_chain_doc(
&mut nodes,
long_curried,
member.object,
span.start,
span,
self,
)
}
pub(super) fn build_import_expression_doc(
&self,
import_expr: &ast::ImportExpression<'_>,
span: Span,
) -> DocId {
import_expr::build_import_expression_doc(self, import_expr, span)
}
pub(super) fn build_meta_property_doc(&self, meta: &ast::MetaProperty<'_>) -> DocId {
import_expr::build_meta_property_doc(self, meta)
}
pub(super) fn build_call_args_doc_for_chain(
&self,
call: &ast::CallExpression<'_>,
span: Span,
facts: ChainCall,
) -> DocId {
chain_args::build_call_args_doc_for_chain(self, call, span, facts)
}
pub(in crate::printer) fn chain_call_head_doc(
&self,
call: &ast::CallExpression<'_>,
span: Span,
) -> Option<DocId> {
chain_args::chain_call_head_doc(self, call, callee_gap(self, call, span))
}
pub(super) fn build_call_args_doc_for_chain_expanded(
&self,
call: &ast::CallExpression<'_>,
span: Span,
facts: ChainCall,
) -> DocId {
chain_args::build_call_args_doc_for_chain_expanded(self, call, span, facts)
}
}