use super::builder::{build_chain_doc, mark_long_curried_calls};
use super::printing::chain_gap_share_any;
use super::types::{ChainGroup, ChainGroupVec, ChainNode, ChainNodeVec};
use crate::ast::{self, Expression, ExpressionKind, IdentName};
use crate::printer::calls::{call_paren_open, is_memberish, sole_template_on_its_own_line};
use crate::printer::comments::{paren_pair_keeps_leading_run, paren_shell_close_after};
use crate::printer::{
ParenContext, Printer, SecondTypeArgs, SpanMarks, instantiation_keeps_pair_before_type_args,
is_called_function, is_multiline_template_expression, needs_parens,
};
use smallvec::SmallVec;
use tsv_lang::doc::arena::DocId;
use tsv_lang::{Comment, Span, TAB_WIDTH, has_line_spanning_comments_to_emit_in_range};
#[derive(Clone, Copy)]
pub(crate) struct LinearizeInput<'i> {
pub(crate) printer: &'i Printer<'i>,
pub(crate) region_kept_shells: Option<&'i SpanMarks>,
}
#[cfg(test)]
fn linearize_chain<'a>(expr: &'a Expression<'_>, input: LinearizeInput<'_>) -> ChainNodeVec<'a> {
let mut nodes = ChainNodeVec::new();
let mut paren_gaps = Vec::new();
linearize_spine(expr, input, &mut nodes, &mut paren_gaps);
let _ = finalize_chain_nodes(&mut nodes, &paren_gaps, input);
nodes
}
pub(crate) fn linearize_chain_from_call_into<'a>(
call: &'a ast::CallExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) -> bool {
debug_assert!(
nodes.is_empty(),
"the linearizer fills its buffer, never appends"
);
let mut paren_gaps = Vec::new();
linearize_call_callee(call, span, input, nodes, &mut paren_gaps);
nodes.push(ChainNode::call(call, span));
finalize_chain_nodes(nodes, &paren_gaps, input)
}
pub(crate) fn linearize_chain_from_member_into<'a>(
member: &'a ast::MemberExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) -> bool {
debug_assert!(
nodes.is_empty(),
"the linearizer fills its buffer, never appends"
);
let mut paren_gaps = Vec::new();
linearize_member_object(member, span, input, nodes, &mut paren_gaps);
linearize_member_node(member, span, input, nodes, &mut paren_gaps);
finalize_chain_nodes(nodes, &paren_gaps, input)
}
pub(crate) fn linearize_chain_from_non_null_into<'a>(
non_null: &'a ast::TSNonNullExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) -> bool {
debug_assert!(
nodes.is_empty(),
"the linearizer fills its buffer, never appends"
);
let mut paren_gaps = Vec::new();
linearize_spine(non_null.expression, input, nodes, &mut paren_gaps);
nodes.push(ChainNode::non_null(non_null, span));
finalize_chain_nodes(nodes, &paren_gaps, input)
}
fn apply_paren_gaps(nodes: &mut [ChainNode<'_>], paren_gaps: &[ParenGap], printer: &Printer<'_>) {
for &(node_index, outer_index, gap_start, object_start) in paren_gaps {
if !in_member_chain_scope(nodes, outer_index - 1) {
continue;
}
let Some((natural_object_end, property_start)) = nodes[node_index].comment_range() else {
continue;
};
let check_end = if nodes[node_index].is_computed() {
super::printing::find_computed_bracket(
printer.source,
natural_object_end,
property_start,
)
.open
} else {
property_start
};
if printer.has_comments_on_page_between(object_start, check_end) {
continue;
}
if let Some(
ChainNode::Member {
object_end,
paren_gap_skip,
..
}
| ChainNode::PrivateMember {
object_end,
paren_gap_skip,
..
}
| ChainNode::ComputedMember {
object_end,
paren_gap_skip,
..
},
) = nodes.get_mut(node_index)
{
*paren_gap_skip = Some(Span::new(object_start, *object_end));
*object_end = gap_start;
}
}
}
fn in_member_chain_scope(nodes: &[ChainNode<'_>], index: usize) -> bool {
let continues =
|call: usize| call > 0 && (nodes[call - 1].is_member() || nodes[call - 1].is_call());
let mut active = false;
for j in (index..nodes.len()).rev() {
let node = &nodes[j];
if active {
active = match node {
ChainNode::Member { .. }
| ChainNode::PrivateMember { .. }
| ChainNode::ComputedMember { .. }
| ChainNode::NonNull { .. } => true,
ChainNode::Call { .. } => continues(j),
ChainNode::Base { .. } => false,
};
if j == index {
return active;
}
}
if !active && node.is_call() && j > 0 && nodes[j - 1].is_member() {
active = true;
}
}
false
}
type ParenGap = (usize, usize, u32, u32);
pub(crate) fn chain_mid_chain_paren_gaps(
root: &Expression<'_>,
input: LinearizeInput<'_>,
) -> SmallVec<[(u32, u32); 2]> {
let mut nodes = ChainNodeVec::new();
match &root.kind {
ExpressionKind::CallExpression(call) => {
let _ = linearize_chain_from_call_into(call, root.span, input, &mut nodes);
}
ExpressionKind::MemberExpression(member) => {
let _ = linearize_chain_from_member_into(member, root.span, input, &mut nodes);
}
_ => return SmallVec::new(),
}
nodes
.iter()
.filter_map(|node| Some((node.comment_range()?.0, node.paren_gap_skip()?.start)))
.collect()
}
fn finalize_chain_nodes(
nodes: &mut [ChainNode<'_>],
paren_gaps: &[ParenGap],
input: LinearizeInput<'_>,
) -> bool {
apply_paren_gaps(nodes, paren_gaps, input.printer);
fix_callee_base_parens(nodes, input);
let long_curried = mark_own_call_layout(nodes, input.printer);
#[cfg(feature = "_internal_buffer_stats")]
crate::printer::buffer_stats::record_chain_nodes(nodes.len());
long_curried
}
fn mark_own_call_layout(nodes: &mut [ChainNode<'_>], printer: &Printer<'_>) -> bool {
let mut entered = false;
let mut long_curried = false;
let mut next_call_args = None;
for node in nodes.iter_mut().rev() {
let ChainNode::Call { call, span, facts } = node else {
next_call_args = None;
continue;
};
long_curried |=
next_call_args.is_some_and(|outer| outer > 0 && call.arguments.len() > outer);
next_call_args = Some(call.arguments.len());
if entered {
let swallowed = is_memberish(call.callee)
|| matches!(call.callee.kind, ExpressionKind::CallExpression(_));
facts.own_call_layout = !swallowed;
entered = swallowed;
} else {
facts.own_call_layout = true;
entered = is_memberish(call.callee)
&& !template_preempts_chain_redirect(printer, call, *span);
}
}
long_curried
}
fn template_preempts_chain_redirect(
printer: &Printer<'_>,
call: &ast::CallExpression<'_>,
span: Span,
) -> bool {
call.arguments.len() == 1
&& is_multiline_template_expression(call.arguments[0])
&& !sole_template_on_its_own_line(
printer,
call_paren_open(printer, call, span),
call.arguments[0].span().start,
)
}
fn fix_callee_base_parens(nodes: &mut [ChainNode<'_>], input: LinearizeInput<'_>) {
let source = input.printer.source;
if let [
ChainNode::Base {
expr,
needs_parens: np,
..
},
ChainNode::Call { call, .. },
..,
] = nodes
{
if expr.has_optional_in_chain() {
return;
}
let second_list = call.type_arguments.is_some()
&& instantiation_keeps_pair_before_type_args(source, expr, SecondTypeArgs::Expression);
let callee_pair = needs_parens(expr, ParenContext::Callee, false)
&& !(is_called_function(expr)
&& input
.printer
.withheld_clarity_pair_targets
.is_marked(expr.span()));
*np = callee_pair
|| second_list
|| input
.region_kept_shells
.is_some_and(|marks| marks.is_marked(expr.span()));
}
}
pub(crate) fn child_stops_optional_chain(
parent_start: u32,
parent_optional: bool,
child: &Expression<'_>,
) -> bool {
!parent_optional && parent_start < child.span().start && child.has_optional_in_chain()
}
pub(crate) fn chain_paren_leading_gap(
expr: &Expression<'_>,
comments: &[Comment],
) -> Option<(u32, u32)> {
match &expr.kind {
ExpressionKind::MemberExpression(member) => {
member_object_paren_leading_start(member, expr.span)
.map(|start| (start, member.object.span().start))
}
ExpressionKind::CallExpression(call) => call_callee_paren_leading_start(call, expr.span)
.map(|start| (start, call.callee.span().start)),
ExpressionKind::TSNonNullExpression(non_null) => {
non_null_operand_paren_leading_start(non_null, expr.span, comments)
.map(|start| (start, non_null.expression.span().start))
}
_ => None,
}
}
pub(crate) fn member_object_paren_leading_start(
member: &ast::MemberExpression<'_>,
span: Span,
) -> Option<u32> {
child_stops_optional_chain(span.start, member.optional, member.object).then_some(span.start)
}
pub(crate) fn call_callee_paren_leading_start(
call: &ast::CallExpression<'_>,
span: Span,
) -> Option<u32> {
(child_stops_optional_chain(span.start, call.optional, call.callee)
|| (span.start < call.callee.span().start && paren_pair_keeps_leading_run(call.callee)))
.then_some(span.start)
}
pub(crate) fn tag_paren_leading_start(
tagged: &ast::TaggedTemplateExpression<'_>,
span: Span,
) -> Option<u32> {
(child_stops_optional_chain(span.start, false, tagged.tag)
|| (span.start < tagged.tag.span().start && paren_pair_keeps_leading_run(tagged.tag)))
.then_some(span.start)
}
fn non_null_operand_paren_leading_start(
non_null: &ast::TSNonNullExpression<'_>,
span: Span,
comments: &[Comment],
) -> Option<u32> {
(non_null.seals_optional_chain(span)
|| has_line_spanning_comments_to_emit_in_range(
comments,
non_null.expression.span().end,
span.end,
))
.then_some(span.start)
}
fn push_sealed_chain_base<'a>(
child: &'a Expression<'_>,
parent_start: u32,
nodes: &mut ChainNodeVec<'a>,
) {
nodes.push(ChainNode::sealed_base(child, parent_start));
}
#[derive(Clone, Copy)]
enum PendingLink<'a> {
Call {
call: &'a ast::CallExpression<'a>,
span: Span,
descended: bool,
},
Member {
member: &'a ast::MemberExpression<'a>,
span: Span,
},
NonNull {
non_null: &'a ast::TSNonNullExpression<'a>,
span: Span,
},
}
type PendingLinks<'a> = SmallVec<[PendingLink<'a>; 8]>;
const CHAIN_FRAMES: u32 = 16;
fn linearize_spine<'a>(
expr: &'a Expression<'_>,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
paren_gaps: &mut Vec<ParenGap>,
) {
linearize_link(expr, input, nodes, paren_gaps, CHAIN_FRAMES);
}
fn linearize_link<'a>(
expr: &'a Expression<'_>,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
paren_gaps: &mut Vec<ParenGap>,
frames: u32,
) {
let Some(frames) = frames.checked_sub(1) else {
linearize_spine_loop(expr, input, nodes, paren_gaps);
return;
};
match &expr.kind {
ExpressionKind::CallExpression(call) => {
if let Some(callee) = call_callee_step(call, expr.span, input, nodes) {
linearize_link(callee, input, nodes, paren_gaps, frames);
finish_call_callee(call, expr.span, nodes);
}
nodes.push(ChainNode::call(call, expr.span));
}
ExpressionKind::MemberExpression(member) => {
if let Some(object) = member_object_step(member, expr.span, input, nodes) {
linearize_link(object, input, nodes, paren_gaps, frames);
}
linearize_member_node(member, expr.span, input, nodes, paren_gaps);
}
ExpressionKind::TSNonNullExpression(non_null) => {
match non_null_step(non_null, expr.span, input, nodes) {
NonNullStep::Done => {}
NonNullStep::Finish => finish_non_null(non_null, expr.span, nodes),
NonNullStep::Descend(inner) => {
linearize_link(inner, input, nodes, paren_gaps, frames);
finish_non_null(non_null, expr.span, nodes);
}
}
}
_ => push_chain_base(expr, input, nodes),
}
}
#[inline(never)]
fn linearize_spine_loop<'a>(
expr: &'a Expression<'_>,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
paren_gaps: &mut Vec<ParenGap>,
) {
let mut pending = PendingLinks::new();
let mut current = expr;
loop {
match ¤t.kind {
ExpressionKind::CallExpression(call) => {
let callee = call_callee_step(call, current.span, input, nodes);
pending.push(PendingLink::Call {
call,
span: current.span,
descended: callee.is_some(),
});
match callee {
Some(callee) => current = callee,
None => break,
}
}
ExpressionKind::MemberExpression(member) => {
let object = member_object_step(member, current.span, input, nodes);
pending.push(PendingLink::Member {
member,
span: current.span,
});
match object {
Some(object) => current = object,
None => break,
}
}
ExpressionKind::TSNonNullExpression(non_null) => {
match non_null_step(non_null, current.span, input, nodes) {
NonNullStep::Done => break,
NonNullStep::Finish => {
pending.push(PendingLink::NonNull {
non_null,
span: current.span,
});
break;
}
NonNullStep::Descend(inner) => {
pending.push(PendingLink::NonNull {
non_null,
span: current.span,
});
current = inner;
}
}
}
_ => {
push_chain_base(current, input, nodes);
break;
}
}
}
while let Some(link) = pending.pop() {
match link {
PendingLink::Call {
call,
span,
descended,
} => {
if descended {
finish_call_callee(call, span, nodes);
}
nodes.push(ChainNode::call(call, span));
}
PendingLink::Member { member, span } => {
linearize_member_node(member, span, input, nodes, paren_gaps);
}
PendingLink::NonNull { non_null, span } => finish_non_null(non_null, span, nodes),
}
}
}
#[expect(clippy::inline_always)] #[inline(always)]
fn push_chain_base<'a>(
expr: &'a Expression<'_>,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) {
let needs_parens = needs_parens(expr, ParenContext::ChainBase, false)
|| input
.region_kept_shells
.is_some_and(|marks| marks.is_marked(expr.span()));
nodes.push(ChainNode::base(expr, needs_parens));
}
enum NonNullStep<'a> {
Done,
Finish,
Descend(&'a Expression<'a>),
}
#[expect(clippy::inline_always)] #[inline(always)]
fn non_null_step<'a>(
non_null: &'a ast::TSNonNullExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) -> NonNullStep<'a> {
let inner = non_null.expression;
if let Some(start) =
non_null_operand_paren_leading_start(non_null, span, input.printer.comments)
{
nodes.push(ChainNode::paren_base_before_non_null(
inner, start, span.end,
));
nodes.push(ChainNode::non_null_after_paren_operand());
NonNullStep::Done
} else if input
.region_kept_shells
.is_some_and(|marks| marks.is_marked(inner.span()))
{
nodes.push(ChainNode::base(inner, true));
NonNullStep::Finish
} else {
NonNullStep::Descend(inner)
}
}
#[expect(clippy::inline_always)] #[inline(always)]
fn finish_non_null(
non_null: &ast::TSNonNullExpression<'_>,
span: Span,
nodes: &mut ChainNodeVec<'_>,
) {
if let Some(ChainNode::Base {
needs_parens: true,
paren_comment_end,
followed_by_non_null,
..
}) = nodes.last_mut()
{
*paren_comment_end = Some(span.end);
*followed_by_non_null = true;
nodes.push(ChainNode::non_null_after_paren_operand());
} else {
nodes.push(ChainNode::non_null(non_null, span));
}
}
fn linearize_member_object<'a>(
member: &'a ast::MemberExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
paren_gaps: &mut Vec<ParenGap>,
) {
if let Some(object) = member_object_step(member, span, input, nodes) {
linearize_spine(object, input, nodes, paren_gaps);
}
}
#[expect(clippy::inline_always)] #[inline(always)]
fn member_object_step<'a>(
member: &'a ast::MemberExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) -> Option<&'a Expression<'a>> {
let object: &Expression<'_> = member.object;
if let Some(start) = member_object_paren_leading_start(member, span) {
push_sealed_chain_base(object, start, nodes);
None
} else if input
.region_kept_shells
.is_some_and(|marks| marks.is_marked(object.span()))
{
nodes.push(ChainNode::base(object, true));
None
} else {
Some(object)
}
}
pub(crate) fn transparent_instantiation_callee<'a>(
call: &'a ast::CallExpression<'a>,
) -> Option<&'a ast::TSInstantiationExpression<'a>> {
match &call.callee.kind {
ExpressionKind::TSInstantiationExpression(inst) if call.type_arguments.is_none() => {
Some(inst)
}
_ => None,
}
}
pub(crate) fn transparent_callee_sealed_chain<'a>(
call: &'a ast::CallExpression<'a>,
) -> Option<(&'a Expression<'a>, u32)> {
let inst = transparent_instantiation_callee(call)?;
let open = call.callee.span().start;
child_stops_optional_chain(open, false, inst.expression).then_some((inst.expression, open))
}
fn linearize_call_callee<'a>(
call: &'a ast::CallExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
paren_gaps: &mut Vec<ParenGap>,
) {
if let Some(callee) = call_callee_step(call, span, input, nodes) {
linearize_spine(callee, input, nodes, paren_gaps);
finish_call_callee(call, span, nodes);
}
}
#[expect(clippy::inline_always)] #[inline(always)]
fn call_callee_step<'a>(
call: &'a ast::CallExpression<'_>,
span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
) -> Option<&'a Expression<'a>> {
if child_stops_optional_chain(span.start, call.optional, call.callee) {
push_sealed_chain_base(call.callee, span.start, nodes);
return None;
}
if let Some((chain, open)) = transparent_callee_sealed_chain(call) {
push_sealed_chain_base(chain, open, nodes);
return None;
}
if input
.region_kept_shells
.is_some_and(|marks| marks.is_marked(call.callee.span()))
{
nodes.push(ChainNode::base(call.callee, true));
return None;
}
Some(transparent_instantiation_callee(call).map_or(call.callee, |inst| inst.expression))
}
#[expect(clippy::inline_always)] #[inline(always)]
fn finish_call_callee(call: &ast::CallExpression<'_>, span: Span, nodes: &mut ChainNodeVec<'_>) {
if let Some(start) = call_callee_paren_leading_start(call, span)
&& let Some(ChainNode::Base {
paren_leading_start,
..
}) = nodes.last_mut()
{
*paren_leading_start = Some(start);
}
}
pub(super) fn member_lookup_node<'a>(
member: &'a ast::MemberExpression<'_>,
member_span: Span,
input: LinearizeInput<'_>,
) -> ChainNode<'a> {
let mut nodes = ChainNodeVec::new();
let mut paren_gaps = Vec::new();
linearize_member_node(member, member_span, input, &mut nodes, &mut paren_gaps);
debug_assert!(paren_gaps.is_empty() && nodes.len() == 1);
#[expect(clippy::unreachable)] let Some(node) = nodes.pop() else {
unreachable!("a member linearizes to its own lookup node");
};
node
}
fn linearize_member_node<'a>(
member: &'a ast::MemberExpression<'_>,
member_span: Span,
input: LinearizeInput<'_>,
nodes: &mut ChainNodeVec<'a>,
paren_gaps: &mut Vec<ParenGap>,
) {
let source = input.printer.source;
let member_start = member_span.start;
let object_start = member.object.span().start;
if member_start < object_start {
for i in (0..nodes.len()).rev() {
match &nodes[i] {
ChainNode::Member { .. }
| ChainNode::PrivateMember { .. }
| ChainNode::ComputedMember { .. } => {
paren_gaps.push((i, nodes.len(), member_start, object_start));
break;
}
ChainNode::Base { .. } => break,
_ => continue,
}
}
}
let operand_end = member.object.span().end;
let object_end = Printer::gap_start_after_owned_pair(
operand_end,
member_object_paren_leading_start(member, member_span)
.and_then(|_| paren_shell_close_after(source, operand_end))
.map(|close| (operand_end, close)),
);
let object_end = if object_end == operand_end {
input.printer.member_gap_start(member.object)
} else {
object_end
};
let property_start = member.property.span().start;
if member.computed {
nodes.push(ChainNode::computed_member(
member.property,
member.optional,
object_end,
member_span.end,
));
} else if let ExpressionKind::Identifier(id) = &member.property.kind {
nodes.push(ChainNode::member(
id.ident_name(),
member.optional,
object_end,
property_start,
));
} else if let ExpressionKind::PrivateIdentifier(pid) = &member.property.kind {
nodes.push(ChainNode::private_member(
pid.name,
member.optional,
object_end,
property_start,
pid.name_span(member.property.span).start,
));
} else {
nodes.push(ChainNode::computed_member(
member.property,
member.optional,
object_end,
member_span.end,
));
}
}
pub(super) fn group_chain_nodes<'a>(
nodes: &'a [ChainNode<'a>],
printer: &Printer<'_>,
) -> ChainGroupVec<'a> {
if nodes.is_empty() {
return ChainGroupVec::new();
}
let mut groups: ChainGroupVec<'a> = ChainGroupVec::new();
let mut open = 0;
let mut i = 1;
while i < nodes.len() {
let node = &nodes[i];
if (node.is_call() || node.is_non_null() || node.is_numeric_accessor())
&& !gap_has_line_comment(node, printer)
{
i += 1;
} else {
break;
}
}
if !nodes[0].is_call() {
while i + 1 < nodes.len()
&& nodes[i].is_member()
&& nodes[i + 1].is_member()
&& !gap_has_line_comment(&nodes[i], printer)
{
i += 1;
}
}
groups.push(ChainGroup::new(&nodes[open..i]));
open = i;
let mut seen_call = false;
while i < nodes.len() {
let node = &nodes[i];
if (seen_call && node.is_member() && !node.is_numeric_accessor())
|| gap_has_line_comment(node, printer)
{
if open < i {
groups.push(ChainGroup::new(&nodes[open..i]));
open = i;
}
seen_call = false;
}
if node.is_call() {
seen_call = true;
}
i += 1;
}
if open < i {
groups.push(ChainGroup::new(&nodes[open..i]));
}
#[cfg(debug_assertions)]
{
let mut offset = 0;
for group in &groups {
debug_assert!(
std::ptr::eq(group.nodes.as_ptr(), nodes[offset..].as_ptr()),
"chain group at node {offset} is not the next run of the linearized chain"
);
offset += group.nodes.len();
}
debug_assert_eq!(
offset,
nodes.len(),
"chain groups must cover the whole linearized chain"
);
}
#[cfg(feature = "_internal_buffer_stats")]
crate::printer::buffer_stats::record_chain_groups(groups.len());
groups
}
pub(super) fn should_merge_first_groups<'a>(
groups: &[ChainGroup<'a>],
in_expression_statement: bool,
printer: &Printer<'_>,
) -> bool {
if groups.len() < 2 {
return false;
}
if gap_has_line_comment(&groups[1].nodes[0], printer) {
return false;
}
should_not_wrap(groups, in_expression_statement, printer)
}
#[expect(clippy::inline_always)]
#[inline(always)]
fn gap_has_line_comment(node: &ChainNode<'_>, printer: &Printer<'_>) -> bool {
chain_gap_share_any(node, printer.source, |start, end| {
printer.has_line_comments_between(start, end)
})
}
fn should_not_wrap<'a>(
groups: &[ChainGroup<'a>],
in_expression_statement: bool,
printer: &Printer<'_>,
) -> bool {
if groups.len() < 2 {
return false;
}
let first = &groups[0];
let has_computed = groups[1].nodes.first().is_some_and(ChainNode::is_computed);
if first.nodes.len() == 1 {
let ChainNode::Base { expr, .. } = &first.nodes[0] else {
return false;
};
match &expr.kind {
ExpressionKind::Super(_) => true,
ExpressionKind::ThisExpression(_) => true,
ExpressionKind::Identifier(id) => {
is_factory_name(id.ident_name(), id.span.start, printer)
|| has_computed
|| (in_expression_statement
&& is_short_name(id.ident_name(), id.span.start, printer))
}
_ => has_computed,
}
} else {
if let Some((prop, prop_start)) = first.nodes.last().and_then(ChainNode::property) {
return is_factory_name(prop, prop_start, printer) || has_computed;
}
false
}
}
fn is_short_name(name: IdentName<'_>, name_start: u32, printer: &Printer<'_>) -> bool {
printer.with_ident_name_at(name, name_start, |name| name.len() <= TAB_WIDTH)
}
fn is_factory_name(name: IdentName<'_>, name_start: u32, printer: &Printer<'_>) -> bool {
printer.with_ident_name_at(
name,
name_start,
crate::printer::expressions::literals::is_factory_identifier_name,
)
}
pub(crate) fn build_linearized_chain_doc(
nodes: &mut [ChainNode<'_>],
long_curried: bool,
head_expr: &Expression<'_>,
chain_start: u32,
span: Span,
printer: &Printer<'_>,
) -> DocId {
let HeadWindow {
outer: head_outer,
start: head_start,
end: head_end,
} = chain_head_comment_window(nodes, head_expr, chain_start);
let frozen_base = match nodes.first() {
Some(ChainNode::Base { expr: base, .. }) => {
printer.left_spine_operand_frozen_span(head_start, base)
}
_ => None,
};
let outer_ungroups = printer
.chain_ungroups_args
.replace(long_curried && mark_long_curried_calls(nodes, printer));
let groups = group_chain_nodes(nodes, printer);
let claim = match nodes.first() {
Some(ChainNode::Base {
expr: base,
needs_parens,
paren_leading_start: None,
..
}) if head_start == chain_start => {
printer.left_spine_run_claim(span, chain_start, base, || {
*needs_parens || printer.left_spine_shell_has_own_line_comment(base)
})
}
_ => None,
};
let right_claim = match &*nodes {
[ChainNode::Base { expr: base, .. }, link, ..] => link
.comment_range()
.map(|(gap_start, _)| gap_start)
.filter(|&gap_start| gap_start < base.span().end)
.map(|gap_start| Span::new(gap_start, base.span().end)),
_ => None,
};
let outer_right_claim = printer.claimed_right_spine_run.replace(right_claim);
let doc = printer.with_left_spine_claim(claim, || {
let chain_doc =
printer.with_frozen_chain_base(frozen_base, || build_chain_doc(&groups, span, printer));
printer.chain_ungroups_args.set(outer_ungroups);
let chain_doc =
printer.prepend_removed_paren_comments(span, head_start, head_end, chain_doc);
head_outer
.iter()
.rev()
.fold(chain_doc, |doc, &(start, end)| {
printer.prepend_removed_paren_comments(span, start, end, doc)
})
});
printer.claimed_right_spine_run.set(outer_right_claim);
doc
}
fn chain_head_comment_window(
nodes: &[ChainNode<'_>],
expr: &Expression<'_>,
chain_start: u32,
) -> HeadWindow {
let start = nodes
.iter()
.filter_map(ChainNode::paren_gap_skip)
.map(|skip| skip.start)
.max()
.unwrap_or(chain_start);
let end = match nodes.first() {
Some(ChainNode::Base {
paren_leading_start: Some(pair_start),
..
}) => *pair_start,
Some(ChainNode::Base { expr: base, .. }) => {
let base_start = base.span().start;
debug_assert_eq!(base_start, get_chain_base_start(expr));
base_start
}
_ => get_chain_base_start(expr),
};
debug_assert!(start <= end, "a widened claim opens past the head's end");
HeadWindow {
outer: widened_claim_holes(nodes, chain_start, start),
start,
end,
}
}
struct HeadWindow {
outer: SmallVec<[(u32, u32); 1]>,
start: u32,
end: u32,
}
fn widened_claim_holes(
nodes: &[ChainNode<'_>],
chain_start: u32,
inner_start: u32,
) -> SmallVec<[(u32, u32); 1]> {
let mut holes = SmallVec::new();
if inner_start == chain_start {
return holes;
}
let mut claims: SmallVec<[(u32, u32); 4]> = nodes
.iter()
.filter_map(|node| Some((node.comment_range()?.0, node.paren_gap_skip()?.start)))
.collect();
claims.sort_unstable();
let mut cursor = chain_start;
for (claim_start, claim_end) in claims {
if cursor < claim_start {
holes.push((cursor, claim_start));
}
cursor = cursor.max(claim_end);
}
debug_assert_eq!(
cursor, inner_start,
"the innermost claim ends where the head resumes"
);
holes
}
fn get_chain_base_start<'a>(mut expr: &'a Expression<'a>) -> u32 {
loop {
expr = match &expr.kind {
ExpressionKind::MemberExpression(member) => member.object,
ExpressionKind::CallExpression(call) => {
transparent_instantiation_callee(call).map_or(call.callee, |inst| inst.expression)
}
ExpressionKind::TSNonNullExpression(non_null) => non_null.expression,
_ => return expr.span().start,
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::__private::PrinterInputs;
use crate::ast::{CallExpression, Identifier, MemberExpression};
use bumpalo::Bump;
use tsv_lang::doc::arena::DocArena;
use tsv_lang::printing::LineBreaks;
use tsv_lang::{EmbedContext, Span};
fn make_printer<'a>(
arena: &'a DocArena,
lines: &'a LineBreaks<'a>,
comments: &'a [Comment],
) -> Printer<'a> {
let inputs = PrinterInputs {
source: "",
comments,
line_table: lines.table(),
has_owned_comments: false,
has_format_ignore: false,
comment_free_window: None,
};
Printer::with_context(arena, &inputs, EmbedContext::line_owning(), 0)
}
fn make_identifier<'arena>(name: &'arena str) -> Expression<'arena> {
let len = name.len() as u32;
let ident_name = IdentName {
escaped: Some(name),
raw_len: 0,
plain_ascii: false,
};
Expression::from_identifier(Identifier::simple(ident_name, Span::new(0, len)))
}
fn make_member<'arena>(
arena: &'arena Bump,
object: Expression<'arena>,
property_name: &'arena str,
object_end: u32,
) -> Expression<'arena> {
let prop_name = IdentName {
escaped: Some(property_name),
raw_len: 0,
plain_ascii: false,
};
let property_start = object_end + 1; let span_end = property_start + property_name.len() as u32;
Expression {
span: Span::new(0, span_end),
kind: ExpressionKind::MemberExpression(MemberExpression {
object: arena.alloc(object),
property: arena.alloc(Expression::from_identifier(Identifier::simple(
prop_name,
Span::new(property_start, span_end),
))),
computed: false,
optional: false,
}),
}
}
fn make_call<'arena>(
arena: &'arena Bump,
callee: Expression<'arena>,
callee_end: u32,
) -> Expression<'arena> {
Expression {
kind: ExpressionKind::CallExpression(CallExpression {
callee: arena.alloc(callee),
arguments: &[],
type_arguments: None,
optional: false,
}),
span: Span::new(0, callee_end + "()".len() as u32),
}
}
#[test]
fn test_linearize_simple_identifier() {
let expr = make_identifier("foo");
let docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&docs, &lines, &[]);
let nodes = linearize_chain(&expr, printer.linearize_input());
assert_eq!(nodes.len(), 1);
assert!(matches!(
nodes[0],
ChainNode::Base {
needs_parens: false,
..
}
));
}
#[test]
fn test_linearize_member_chain() {
let arena = Bump::new();
let a = make_identifier("a");
let ab = make_member(&arena, a, "b", 1);
let abc = make_member(&arena, ab, "c", 3);
let docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&docs, &lines, &[]);
let nodes = linearize_chain(&abc, printer.linearize_input());
assert_eq!(nodes.len(), 3);
assert!(matches!(nodes[0], ChainNode::Base { .. }));
assert!(matches!(nodes[1], ChainNode::Member { .. }));
assert!(matches!(nodes[2], ChainNode::Member { .. }));
}
#[test]
fn test_linearize_call_chain() {
let arena = Bump::new();
let a = make_identifier("a");
let a_call = make_call(&arena, a, 1);
let ab = make_member(&arena, a_call, "b", 3);
let ab_call = make_call(&arena, ab, 5);
let docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&docs, &lines, &[]);
let nodes = linearize_chain(&ab_call, printer.linearize_input());
assert_eq!(nodes.len(), 4);
assert!(matches!(nodes[0], ChainNode::Base { .. }));
assert!(nodes[1].is_call());
assert!(nodes[2].is_member());
assert!(nodes[3].is_call());
}
#[test]
fn test_group_member_only_chain() {
let arena = Bump::new();
let a = make_identifier("a");
let ab = make_member(&arena, a, "b", 1);
let abc = make_member(&arena, ab, "c", 3);
let abcd = make_member(&arena, abc, "d", 5);
let docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&docs, &lines, &[]);
let nodes = linearize_chain(&abcd, printer.linearize_input());
let groups = group_chain_nodes(&nodes, &printer);
assert!(!groups.is_empty());
assert!(
groups[0]
.nodes
.iter()
.any(|n| matches!(n, ChainNode::Base { .. }))
);
}
#[test]
fn test_group_call_chain_breaks_after_call() {
let arena = Bump::new();
let a = make_identifier("a");
let a_call = make_call(&arena, a, 1);
let ab = make_member(&arena, a_call, "b", 3);
let ab_call = make_call(&arena, ab, 5);
let abc = make_member(&arena, ab_call, "c", 7);
let docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&docs, &lines, &[]);
let nodes = linearize_chain(&abc, printer.linearize_input());
let groups = group_chain_nodes(&nodes, &printer);
assert!(groups.len() >= 2, "Should have at least 2 groups");
assert!(
groups[0]
.nodes
.iter()
.any(|n| matches!(n, ChainNode::Base { .. }))
);
assert!(groups[0].nodes.iter().any(ChainNode::is_call));
}
#[test]
fn test_group_empty_input() {
let arena_docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&arena_docs, &lines, &[]);
let groups = group_chain_nodes(&[], &printer);
assert!(groups.is_empty());
}
fn make_line_comment(start: u32) -> Comment {
Comment {
content_span: Span::new(start + 2, start + 4),
is_block: false,
multiline: false,
span: Span::new(start, start + 4),
from_template_reader: false,
owned_by_node: false,
}
}
fn make_member_node<'arena>(
name: &'arena str,
object_end: u32,
property_start: u32,
) -> ChainNode<'arena> {
let ident_name = IdentName {
escaped: Some(name),
raw_len: 0,
plain_ascii: false,
};
ChainNode::member(ident_name, false, object_end, property_start)
}
#[test]
fn test_group_splits_at_a_member_whose_gap_holds_a_line_comment() {
let arena = Bump::new();
let base = make_identifier("a");
let call_expr = make_call(&arena, make_identifier("a"), 1);
let ExpressionKind::CallExpression(call) = &call_expr.kind else {
panic!("make_call builds a CallExpression")
};
let members: Vec<ChainNode<'_>> = (0..4)
.map(|i| make_member_node("m", 20 + i * 10, 26 + i * 10))
.collect();
for (label, nodes) in [
("member-only", {
let mut v = vec![ChainNode::base(&base, false)];
v.extend(members.iter().copied());
v
}),
("after a call", {
let mut v = vec![
ChainNode::base(&base, false),
ChainNode::call(call, call_expr.span),
];
v.extend(members.iter().copied());
v
}),
] {
for (commented, member) in members.iter().enumerate() {
let (gap_start, _) = member.comment_range().unwrap();
let comments = [make_line_comment(gap_start + 1)];
let arena_docs = DocArena::new();
let lines = LineBreaks::new("", Vec::new());
let printer = make_printer(&arena_docs, &lines, &comments);
let groups = group_chain_nodes(&nodes, &printer);
let mut found = false;
for group in &groups {
for (idx, node) in group.nodes.iter().enumerate() {
if node.comment_range() != member.comment_range() {
continue;
}
found = true;
assert_eq!(
idx, 0,
"{label}: member {commented} with a `//` in its gap must \
start its group, not sit at index {idx}"
);
}
}
assert!(
found,
"{label}: member {commented} vanished from the grouping"
);
}
}
}
fn linearized(root: &Expression<'_>, input: LinearizeInput<'_>, frames: Option<u32>) -> String {
let (mut nodes, mut paren_gaps) = (ChainNodeVec::new(), Vec::new());
match frames {
Some(frames) => linearize_link(root, input, &mut nodes, &mut paren_gaps, frames),
None => linearize_spine_loop(root, input, &mut nodes, &mut paren_gaps),
}
format!("{nodes:?} {paren_gaps:?}")
}
fn chain_roots<'a>(expr: &'a Expression<'a>, out: &mut Vec<&'a Expression<'a>>) {
out.push(expr);
match &expr.kind {
ExpressionKind::CallExpression(call) => {
chain_roots(call.callee, out);
for argument in call.arguments {
chain_roots(argument, out);
}
}
ExpressionKind::MemberExpression(member) => chain_roots(member.object, out),
ExpressionKind::TSNonNullExpression(non_null) => {
chain_roots(non_null.expression, out);
}
ExpressionKind::TSInstantiationExpression(inst) => chain_roots(inst.expression, out),
ExpressionKind::TaggedTemplateExpression(tagged) => chain_roots(tagged.tag, out),
_ => {}
}
}
#[test]
fn linearize_spine_runs_the_halves_in_the_recursions_order() {
let short = [
"a.b.c.d;",
"a().b().c!.d;",
"a?.b.c?.().d;",
"(a?.b).c.d();",
"(a?.b)!.c();",
"(a?.b!).c().d;",
"(a?.b)()!.c;",
"(a?.b)<T>?.(c).d;",
"((a?.b)<T>)?.(c).d;",
"f<T>(x).y<U>().z;",
"f<T><U>(x).y;",
"( // c\n() => {})().p.q();",
"(function () {})().p;",
"(/* c */ () => 1)()().p!;",
"(aaa // c\n)!.bbb.ccc();",
"(aaa /* a\nb */)!.bbb!.ccc;",
"(x + y /* c */)!.foo().bar;",
"/* c */ (a).b.c();",
"(/* c */ (a).b).c().d;",
"((a).b /* d */).c().d();",
"(((a).b).c /* e */).d().e;",
"a[0][1].b!()!.c;",
"new a.b().c.d();",
"a`t`.b.c();",
"(a, b).c().d;",
"(await x).y.z();",
"a!.b!!.c!();",
"a.b(c.d().e, (f?.g).h()).i;",
"a.b`x${c.d()}`.e();",
"x.y(() => z.w().v).u;",
];
let long_tail = ".b!/* c */.c()?.d[0]".repeat(CHAIN_FRAMES as usize / 2);
let sources: Vec<String> = short
.iter()
.map(|&source| source.to_owned())
.chain(short.iter().map(|source| {
let chain = source.trim_end_matches(";\n").trim_end_matches(';');
format!("({chain}){long_tail}.e;")
}))
.collect();
for source in &sources {
let source = source.as_str();
let arena = Bump::new();
let program = crate::__private::parse(source, &arena).expect("parses");
let mut roots = Vec::new();
for statement in program.body {
if let ast::StatementKind::ExpressionStatement(statement) = &statement.kind {
chain_roots(statement.expression, &mut roots);
}
}
let every = SpanMarks::default();
let alternate = SpanMarks::default();
for (i, root) in roots.iter().enumerate() {
every.mark(root.span);
if i % 2 == 1 {
alternate.mark(root.span);
}
}
let docs = DocArena::new();
let lines = LineBreaks::new(source, Vec::new());
let inputs = PrinterInputs {
source,
comments: program.comments,
line_table: lines.table(),
has_owned_comments: false,
has_format_ignore: false,
comment_free_window: None,
};
let printer = Printer::with_context(&docs, &inputs, EmbedContext::line_owning(), 0);
for marks in [None, Some(&every), Some(&alternate)] {
let input = LinearizeInput {
printer: &printer,
region_kept_shells: marks,
};
for root in &roots {
let recursed = linearized(root, input, Some(u32::MAX));
let looped = linearized(root, input, None);
let switched = linearized(root, input, Some(CHAIN_FRAMES));
let at = &source[root.span.range()];
assert_eq!(looped, recursed, "{source:?} from {at:?}");
assert_eq!(switched, recursed, "{source:?} from {at:?}");
}
}
}
}
}