use crate::ast;
use crate::printer::comments::CommentVec;
use smallvec::smallvec;
use tsv_lang::doc::DocBuf;
use tsv_lang::{Comment, CommentPosition, Span, classify_comment, doc::arena::DocId};
use super::Printer;
struct DecoratorItems {
items: DocBuf,
has_line_comment: bool,
has_own_line_run: bool,
}
impl<'a> Printer<'a> {
pub(in crate::printer) fn build_decorator_expression_doc(
&self,
decorator: &ast::Decorator<'_>,
) -> DocId {
let d = self.d();
let expr_doc = self.build_expression_doc(&decorator.expression);
if can_decorator_expression_unparenthesized(&decorator.expression) {
expr_doc
} else {
d.parens(expr_doc)
}
}
fn push_decorator_head(&self, parts: &mut DocBuf, decorator: &ast::Decorator<'_>) {
let d = self.d();
parts.push(d.text("@"));
if let Some(c) =
self.build_rhs_comments_opt(decorator.span.start + 1, decorator.expression.span().start)
{
parts.push(c);
}
parts.push(self.build_decorator_expression_doc(decorator));
self.push_trailing_comments_in_range(
parts,
decorator.expression.span().end,
decorator.span.end,
);
}
fn push_decorator_run_blank(
&self,
parts: &mut DocBuf,
decorator: &ast::Decorator<'_>,
own_line_run: &[&Comment],
) {
let Some(first) = own_line_run.first() else {
return;
};
let from = self.blank_scan_start(decorator.span.end, first.span.start);
if self.has_blank_line_between(from, first.span.start) {
parts.push(self.d().literalline());
}
}
fn collect_decorator_gap_comments<'c>(
&'c self,
buf: &mut DocBuf,
pending: &mut DocBuf,
decorator: &ast::Decorator<'_>,
boundary: u32,
is_last: bool,
) -> (CommentVec<'c>, bool) {
let mut own_line: CommentVec<'c> = CommentVec::new();
let mut has_line = false;
for comment in self.comments_to_emit_between(decorator.span.end, boundary) {
has_line |= !comment.is_block;
match self.classify_decorator_gap_comment(
comment,
decorator.span.end,
boundary,
is_last,
) {
CommentPosition::Trailing => {
buf.push(self.build_trailing_comment_doc(comment));
}
CommentPosition::LeadingInline => pending.push(self.build_comment_doc(comment)),
CommentPosition::LeadingOwnLine => own_line.push(comment),
}
}
(own_line, has_line)
}
fn build_decorator_own_line_comment_run(&self, comments: &[&Comment]) -> DocId {
let d = self.d();
let mut parts = DocBuf::new();
let mut prev_end: Option<u32> = None;
for comment in comments {
if let Some(pe) = prev_end {
if self.is_same_line(pe, comment.span.start) {
parts.push(d.text(" "));
} else {
self.push_blank_preserving_hardline(&mut parts, pe, comment.span.start);
}
}
parts.push(self.build_comment_doc(comment));
prev_end = Some(comment.span.end);
}
d.concat(&parts)
}
fn push_pending_decorator_prefix(&self, buf: &mut DocBuf, pending: &mut DocBuf) {
let d = self.d();
for leading in std::mem::take(pending) {
buf.push(leading);
buf.push(d.text(" "));
}
}
fn collect_decorator_items(
&self,
decorators: &[ast::Decorator<'_>],
next_token_start: u32,
) -> DecoratorItems {
let d = self.d();
let mut collected = DecoratorItems {
items: DocBuf::new(),
has_line_comment: false,
has_own_line_run: false,
};
let mut pending: DocBuf = DocBuf::new();
for (i, decorator) in decorators.iter().enumerate() {
let next = decorators.get(i + 1);
let boundary = next.map_or(next_token_start, |n| n.span.start);
let is_last = next.is_none();
let mut item: DocBuf = DocBuf::new();
self.push_pending_decorator_prefix(&mut item, &mut pending);
self.push_decorator_head(&mut item, decorator);
let (own_line_refs, gap_has_line) = self.collect_decorator_gap_comments(
&mut item,
&mut pending,
decorator,
boundary,
is_last,
);
collected.has_line_comment |= gap_has_line;
collected.has_own_line_run |= !own_line_refs.is_empty();
self.push_decorator_run_blank(&mut item, decorator, &own_line_refs);
collected.items.push(d.concat(&item));
if !own_line_refs.is_empty() {
collected
.items
.push(self.build_decorator_own_line_comment_run(&own_line_refs));
}
}
collected
}
fn classify_decorator_gap_comment(
&self,
comment: &Comment,
prev_end: u32,
boundary: u32,
is_last: bool,
) -> CommentPosition {
if is_last {
return match classify_comment(comment, prev_end, boundary, self.source) {
CommentPosition::Trailing => CommentPosition::Trailing,
_ => CommentPosition::LeadingOwnLine,
};
}
if self.is_same_line(comment.span.end, boundary) {
CommentPosition::LeadingInline
} else if self.is_same_line(prev_end, comment.span.start) {
CommentPosition::Trailing
} else {
CommentPosition::LeadingOwnLine
}
}
fn has_newline_after_any_decorator(&self, decorators: &[ast::Decorator<'_>]) -> bool {
if self.canonical {
return false;
}
decorators.iter().any(|dec| {
let end = dec.span.end as usize;
self.source[end..]
.bytes()
.find(|&b| b != b' ' && b != b'\t')
.is_some_and(|b| b == b'\n' || b == b'\r')
})
}
pub(in crate::printer) fn member_decorator_boundary(
&self,
decorators: Option<&[ast::Decorator<'_>]>,
span_start: u32,
) -> u32 {
decorators
.and_then(|decs| decs.last())
.map_or(span_start, |dec| self.find_first_token_after(dec.span.end))
}
pub(in crate::printer) fn param_has_decorators(&self, expr: &ast::Expression<'_>) -> bool {
param_decorators(expr).is_some()
}
pub(in crate::printer) fn param_start_with_decorators(
&self,
expr: &ast::Expression<'_>,
) -> u32 {
param_decorators(expr).map_or_else(|| expr.span().start, |decs| decs[0].span.start)
}
pub(in crate::printer) fn param_render_span(&self, expr: &ast::Expression<'_>) -> Span {
Span::new(self.param_start_with_decorators(expr), expr.span().end)
}
#[inline]
pub(in crate::printer) fn param_binding_frozen_span(
&self,
param: &ast::Expression<'_>,
) -> Option<Span> {
if !self.has_format_ignore {
return None;
}
let decorators = param_decorators(param)?;
self.member_gap_frozen(decorators.last()?.span.end, param.span().start)
.then(|| param.span())
}
#[inline]
pub(in crate::printer) fn build_frozen_param_binding_doc(
&self,
param: &ast::Expression<'_>,
) -> Option<DocId> {
let frozen = self.param_binding_frozen_span(param)?;
Some(self.with_param_decorators(
param_decorators(param),
self.build_frozen_span_doc(frozen),
frozen.start,
))
}
pub(in crate::printer) fn with_param_decorators(
&self,
decorators: Option<&[ast::Decorator<'_>]>,
inner: DocId,
inner_start: u32,
) -> DocId {
let Some(decorators) = non_empty_decorators(decorators) else {
return inner;
};
if !self.has_comments_to_emit_between(decorators[0].span.start, inner_start) {
let d = self.d();
let mut parts = DocBuf::new();
if self.has_newline_after_any_decorator(decorators) {
parts.push(d.break_parent());
}
let sep = d.line();
for decorator in decorators {
parts.push(d.text("@"));
parts.push(self.build_decorator_expression_doc(decorator));
parts.push(sep);
}
parts.push(inner);
return d.group(d.concat(&parts));
}
self.build_param_decorators_doc(decorators, inner, inner_start)
}
fn build_param_decorators_doc(
&self,
decorators: &[ast::Decorator<'_>],
inner: DocId,
inner_start: u32,
) -> DocId {
let d = self.d();
let collected = self.collect_decorator_items(decorators, inner_start);
let own_line = self.has_newline_after_any_decorator(decorators)
|| collected.has_line_comment
|| collected.has_own_line_run;
let mut segments = collected.items;
segments.push(inner);
let joined = d.join_doc(segments, d.line());
let body = if own_line {
d.concat(&[d.break_parent(), joined])
} else {
joined
};
d.group(body)
}
pub(in crate::printer) fn build_decorators_doc(
&self,
decorators: Option<&[ast::Decorator<'_>]>,
next_token_start: u32,
) -> Option<DocId> {
let decorators = non_empty_decorators(decorators)?;
let d = self.d();
let items = self
.collect_decorator_items(decorators, next_token_start)
.items;
Some(d.concat(&[d.join_doc(items, d.hardline()), d.hardline()]))
}
pub(in crate::printer) fn build_class_member_decorators_doc(
&self,
decorators: Option<&[ast::Decorator<'_>]>,
next_token_start: u32,
) -> Option<DocId> {
let decorators = non_empty_decorators(decorators)?;
let d = self.d();
let has_newline_after = self.has_newline_after_any_decorator(decorators);
let collected = self.collect_decorator_items(decorators, next_token_start);
let trailing = if has_newline_after {
d.hardline()
} else {
d.line()
};
let needs_break = collected.has_line_comment || collected.has_own_line_run;
let joined = d.join_doc(collected.items, d.line());
let mut group_parts: DocBuf = smallvec![joined, trailing];
if needs_break {
group_parts.push(d.break_parent());
}
Some(d.group(d.concat(&group_parts)))
}
}
fn param_decorators<'arena>(
expr: &ast::Expression<'arena>,
) -> Option<&'arena [ast::Decorator<'arena>]> {
let decorators = match &expr.kind {
ast::ExpressionKind::Identifier(id) => id.decorators(),
ast::ExpressionKind::ObjectPattern(obj) => obj.decorators,
ast::ExpressionKind::ArrayPattern(arr) => arr.decorators,
ast::ExpressionKind::AssignmentPattern(ap) => ap.decorators,
ast::ExpressionKind::TSParameterProperty(pp) => param_decorators(pp.parameter),
_ => None,
};
non_empty_decorators(decorators)
}
pub(in crate::printer) fn non_empty_decorators<'d, 'arena>(
decorators: Option<&'d [ast::Decorator<'arena>]>,
) -> Option<&'d [ast::Decorator<'arena>]> {
decorators.filter(|decs| !decs.is_empty())
}
pub(in crate::printer) fn class_expr_has_decorators(c: &ast::ClassExpression<'_>) -> bool {
non_empty_decorators(c.decorators).is_some()
}
fn is_decorator_member_expression(mut expr: &ast::Expression<'_>) -> bool {
loop {
match &expr.kind {
ast::ExpressionKind::Identifier(_) => return true,
ast::ExpressionKind::MemberExpression(member)
if !member.computed
&& !member.optional
&& matches!(member.property.kind, ast::ExpressionKind::Identifier(_)) =>
{
expr = member.object;
}
_ => return false,
}
}
}
pub(crate) fn decorator_takes_pair(expr: &ast::Expression<'_>) -> bool {
fn unwrap<'e, 'a>(mut expr: &'e ast::Expression<'a>) -> &'e ast::Expression<'a> {
while let ast::ExpressionKind::ParenthesizedExpression(paren) = &expr.kind {
expr = paren.expression;
}
expr
}
!match &expr.kind {
ast::ExpressionKind::CallExpression(call) => {
!call.optional && is_decorator_member_expression(unwrap(call.callee))
}
_ => is_decorator_member_expression(unwrap(expr)),
}
}
fn can_decorator_expression_unparenthesized(expr: &ast::Expression<'_>) -> bool {
match &expr.kind {
ast::ExpressionKind::CallExpression(call) => {
!call.optional && is_decorator_member_expression(call.callee)
}
_ => is_decorator_member_expression(expr),
}
}