mod builder;
mod map;
use self::{builder::CommentsBuilderVisitor, map::CommentsMap};
use crate::formatter::Formatter;
use crate::{buffer::Buffer, write};
use crate::{CstFormatContext, FormatResult, FormatRule, TextSize, TransformSourceMap};
use biome_rowan::syntax::SyntaxElementKey;
use biome_rowan::{Language, SyntaxNode, SyntaxToken, SyntaxTriviaPieceComments};
use rustc_hash::FxHashSet;
#[cfg(debug_assertions)]
use std::cell::{Cell, RefCell};
use std::marker::PhantomData;
use std::rc::Rc;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum CommentKind {
InlineBlock,
Block,
Line,
}
impl CommentKind {
pub const fn is_line(&self) -> bool {
matches!(self, CommentKind::Line)
}
pub const fn is_block(&self) -> bool {
matches!(self, CommentKind::Block)
}
pub const fn is_inline_block(&self) -> bool {
matches!(self, CommentKind::InlineBlock)
}
pub const fn is_inline(&self) -> bool {
matches!(self, CommentKind::InlineBlock | CommentKind::Block)
}
}
#[derive(Debug, Clone)]
pub struct SourceComment<L: Language> {
pub(crate) lines_before: u32,
pub(crate) lines_after: u32,
pub(crate) piece: SyntaxTriviaPieceComments<L>,
pub(crate) kind: CommentKind,
#[cfg(debug_assertions)]
pub(crate) formatted: Cell<bool>,
}
impl<L: Language> SourceComment<L> {
pub fn piece(&self) -> &SyntaxTriviaPieceComments<L> {
&self.piece
}
pub fn lines_before(&self) -> u32 {
self.lines_before
}
pub fn lines_after(&self) -> u32 {
self.lines_after
}
pub fn kind(&self) -> CommentKind {
self.kind
}
#[cfg(not(debug_assertions))]
#[inline(always)]
pub fn mark_formatted(&self) {}
#[cfg(debug_assertions)]
pub fn mark_formatted(&self) {
self.formatted.set(true)
}
}
#[derive(Debug, Clone)]
pub struct DecoratedComment<L: Language> {
enclosing: SyntaxNode<L>,
preceding: Option<SyntaxNode<L>>,
following: Option<SyntaxNode<L>>,
following_token: Option<SyntaxToken<L>>,
text_position: CommentTextPosition,
lines_before: u32,
lines_after: u32,
comment: SyntaxTriviaPieceComments<L>,
kind: CommentKind,
}
impl<L: Language> DecoratedComment<L> {
pub fn enclosing_node(&self) -> &SyntaxNode<L> {
&self.enclosing
}
pub fn piece(&self) -> &SyntaxTriviaPieceComments<L> {
&self.comment
}
pub fn preceding_node(&self) -> Option<&SyntaxNode<L>> {
self.preceding.as_ref()
}
fn take_preceding_node(&mut self) -> Option<SyntaxNode<L>> {
self.preceding.take()
}
pub fn following_node(&self) -> Option<&SyntaxNode<L>> {
self.following.as_ref()
}
fn take_following_node(&mut self) -> Option<SyntaxNode<L>> {
self.following.take()
}
pub fn lines_before(&self) -> u32 {
self.lines_before
}
pub fn lines_after(&self) -> u32 {
self.lines_after
}
pub fn kind(&self) -> CommentKind {
self.kind
}
pub fn text_position(&self) -> CommentTextPosition {
self.text_position
}
pub fn following_token(&self) -> Option<&SyntaxToken<L>> {
self.following_token.as_ref()
}
}
impl<L: Language> From<DecoratedComment<L>> for SourceComment<L> {
fn from(decorated: DecoratedComment<L>) -> Self {
Self {
lines_before: decorated.lines_before,
lines_after: decorated.lines_after,
piece: decorated.comment,
kind: decorated.kind,
#[cfg(debug_assertions)]
formatted: Cell::new(false),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CommentTextPosition {
EndOfLine,
OwnLine,
SameLine,
}
impl CommentTextPosition {
pub const fn is_same_line(&self) -> bool {
matches!(self, CommentTextPosition::SameLine)
}
pub const fn is_own_line(&self) -> bool {
matches!(self, CommentTextPosition::OwnLine)
}
pub const fn is_end_of_line(&self) -> bool {
matches!(self, CommentTextPosition::EndOfLine)
}
}
#[derive(Debug)]
pub enum CommentPlacement<L: Language> {
Leading {
node: SyntaxNode<L>,
comment: SourceComment<L>,
},
Trailing {
node: SyntaxNode<L>,
comment: SourceComment<L>,
},
Dangling {
node: SyntaxNode<L>,
comment: SourceComment<L>,
},
Default(DecoratedComment<L>),
}
impl<L: Language> CommentPlacement<L> {
#[inline]
pub fn leading(node: SyntaxNode<L>, comment: impl Into<SourceComment<L>>) -> Self {
Self::Leading {
node,
comment: comment.into(),
}
}
pub fn dangling(node: SyntaxNode<L>, comment: impl Into<SourceComment<L>>) -> Self {
Self::Dangling {
node,
comment: comment.into(),
}
}
#[inline]
pub fn trailing(node: SyntaxNode<L>, comment: impl Into<SourceComment<L>>) -> Self {
Self::Trailing {
node,
comment: comment.into(),
}
}
#[inline]
pub fn or_else<F>(self, f: F) -> Self
where
F: FnOnce(DecoratedComment<L>) -> CommentPlacement<L>,
{
match self {
CommentPlacement::Default(comment) => f(comment),
placement => placement,
}
}
}
pub trait CommentStyle: Default {
type Language: Language;
fn is_suppression(_text: &str) -> bool {
false
}
fn get_comment_kind(comment: &SyntaxTriviaPieceComments<Self::Language>) -> CommentKind;
fn place_comment(
&self,
comment: DecoratedComment<Self::Language>,
) -> CommentPlacement<Self::Language> {
CommentPlacement::Default(comment)
}
}
#[derive(Debug, Clone, Default)]
pub struct Comments<L: Language> {
data: Rc<CommentsData<L>>,
}
impl<L: Language> Comments<L> {
pub fn from_node<Style>(
root: &SyntaxNode<L>,
style: &Style,
source_map: Option<&TransformSourceMap>,
) -> Self
where
Style: CommentStyle<Language = L>,
{
let builder = CommentsBuilderVisitor::new(style, source_map);
let (comments, skipped) = builder.visit(root);
Self {
data: Rc::new(CommentsData {
root: Some(root.clone()),
is_suppression: Style::is_suppression,
comments,
with_skipped: skipped,
#[cfg(debug_assertions)]
checked_suppressions: RefCell::new(Default::default()),
}),
}
}
#[inline]
pub fn has_comments(&self, node: &SyntaxNode<L>) -> bool {
self.data.comments.has(&node.key())
}
#[inline]
pub fn has_leading_comments(&self, node: &SyntaxNode<L>) -> bool {
!self.leading_comments(node).is_empty()
}
pub fn has_leading_own_line_comment(&self, node: &SyntaxNode<L>) -> bool {
self.leading_comments(node)
.iter()
.any(|comment| comment.lines_after() > 0)
}
#[inline]
pub fn leading_comments(&self, node: &SyntaxNode<L>) -> &[SourceComment<L>] {
self.data.comments.leading(&node.key())
}
pub fn has_dangling_comments(&self, node: &SyntaxNode<L>) -> bool {
!self.dangling_comments(node).is_empty()
}
pub fn dangling_comments(&self, node: &SyntaxNode<L>) -> &[SourceComment<L>] {
self.data.comments.dangling(&node.key())
}
#[inline]
pub fn trailing_comments(&self, node: &SyntaxNode<L>) -> &[SourceComment<L>] {
self.data.comments.trailing(&node.key())
}
pub fn has_trailing_line_comment(&self, node: &SyntaxNode<L>) -> bool {
self.trailing_comments(node)
.iter()
.any(|comment| comment.kind().is_line())
}
#[inline]
pub fn has_trailing_comments(&self, node: &SyntaxNode<L>) -> bool {
!self.trailing_comments(node).is_empty()
}
pub fn leading_trailing_comments(
&self,
node: &SyntaxNode<L>,
) -> impl Iterator<Item = &SourceComment<L>> {
self.leading_comments(node)
.iter()
.chain(self.trailing_comments(node).iter())
}
pub fn leading_dangling_trailing_comments<'a>(
&'a self,
node: &'a SyntaxNode<L>,
) -> impl Iterator<Item = &SourceComment<L>> + 'a {
self.data.comments.parts(&node.key())
}
#[inline]
pub fn has_skipped(&self, token: &SyntaxToken<L>) -> bool {
self.data.with_skipped.contains(&token.key())
}
pub fn is_suppressed(&self, node: &SyntaxNode<L>) -> bool {
self.mark_suppression_checked(node);
let is_suppression = self.data.is_suppression;
self.leading_dangling_trailing_comments(node)
.any(|comment| is_suppression(comment.piece().text()))
}
#[cfg(not(debug_assertions))]
#[inline(always)]
pub fn mark_suppression_checked(&self, _: &SyntaxNode<L>) {}
#[cfg(debug_assertions)]
pub fn mark_suppression_checked(&self, node: &SyntaxNode<L>) {
let mut checked_nodes = self.data.checked_suppressions.borrow_mut();
checked_nodes.insert(node.clone());
}
#[cfg(not(debug_assertions))]
#[inline(always)]
pub(crate) fn assert_checked_all_suppressions(&self, _: &SyntaxNode<L>) {}
#[cfg(debug_assertions)]
pub(crate) fn assert_checked_all_suppressions(&self, root: &SyntaxNode<L>) {
use biome_rowan::SyntaxKind;
let checked_nodes = self.data.checked_suppressions.borrow();
for node in root.descendants() {
if node.kind().is_list() || node.kind().is_root() {
continue;
}
if !checked_nodes.contains(&node) {
panic!(
r#"
The following node has been formatted without checking if it has suppression comments.
Ensure that the formatter calls into the node's formatting rule by using `node.format()` or
manually test if the node has a suppression comment using `f.context().comments().is_suppressed(node.syntax())`
if using the node's format rule isn't an option."
Node:
{node:#?}"#
);
}
}
}
#[inline(always)]
#[cfg(not(debug_assertions))]
pub(crate) fn assert_formatted_all_comments(&self) {}
#[cfg(debug_assertions)]
pub(crate) fn assert_formatted_all_comments(&self) {
let has_unformatted_comments = self
.data
.comments
.all_parts()
.any(|comment| !comment.formatted.get());
if has_unformatted_comments {
let mut unformatted_comments = Vec::new();
for node in self
.data
.root
.as_ref()
.expect("Expected root for comments with data")
.descendants()
{
unformatted_comments.extend(self.leading_comments(&node).iter().filter_map(
|comment| {
(!comment.formatted.get()).then_some(DebugComment::Leading {
node: node.clone(),
comment,
})
},
));
unformatted_comments.extend(self.dangling_comments(&node).iter().filter_map(
|comment| {
(!comment.formatted.get()).then_some(DebugComment::Dangling {
node: node.clone(),
comment,
})
},
));
unformatted_comments.extend(self.trailing_comments(&node).iter().filter_map(
|comment| {
(!comment.formatted.get()).then_some(DebugComment::Trailing {
node: node.clone(),
comment,
})
},
));
}
panic!("The following comments have not been formatted.\n{unformatted_comments:#?}")
}
}
}
struct CommentsData<L: Language> {
root: Option<SyntaxNode<L>>,
is_suppression: fn(&str) -> bool,
comments: CommentsMap<SyntaxElementKey, SourceComment<L>>,
with_skipped: FxHashSet<SyntaxElementKey>,
#[cfg(debug_assertions)]
checked_suppressions: RefCell<FxHashSet<SyntaxNode<L>>>,
}
impl<L: Language> Default for CommentsData<L> {
fn default() -> Self {
Self {
root: None,
is_suppression: |_| false,
comments: Default::default(),
with_skipped: Default::default(),
#[cfg(debug_assertions)]
checked_suppressions: Default::default(),
}
}
}
impl<L: Language> std::fmt::Debug for CommentsData<L> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut comments = Vec::new();
if let Some(root) = &self.root {
for node in root.descendants() {
for leading in self.comments.leading(&node.key()) {
comments.push(DebugComment::Leading {
node: node.clone(),
comment: leading,
});
}
for dangling in self.comments.dangling(&node.key()) {
comments.push(DebugComment::Dangling {
node: node.clone(),
comment: dangling,
});
}
for trailing in self.comments.trailing(&node.key()) {
comments.push(DebugComment::Trailing {
node: node.clone(),
comment: trailing,
});
}
}
}
comments.sort_by_key(|comment| comment.start());
f.debug_list().entries(comments).finish()
}
}
enum DebugComment<'a, L: Language> {
Leading {
comment: &'a SourceComment<L>,
node: SyntaxNode<L>,
},
Trailing {
comment: &'a SourceComment<L>,
node: SyntaxNode<L>,
},
Dangling {
comment: &'a SourceComment<L>,
node: SyntaxNode<L>,
},
}
impl<L: Language> DebugComment<'_, L> {
fn start(&self) -> TextSize {
match self {
DebugComment::Leading { comment, .. }
| DebugComment::Trailing { comment, .. }
| DebugComment::Dangling { comment, .. } => comment.piece.text_range().start(),
}
}
}
impl<L: Language> std::fmt::Debug for DebugComment<'_, L> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DebugComment::Leading { node, comment } => f
.debug_struct("Leading")
.field("node", node)
.field("comment", comment)
.finish(),
DebugComment::Dangling { node, comment } => f
.debug_struct("Dangling")
.field("node", node)
.field("comment", comment)
.finish(),
DebugComment::Trailing { node, comment } => f
.debug_struct("Trailing")
.field("node", node)
.field("comment", comment)
.finish(),
}
}
}
pub struct FormatPlainComment<C> {
context: PhantomData<C>,
}
impl<C> Default for FormatPlainComment<C> {
fn default() -> Self {
FormatPlainComment {
context: PhantomData,
}
}
}
impl<C> FormatRule<SourceComment<C::Language>> for FormatPlainComment<C>
where
C: CstFormatContext,
{
type Context = C;
fn fmt(
&self,
item: &SourceComment<C::Language>,
f: &mut Formatter<Self::Context>,
) -> FormatResult<()> {
write!(f, [item.piece.as_piece()])
}
}
pub fn is_alignable_comment<L: Language>(comment: &SyntaxTriviaPieceComments<L>) -> bool {
if !comment.has_newline() {
return false;
}
let text = comment.text();
text.lines().enumerate().all(|(index, line)| {
if index == 0 {
line.starts_with("/*")
} else {
line.trim_start().starts_with('*')
}
})
}
pub fn is_doc_comment<L: Language>(comment: &SyntaxTriviaPieceComments<L>) -> bool {
if !comment.has_newline() {
return false;
}
let text = comment.text();
text.lines().enumerate().all(|(index, line)| {
if index == 0 {
line.starts_with("/**")
} else {
line.trim_start().starts_with('*')
}
})
}