Repository: dprint/dprint-plugin-typescript (rust)

Issue:
Better handling of excessive nested assignments
**Input / Expected Output**

```ts
Object.defineProperty(t, "__esModule", { value: !0 }),
  (t.CancellationSenderStrategy =
    t.CancellationReceiverStrategy =
    t.ConnectionError =
    t.ConnectionErrors =
    t.LogTraceNotification =
    t.SetTraceNotification =
    t.TraceFormat =
    t.Trace =
    t.ProgressType =
    t.createMessageConnection =
    t.NullLogger =
    t.ConnectionOptions =
    t.ConnectionStrategy =
    t.WriteableStreamMessageWriter =
    t.AbstractMessageWriter =
    t.MessageWriter =
    t.ReadableStreamMessageReader =
    t.AbstractMessageReader =
    t.MessageReader =
    t.CancellationToken =
    t.CancellationTokenSource =
    t.Emitter =
    t.Event =
    t.Disposable =
    t.ParameterStructures =
    t.NotificationType9 =
    t.NotificationType8 =
    t.NotificationType7 =
    t.NotificationType6 =
    t.NotificationType5 =
    t.NotificationType4 =
    t.NotificationType3 =
    t.NotificationType2 =
    t.NotificationType1 =
    t.NotificationType0 =
    t.NotificationType =
    t.ErrorCodes =
    t.ResponseError =
    t.RequestType9 =
    t.RequestType8 =
    t.RequestType7 =
    t.RequestType6 =
    t.RequestType5 =
    t.RequestType4 =
    t.RequestType3 =
    t.RequestType2 =
    t.RequestType1 =
    t.RequestType0 =
    t.RequestType =
    t.RAL =
      void 0),
  (t.CancellationStrategy = void 0);
```

**Actual Output**

```ts
Object.defineProperty(t, "__esModule", { value: !0 }),
  (t.CancellationSenderStrategy = t.CancellationReceiverStrategy = t
    .ConnectionError = t.ConnectionErrors = t.LogTraceNotification = t
      .SetTraceNotification = t.TraceFormat = t.Trace = t.ProgressType = t
        .createMessageConnection = t.NullLogger = t.ConnectionOptions = t
          .ConnectionStrategy = t.WriteableStreamMessageWriter = t
            .AbstractMessageWriter = t.MessageWriter = t
              .ReadableStreamMessageReader = t.AbstractMessageReader = t
                .MessageReader = t.CancellationToken = t
                  .CancellationTokenSource = t.Emitter = t.Event = t
                    .Disposable = t.ParameterStructures = t.NotificationType9 =
                      t.NotificationType8 = t.NotificationType7 = t
                        .NotificationType6 = t.NotificationType5 = t
                          .NotificationType4 = t.NotificationType3 = t
                            .NotificationType2 = t.NotificationType1 = t
                              .NotificationType0 = t.NotificationType = t
                                .ErrorCodes = t.ResponseError = t.RequestType9 =
                                  t.RequestType8 = t.RequestType7 = t
                                    .RequestType6 = t.RequestType5 = t
                                      .RequestType4 = t.RequestType3 = t
                                        .RequestType2 = t.RequestType1 = t
                                          .RequestType0 = t.RequestType = t
                                            .RAL = void 0),
  (t.CancellationStrategy = void 0);
```

For #363

Pull request description:
fix: better handling of excessive nested assignments
Closes #364

Patch:
diff --git a/src/generation/generate.rs b/src/generation/generate.rs
index e1d079f..4071727 100644
--- a/src/generation/generate.rs
+++ b/src/generation/generate.rs
@@ -1658,6 +1658,60 @@ fn gen_const_assertion<'a>(node: &'a TsConstAssertion, context: &mut Context<'a>
 }
 
 fn gen_assignment_expr<'a>(node: &'a AssignExpr, context: &mut Context<'a>) -> PrintItems {
+  // check for a nested assignment (ex. `a = b = c`)
+  if node.op() == AssignOp::Assign {
+    if let Expr::Assign(right) = node.right {
+      if right.op() == AssignOp::Assign {
+        let flattened_items = get_flattened_assign_expr(node, context);
+        // if the number of assignments chained is excessive, then it's probably minified
+        // so force using newlines to optimize formatting
+        let force_use_new_lines = flattened_items.len() > 4
+          || node_helpers::get_use_new_lines_for_nodes(&flattened_items[0].expr, &flattened_items.last().unwrap().expr, context.program);
+        let indent_width = context.config.indent_width;
+        return ir_helpers::gen_separated_values(
+          |_| {
+            let mut generated_nodes = Vec::new();
+            for item in flattened_items.into_iter() {
+              let lines_span = Some(ir_helpers::LinesSpan {
+                start_line: item.expr.span().start_line_fast(context.program),
+                end_line: item.expr.span().end_line_fast(context.program),
+              });
+              let mut items = gen_node(item.expr, context);
+              if let Some(op) = item.post_op {
+                items.push_str(" =");
+                items.extend(gen_trailing_comments(&op, context));
+              } else {
+                items = indent_if_start_of_line(items).into();
+              }
+
+              generated_nodes.push(ir_helpers::GeneratedValue {
+                items,
+                lines_span,
+                allow_inline_multi_line: true,
+                allow_inline_single_line: true,
+              });
+            }
+
+            generated_nodes
+          },
+          ir_helpers::GenSeparatedValuesOptions {
+            prefer_hanging: false,
+            force_use_new_lines,
+            allow_blank_lines: false,
+            single_line_space_at_start: false,
+            single_line_space_at_end: false,
+            single_line_separator: Signal::SpaceOrNewLine.into(),
+            indent_width,
+            multi_line_options: ir_helpers::MultiLineOptions::same_line_start_hanging_indent(),
+            force_possible_newline_at_start: false,
+          },
+        )
+        .items;
+      }
+    }
+  }
+
+  // parse a single assignment (ex. `a = b` and not `a = b = c`)
   let mut items = PrintItems::new();
   items.extend(gen_node(node.left.into(), context));
   items.extend(gen_assignment(node.right.into(), node.op().as_str(), context));
@@ -6511,15 +6565,18 @@ where
   let custom_close_paren = opts.custom_close_paren;
   let first_member_span = opts.nodes.iter().map(|n| n.span()).next();
   let nodes = opts.nodes;
+  let nodes_length = nodes.len();
   let prefer_hanging = if is_parameters {
     context.config.parameters_prefer_hanging
   } else {
     context.config.arguments_prefer_hanging
   };
-  let space_around = if is_parameters {
+  let space_around = if nodes_length > 0 && is_parameters {
     context.config.parameters_space_around
-  } else {
+  } else if nodes_length > 0 {
     context.config.arguments_space_around
+  } else {
+    false
   };
   let trailing_commas = get_trailing_commas(&opts.node, &nodes, is_parameters, context);
 
@@ -8290,7 +8347,7 @@ fn gen_assignment_like_with_token<'a>(expr: Node<'a>, op: &str, op_token: Option
     items.push_str(op)
   } else {
     items.push_string(format!(" {}", op))
-  }; // good enough for now...
+  }
 
   let op_end = op_token
     .map(|x| x.hi())
diff --git a/src/generation/swc/flatten_assign_expr.rs b/src/generation/swc/flatten_assign_expr.rs
new file mode 100644
index 0000000..be841d1
--- /dev/null
+++ b/src/generation/swc/flatten_assign_expr.rs
@@ -0,0 +1,37 @@
+use super::super::Context;
+use deno_ast::swc::parser::token::TokenAndSpan;
+use deno_ast::view::*;
+
+pub struct AssignExprItem<'a> {
+  pub post_op: Option<&'a TokenAndSpan>,
+  pub expr: Node<'a>,
+}
+
+pub fn get_flattened_assign_expr<'a>(node: &'a AssignExpr, context: &mut Context<'a>) -> Vec<AssignExprItem<'a>> {
+  assert_eq!(node.op(), AssignOp::Assign);
+  let mut items = Vec::new();
+  let mut current_node = Some(node);
+  while let Some(node) = current_node.take() {
+    let operator_token = context.token_finder.get_first_operator_after(&node.left, node.op().as_str()).unwrap();
+    items.push(AssignExprItem {
+      post_op: Some(operator_token),
+      expr: node.left.into(),
+    });
+
+    if let Expr::Assign(node) = node.right {
+      if node.op() == AssignOp::Assign {
+        current_node = Some(node);
+      }
+    }
+
+    if current_node.is_none() {
+      // add the last item
+      items.push(AssignExprItem {
+        post_op: None,
+        expr: node.right.into(),
+      });
+    }
+  }
+
+  items
+}
diff --git a/src/generation/swc/mod.rs b/src/generation/swc/mod.rs
index 35de1bb..2ffb0da 100644
--- a/src/generation/swc/mod.rs
+++ b/src/generation/swc/mod.rs
@@ -1,7 +1,9 @@
 mod extensions;
+mod flatten_assign_expr;
 mod flatten_binary_expr;
 mod flatten_member_like_expr;
 
 pub use extensions::*;
+pub use flatten_assign_expr::*;
 pub use flatten_binary_expr::*;
 pub use flatten_member_like_expr::*;


Test patch:
diff --git a/tests/specs/expressions/AssignmentExpression/AssignmentExpression_All.txt b/tests/specs/expressions/AssignmentExpression/AssignmentExpression_All.txt
index d4d9743..70e6af1 100644
--- a/tests/specs/expressions/AssignmentExpression/AssignmentExpression_All.txt
+++ b/tests/specs/expressions/AssignmentExpression/AssignmentExpression_All.txt
@@ -45,3 +45,15 @@ a ??= b;
 a &&= b;
 a ||= b;
 a ??= b;
+
+== should go multi-line once excessive -- probably minified file, so don't really care about this ==
+a = b = c = d;
+a = b = c = d = e;
+
+[expect]
+a = b = c = d;
+a =
+    b =
+    c =
+    d =
+        e;
diff --git a/tests/specs/general/Arguments_SpaceAround_True.txt b/tests/specs/general/Arguments_SpaceAround_True.txt
index 5e4de43..4ae39e0 100644
--- a/tests/specs/general/Arguments_SpaceAround_True.txt
+++ b/tests/specs/general/Arguments_SpaceAround_True.txt
@@ -20,3 +20,11 @@ testing(
     123456,
     123456,
 );
+
+== should not add spaces on empty arguments ==
+testing()
+testing() === true;
+
+[expect]
+testing();
+testing() === true;
diff --git a/tests/specs/issues/issue0364.txt b/tests/specs/issues/issue0364.txt
new file mode 100644
index 0000000..301e68b
--- /dev/null
+++ b/tests/specs/issues/issue0364.txt
@@ -0,0 +1,110 @@
+~~ deno: true ~~
+== should format ==
+Object.defineProperty(t, "__esModule", { value: !0 }),
+  (t.CancellationSenderStrategy =
+    t.CancellationReceiverStrategy =
+    t.ConnectionError =
+    t.ConnectionErrors =
+    t.LogTraceNotification =
+    t.SetTraceNotification =
+    t.TraceFormat =
+    t.Trace =
+    t.ProgressType =
+    t.createMessageConnection =
+    t.NullLogger =
+    t.ConnectionOptions =
+    t.ConnectionStrategy =
+    t.WriteableStreamMessageWriter =
+    t.AbstractMessageWriter =
+    t.MessageWriter =
+    t.ReadableStreamMessageReader =
+    t.AbstractMessageReader =
+    t.MessageReader =
+    t.CancellationToken =
+    t.CancellationTokenSource =
+    t.Emitter =
+    t.Event =
+    t.Disposable =
+    t.ParameterStructures =
+    t.NotificationType9 =
+    t.NotificationType8 =
+    t.NotificationType7 =
+    t.NotificationType6 =
+    t.NotificationType5 =
+    t.NotificationType4 =
+    t.NotificationType3 =
+    t.NotificationType2 =
+    t.NotificationType1 =
+    t.NotificationType0 =
+    t.NotificationType =
+    t.ErrorCodes =
+    t.ResponseError =
+    t.RequestType9 =
+    t.RequestType8 =
+    t.RequestType7 =
+    t.RequestType6 =
+    t.RequestType5 =
+    t.RequestType4 =
+    t.RequestType3 =
+    t.RequestType2 =
+    t.RequestType1 =
+    t.RequestType0 =
+    t.RequestType =
+    t.RAL =
+      void 0),
+  (t.CancellationStrategy = void 0);
+
+[expect]
+Object.defineProperty(t, "__esModule", { value: !0 }),
+  (t.CancellationSenderStrategy =
+    t.CancellationReceiverStrategy =
+    t.ConnectionError =
+    t.ConnectionErrors =
+    t.LogTraceNotification =
+    t.SetTraceNotification =
+    t.TraceFormat =
+    t.Trace =
+    t.ProgressType =
+    t.createMessageConnection =
+    t.NullLogger =
+    t.ConnectionOptions =
+    t.ConnectionStrategy =
+    t.WriteableStreamMessageWriter =
+    t.AbstractMessageWriter =
+    t.MessageWriter =
+    t.ReadableStreamMessageReader =
+    t.AbstractMessageReader =
+    t.MessageReader =
+    t.CancellationToken =
+    t.CancellationTokenSource =
+    t.Emitter =
+    t.Event =
+    t.Disposable =
+    t.ParameterStructures =
+    t.NotificationType9 =
+    t.NotificationType8 =
+    t.NotificationType7 =
+    t.NotificationType6 =
+    t.NotificationType5 =
+    t.NotificationType4 =
+    t.NotificationType3 =
+    t.NotificationType2 =
+    t.NotificationType1 =
+    t.NotificationType0 =
+    t.NotificationType =
+    t.ErrorCodes =
+    t.ResponseError =
+    t.RequestType9 =
+    t.RequestType8 =
+    t.RequestType7 =
+    t.RequestType6 =
+    t.RequestType5 =
+    t.RequestType4 =
+    t.RequestType3 =
+    t.RequestType2 =
+    t.RequestType1 =
+    t.RequestType0 =
+    t.RequestType =
+    t.RAL =
+      void 0),
+  (t.CancellationStrategy = void 0);