use crate::ast;
use crate::printer::CommentSpacing;
use crate::printer::HeritageKeyword;
use crate::printer::Printer;
use crate::printer::class_expr_has_decorators;
use crate::printer::types::ShiftRescan;
use smallvec::smallvec;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
pub(in crate::printer) struct ClassHeritagePositions {
pub pre_heritage_end: u32,
pub extends_keyword_start: Option<u32>,
pub implements_keyword_start: Option<u32>,
pub first_heritage_start: Option<u32>,
pub extends_clause_end: Option<u32>,
pub header_end: u32,
}
pub(in crate::printer) enum ClassTypeParamsGap {
Name(u32),
Keyword(u32),
}
pub(in crate::printer) enum ClassHeaderLayout {
Independent,
Group,
GroupBreak,
}
impl ClassHeaderLayout {
fn from_flags(group_mode: bool, has_heritage_line_comments: bool) -> Self {
match (group_mode, has_heritage_line_comments) {
(false, _) => Self::Independent,
(true, false) => Self::Group,
(true, true) => Self::GroupBreak,
}
}
pub(in crate::printer) fn is_group(&self) -> bool {
!matches!(self, Self::Independent)
}
}
pub(in crate::printer) struct ClassHeaderOptions {
pub body_is_empty: bool,
pub body_start: u32,
pub layout: ClassHeaderLayout,
pub body_frozen: bool,
}
impl<'a> Printer<'a> {
pub(in crate::printer) fn class_heritage_positions(
&self,
class_keyword_start: u32,
id: Option<&ast::Identifier<'_>>,
type_parameters: Option<&ast::TSTypeParameterDeclaration<'_>>,
super_class: Option<&ast::Expression<'_>>,
super_type_parameters: Option<&ast::TSTypeParameterInstantiation<'_>>,
implements: &[ast::TSInterfaceHeritage<'_>],
) -> ClassHeritagePositions {
let pre_heritage_end = type_parameters.map_or_else(
|| id.map_or(class_keyword_start + "class".len() as u32, |id| id.span.end),
|tp| tp.span.end,
);
let extends_keyword_start = super_class.and_then(|sc| {
self.find_keyword_in_range(pre_heritage_end, sc.span().start, "extends")
});
let implements_keyword_start = if implements.is_empty() {
None
} else {
let search_start = extends_keyword_start.map_or(pre_heritage_end, |ek| {
super_class.map_or(ek + "extends".len() as u32, |sc| {
super_type_parameters.map_or_else(|| sc.span().end, |tp| tp.span.end)
})
});
self.find_keyword_in_range(search_start, implements[0].span.start, "implements")
};
let first_heritage_start = extends_keyword_start.or(implements_keyword_start);
let extends_clause_end = super_class
.map(|sc| super_type_parameters.map_or_else(|| sc.span().end, |tp| tp.span.end));
let header_end = implements
.last()
.map(|i| i.span.end)
.or(extends_clause_end)
.or_else(|| type_parameters.map(|tp| tp.span.end))
.or_else(|| id.map(|id| id.span.end))
.unwrap_or(class_keyword_start + "class".len() as u32);
ClassHeritagePositions {
pre_heritage_end,
extends_keyword_start,
implements_keyword_start,
first_heritage_start,
extends_clause_end,
header_end,
}
}
pub(in crate::printer) fn class_header_layout(
&self,
positions: &ClassHeritagePositions,
super_class: Option<&ast::Expression<'_>>,
super_type_parameters: Option<&ast::TSTypeParameterInstantiation<'_>>,
implements: &[ast::TSInterfaceHeritage<'_>],
) -> ClassHeaderLayout {
let extends_to_implements = positions
.extends_clause_end
.filter(|_| !implements.is_empty())
.map(|ext_end| (ext_end, implements[0].span.start));
let has_heritage_comments = positions
.first_heritage_start
.is_some_and(|hs| self.has_comments_on_page_between(positions.pre_heritage_end, hs))
|| extends_to_implements.is_some_and(|(a, b)| self.has_comments_on_page_between(a, b));
let group_mode =
self.should_class_group_mode(super_class, super_type_parameters, implements)
|| has_heritage_comments;
let has_heritage_line_comments = positions
.first_heritage_start
.is_some_and(|hs| self.has_line_comments_between(positions.pre_heritage_end, hs))
|| extends_to_implements.is_some_and(|(a, b)| self.has_line_comments_between(a, b));
ClassHeaderLayout::from_flags(group_mode, has_heritage_line_comments)
}
pub(in crate::printer) fn push_class_type_params(
&self,
parts: &mut DocBuf,
type_parameters: Option<&ast::TSTypeParameterDeclaration<'_>>,
gap: ClassTypeParamsGap,
) {
let Some(type_params) = type_parameters else {
return;
};
let list_start = type_params.span.start;
match gap {
ClassTypeParamsGap::Name(name_end) => {
self.push_name_to_type_params_comments(
parts,
name_end,
list_start,
CommentSpacing::Trailing,
);
parts.push(self.build_type_parameter_declaration_doc_wrapping(type_params));
}
ClassTypeParamsGap::Keyword(keyword_start) => {
let list_doc = self.build_type_parameter_declaration_doc_wrapping(type_params);
if !self.has_comments_to_emit_between(keyword_start, list_start) {
parts.push(list_doc);
return;
}
let d = self.d();
let run = self.build_name_to_type_params_comments(
keyword_start,
list_start,
CommentSpacing::Trailing,
);
let body = d.concat(&[run, list_doc]);
parts.push(if self.keyword_gap_breaks(keyword_start, list_start) {
d.indent(body)
} else {
body
});
}
}
}
pub(in crate::printer) fn should_class_group_mode(
&self,
super_class: Option<&ast::Expression<'_>>,
super_type_parameters: Option<&ast::TSTypeParameterInstantiation<'_>>,
implements: &[ast::TSInterfaceHeritage<'_>],
) -> bool {
let mut count = if super_class.is_some() { 1 } else { 0 };
count += implements.len();
if count > 1 {
return true;
}
if let Some(super_class) = super_class
&& super_type_parameters.is_none()
&& matches!(super_class.kind, ast::ExpressionKind::MemberExpression(_))
{
return true;
}
false
}
pub(in crate::printer) fn build_class_extends_doc(
&self,
super_class: Option<&ast::Expression<'_>>,
super_type_parameters: Option<&ast::TSTypeParameterInstantiation<'_>>,
extends_keyword_start: Option<u32>,
) -> Option<DocId> {
let d = self.d();
let super_class = super_class?;
if let Some(kw_start) = extends_keyword_start {
let kw_end = kw_start + "extends".len() as u32;
if self.comments_force_own_line_between(kw_end, super_class.span().start) {
let mut value_parts: DocBuf =
smallvec![self.build_super_class_doc(super_class, super_type_parameters)];
if let Some(type_args) = super_type_parameters {
let gap_start = super_class.span().end;
if let Some(doc) = self.build_name_to_type_params_comments_opt(
gap_start,
type_args.span.start,
CommentSpacing::Trailing,
) {
value_parts.push(doc);
}
value_parts.push(self.build_type_arguments_doc(type_args, ShiftRescan::Never));
}
let value_doc = d.concat(&value_parts);
let mut ext_parts = smallvec![d.text("extends")];
self.append_keyword_value_line_comments(
&mut ext_parts,
kw_end,
super_class.span().start,
value_doc,
);
return Some(d.concat(&ext_parts));
}
}
let mut ext_parts: DocBuf = smallvec![d.text("extends ")];
if let Some(kw_start) = extends_keyword_start {
let kw_end = kw_start + "extends".len() as u32;
if let Some(comments) = self.build_inline_comments_between_doc_trailing_space_opt(
kw_end,
super_class.span().start,
) {
ext_parts.push(comments);
}
}
ext_parts.push(self.build_super_class_doc(super_class, super_type_parameters));
if let Some(type_args) = super_type_parameters {
let gap_start = super_class.span().end;
let gap_end = type_args.span.start;
if let Some(doc) = self.build_name_to_type_params_comments_opt(
gap_start,
gap_end,
CommentSpacing::Trailing,
) {
ext_parts.push(doc);
}
ext_parts.push(self.build_type_arguments_doc(type_args, ShiftRescan::Never));
}
Some(d.concat(&ext_parts))
}
fn build_super_class_doc(
&self,
super_class: &ast::Expression<'_>,
super_type_parameters: Option<&ast::TSTypeParameterInstantiation<'_>>,
) -> DocId {
let doc = self.build_expression_doc(super_class);
if self.needs_parens(super_class, super::ParenContext::SuperClass)
|| (super_type_parameters.is_some()
&& self.instantiation_keeps_pair(super_class, super::SecondTypeArgs::Heritage))
{
if matches!(
&super_class.kind,
ast::ExpressionKind::ClassExpression(c) if class_expr_has_decorators(c)
) {
self.build_break_open_parens(doc)
} else {
self.d().parens(doc)
}
} else {
doc
}
}
pub(in crate::printer) fn build_class_implements_doc(
&self,
implements: &[ast::TSInterfaceHeritage<'_>],
group_mode: bool,
implements_keyword_start: Option<u32>,
) -> Option<DocId> {
if implements.is_empty() {
return None;
}
Some(self.build_heritage_clause_doc(
HeritageKeyword::Implements,
implements,
group_mode,
implements_keyword_start,
))
}
pub(in crate::printer) fn build_class_header_doc(
&self,
mut parts: DocBuf,
positions: &ClassHeritagePositions,
extends_doc: Option<DocId>,
implements_doc: Option<DocId>,
implements: &[ast::TSInterfaceHeritage<'_>],
options: ClassHeaderOptions,
) -> DocId {
let d = self.d();
let header_end = positions.header_end;
if matches!(options.layout, ClassHeaderLayout::Independent) {
if let Some(ext) = extends_doc {
parts.push(d.text(" "));
parts.push(ext);
}
if let Some(impl_doc) = implements_doc {
parts.push(d.text(" "));
parts.push(impl_doc);
}
parts.push(self.build_header_pre_body_doc(
header_end,
options.body_start,
options.body_frozen,
));
return d.concat(&parts);
}
let mut extra_heritage_comments = DocBuf::new();
if let Some(heritage_start) = positions.first_heritage_start {
let (inline, indent) = self
.build_heritage_leading_comment_parts(positions.pre_heritage_end, heritage_start);
parts.extend(inline);
extra_heritage_comments = indent;
}
let mut heritage_parts = extra_heritage_comments;
if let Some(ext) = extends_doc {
heritage_parts.push(d.line());
heritage_parts.push(ext);
if let Some(ext_end) = positions.extends_clause_end
&& !implements.is_empty()
{
let mid_end = positions
.implements_keyword_start
.unwrap_or(implements[0].span.start);
if let Some(mid_comments) =
self.build_inline_comments_between_doc_opt(ext_end, mid_end)
{
heritage_parts.push(mid_comments);
}
}
}
if let Some(impl_doc) = implements_doc {
heritage_parts.push(d.line());
heritage_parts.push(impl_doc);
}
if !heritage_parts.is_empty() {
parts.push(d.indent(d.concat(&heritage_parts)));
}
let (gap_comments, gap_breaks) =
self.pre_body_gap(header_end, options.body_start, options.body_frozen);
if let Some(comments) = gap_comments {
parts.push(comments);
}
if gap_breaks {
parts.push(d.hardline());
} else if options.body_is_empty {
parts.push(d.text(" "));
} else {
parts.push(d.line());
}
let parts_doc = d.concat(&parts);
if matches!(options.layout, ClassHeaderLayout::GroupBreak) {
d.group_break(parts_doc)
} else {
d.group(parts_doc)
}
}
}