use crate::ast::{self, AssignmentOperator, Expression, ExpressionKind};
use crate::printer::calls::chain_has_calls;
use crate::printer::chain::{chain_paren_leading_gap, find_bracket_position};
use crate::printer::class_expr_has_decorators;
use crate::printer::comments::ValueGap;
use crate::printer::conditional_should_break_after_op;
use crate::printer::expressions::functions::HeadForcedChainLead;
use crate::printer::expressions::literals::format_string_literal_from_ast;
use crate::printer::is_string_literal;
use crate::printer::types::helpers::unwrap_parenthesized;
use crate::printer::{AssumedBreak, Printer};
use tsv_lang::PRINT_WIDTH;
use tsv_lang::Span;
use tsv_lang::doc::after_operator::{
after_operator_assumed_fluid, fluid_after_operator, hang_after_operator,
};
use tsv_lang::doc::arena::{DocArena, DocId};
pub(crate) const MIN_OVERLAP_FOR_BREAK: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AssignmentLayout {
BreakAfterOperator,
NeverBreakAfterOperator,
Fluid,
BreakLhs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AssignmentLeft {
Plain,
ShortKey,
ComplexPattern,
}
impl AssignmentLeft {
pub(crate) fn short_key(is_short_key: bool) -> Self {
if is_short_key {
Self::ShortKey
} else {
Self::Plain
}
}
}
impl<'a> Printer<'a> {
pub(in crate::printer) fn mark_jsdoc_cast_value_gap(&self, value: &Expression<'_>) {
self.jsdoc_cast_value_gap_target.set(match &value.kind {
ExpressionKind::CommentBoundPair(_) => Some(value.span),
_ => None,
});
}
pub(in crate::printer) fn jsdoc_cast_in_value_gap(&self, span: Span) -> bool {
self.jsdoc_cast_value_gap_target.get() == Some(span)
}
}
pub(crate) fn choose_layout(
right_expr: &Expression<'_>,
left: AssignmentLeft,
can_break_left: bool,
printer: &Printer<'_>,
) -> AssignmentLayout {
if left == AssignmentLeft::ComplexPattern {
return AssignmentLayout::BreakLhs;
}
if is_curried_arrow_chain_owning_break(right_expr) {
return AssignmentLayout::Fluid;
}
if let ExpressionKind::BinaryExpression(binary) = &right_expr.kind
&& !should_inline_logical_expression(binary)
{
return AssignmentLayout::BreakAfterOperator;
}
if matches!(right_expr.kind, ExpressionKind::SequenceExpression(_)) {
return AssignmentLayout::BreakAfterOperator;
}
if let ExpressionKind::ClassExpression(c) = &right_expr.kind
&& class_expr_has_decorators(c)
{
return AssignmentLayout::BreakAfterOperator;
}
if conditional_should_break_after_op(right_expr) {
return AssignmentLayout::BreakAfterOperator;
}
if is_curried_arrow_chain_that_breaks(right_expr) {
return AssignmentLayout::BreakAfterOperator;
}
if left == AssignmentLeft::ShortKey && !can_break_left {
return AssignmentLayout::NeverBreakAfterOperator;
}
if should_break_after_operator(right_expr, printer) {
return AssignmentLayout::BreakAfterOperator;
}
if is_simple_value(right_expr) && !can_break_left {
return AssignmentLayout::NeverBreakAfterOperator;
}
AssignmentLayout::Fluid
}
pub fn should_inline_logical_expression(binary: &ast::BinaryExpression<'_>) -> bool {
if !binary.operator.is_logical() {
return false;
}
match &binary.rebalanced_right().kind {
ExpressionKind::ObjectExpression(obj) => !obj.properties.is_empty(),
ExpressionKind::ArrayExpression(arr) => !arr.elements.is_empty(),
_ => false,
}
}
pub(crate) fn is_curried_arrow_chain_that_breaks(expr: &Expression<'_>) -> bool {
is_curried_arrow_chain(expr)
&& matches!(&expr.kind, ExpressionKind::ArrowFunctionExpression(arrow) if arrow_chain_should_break(arrow))
}
pub(crate) fn is_curried_arrow_chain_owning_break(expr: &Expression<'_>) -> bool {
is_curried_arrow_chain(expr) && !is_curried_arrow_chain_that_breaks(expr)
}
pub(crate) fn is_curried_arrow_chain(expr: &Expression<'_>) -> bool {
if let ExpressionKind::ArrowFunctionExpression(arrow) = &expr.kind {
matches!(
&arrow.body,
ast::ArrowFunctionBody::Expression(body)
if matches!(body.kind, ExpressionKind::ArrowFunctionExpression(_))
)
} else {
false
}
}
pub(crate) fn arrow_chain_should_break(arrow: &ast::ArrowFunctionExpression<'_>) -> bool {
let has_non_identifier_param = arrow
.params
.iter()
.any(|p| !matches!(p.kind, ExpressionKind::Identifier(_)));
let should_break = (arrow.return_type.is_some() && !arrow.params.is_empty())
|| arrow.type_parameters.is_some()
|| has_non_identifier_param;
if should_break {
return true;
}
if let ast::ArrowFunctionBody::Expression(body) = &arrow.body
&& let ExpressionKind::ArrowFunctionExpression(inner) = &body.kind
{
return arrow_chain_should_break(inner);
}
false
}
pub(crate) fn should_break_after_operator(expr: &Expression<'_>, printer: &Printer<'_>) -> bool {
let core_expr = unwrap_expression(expr);
if is_string_literal(core_expr) {
return true;
}
is_poorly_breakable_chain(core_expr, printer)
}
fn unwrap_expression<'a>(mut expr: &'a Expression<'a>) -> &'a Expression<'a> {
loop {
expr = match &expr.kind {
ExpressionKind::TSNonNullExpression(non_null) => non_null.expression,
ExpressionKind::AwaitExpression(await_expr) => await_expr.argument,
ExpressionKind::UnaryExpression(unary) => unary.argument,
ExpressionKind::YieldExpression(yield_expr) => match yield_expr.argument {
Some(arg) => arg,
None => return expr,
},
_ => return expr,
};
}
}
fn is_call_with_complex_type_arguments(
call: &ast::CallExpression<'_>,
printer: &Printer<'_>,
) -> bool {
use ast::TSType;
let Some(type_args) = &call.type_arguments else {
return false;
};
if type_args.params.len() > 1 {
return true;
}
if printer.has_comments_on_page_between(type_args.span.start, type_args.span.end)
&& type_args.span.extract(printer.source).contains('\n')
{
return true;
}
let listed = matches!(
type_args.params.first().copied().map(unwrap_parenthesized),
Some(TSType::TypeLiteral(_) | TSType::Union(_) | TSType::Intersection(_))
);
listed
|| (type_args.span.extract(printer.source).contains('\n')
&& printer
.d()
.will_break(printer.build_type_parameter_instantiation_doc(type_args)))
}
pub(in crate::printer) struct MappedTypeArgHug {
mapped: u32,
line: Option<DocId>,
}
impl Printer<'_> {
pub(in crate::printer) fn mapped_type_arg_hug(
&self,
value: &Expression<'_>,
gap_comment_free: impl FnOnce() -> bool,
) -> Option<MappedTypeArgHug> {
let mut core = value;
let mut awaited = false;
let call = loop {
match &core.kind {
ExpressionKind::CallExpression(call) => break call,
ExpressionKind::TSNonNullExpression(non_null) => core = non_null.expression,
ExpressionKind::AwaitExpression(await_expr) if !awaited => {
awaited = true;
core = await_expr.argument;
}
_ => return None,
}
};
let type_args = call.type_arguments.as_ref()?;
let [ast::TSType::Mapped(mapped)] = type_args.params else {
return None;
};
if !gap_comment_free() || call.optional {
return None;
}
if self.has_comments_on_page_between(value.span.start, value.span.end)
|| !is_poorly_breakable_chain(core, self)
{
return None;
}
let d = self.d();
let line = (!call.arguments.is_empty()).then(|| {
d.concat(&[
d.text(if awaited { "await " } else { "" }),
self.build_expression_doc(call.callee),
self.build_type_parameter_instantiation_doc(type_args),
d.text("("),
])
});
Some(MappedTypeArgHug {
mapped: mapped.span.start,
line,
})
}
pub(in crate::printer) fn build_mapped_type_arg_hug_tail(
&self,
hug: &MappedTypeArgHug,
value_doc: DocId,
build_assumed: impl FnOnce() -> DocId,
) -> DocId {
let d = self.d();
let probe = match hug.line {
Some(line) => d.line_probe(0, line),
None => value_doc,
};
let assumed = self.with_assumed_break(AssumedBreak::MappedType(hug.mapped), build_assumed);
after_operator_assumed_fluid(d, value_doc, probe, assumed)
}
}
pub(crate) fn is_poorly_breakable_chain(expr: &Expression<'_>, printer: &Printer<'_>) -> bool {
let walk = PoorlyBreakableWalk {
printer,
chain_has_calls: chain_has_calls(expr),
};
is_poorly_breakable_chain_walk(expr, false, &walk)
}
struct PoorlyBreakableWalk<'a, 'src> {
printer: &'a Printer<'src>,
chain_has_calls: bool,
}
fn is_poorly_breakable_chain_walk(
mut expr: &Expression<'_>,
mut deep: bool,
walk: &PoorlyBreakableWalk<'_, '_>,
) -> bool {
let printer = walk.printer;
loop {
match &expr.kind {
ExpressionKind::TSNonNullExpression(non_null) => {
expr = non_null.expression;
}
ExpressionKind::CallExpression(call) => {
let is_trivial_call = call.arguments.is_empty()
|| (call.arguments.len() == 1
&& is_short_arg(call.arguments[0], printer.source)
&& !call_arg_has_comments(call, expr.span, printer));
if !is_trivial_call {
return false;
}
if is_call_with_complex_type_arguments(call, printer) {
return false;
}
if matches!(
call.callee.kind,
ExpressionKind::MemberExpression(_) | ExpressionKind::TSNonNullExpression(_)
) && crate::printer::chain::call_prints_as_member_chain(call, expr.span, printer)
{
return false;
}
expr = call.callee;
deep = true;
}
ExpressionKind::MemberExpression(member) => {
if walk.chain_has_calls {
let object_end = printer.member_gap_start(member.object);
let gap_end = if member.computed {
find_bracket_position(
printer.source,
object_end,
member.property.span().start,
)
} else {
member.property.span().start
};
if printer
.comments_on_page_between(object_end, gap_end)
.any(|c| {
!tsv_lang::source_scan::has_newline_before_position(
printer.source,
c.span.start,
)
})
{
return false;
}
}
expr = member.object;
deep = true;
}
ExpressionKind::Identifier(_) | ExpressionKind::ThisExpression(_) => return deep,
_ => return false,
}
}
}
fn is_short_arg(expr: &Expression<'_>, source: &str) -> bool {
let threshold = PRINT_WIDTH / 4;
match &expr.kind {
ExpressionKind::Identifier(id) => id.span.extract(source).len() <= threshold,
ExpressionKind::UnaryExpression(unary) => is_short_arg(unary.argument, source),
ExpressionKind::RegexLiteral(regex) => regex.pattern(source).len() <= threshold,
ExpressionKind::Literal(lit) if matches!(lit.value, ast::LiteralValue::String { .. }) => {
format_string_literal_from_ast(lit, source).len() <= threshold
}
ExpressionKind::TemplateLiteral(template) => {
template.expressions.is_empty()
&& !template.quasis.is_empty()
&& template.quasis[0].raw(source).len() <= threshold
&& !crate::printer::template_literal_has_newlines(template)
}
ExpressionKind::CallExpression(call) => {
call.arguments.is_empty()
&& matches!(&call.callee.kind, ExpressionKind::Identifier(id)
if id.span.extract(source).len() <= threshold.saturating_sub(2))
}
ExpressionKind::Literal(_) => true,
ExpressionKind::ThisExpression(_) | ExpressionKind::Super(_) => true,
_ => false,
}
}
fn chain_has_multiline_string_arg(mut expr: &Expression<'_>, source: &str) -> bool {
loop {
expr = match &expr.kind {
ExpressionKind::CallExpression(call) => {
if call
.arguments
.iter()
.any(|arg| arg_is_multiline_string(arg, source))
{
return true;
}
call.callee
}
ExpressionKind::MemberExpression(member) => member.object,
ExpressionKind::TSNonNullExpression(non_null) => non_null.expression,
_ => return false,
};
}
}
fn arg_is_multiline_string(expr: &Expression<'_>, source: &str) -> bool {
match &expr.kind {
ExpressionKind::UnaryExpression(unary) => arg_is_multiline_string(unary.argument, source),
ExpressionKind::Literal(lit) if matches!(lit.value, ast::LiteralValue::String { .. }) => {
let raw = lit.span.extract(source);
raw.contains('\n') || raw.contains('\r')
}
_ => false,
}
}
fn call_arg_has_comments(
call: &ast::CallExpression<'_>,
span: Span,
printer: &Printer<'_>,
) -> bool {
if call.arguments.is_empty() {
return false;
}
let args_region_start = call.callee.span().end;
let args_region_end = span.end;
printer.has_comments_on_page_between(args_region_start, args_region_end)
}
pub(crate) fn is_type_assertion_call(expr: &Expression<'_>, source: &str) -> bool {
let call = match &expr.kind {
ExpressionKind::TSAsExpression(as_expr) => match &as_expr.expression.kind {
ExpressionKind::CallExpression(call) => call,
_ => return false,
},
ExpressionKind::TSSatisfiesExpression(sat_expr) => match &sat_expr.expression.kind {
ExpressionKind::CallExpression(call) => call,
_ => return false,
},
_ => return false,
};
!(call.arguments.is_empty()
|| call.arguments.len() == 1 && is_short_arg(call.arguments[0], source))
}
fn is_member_only_chain(mut expr: &Expression<'_>) -> bool {
loop {
expr = match &expr.kind {
ExpressionKind::MemberExpression(member) => member.object,
ExpressionKind::TSNonNullExpression(non_null) => non_null.expression,
ExpressionKind::Identifier(_)
| ExpressionKind::ThisExpression(_)
| ExpressionKind::Super(_) => return true,
_ => return false,
};
}
}
pub(crate) fn is_simple_value(expr: &Expression<'_>) -> bool {
matches!(
&expr.kind,
ExpressionKind::Literal(lit) if matches!(
lit.value,
ast::LiteralValue::Boolean(_)
| ast::LiteralValue::Number(_)
)
) || matches!(
expr.kind,
ExpressionKind::TemplateLiteral(_) | ExpressionKind::TaggedTemplateExpression(_)
) || matches!(&expr.kind, ExpressionKind::ClassExpression(c) if !class_expr_has_decorators(c))
}
pub(crate) struct RhsCommentInfo {
pub comments: Option<DocId>,
pub has_line_comment: bool,
pub glued_through: bool,
pub gap: Option<ValueGap>,
pub boundary: Option<u32>,
pub frozen: Option<Span>,
}
impl RhsCommentInfo {
pub(crate) fn indentable_leads_value(&self) -> bool {
self.gap.is_some_and(|gap| gap.indentable_leads_value)
}
pub(crate) fn frozen_only(frozen: Option<Span>) -> Self {
Self {
comments: None,
has_line_comment: false,
glued_through: false,
gap: None,
boundary: None,
frozen,
}
}
}
impl<'a> Printer<'a> {
pub(crate) fn mark_assignment_value(&self, value: &Expression<'_>) {
self.assignment_value_target.set(Some(value.span()));
}
pub(crate) fn build_assignment_layout(
&self,
left_doc: DocId,
operator: DocId,
right_expr: &Expression<'_>,
left: AssignmentLeft,
rhs_info: RhsCommentInfo,
) -> DocId {
let d = self.d();
if let Some(frozen) = rhs_info.frozen {
return self.build_frozen_assignment_doc(
left_doc,
operator,
right_expr,
frozen,
rhs_info.comments,
rhs_info.boundary,
);
}
let can_break_left = (left == AssignmentLeft::ShortKey || is_simple_value(right_expr))
&& d.can_break(left_doc);
let mut layout = choose_layout(right_expr, left, can_break_left, self);
if layout == AssignmentLayout::BreakAfterOperator
&& rhs_info
.boundary
.is_some_and(|boundary| self.value_shell_forced_open(right_expr, boundary))
{
layout = AssignmentLayout::NeverBreakAfterOperator;
}
if rhs_info.has_line_comment && layout != AssignmentLayout::BreakAfterOperator {
layout = AssignmentLayout::BreakAfterOperator;
}
if layout != AssignmentLayout::BreakAfterOperator
&& self.left_spine_shell_has_own_line_comment(right_expr)
{
layout = AssignmentLayout::BreakAfterOperator;
}
if layout != AssignmentLayout::BreakAfterOperator
&& let Some(comments_doc) = rhs_info.comments
&& d.will_break(comments_doc)
&& !rhs_info.glued_through
{
layout = AssignmentLayout::BreakAfterOperator;
}
if (self.has_line_comments_in_member_chain(right_expr)
|| (layout == AssignmentLayout::BreakAfterOperator
&& self.has_line_comments_in_call_chain(right_expr)))
&& !self.left_spine_shell_has_own_line_comment(right_expr)
{
layout = AssignmentLayout::NeverBreakAfterOperator;
}
if layout != AssignmentLayout::BreakAfterOperator
&& self.value_hangs_under_operator(rhs_info.indentable_leads_value(), right_expr)
{
layout = AssignmentLayout::BreakAfterOperator;
}
let is_curried_chain = is_curried_arrow_chain(right_expr);
let chain_hangs = layout == AssignmentLayout::BreakAfterOperator;
self.mark_jsdoc_cast_value_gap(right_expr);
self.mark_assignment_value(right_expr);
let seam_prints_pair = self.for_init_in_needs_parens(right_expr);
let chain_leads = is_curried_arrow_chain_owning_break(right_expr);
let chain_prints_run = chain_leads && !chain_hangs;
let forced_chain_lead = (is_curried_chain && !chain_leads).then(|| {
rhs_info.gap.map_or(HeadForcedChainLead::Stacked, |gap| {
self.head_forced_chain_lead(right_expr, gap.start)
})
});
if forced_chain_lead == Some(HeadForcedChainLead::PreservedBlock)
&& layout == AssignmentLayout::BreakAfterOperator
{
layout = AssignmentLayout::NeverBreakAfterOperator;
}
let build_right = |hoisted_run: Option<DocId>| {
let build = || {
if let Some(boundary) = rhs_info.boundary {
self.build_value_doc_keeping_relational_sibling_pair(right_expr, boundary)
} else {
self.wrap_relational_sibling_pair(
right_expr,
false,
self.build_expression_doc(right_expr),
)
}
};
if !is_curried_chain {
build()
} else if let Some(lead) = forced_chain_lead {
self.build_with_arrow_chain_context(lead.chain_context(), build)
} else if chain_hangs {
match rhs_info.gap {
Some(gap) => self.build_hung_value_doc(right_expr, gap.start, build),
None => build(),
}
} else {
let run = hoisted_run.or(rhs_info.comments);
let value_start = right_expr.span().start;
let gap_start = rhs_info.gap.map_or(value_start, |gap| gap.start);
self.build_led_curried_chain_doc(run, (gap_start, value_start), build)
}
};
#[cfg(debug_assertions)]
let outer_index_breaks = self.computed_index_breaks.replace(false);
let (hoisted_run, right_doc) = match rhs_info.gap {
Some(gap) => {
self.hoist_owned_value_gap_run(gap.start, right_expr, seam_prints_pair, build_right)
}
None => (None, build_right(None)),
};
#[cfg(debug_assertions)]
let index_breaks = {
let index_breaks = self.computed_index_breaks.get();
self.computed_index_breaks
.set(outer_index_breaks || index_breaks);
index_breaks
};
#[cfg(not(debug_assertions))]
let index_breaks = false;
let right_doc = if self.shell_supplies_for_init_pair(right_expr, rhs_info.boundary) {
right_doc
} else {
self.wrap_for_init_in(right_expr, right_doc)
};
debug_assert!(
{
let core_expr = unwrap_expression(right_expr);
let rhs_span = right_expr.span();
let shell_gap_forces_break = rhs_info.boundary.is_some_and(|boundary| {
self.value_shell_forced_open(right_expr, boundary)
|| self.has_multiline_block_comments_on_page_between(rhs_span.end, boundary)
});
self.owned_leading_comment_at(rhs_span.start).is_some()
|| self.has_comments_on_page_between(rhs_span.start, rhs_span.end)
|| shell_gap_forces_break
|| chain_has_multiline_string_arg(core_expr, self.source)
|| index_breaks
|| !is_poorly_breakable_chain(core_expr, self)
|| !d.will_break(right_doc)
},
"is_poorly_breakable_chain classified expression as poorly breakable but the \
printed doc contains forced breaks — static analysis missed a break-emitting node"
);
let gap_comments = hoisted_run.or(rhs_info.comments);
let right_doc_with_comments = if chain_prints_run {
right_doc
} else if let Some(comments_doc) = gap_comments {
d.concat(&[comments_doc, right_doc])
} else {
right_doc
};
match layout {
AssignmentLayout::BreakAfterOperator => {
let value_span = right_expr.span();
let comment_free = || {
gap_comments.is_none()
&& rhs_info.gap.is_none_or(|gap| {
!self.has_comments_on_page_between(gap.start, value_span.start)
})
&& rhs_info.boundary.is_none_or(|boundary| {
!self.has_comments_on_page_between(value_span.end, boundary)
})
};
let tail = self
.mapped_type_arg_hug(right_expr, comment_free)
.map(|hug| {
self.build_mapped_type_arg_hug_tail(&hug, right_doc_with_comments, || {
build_right(None)
})
});
let tail = tail.unwrap_or_else(|| hang_after_operator(d, right_doc_with_comments));
d.group(d.concat(&[d.group(left_doc), operator, tail]))
}
AssignmentLayout::NeverBreakAfterOperator => {
d.group(d.concat(&[
d.group(left_doc),
operator,
d.text(" "),
right_doc_with_comments,
]))
}
AssignmentLayout::BreakLhs => {
d.group(d.concat(&[
d.unwrap_group(left_doc),
operator,
d.text(" "),
d.group(right_doc_with_comments),
]))
}
AssignmentLayout::Fluid => {
let tail = self
.after_operator_value_nullable_union_doc(right_doc_with_comments, right_expr)
.unwrap_or_else(|| fluid_after_operator(d, right_doc_with_comments));
d.group(d.concat(&[d.group(left_doc), operator, tail]))
}
}
}
fn build_frozen_assignment_doc(
&self,
left_doc: DocId,
operator: DocId,
right_expr: &Expression<'_>,
frozen: Span,
comments: Option<DocId>,
boundary: Option<u32>,
) -> DocId {
let d = self.d();
let frozen_doc = match boundary {
Some(boundary) => {
self.build_frozen_value_shell_doc(right_expr, frozen, boundary, false)
}
None => self.build_frozen_expression_doc(right_expr, frozen),
};
let shell_keeps_parens = self.shell_supplies_for_init_pair(right_expr, boundary);
let frozen_doc =
self.wrap_frozen_for_init_in(right_expr, frozen, shell_keeps_parens, frozen_doc);
let rhs = match comments {
Some(comments_doc) => d.concat(&[comments_doc, frozen_doc]),
None => frozen_doc,
};
d.group(d.concat(&[d.group(left_doc), operator, hang_after_operator(d, rhs)]))
}
pub(crate) fn has_line_comments_in_member_chain(&self, expr: &Expression<'_>) -> bool {
if !is_member_only_chain(expr) {
return false;
}
self.has_line_comments_in_chain(expr)
}
pub(crate) fn has_line_comments_in_call_chain(&self, expr: &Expression<'_>) -> bool {
chain_has_calls(expr) && self.has_line_comments_in_chain(expr)
}
pub(crate) fn has_import_with_trailing_comments(&self, expr: &Expression<'_>) -> bool {
match &expr.kind {
ExpressionKind::ImportExpression(import) => {
let paren_close = expr.span.end;
let last_arg_end = import
.options
.as_ref()
.map_or_else(|| import.source.span().end, |opts| opts.span().end);
self.has_comments_to_emit_between(last_arg_end, paren_close)
}
ExpressionKind::AwaitExpression(await_expr) => {
self.has_import_with_trailing_comments(await_expr.argument)
}
_ => false,
}
}
fn has_line_comments_in_chain(&self, mut expr: &Expression<'_>) -> bool {
loop {
if let Some((start, base_start)) = chain_paren_leading_gap(expr, self.comments)
&& self.has_line_comments_between(start, base_start)
{
return true;
}
expr = match &expr.kind {
ExpressionKind::CallExpression(call) => {
let args_gap_start = call
.type_arguments
.as_ref()
.map_or_else(|| call.callee.span().end, |ta| ta.span.end);
let args_gap_end = call
.arguments
.first()
.map_or(expr.span.end, |arg| arg.span().start);
let callee_gap_breaks = self
.has_line_comments_between(args_gap_start, args_gap_end)
&& tsv_lang::source_scan::find_char_skipping_comments(
self.source.as_bytes(),
args_gap_start as usize,
args_gap_end as usize,
b'(',
)
.is_some_and(|paren| {
self.has_line_comments_between(args_gap_start, paren as u32)
});
if callee_gap_breaks {
return true;
}
call.callee
}
ExpressionKind::MemberExpression(member) => {
let obj_end = self.member_gap_start(member.object);
let prop_start = member.property.span().start;
if self.has_line_comments_between(obj_end, prop_start) {
let prop_start = if member.computed {
find_bracket_position(self.source, obj_end, prop_start)
} else {
prop_start
};
if !self.member_gap_frozen(obj_end, prop_start)
&& self.has_line_comments_between(obj_end, prop_start)
{
return true;
}
}
member.object
}
ExpressionKind::TSNonNullExpression(non_null) => {
let operand_end = non_null.expression.span().end;
if self.has_line_spanning_comments_to_emit_between(operand_end, expr.span.end) {
return true;
}
non_null.expression
}
_ => return false,
};
}
}
}
impl AssignmentOperator {
#[inline]
pub(in crate::printer) fn doc_with_leading_space(self, d: &DocArena) -> DocId {
match self {
AssignmentOperator::Assign => d.text(" ="),
AssignmentOperator::AddAssign => d.text(" +="),
AssignmentOperator::SubtractAssign => d.text(" -="),
AssignmentOperator::MultiplyAssign => d.text(" *="),
AssignmentOperator::DivideAssign => d.text(" /="),
AssignmentOperator::RemainderAssign => d.text(" %="),
AssignmentOperator::ExponentiateAssign => d.text(" **="),
AssignmentOperator::LeftShiftAssign => d.text(" <<="),
AssignmentOperator::RightShiftAssign => d.text(" >>="),
AssignmentOperator::UnsignedRightShiftAssign => d.text(" >>>="),
AssignmentOperator::BitwiseOrAssign => d.text(" |="),
AssignmentOperator::BitwiseXorAssign => d.text(" ^="),
AssignmentOperator::BitwiseAndAssign => d.text(" &="),
AssignmentOperator::LogicalOrAssign => d.text(" ||="),
AssignmentOperator::LogicalAndAssign => d.text(" &&="),
AssignmentOperator::NullishAssign => d.text(" ??="),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assignment_operator_doc_matches_its_string_table() {
let d = DocArena::new();
for op in [
AssignmentOperator::Assign,
AssignmentOperator::AddAssign,
AssignmentOperator::SubtractAssign,
AssignmentOperator::MultiplyAssign,
AssignmentOperator::DivideAssign,
AssignmentOperator::RemainderAssign,
AssignmentOperator::ExponentiateAssign,
AssignmentOperator::LeftShiftAssign,
AssignmentOperator::RightShiftAssign,
AssignmentOperator::UnsignedRightShiftAssign,
AssignmentOperator::BitwiseOrAssign,
AssignmentOperator::BitwiseXorAssign,
AssignmentOperator::BitwiseAndAssign,
AssignmentOperator::LogicalOrAssign,
AssignmentOperator::LogicalAndAssign,
AssignmentOperator::NullishAssign,
] {
assert_eq!(
op.doc_with_leading_space(&d),
d.text(op.as_str_with_leading_space()),
"{op:?}"
);
}
}
}