feat(rome_js_formatter): Binary like object literal member expression
MODIFY
crates/rome_js_formatter/src/js/objects/property_object_member.rs
crates/rome_js_formatter/src/js/objects/property_object_member.rs
use crate::prelude::*;
-use crate::utils::StringLiteralParentKind;
-use crate::utils::{FormatLiteralStringToken, JsAnyBinaryLikeExpression};
+use crate::utils::{property_object_member_layout, write_member_name, PropertyObjectMemberLayout};
use crate::FormatNodeFields;
use rome_formatter::{format_args, write};
-use rome_js_syntax::JsAnyLiteralExpression;
use rome_js_syntax::JsPropertyObjectMember;
use rome_js_syntax::JsPropertyObjectMemberFields;
-use rome_js_syntax::JsSyntaxKind::JS_STRING_LITERAL;
-use rome_js_syntax::{JsAnyExpression, JsAnyObjectMemberName};
-use rome_rowan::{AstNode, SyntaxResult};
-use unicode_width::UnicodeWidthStr;
impl FormatNodeFields<JsPropertyObjectMember> for FormatNodeRule<JsPropertyObjectMember> {
fn fmt_fields(node: &JsPropertyObjectMember, f: &mut JsFormatter) -> FormatResult<()> {
@@ -65,139 +59,3 @@ impl FormatNodeFields<JsPropertyObjectMember> for FormatNodeRule<JsPropertyObjec
write!(f, [group_elements(&format_content)])
}
}
-
-fn write_member_name(name: &JsAnyObjectMemberName, f: &mut JsFormatter) -> FormatResult<usize> {
- match name {
- name @ JsAnyObjectMemberName::JsLiteralMemberName(literal) => {
- let value = literal.value()?;
-
- if value.kind() == JS_STRING_LITERAL {
- let format = FormatLiteralStringToken::new(&value, StringLiteralParentKind::Member);
- let cleaned = format.clean_text(f.context());
-
- cleaned.fmt(f)?;
-
- Ok(cleaned.width())
- } else {
- name.format().fmt(f)?;
-
- Ok(value.text_trimmed().width())
- }
- }
- name => {
- write!(f, [group_elements(&name.format())])?;
- Ok(name.text().width())
- }
- }
-}
-
-/// Determines how a property object member should be formatted
-enum PropertyObjectMemberLayout {
- /// First break right-hand side, then after operator.
- /// ```js
- /// {
- /// "array-key": [
- /// {
- /// "nested-key-1": 1,
- /// "nested-key-2": 2,
- /// },
- /// ]
- /// }
- /// ```
- Fluid,
- /// First break after operator, then the sides are broken independently on their own lines.
- /// There is a soft line break after colon token.
- /// ```js
- /// {
- /// "enough-long-key-to-break-line":
- /// 1 + 2,
- /// "not-long-enough-key":
- /// "but long enough string to break line",
- /// }
- /// ```
- BreakAfterColon,
- /// First break right-hand side, then left-hand side. There are not any soft line breaks
- /// between property name and property value
- /// ```js
- /// {
- /// key1: "123",
- /// key2: 123,
- /// key3: class MyClass {
- /// constructor() {},
- /// },
- /// }
- /// ```
- NeverBreakAfterColon,
-}
-
-const MIN_OVERLAP_FOR_BREAK: u8 = 3;
-
-/// Returns the layout variant for an object member depending on value expression and name length
-/// [Prettier applies]: https://github.com/prettier/prettier/blob/main/src/language-js/print/assignment.js
-fn property_object_member_layout(
- formatter: &JsFormatter,
- name_width: usize,
- value: &JsAnyExpression,
-) -> FormatResult<PropertyObjectMemberLayout> {
- let text_width_for_break = (formatter.context().tab_width() + MIN_OVERLAP_FOR_BREAK) as usize;
- let is_name_short = name_width < text_width_for_break;
-
- if is_break_after_colon(value)? {
- Ok(PropertyObjectMemberLayout::BreakAfterColon)
- } else if is_name_short {
- Ok(PropertyObjectMemberLayout::NeverBreakAfterColon)
- } else if matches!(
- value,
- JsAnyExpression::JsAnyLiteralExpression(JsAnyLiteralExpression::JsStringLiteralExpression(
- _
- ))
- ) {
- Ok(PropertyObjectMemberLayout::BreakAfterColon)
- } else if is_never_break_after_colon(value)? {
- Ok(PropertyObjectMemberLayout::NeverBreakAfterColon)
- } else {
- Ok(PropertyObjectMemberLayout::Fluid)
- }
-}
-
-fn is_break_after_colon(value: &JsAnyExpression) -> SyntaxResult<bool> {
- if JsAnyBinaryLikeExpression::can_cast(value.syntax().kind()) {
- return Ok(true);
- }
-
- if matches!(value, JsAnyExpression::JsSequenceExpression(_)) {
- return Ok(true);
- }
-
- if let JsAnyExpression::JsConditionalExpression(conditional) = &value {
- if JsAnyBinaryLikeExpression::can_cast(conditional.test()?.syntax().kind()) {
- return Ok(true);
- }
- }
-
- Ok(false)
-}
-
-fn is_never_break_after_colon(value: &JsAnyExpression) -> SyntaxResult<bool> {
- if let JsAnyExpression::JsCallExpression(call_expression) = &value {
- if call_expression.callee()?.syntax().text() == "require" {
- return Ok(true);
- }
- }
-
- if matches!(
- value,
- JsAnyExpression::JsClassExpression(_)
- | JsAnyExpression::JsTemplate(_)
- | JsAnyExpression::JsAnyLiteralExpression(
- JsAnyLiteralExpression::JsBooleanLiteralExpression(_),
- )
- | JsAnyExpression::JsAnyLiteralExpression(
- JsAnyLiteralExpression::JsNumberLiteralExpression(_)
- )
- ) {
- return Ok(true);
- }
-
- Ok(false)
-}

MODIFY
crates/rome_js_formatter/src/utils/binary_like_expression.rs
crates/rome_js_formatter/src/utils/binary_like_expression.rs
@@ -7,6 +7,7 @@ use rome_js_syntax::{
JsSyntaxNode, JsSyntaxToken,
};
+use crate::utils::is_break_after_colon;
use rome_rowan::{declare_node_union, AstNode, SyntaxResult};
use std::cmp::Ordering;
use std::fmt::Debug;
@@ -278,13 +279,19 @@ fn is_inside_parenthesis(current_node: &JsSyntaxNode) -> bool {
///
/// There are some cases where the indentation is done by the parent, so if the parent is already doing
/// the indentation, then there's no need to do a second indentation.
-fn should_not_indent_if_parent_indents(current_node: &JsSyntaxNode) -> bool {
- let parent_kind = current_node.parent().map(|parent| parent.kind());
+fn should_not_indent_if_parent_indents(current_node: &JsAnyBinaryLikeLeftExpression) -> bool {
+ let parent_kind = current_node.syntax().parent().map(|parent| parent.kind());
- matches!(
- parent_kind,
- Some(JsSyntaxKind::JS_RETURN_STATEMENT | JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION)
- )
+ match parent_kind {
+ Some(JsSyntaxKind::JS_PROPERTY_OBJECT_MEMBER) => current_node
+ .as_expression()
+ .and_then(|expression| is_break_after_colon(expression).ok())
+ .unwrap_or(false),
+ Some(JsSyntaxKind::JS_RETURN_STATEMENT | JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION) => {
+ true
+ }
+ _ => false,
+ }
}
/// There are other cases where the parent decides to inline the the element; in
@@ -534,7 +541,7 @@ impl FlattenedBinaryExpressionPart {
f.join_with(soft_line_break_or_space())
.entries(groups)
.finish()
- } else if should_not_indent_if_parent_indents(current.syntax()) {
+ } else if should_not_indent_if_parent_indents(current) {
write!(
f,
[group_elements(&format_once(|f| {
@@ -800,6 +807,23 @@ impl JsAnyBinaryLikeExpression {
}
impl JsAnyBinaryLikeExpression {
+ /// Determines if a binary like expression should be inline or not.
+ pub fn should_inline(&self) -> bool {
+ match self {
+ JsAnyBinaryLikeExpression::JsLogicalExpression(logical_expression) => {
+ match logical_expression.right() {
+ Ok(JsAnyExpression::JsObjectExpression(object_expression)) => {
+ object_expression.members().iter().count() > 0
+ }
+ Ok(JsAnyExpression::JsArrayExpression(array_expression)) => {
+ array_expression.elements().iter().count() > 0
+ }
+ _ => false,
+ }
+ }
+ _ => false,
+ }
+ }
/// Determines if a binary like expression should be flattened or not. As a rule of thumb, an expression
/// can be flattened if it is of the same kind as the left-hand side sub-expression and uses the same operator.
fn can_flatten(&self) -> SyntaxResult<bool> {

MODIFY
crates/rome_js_formatter/src/utils/mod.rs
crates/rome_js_formatter/src/utils/mod.rs
pub(crate) mod array;
mod binary_like_expression;
mod format_conditional;
+mod object;
mod simple;
pub mod string_utils;
@@ -12,6 +13,10 @@ use crate::prelude::*;
pub(crate) use binary_like_expression::{format_binary_like_expression, JsAnyBinaryLikeExpression};
pub(crate) use format_conditional::{format_conditional, Conditional};
pub(crate) use member_chain::format_call_expression;
+pub(crate) use object::{
+ is_break_after_colon, property_object_member_layout, write_member_name,
+ PropertyObjectMemberLayout,
+};
use rome_formatter::{normalize_newlines, write, Buffer, VecBuffer};
use rome_js_syntax::suppression::{has_suppressions_category, SuppressionCategory};
use rome_js_syntax::JsSyntaxKind::JS_STRING_LITERAL;

ADD
crates/rome_js_formatter/src/utils/object.rs
+use crate::prelude::*;
+use rome_formatter::write;
+
+use crate::utils::StringLiteralParentKind;
+use crate::utils::{FormatLiteralStringToken, JsAnyBinaryLikeExpression};
+use rome_js_syntax::JsAnyLiteralExpression;
+use rome_js_syntax::JsSyntaxKind::JS_STRING_LITERAL;
+use rome_js_syntax::{JsAnyExpression, JsAnyObjectMemberName};
+use rome_rowan::{AstNode, SyntaxResult};
+use unicode_width::UnicodeWidthStr;
+
+pub(crate) fn write_member_name(
+ name: &JsAnyObjectMemberName,
+ f: &mut JsFormatter,
+) -> FormatResult<usize> {
+ match name {
+ name @ JsAnyObjectMemberName::JsLiteralMemberName(literal) => {
+ let value = literal.value()?;
+
+ if value.kind() == JS_STRING_LITERAL {
+ let format = FormatLiteralStringToken::new(&value, StringLiteralParentKind::Member);
+ let cleaned = format.clean_text(f.context());
+
+ cleaned.fmt(f)?;
+
+ Ok(cleaned.width())
+ } else {
+ name.format().fmt(f)?;
+
+ Ok(value.text_trimmed().width())
+ }
+ }
+ name => {
+ write!(f, [group_elements(&name.format())])?;
+ Ok(name.text().width())
+ }
+ }
+}
+
+/// Determines how a property object member should be formatted
+pub(crate) enum PropertyObjectMemberLayout {
+ /// First break right-hand side, then after operator.
+ /// ```js
+ /// {
+ /// "array-key": [
+ /// {
+ /// "nested-key-1": 1,
+ /// "nested-key-2": 2,
+ /// },
+ /// ]
+ /// }
+ /// ```
+ Fluid,
+ /// First break after operator, then the sides are broken independently on their own lines.
+ /// There is a soft line break after colon token.
+ /// ```js
+ /// {
+ /// "enough-long-key-to-break-line":
+ /// 1 + 2,
+ /// "not-long-enough-key":
+ /// "but long enough string to break line",
+ /// }
+ /// ```
+ BreakAfterColon,
+ /// First break right-hand side, then left-hand side. There are not any soft line breaks
+ /// between property name and property value
+ /// ```js
+ /// {
+ /// key1: "123",
+ /// key2: 123,
+ /// key3: class MyClass {
+ /// constructor() {},
+ /// },
+ /// }
+ /// ```
+ NeverBreakAfterColon,
+}
+
+const MIN_OVERLAP_FOR_BREAK: u8 = 3;
+
+/// Returns the layout variant for an object member depending on value expression and name length
+/// [Prettier applies]: https://github.com/prettier/prettier/blob/main/src/language-js/print/assignment.js
+pub(crate) fn property_object_member_layout(
+ formatter: &JsFormatter,
+ name_width: usize,
+ value: &JsAnyExpression,
+) -> FormatResult<PropertyObjectMemberLayout> {
+ let text_width_for_break = (formatter.context().tab_width() + MIN_OVERLAP_FOR_BREAK) as usize;
+ let is_name_short = name_width < text_width_for_break;
+
+ if is_break_after_colon(value)? {
+ Ok(PropertyObjectMemberLayout::BreakAfterColon)
+ } else if is_name_short {
+ Ok(PropertyObjectMemberLayout::NeverBreakAfterColon)
+ } else if matches!(
+ value,
+ JsAnyExpression::JsAnyLiteralExpression(JsAnyLiteralExpression::JsStringLiteralExpression(
+ _
+ ))
+ ) {
+ Ok(PropertyObjectMemberLayout::BreakAfterColon)
+ } else if is_never_break_after_colon(value)? {
+ Ok(PropertyObjectMemberLayout::NeverBreakAfterColon)
+ } else {
+ Ok(PropertyObjectMemberLayout::Fluid)
+ }
+}
+
+pub(crate) fn is_break_after_colon(value: &JsAnyExpression) -> SyntaxResult<bool> {
+ if JsAnyBinaryLikeExpression::cast(value.syntax().clone())
+ .map_or(false, |expression| !expression.should_inline())
+ {
+ return Ok(true);
+ }
+
+ if matches!(value, JsAnyExpression::JsSequenceExpression(_)) {
+ return Ok(true);
+ }
+
+ if let JsAnyExpression::JsConditionalExpression(conditional) = &value {
+ if JsAnyBinaryLikeExpression::cast(conditional.test()?.syntax().clone())
+ .map_or(false, |expression| !expression.should_inline())
+ {
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+}
+
+fn is_never_break_after_colon(value: &JsAnyExpression) -> SyntaxResult<bool> {
+ if let JsAnyExpression::JsCallExpression(call_expression) = &value {
+ if call_expression.callee()?.syntax().text() == "require" {
+ return Ok(true);
+ }
+ }
+
+ if matches!(
+ value,
+ JsAnyExpression::JsClassExpression(_)
+ | JsAnyExpression::JsTemplate(_)
+ | JsAnyExpression::JsAnyLiteralExpression(
+ JsAnyLiteralExpression::JsBooleanLiteralExpression(_),
+ )
+ | JsAnyExpression::JsAnyLiteralExpression(
+ JsAnyLiteralExpression::JsNumberLiteralExpression(_)
+ )
+ ) {
+ return Ok(true);
+ }
+
+ Ok(false)
+}

MODIFY
crates/rome_js_formatter/tests/specs/js/module/object/property_object_member.js
crates/rome_js_formatter/tests/specs/js/module/object/property_object_member.js
@@ -48,6 +48,10 @@ const breakAfterColonObject = {
c: 13321321312312321311332132131231232131232132132132232132132132 + 1332132131231232131232132132132 ? {} : {},
d: '321321312312ddddddddddddddddddddddd312312312312' instanceof Object ? {} : {},
g: {'long-key': '123123213123213123edwqdqwdasdasdsaewqewqewqdas'} in 'long-key',
+ blablah:
+ "aldkfkladfskladklsfkladklfkaldfadfkdaf" +
+ "adlfasdklfkldsklfakldsfkladsfkadsfladsfa" +
+ "dflkadfkladsfklkadlfkladlfkadklfjadlfdfdaf",
};
const fluidObject = {
@@ -78,4 +82,7 @@ const fluidObject = {
return 'image';
}
),
+ loadNext: (stateIsOK && hasNext) || {
+ skipNext: true,
+ },
};

MODIFY
crates/rome_js_formatter/tests/specs/js/module/object/property_object_member.js.snap
crates/rome_js_formatter/tests/specs/js/module/object/property_object_member.js.snap
@@ -53,6 +53,10 @@ const breakAfterColonObject = {
c: 13321321312312321311332132131231232131232132132132232132132132 + 1332132131231232131232132132132 ? {} : {},
d: '321321312312ddddddddddddddddddddddd312312312312' instanceof Object ? {} : {},
g: {'long-key': '123123213123213123edwqdqwdasdasdsaewqewqewqdas'} in 'long-key',
+ blablah:
+ "aldkfkladfskladklsfkladklfkaldfadfkdaf" +
+ "adlfasdklfkldsklfakldsfkladsfkadsfladsfa" +
+ "dflkadfkladsfklkadlfkladlfkadklfjadlfdfdaf",
};
const fluidObject = {
@@ -83,6 +87,9 @@ const fluidObject = {
return 'image';
}
),
+ loadNext: (stateIsOK && hasNext) || {
+ skipNext: true,
+ },
};
=============================
@@ -182,6 +189,10 @@ const breakAfterColonObject = {
{
"long-key": "123123213123213123edwqdqwdasdasdsaewqewqewqdas",
} in "long-key",
+ blablah:
+ "aldkfkladfskladklsfkladklfkaldfadfkdaf" +
+ "adlfasdklfkldsklfakldsfkladsfkadsfladsfa" +
+ "dflkadfkladsfklkadlfkladlfkadklfjadlfdfdaf",
};
const fluidObject = {
@@ -213,6 +224,9 @@ const fluidObject = {
return "image";
},
),
+ loadNext: (stateIsOK && hasNext) || {
+ skipNext: true,
+ },
};

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/inline-object-array.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/inline-object-array.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
-assertion_line: 182
expression: inline-object-array.js
---
# Input
@@ -192,15 +191,13 @@ const create2 = () => {
};
const obj = {
- state:
- shouldHaveState &&
+ state: shouldHaveState &&
stateIsOK &&
{
loadState: LOADED,
opened: false,
},
- loadNext:
- (stateIsOK && hasNext) || {
+ loadNext: (stateIsOK && hasNext) || {
skipNext: true,
},
loaded: true,

feat(rome_js_formatter): Binary like object literal member expression (#2688)

Co-authored-by: Denis Bezrukov <dbezrukov@ctrader.local>