Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feat(rome_js_formatter): EcmaScript @decorators #4252 (#4442)
Browse files Browse the repository at this point in the history
feat(rome_js_formatter): EcmaScript @decorators #4252
  • Loading branch information
denbezrukov committed May 8, 2023
1 parent 6abe553 commit ff02317
Show file tree
Hide file tree
Showing 61 changed files with 2,374 additions and 1,462 deletions.
24 changes: 0 additions & 24 deletions crates/rome_js_formatter/src/comments.rs
Expand Up @@ -166,7 +166,6 @@ impl CommentStyle for JsCommentStyle {
.or_else(handle_try_comment)
.or_else(handle_class_comment)
.or_else(handle_method_comment)
.or_else(handle_property_comments)
.or_else(handle_for_comment)
.or_else(handle_root_comments)
.or_else(handle_array_hole_comment)
Expand All @@ -185,7 +184,6 @@ impl CommentStyle for JsCommentStyle {
.or_else(handle_try_comment)
.or_else(handle_class_comment)
.or_else(handle_method_comment)
.or_else(handle_property_comments)
.or_else(handle_for_comment)
.or_else(handle_root_comments)
.or_else(handle_parameter_comment)
Expand Down Expand Up @@ -510,28 +508,6 @@ fn handle_method_comment(comment: DecoratedComment<JsLanguage>) -> CommentPlacem
CommentPlacement::Default(comment)
}

fn handle_property_comments(comment: DecoratedComment<JsLanguage>) -> CommentPlacement<JsLanguage> {
let enclosing = comment.enclosing_node();

let is_property = matches!(
enclosing.kind(),
JsSyntaxKind::JS_PROPERTY_OBJECT_MEMBER | JsSyntaxKind::JS_PROPERTY_CLASS_MEMBER
);

if !is_property {
return CommentPlacement::Default(comment);
}

if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node())
{
if preceding.kind() == JsSyntaxKind::JS_DECORATOR {
return CommentPlacement::leading(following.clone(), comment);
}
}

CommentPlacement::Default(comment)
}

/// Handle a all comments document.
/// See `blank.js`
fn handle_root_comments(comment: DecoratedComment<JsLanguage>) -> CommentPlacement<JsLanguage> {
Expand Down
18 changes: 16 additions & 2 deletions crates/rome_js_formatter/src/js/expressions/class_expression.rs
@@ -1,5 +1,6 @@
use crate::prelude::*;
use crate::utils::format_class::FormatClass;
use rome_formatter::{format_args, write};

use crate::parentheses::{
is_callee, is_first_in_statement, FirstInStatementMode, NeedsParentheses,
Expand All @@ -11,11 +12,24 @@ pub(crate) struct FormatJsClassExpression;

impl FormatNodeRule<JsClassExpression> for FormatJsClassExpression {
fn fmt_fields(&self, node: &JsClassExpression, f: &mut JsFormatter) -> FormatResult<()> {
FormatClass::from(&node.clone().into()).fmt(f)
if node.decorators().is_empty() {
FormatClass::from(&node.clone().into()).fmt(f)
} else {
write!(
f,
[
indent(&format_args![
soft_line_break_or_space(),
&FormatClass::from(&node.clone().into()),
]),
soft_line_break_or_space()
]
)
}
}

fn needs_parentheses(&self, item: &JsClassExpression) -> bool {
item.needs_parentheses()
!item.decorators().is_empty() || item.needs_parentheses()
}

fn fmt_dangling_comments(
Expand Down
87 changes: 82 additions & 5 deletions crates/rome_js_formatter/src/js/lists/decorator_list.rs
@@ -1,6 +1,10 @@
use crate::prelude::*;
use rome_formatter::write;
use rome_js_syntax::JsDecoratorList;
use rome_js_syntax::JsSyntaxKind::JS_CLASS_EXPRESSION;
use rome_js_syntax::{
AnyJsDeclarationClause, AnyJsExportClause, AnyJsExportDefaultDeclaration, JsDecoratorList,
JsExport,
};

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatJsDecoratorList;
Expand All @@ -11,10 +15,83 @@ impl FormatRule<JsDecoratorList> for FormatJsDecoratorList {
return Ok(());
}

f.join_with(&soft_line_break_or_space())
.entries(node.iter().formatted())
.finish()?;
// we need to rearrange decorators to be before export if we have decorators before class and after export
if let Some(export) = node.parent::<JsExport>() {
let mut join = f.join_nodes_with_hardline();

write!(f, [soft_line_break_or_space()])
// write decorators before export first
for decorator in node {
join.entry(decorator.syntax(), &format_or_verbatim(decorator.format()));
}

// try to find class decorators
let class_decorators = match export.export_clause()? {
AnyJsExportClause::AnyJsDeclarationClause(
AnyJsDeclarationClause::JsClassDeclaration(class),
) => {
// @before export @after class Foo {}
Some(class.decorators())
}
AnyJsExportClause::JsExportDefaultDeclarationClause(export_default_declaration) => {
match export_default_declaration.declaration()? {
AnyJsExportDefaultDeclaration::JsClassExportDefaultDeclaration(class) => {
// @before export default @after class Foo {}
Some(class.decorators())
}
_ => None,
}
}
_ => None,
};

// write decorators after export
if let Some(class_decorators) = class_decorators {
for decorator in class_decorators {
join.entry(decorator.syntax(), &format_or_verbatim(decorator.format()));
}
}

join.finish()?;

write!(f, [hard_line_break()])
} else if matches!(
node.syntax().parent().map(|parent| parent.kind()),
Some(JS_CLASS_EXPRESSION)
) {
write!(f, [expand_parent()])?;
f.join_with(&soft_line_break_or_space())
.entries(node.iter().formatted())
.finish()?;

write!(f, [soft_line_break_or_space()])
} else {
// If the parent node is an export declaration and the decorator
// was written before the export, the export will be responsible
// for printing the decorators.
let export = node.syntax().grand_parent().and_then(|grand_parent| {
JsExport::cast_ref(&grand_parent)
.or_else(|| grand_parent.parent().and_then(JsExport::cast))
});
let is_export = export.is_some();

let has_decorators_before_export =
export.map_or(false, |export| !export.decorators().is_empty());

if has_decorators_before_export {
return Ok(());
}

if is_export {
write!(f, [hard_line_break()])?;
} else {
write!(f, [expand_parent()])?;
}

f.join_with(&soft_line_break_or_space())
.entries(node.iter().formatted())
.finish()?;

write!(f, [soft_line_break_or_space()])
}
}
}
6 changes: 2 additions & 4 deletions crates/rome_js_formatter/src/js/lists/method_modifier_list.rs
@@ -1,5 +1,5 @@
use crate::prelude::*;
use crate::utils::sort_modifiers_by_precedence;
use crate::utils::format_modifiers::FormatModifiers;
use rome_js_syntax::JsMethodModifierList;

#[derive(Debug, Clone, Default)]
Expand All @@ -9,8 +9,6 @@ impl FormatRule<JsMethodModifierList> for FormatJsMethodModifierList {
type Context = JsFormatContext;

fn fmt(&self, node: &JsMethodModifierList, f: &mut JsFormatter) -> FormatResult<()> {
f.join_with(&space())
.entries(sort_modifiers_by_precedence(node).into_iter().formatted())
.finish()
FormatModifiers::from(node.clone()).fmt(f)
}
}
@@ -1,5 +1,5 @@
use crate::prelude::*;
use crate::utils::sort_modifiers_by_precedence;
use crate::utils::format_modifiers::FormatModifiers;
use rome_js_syntax::JsPropertyModifierList;

#[derive(Debug, Clone, Default)]
Expand All @@ -9,8 +9,6 @@ impl FormatRule<JsPropertyModifierList> for FormatJsPropertyModifierList {
type Context = JsFormatContext;

fn fmt(&self, node: &JsPropertyModifierList, f: &mut JsFormatter) -> FormatResult<()> {
f.join_with(&space())
.entries(sort_modifiers_by_precedence(node).into_iter().formatted())
.finish()
FormatModifiers::from(node.clone()).fmt(f)
}
}
@@ -1,5 +1,5 @@
use crate::prelude::*;
use crate::utils::sort_modifiers_by_precedence;
use crate::utils::format_modifiers::FormatModifiers;
use rome_js_syntax::TsMethodSignatureModifierList;

#[derive(Debug, Clone, Default)]
Expand All @@ -9,8 +9,6 @@ impl FormatRule<TsMethodSignatureModifierList> for FormatTsMethodSignatureModifi
type Context = JsFormatContext;

fn fmt(&self, node: &TsMethodSignatureModifierList, f: &mut JsFormatter) -> FormatResult<()> {
f.join_with(&space())
.entries(sort_modifiers_by_precedence(node).into_iter().formatted())
.finish()
FormatModifiers::from(node.clone()).fmt(f)
}
}
@@ -1,5 +1,5 @@
use crate::prelude::*;
use crate::utils::sort_modifiers_by_precedence;
use crate::utils::format_modifiers::FormatModifiers;
use rome_js_syntax::TsPropertySignatureModifierList;

#[derive(Debug, Clone, Default)]
Expand All @@ -9,8 +9,6 @@ impl FormatRule<TsPropertySignatureModifierList> for FormatTsPropertySignatureMo
type Context = JsFormatContext;

fn fmt(&self, node: &TsPropertySignatureModifierList, f: &mut JsFormatter) -> FormatResult<()> {
f.join_with(&space())
.entries(sort_modifiers_by_precedence(node).into_iter().formatted())
.finish()
FormatModifiers::from(node.clone()).fmt(f)
}
}
2 changes: 2 additions & 0 deletions crates/rome_js_formatter/src/utils/assignment_like.rs
Expand Up @@ -941,6 +941,8 @@ pub(crate) fn should_break_after_operator(
})
}

AnyJsExpression::JsClassExpression(class) => !class.decorators().is_empty(),

_ => false,
};

Expand Down
93 changes: 93 additions & 0 deletions crates/rome_js_formatter/src/utils/format_modifiers.rs
@@ -0,0 +1,93 @@
use crate::prelude::*;
use crate::utils::sort_modifiers_by_precedence;
use crate::{AsFormat, IntoFormat};
use rome_formatter::{format_args, write};
use rome_js_syntax::JsSyntaxKind::JS_DECORATOR;
use rome_js_syntax::{JsLanguage, Modifiers};
use rome_rowan::{AstNode, AstNodeList, NodeOrToken};

pub(crate) struct FormatModifiers<List> {
pub(crate) list: List,
}

impl<List> FormatModifiers<List> {
pub(crate) fn from(list: List) -> Self {
Self { list }
}
}

impl<List, Node> Format<JsFormatContext> for FormatModifiers<List>
where
Node: AstNode<Language = JsLanguage> + AsFormat<JsFormatContext> + IntoFormat<JsFormatContext>,
List: AstNodeList<Language = JsLanguage, Node = Node>,
Modifiers: for<'a> From<&'a Node>,
{
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
let modifiers = sort_modifiers_by_precedence(&self.list);
let should_expand = should_expand_decorators(&self.list);

// need to use peek the iterator to check if the current node is a decorator and don't advance the iterator
let mut iter = modifiers.into_iter().peekable();
let decorators = format_once(|f| {
let mut join = f.join_nodes_with_soft_line();

// join only decorators here
while let Some(node) = iter.peek() {
// check if the current node is a decorator
match node.syntax().kind() {
JS_DECORATOR => {
join.entry(node.syntax(), &node.format());
// advance the iterator
iter.next();
}
_ => {
// if we encounter a non-decorator we break out of the loop
break;
}
}
}

join.finish()
});

write!(
f,
[group(&format_args![decorators, soft_line_break_or_space()])
.should_expand(should_expand)]
)?;

// join the rest of the modifiers
f.join_with(&space()).entries(iter.formatted()).finish()
}
}

/// This function expands decorators enclosing a group if there is a newline between decorators or after the last decorator.
fn should_expand_decorators<List, Node>(list: &List) -> bool
where
Node: AstNode<Language = JsLanguage>,
List: AstNodeList<Language = JsLanguage, Node = Node>,
{
// we need to skip the first node because we look for newlines between decorators or after the last decorator
for node in list.iter().skip(1) {
match node.syntax().kind() {
JS_DECORATOR => {
if node.syntax().has_leading_newline() {
return true;
}
}
_ => {
// if we encounter a non-decorator with a leading newline after a decorator and the next modifier
return node.syntax().has_leading_newline();
}
}
}

// if we encounter a non-decorator with a leading newline after a decorator and the next node or token
list.syntax_list()
.node()
.next_sibling_or_token()
.map_or(false, |node| match node {
NodeOrToken::Node(node) => node.has_leading_newline(),
NodeOrToken::Token(token) => token.has_leading_newline(),
})
}
1 change: 1 addition & 0 deletions crates/rome_js_formatter/src/utils/mod.rs
Expand Up @@ -5,6 +5,7 @@ mod conditional;
pub mod string_utils;

pub(crate) mod format_class;
pub(crate) mod format_modifiers;
pub(crate) mod function_body;
pub mod jsx;
pub(crate) mod member_chain;
Expand Down
9 changes: 3 additions & 6 deletions crates/rome_js_formatter/tests/quick_test.rs
Expand Up @@ -13,12 +13,9 @@ mod language {
// use this test check if your snippet prints as you wish, without using a snapshot
fn quick_test() {
let src = r#"
class Test2 {
@anotherDecorator() // leading comment
prop: string;
}
const foo = @deco class {
//
};
"#;
let syntax = SourceType::tsx();
let tree = parse(src, syntax);
Expand Down

0 comments on commit ff02317

Please sign in to comment.