feat(rome_js_formatter): assignment expressions with layout
MODIFY
crates/rome_js_formatter/src/js/expressions/assignment_expression.rs
crates/rome_js_formatter/src/js/expressions/assignment_expression.rs
use crate::prelude::*;
-
use crate::utils::JsAnyAssignmentLike;
use crate::FormatNodeFields;
use rome_formatter::write;

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::JsAnyAssignmentLike;
use crate::FormatNodeFields;
use rome_formatter::write;

MODIFY
crates/rome_js_formatter/src/lib.rs
crates/rome_js_formatter/src/lib.rs
@@ -526,20 +526,9 @@ mod test {
// use this test check if your snippet prints as you wish, without using a snapshot
fn quick_test() {
let src = r#"
- app.get("/", (req, res): void => {
- res.send("Hello World!");
- });
-
- export class Thing implements OtherThing {
- do: (type: Type) => Provider<Prop> = memoize(
- (type: ObjectType): Provider<Opts> => {}
- );
- }
-
-"#;
- // let src = r#"useEffect(() => { hey; }, [1, 4])"#;
-
- let syntax = SourceType::tsx();
+bifornCringerMoshedPerplex =
+ bifornCringerMoshedPerplexSawder = arrayOfNumb = a = "test"; "#;
+ let syntax = SourceType::ts();
let tree = parse(src, 0, syntax);
let result = format_node(JsFormatContext::default(), &tree.syntax())
.unwrap()

MODIFY
crates/rome_js_formatter/src/utils/assignment_like.rs
crates/rome_js_formatter/src/utils/assignment_like.rs
use crate::prelude::*;
-
use crate::utils::object::write_member_name;
use crate::utils::JsAnyBinaryLikeExpression;
use rome_formatter::{format_args, write};
use rome_js_syntax::{
- JsAnyExpression, JsAnyLiteralExpression, JsAssignmentExpression, JsPropertyObjectMember,
- JsSyntaxNode,
+ JsAnyAssignmentPattern, JsAnyExpression, JsAnyObjectMemberName, JsAssignmentExpression,
+ JsPropertyObjectMember, JsSyntaxKind,
};
+use rome_js_syntax::{JsAnyLiteralExpression, JsSyntaxNode};
use rome_rowan::{declare_node_union, AstNode, SyntaxResult};
-pub(crate) fn write_assignment_like(
- assignment_like: &JsAnyAssignmentLike,
- f: &mut JsFormatter,
-) -> FormatResult<()> {
- let right = assignment_like.right()?;
- let format_content = format_with(|f| {
- // Compare name only if we are in a position of computing it.
- // If not (for example, left is not an identifier), then let's fallback to false,
- // so we can continue the chain of checks
- let is_left_short = assignment_like.write_left(f)?;
- assignment_like.write_operator(f)?;
-
- let layout = assignment_like.layout(is_left_short)?;
- match layout {
- AssignmentLikeLayout::Fluid => {
- let group_id = f.group_id("assignment_like");
-
- let right = right.format().memoized();
-
- write![
- f,
- [
- group_elements(&indent(&soft_line_break_or_space()),)
- .with_group_id(Some(group_id)),
- line_suffix_boundary(),
- if_group_breaks(&indent(&right)).with_group_id(Some(group_id)),
- if_group_fits_on_line(&right).with_group_id(Some(group_id)),
- ]
- ]
- }
- AssignmentLikeLayout::BreakAfterOperator => {
- write![
- f,
- [group_elements(&indent(&format_args![
- soft_line_break_or_space(),
- right.format()
- ])),]
- ]
- }
- AssignmentLikeLayout::NeverBreakAfterOperator => {
- write![f, [space_token(), right.format(),]]
- }
- }
- });
+declare_node_union! {
+ pub(crate) JsAnyAssignmentLike = JsPropertyObjectMember | JsAssignmentExpression
- write!(f, [group_elements(&format_content)])
}
declare_node_union! {
- pub(crate) JsAnyAssignmentLike = JsPropertyObjectMember | JsAssignmentExpression
+ pub(crate) LeftAssignmentLike = JsAnyAssignmentPattern | JsAnyObjectMemberName
}
/// Determines how a assignment like be formatted
@@ -66,6 +23,7 @@ declare_node_union! {
/// Assignment like are:
/// - Assignment
/// - Object property member
+#[derive(Debug)]
pub(crate) enum AssignmentLikeLayout {
/// First break right-hand side, then after operator.
/// ```js
@@ -102,6 +60,28 @@ pub(crate) enum AssignmentLikeLayout {
/// }
/// ```
NeverBreakAfterOperator,
+
+ /// This is a special layout usually used for long variable declarations or assignment expressions
+ /// This layout is hit, usually, when we are in the "middle" of the chain:
+ ///
+ /// ```js
+ /// var a =
+ /// loreum =
+ /// ipsum =
+ /// "foo";
+ /// ```
+ ///
+ /// Given the previous snippet, then `loreum` and `ipsum` will be formatted using the [Chain] layout.
+ Chain,
+
+ /// This is a special layout usually used for long variable declarations or assignment expressions
+ /// This layout is hit, usually, when we are in the end of a chain:
+ /// ```js
+ /// var a = loreum = ipsum = "foo";
+ /// ```
+ ///
+ /// Given the previous snippet, then `"foo"` formatted using the [ChainTail] layout.
+ ChainTail,
}
impl JsAnyAssignmentLike {
@@ -144,14 +124,14 @@ impl JsAnyAssignmentLike {
}
}
}
-}
-impl JsAnyAssignmentLike {
/// Returns the layout variant for an assignment like depending on right expression and left part length
/// [Prettier applies]: https://github.com/prettier/prettier/blob/main/src/language-js/print/assignment.js
fn layout(&self, is_left_short: bool) -> FormatResult<AssignmentLikeLayout> {
let right = self.right()?;
- if is_break_after_operator(&right)? {
+ if let Some(layout) = self.chain_formatting_layout()? {
+ Ok(layout)
+ } else if is_break_after_operator(&right)? {
Ok(AssignmentLikeLayout::BreakAfterOperator)
} else if is_left_short {
Ok(AssignmentLikeLayout::NeverBreakAfterOperator)
@@ -162,19 +142,104 @@ impl JsAnyAssignmentLike {
)
) {
Ok(AssignmentLikeLayout::BreakAfterOperator)
- } else if is_never_break_after_operator(&right)? {
+ } else if self.is_never_break_after_operator()? {
Ok(AssignmentLikeLayout::NeverBreakAfterOperator)
} else {
Ok(AssignmentLikeLayout::Fluid)
}
}
+
+ /// Checks if the right node is entitled of the chain formatting,
+ /// and if so, it return the layout type
+ fn chain_formatting_layout(&self) -> SyntaxResult<Option<AssignmentLikeLayout>> {
+ let right = self.right()?;
+ let right_is_tail = !matches!(right, JsAnyExpression::JsAssignmentExpression(_));
+ // The chain goes up two levels, by checking up to the great parent if all the conditions
+ // are correctly met.
+ let upper_chain_is_eligible =
+ // First, we check if the current node is an assignment expression
+ if let JsAnyAssignmentLike::JsAssignmentExpression(assignment) = self {
+ assignment.syntax().parent().map_or(false, |parent| {
+ // Then we check if the parent is assignment expression or variable declarator
+ if matches!(
+ parent.kind(),
+ JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION
+ | JsSyntaxKind::JS_VARIABLE_DECLARATOR
+ ) {
+ let great_parent_kind = parent.parent().map(|n| n.kind());
+ // Finally, we check the great parent.
+ // The great parent triggers the eligibility when
+ // - the current node that we were inspecting is not a "tail"
+ // - or the great parent is not an expression statement or a variable declarator
+ !right_is_tail
+ || !matches!(
+ great_parent_kind,
+ Some(
+ JsSyntaxKind::JS_EXPRESSION_STATEMENT
+ | JsSyntaxKind::JS_VARIABLE_DECLARATOR
+ )
+ )
+ } else {
+ false
+ }
+ })
+ } else {
+ false
+ };
+
+ let result = if upper_chain_is_eligible {
+ if right_is_tail {
+ Some(AssignmentLikeLayout::ChainTail)
+ } else {
+ Some(AssignmentLikeLayout::Chain)
+ }
+ } else {
+ None
+ };
+
+ Ok(result)
+ }
+
+ fn is_never_break_after_operator(&self) -> SyntaxResult<bool> {
+ let right = self.right()?;
+ if let JsAnyExpression::JsCallExpression(call_expression) = &right {
+ if call_expression.callee()?.syntax().text() == "require" {
+ return Ok(true);
+ }
}
+ if matches!(
+ right,
+ JsAnyExpression::JsClassExpression(_)
+ | JsAnyExpression::JsTemplate(_)
+ | JsAnyExpression::JsAnyLiteralExpression(
+ JsAnyLiteralExpression::JsBooleanLiteralExpression(_),
+ )
+ | JsAnyExpression::JsAnyLiteralExpression(
+ JsAnyLiteralExpression::JsNumberLiteralExpression(_)
+ )
+ ) {
+ return Ok(true);
+ }
+
+ Ok(false)
+ }
+}
+
+/// Checks if the function is entitled to be printed with layout [AssignmentLikeLayout::BreakAfterOperator]
pub(crate) fn is_break_after_operator(right: &JsAnyExpression) -> SyntaxResult<bool> {
if has_new_line_before_comment(right.syntax()) {
return Ok(true);
}
+ // head is a long chain, meaning that right -> right are both assignment expressions
+ if let JsAnyExpression::JsAssignmentExpression(assignment) = right {
+ let right = assignment.right()?;
+ if matches!(right, JsAnyExpression::JsAssignmentExpression(_)) {
+ return Ok(true);
+ }
+ }
+
if JsAnyBinaryLikeExpression::cast(right.syntax().clone())
.map_or(false, |expression| !expression.should_inline())
{
@@ -195,7 +260,6 @@ pub(crate) fn is_break_after_operator(right: &JsAnyExpression) -> SyntaxResult<b
Ok(false)
}
-
/// If checks if among leading trivias, we there's a sequence of [Newline, Comment]
pub(crate) fn has_new_line_before_comment(node: &JsSyntaxNode) -> bool {
if let Some(leading_trivia) = node.first_leading_trivia() {
@@ -212,32 +276,74 @@ pub(crate) fn has_new_line_before_comment(node: &JsSyntaxNode) -> bool {
false
}
-fn is_never_break_after_operator(right: &JsAnyExpression) -> SyntaxResult<bool> {
- if let JsAnyExpression::JsCallExpression(call_expression) = &right {
- if call_expression.callee()?.syntax().text() == "require" {
- return Ok(true);
+impl Format<JsFormatContext> for JsAnyAssignmentLike {
+ fn fmt(&self, f: &mut JsFormatter) -> FormatResult<()> {
+ let right = self.right()?;
+ let format_content = format_with(|f| {
+ // Compare name only if we are in a position of computing it.
+ // If not (for example, left is not an identifier), then let's fallback to false,
+ // so we can continue the chain of checks
+ let is_left_short = self.write_left(f)?;
+ self.write_operator(f)?;
+
+ let layout = self.layout(is_left_short)?;
+
+ let inner_content = format_with(|f| match &layout {
+ AssignmentLikeLayout::Fluid => {
+ let group_id = f.group_id("assignment_like");
+
+ let right = right.format().memoized();
+
+ write![
+ f,
+ [
+ group_elements(&indent(&soft_line_break_or_space()),)
+ .with_group_id(Some(group_id)),
+ line_suffix_boundary(),
+ if_group_breaks(&indent(&right)).with_group_id(Some(group_id)),
+ if_group_fits_on_line(&right).with_group_id(Some(group_id)),
+ ]
+ ]
+ }
+ AssignmentLikeLayout::BreakAfterOperator => {
+ write![
+ f,
+ [group_elements(&indent(&format_args![
+ soft_line_break_or_space(),
+ right.format()
+ ])),]
+ ]
}
+ AssignmentLikeLayout::NeverBreakAfterOperator => {
+ write![f, [space_token(), right.format(),]]
}
- if matches!(
- right,
- JsAnyExpression::JsClassExpression(_)
- | JsAnyExpression::JsTemplate(_)
- | JsAnyExpression::JsAnyLiteralExpression(
- JsAnyLiteralExpression::JsBooleanLiteralExpression(_),
- )
- | JsAnyExpression::JsAnyLiteralExpression(
- JsAnyLiteralExpression::JsNumberLiteralExpression(_)
+ AssignmentLikeLayout::Chain => {
+ write!(f, [soft_line_break_or_space(), right.format()])
+ }
+
+ AssignmentLikeLayout::ChainTail => {
+ write!(
+ f,
+ [&indent(&format_args![
+ soft_line_break_or_space(),
+ right.format()
+ ])]
)
- ) {
- return Ok(true);
}
+ });
- Ok(false)
+ match layout {
+ // Layouts that don't need enclosing group
+ AssignmentLikeLayout::Chain | AssignmentLikeLayout::ChainTail => {
+ write!(f, [&inner_content])
}
+ _ => {
+ write!(f, [group_elements(&inner_content)])
+ }
+ }
+ });
-impl Format<JsFormatContext> for JsAnyAssignmentLike {
- fn fmt(&self, f: &mut JsFormatter) -> FormatResult<()> {
- write_assignment_like(self, f)
+ write!(f, [format_content])
}
}

MODIFY
crates/rome_js_formatter/src/utils/binary_like_expression.rs
crates/rome_js_formatter/src/utils/binary_like_expression.rs
use crate::prelude::*;
use rome_formatter::{write, Buffer};
-
use rome_js_syntax::{
JsAnyExpression, JsAnyInProperty, JsBinaryExpression, JsBinaryOperator, JsInExpression,
JsInstanceofExpression, JsLogicalExpression, JsLogicalOperator, JsPrivateName, JsSyntaxKind,

MODIFY
crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js
crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js
@@ -109,3 +109,10 @@ render = withGraphQLQuery(
loadNext = (stateIsOK && hasNext) || {
skipNext: true,
};
+
+// chain and chain tail where it breaks
+bifornCringerMoshedPerplex = bifornCringerMoshedPerplexSawder = arrayOfNumb = a = "test"
+
+// chain and chain tail where it doesn't break
+loreum = ipsum = arrayOfNumb = a = "test"
+

MODIFY
crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js.snap
crates/rome_js_formatter/tests/specs/js/module/assignment/assignment.js.snap
---
source: crates/rome_js_formatter/tests/spec_test.rs
+assertion_line: 257
expression: assignment.js
---
# Input
@@ -114,6 +115,14 @@ render = withGraphQLQuery(
loadNext = (stateIsOK && hasNext) || {
skipNext: true,
};
+
+// chain and chain tail where it breaks
+bifornCringerMoshedPerplex = bifornCringerMoshedPerplexSawder = arrayOfNumb = a = "test"
+
+// chain and chain tail where it doesn't break
+loreum = ipsum = arrayOfNumb = a = "test"
+
+
=============================
# Outputs
## Output 1
@@ -280,6 +289,16 @@ loadNext = (stateIsOK && hasNext) || {
skipNext: true,
};
+// chain and chain tail where it breaks
+bifornCringerMoshedPerplex =
+ bifornCringerMoshedPerplexSawder =
+ arrayOfNumb =
+ a =
+ "test";
+
+// chain and chain tail where it doesn't break
+loreum = ipsum = arrayOfNumb = a = "test";
+
## Lines exceeding width of 80 characters

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/assignment-expression/assignment_expression.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/assignment-expression/assignment_expression.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: assignment_expression.js
---
# Input

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/assignment/chain-two-segments.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/assignment/chain-two-segments.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: chain-two-segments.js
---
# Input

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/assignment/chain.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/assignment/chain.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: chain.js
---
# Input
@@ -37,17 +38,26 @@ a=b=c;
# Output
```js
let bifornCringerMoshedPerplexSawder = askTrovenaBeenaDependsRowans =
- glimseGlyphsHazardNoopsTieTie = averredBathersBoxroomBuggyNurl =
- anodyneCondosMalateOverateRetinol = annularCooeedSplicesWalksWayWay =
+ glimseGlyphsHazardNoopsTieTie =
+ averredBathersBoxroomBuggyNurl =
+ anodyneCondosMalateOverateRetinol =
+ annularCooeedSplicesWalksWayWay =
kochabCooieGameOnOboleUnweave;
-bifornCringerMoshedPerplexSawder = askTrovenaBeenaDependsRowans =
- glimseGlyphsHazardNoopsTieTie = x = averredBathersBoxroomBuggyNurl =
+bifornCringerMoshedPerplexSawder =
+ askTrovenaBeenaDependsRowans =
+ glimseGlyphsHazardNoopsTieTie =
+ x =
+ averredBathersBoxroomBuggyNurl =
anodyneCondosMal(sdsadsa, dasdas, asd(() => sdf)).ateOverateRetinol =
- annularCooeedSplicesWalksWayWay = kochabCooieGameOnOboleUnweave;
+ annularCooeedSplicesWalksWayWay =
+ kochabCooieGameOnOboleUnweave;
-bifornCringerMoshedPerplexSawder = askTrovenaBeenaDependsRowans =
- glimseGlyphsHazardNoopsTieTie = x = averredBathersBoxroomBuggyNurl =
+bifornCringerMoshedPerplexSawder =
+ askTrovenaBeenaDependsRowans =
+ glimseGlyphsHazardNoopsTieTie =
+ x =
+ averredBathersBoxroomBuggyNurl =
anodyneCondosMal(sdsadsa, dasdas, asd(() => sdf)).ateOverateRetinol =
annularCooeedSplicesWalksWayWay =
kochabCooieGameOnOboleUnweave + kochabCooieGameOnOboleUnweave;

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/assignment/issue-1966.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/assignment/issue-1966.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: issue-1966.js
---
# Input

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/assignment/issue-3819.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/assignment/issue-3819.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: issue-3819.js
---
# Input
@@ -27,7 +28,8 @@ this.dummy.type1.dummyPropertyFunction =
this.dummy.type5.dummyPropertyFunction =
this.dummy.type6.dummyPropertyFunction =
this.dummy.type7.dummyPropertyFunction =
- this.dummy.type8.dummyPropertyFunction = () => {
+ this.dummy.type8.dummyPropertyFunction =
+ () => {
return "dummy";
};

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/babel-plugins/import-meta.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/babel-plugins/import-meta.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: import-meta.js
---
# Input

MODIFY
crates/rome_js_formatter/tests/specs/prettier/js/logical-assignment/logical-assignment.js.snap
crates/rome_js_formatter/tests/specs/prettier/js/logical-assignment/logical-assignment.js.snap
---
source: crates/rome_js_formatter/tests/prettier_tests.rs
+assertion_line: 182
expression: logical-assignment.js
---
# Input

MODIFY
crates/rome_js_formatter/tests/specs/prettier/typescript/as/assignment2.ts.snap
crates/rome_js_formatter/tests/specs/prettier/typescript/as/assignment2.ts.snap
@@ -42,7 +42,9 @@ const defaultMaskGetter = $parse(attrs[directiveName]) as (
scope: ng.IScope,
) => Mask;
-(this.configuration as any) = (this.editor as any) = (this.editorBody as any) =
+(this.configuration as any) =
+ (this.editor as any) =
+ (this.editorBody as any) =
undefined;
angular.module("foo").directive("formIsolator", () => {
@@ -54,7 +56,9 @@ angular.module("foo").directive("formIsolator", () => {
};
});
-(this.selectorElem as any) = this.multiselectWidget = this.initialValues =
+(this.selectorElem as any) =
+ this.multiselectWidget =
+ this.initialValues =
undefined;
const extraRendererAttrs = (

feat(rome_js_formatter): assignment expressions with layout (#2728)