Repository: swc-project/swc (rust)

Issue:
Optional chaining parentheses semantics lost
### Describe the bug

An optional chaining sequence short circuits the entire chain of operations, but does not cross the boundary of parentheses. Parentheses are lost when compiling an optional chain, so it behaves differently (which will almost always result in a `TypeError` but hey, it's what the user asked for).

Something to note is that `tsc`'s behavior is that when the parentheses only contain a cast is the parentheses are dropped. Not sure if that behavior is to be kept but something I discovered. If transpiling to <ES2020, tsc _keeps_ the parens and the semantics are different, which I found interesting and confusing:

`tsc` in: `(a?.b as string[]).c`
`tsc` out (ES2020): `a?.b.c;`
`tsc` out (ES2016): `(a === null || a === void 0 ? void 0 : a.b).c;`

`tsc` in: `(a?.b).c`
`tsc` out: `(a?.b).c`

### Input code

```typescript
(a?.b).c
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false
    },
    "target": "es2020",
    "loose": false,
    "minify": {
      "compress": false,
      "mangle": false
    }
  },
  "module": {
    "type": "commonjs"
  },
  "minify": false,
  "isModule": true
}
```


### Playground link

https://play.swc.rs/?version=1.2.220&code=H4sIAAAAAAAAA9NItNdL0tRLBgDvvqKQCAAAAA%3D%3D&config=H4sIAAAAAAAAA0WNSwrDMAxE76J1FiHL3KGHMK4SHPxD40CN8d0rF5fsJM2bp0YXLO2NshGwjAk1FvOhnUrNDCsuF1qoQE%2BH8eCui5GTiyKMbd1WjX1K4AksFFx0Rx0ym0IWBp7IxNP%2Fya6ukN73OLTfP3VqJaR4gfojmmWH16SL3Ny%2F3SauU70AAAA%3D

### Expected behavior

```js
"use strict";
(a?.b).c;
```

### Actual behavior

```js
"use strict";
a?.b.c;
```

### Version

1.2.220

### Additional context

_No response_

Pull request description:
fix(es/base): Preserve paren expr for optional chaining expr
<!-- Note: CI script will automatically rebase your PR so please do not rebase unless required -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:**

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

**BREAKING CHANGE:**

<!--
If this PR introduces a breaking change, it must contain a notice for it to be included in the CHANGELOG. Add description or remove entirely if not breaking.

You may need to update `MIGRATION.md` for the breaking changes.
-->

**Related issue (if exists):**
- fix #5332

Patch:
diff --git a/crates/swc_ecma_transforms_base/src/fixer.rs b/crates/swc_ecma_transforms_base/src/fixer.rs
index dfc7914bbb..906f84668a 100644
--- a/crates/swc_ecma_transforms_base/src/fixer.rs
+++ b/crates/swc_ecma_transforms_base/src/fixer.rs
@@ -905,6 +905,8 @@ impl Fixer<'_> {
                 ..
             }) => {
                 match &**expr {
+                    // `(a?.b).c !== a?.b.c`
+                    Expr::OptChain(..) => return,
                     Expr::Bin(bin_expr) if bin_expr.left.is_object() => {
                         return;
                     }


Test patch:
diff --git a/crates/swc/tests/fixture/issues-5xxx/5332/input/.swcrc b/crates/swc/tests/fixture/issues-5xxx/5332/input/.swcrc
new file mode 100644
index 0000000000..1a275fa287
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-5xxx/5332/input/.swcrc
@@ -0,0 +1,9 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "ecmascript"
+        },
+        "target": "es2022"
+    },
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-5xxx/5332/input/index.js b/crates/swc/tests/fixture/issues-5xxx/5332/input/index.js
new file mode 100644
index 0000000000..0f6b45f128
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-5xxx/5332/input/index.js
@@ -0,0 +1,1 @@
+(a?.b).c;
diff --git a/crates/swc/tests/fixture/issues-5xxx/5332/output/index.js b/crates/swc/tests/fixture/issues-5xxx/5332/output/index.js
new file mode 100644
index 0000000000..0f6b45f128
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-5xxx/5332/output/index.js
@@ -0,0 +1,1 @@
+(a?.b).c;
diff --git a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.1.normal.js b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.1.normal.js
index 8cb9f31cd5..f124d97aa4 100644
--- a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.1.normal.js
+++ b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.1.normal.js
@@ -8,3 +8,8 @@ const foo = new Foo();
 (ref1 = foo.m) === null || ref1 === void 0 ? void 0 : ref1();
 (ref2 = (/*a2*/ foo.m)) === null || ref2 === void 0 ? void 0 : ref2();
 (ref3 = (foo.m /*b3*/ )) === null || ref3 === void 0 ? void 0 : ref3();
+// https://github.com/microsoft/TypeScript/issues/50148
+(foo === null || foo === void 0 ? void 0 : foo.m).length;
+(foo === null || foo === void 0 ? void 0 : foo.m).length;
+(foo === null || foo === void 0 ? void 0 : foo["m"]).length;
+(foo === null || foo === void 0 ? void 0 : foo["m"]).length;
diff --git a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.2.minified.js b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.2.minified.js
index 13ac3cf617..281d7c1c22 100644
--- a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.2.minified.js
+++ b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es2015.2.minified.js
@@ -2,4 +2,4 @@ var ref, ref1, ref2, ref3;
 const foo = new class {
     m() {}
 }();
-null === (ref = foo.m) || void 0 === ref || ref(), null === (ref1 = foo.m) || void 0 === ref1 || ref1(), null === (ref2 = foo.m) || void 0 === ref2 || ref2(), null === (ref3 = foo.m) || void 0 === ref3 || ref3();
+null === (ref = foo.m) || void 0 === ref || ref(), null === (ref1 = foo.m) || void 0 === ref1 || ref1(), null === (ref2 = foo.m) || void 0 === ref2 || ref2(), null === (ref3 = foo.m) || void 0 === ref3 || ref3(), (null == foo ? void 0 : foo.m).length, (null == foo ? void 0 : foo.m).length, (null == foo ? void 0 : foo.m).length, (null == foo ? void 0 : foo.m).length;
diff --git a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.1.normal.js b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.1.normal.js
index d3138a13c4..606bfa61b9 100644
--- a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.1.normal.js
+++ b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.1.normal.js
@@ -15,3 +15,8 @@ var foo = new Foo();
 (ref1 = foo.m) === null || ref1 === void 0 ? void 0 : ref1();
 (ref2 = (/*a2*/ foo.m)) === null || ref2 === void 0 ? void 0 : ref2();
 (ref3 = (foo.m /*b3*/ )) === null || ref3 === void 0 ? void 0 : ref3();
+// https://github.com/microsoft/TypeScript/issues/50148
+(foo === null || foo === void 0 ? void 0 : foo.m).length;
+(foo === null || foo === void 0 ? void 0 : foo.m).length;
+(foo === null || foo === void 0 ? void 0 : foo["m"]).length;
+(foo === null || foo === void 0 ? void 0 : foo["m"]).length;
diff --git a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.2.minified.js b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.2.minified.js
index 7ef6636955..92460fc598 100644
--- a/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.2.minified.js
+++ b/crates/swc/tests/tsc-references/optionalChainingInTypeAssertions_es5.2.minified.js
@@ -6,4 +6,4 @@ var ref, ref1, ref2, ref3, Foo = function() {
     }
     return Foo.prototype.m = function() {}, Foo;
 }(), foo = new Foo();
-null === (ref = foo.m) || void 0 === ref || ref(), null === (ref1 = foo.m) || void 0 === ref1 || ref1(), null === (ref2 = foo.m) || void 0 === ref2 || ref2(), null === (ref3 = foo.m) || void 0 === ref3 || ref3();
+null === (ref = foo.m) || void 0 === ref || ref(), null === (ref1 = foo.m) || void 0 === ref1 || ref1(), null === (ref2 = foo.m) || void 0 === ref2 || ref2(), null === (ref3 = foo.m) || void 0 === ref3 || ref3(), (null == foo ? void 0 : foo.m).length, (null == foo ? void 0 : foo.m).length, (null == foo ? void 0 : foo.m).length, (null == foo ? void 0 : foo.m).length;
diff --git a/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.json b/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.json
index b42766e009..60b641c4ef 100644
--- a/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.json
+++ b/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.json
@@ -2,7 +2,7 @@
   "type": "Script",
   "span": {
     "start": 29,
-    "end": 203,
+    "end": 360,
     "ctxt": 0
   },
   "body": [
@@ -466,6 +466,390 @@
           "typeArguments": null
         }
       }
+    },
+    {
+      "type": "ExpressionStatement",
+      "span": {
+        "start": 261,
+        "end": 284,
+        "ctxt": 0
+      },
+      "expression": {
+        "type": "MemberExpression",
+        "span": {
+          "start": 261,
+          "end": 283,
+          "ctxt": 0
+        },
+        "object": {
+          "type": "ParenthesisExpression",
+          "span": {
+            "start": 261,
+            "end": 276,
+            "ctxt": 0
+          },
+          "expression": {
+            "type": "TsAsExpression",
+            "span": {
+              "start": 262,
+              "end": 275,
+              "ctxt": 0
+            },
+            "expression": {
+              "type": "OptionalChainingExpression",
+              "span": {
+                "start": 262,
+                "end": 268,
+                "ctxt": 0
+              },
+              "questionDotToken": {
+                "start": 265,
+                "end": 266,
+                "ctxt": 0
+              },
+              "base": {
+                "type": "MemberExpression",
+                "span": {
+                  "start": 262,
+                  "end": 268,
+                  "ctxt": 0
+                },
+                "object": {
+                  "type": "Identifier",
+                  "span": {
+                    "start": 262,
+                    "end": 265,
+                    "ctxt": 0
+                  },
+                  "value": "foo",
+                  "optional": false
+                },
+                "property": {
+                  "type": "Identifier",
+                  "span": {
+                    "start": 267,
+                    "end": 268,
+                    "ctxt": 0
+                  },
+                  "value": "m",
+                  "optional": false
+                }
+              }
+            },
+            "typeAnnotation": {
+              "type": "TsKeywordType",
+              "span": {
+                "start": 272,
+                "end": 275,
+                "ctxt": 0
+              },
+              "kind": "any"
+            }
+          }
+        },
+        "property": {
+          "type": "Identifier",
+          "span": {
+            "start": 277,
+            "end": 283,
+            "ctxt": 0
+          },
+          "value": "length",
+          "optional": false
+        }
+      }
+    },
+    {
+      "type": "ExpressionStatement",
+      "span": {
+        "start": 285,
+        "end": 306,
+        "ctxt": 0
+      },
+      "expression": {
+        "type": "MemberExpression",
+        "span": {
+          "start": 285,
+          "end": 305,
+          "ctxt": 0
+        },
+        "object": {
+          "type": "ParenthesisExpression",
+          "span": {
+            "start": 285,
+            "end": 298,
+            "ctxt": 0
+          },
+          "expression": {
+            "type": "TsTypeAssertion",
+            "span": {
+              "start": 286,
+              "end": 297,
+              "ctxt": 0
+            },
+            "expression": {
+              "type": "OptionalChainingExpression",
+              "span": {
+                "start": 291,
+                "end": 297,
+                "ctxt": 0
+              },
+              "questionDotToken": {
+                "start": 294,
+                "end": 295,
+                "ctxt": 0
+              },
+              "base": {
+                "type": "MemberExpression",
+                "span": {
+                  "start": 291,
+                  "end": 297,
+                  "ctxt": 0
+                },
+                "object": {
+                  "type": "Identifier",
+                  "span": {
+                    "start": 291,
+                    "end": 294,
+                    "ctxt": 0
+                  },
+                  "value": "foo",
+                  "optional": false
+                },
+                "property": {
+                  "type": "Identifier",
+                  "span": {
+                    "start": 296,
+                    "end": 297,
+                    "ctxt": 0
+                  },
+                  "value": "m",
+                  "optional": false
+                }
+              }
+            },
+            "typeAnnotation": {
+              "type": "TsKeywordType",
+              "span": {
+                "start": 287,
+                "end": 290,
+                "ctxt": 0
+              },
+              "kind": "any"
+            }
+          }
+        },
+        "property": {
+          "type": "Identifier",
+          "span": {
+            "start": 299,
+            "end": 305,
+            "ctxt": 0
+          },
+          "value": "length",
+          "optional": false
+        }
+      }
+    },
+    {
+      "type": "ExpressionStatement",
+      "span": {
+        "start": 307,
+        "end": 334,
+        "ctxt": 0
+      },
+      "expression": {
+        "type": "MemberExpression",
+        "span": {
+          "start": 307,
+          "end": 333,
+          "ctxt": 0
+        },
+        "object": {
+          "type": "ParenthesisExpression",
+          "span": {
+            "start": 307,
+            "end": 326,
+            "ctxt": 0
+          },
+          "expression": {
+            "type": "TsAsExpression",
+            "span": {
+              "start": 308,
+              "end": 325,
+              "ctxt": 0
+            },
+            "expression": {
+              "type": "OptionalChainingExpression",
+              "span": {
+                "start": 308,
+                "end": 318,
+                "ctxt": 0
+              },
+              "questionDotToken": {
+                "start": 311,
+                "end": 312,
+                "ctxt": 0
+              },
+              "base": {
+                "type": "MemberExpression",
+                "span": {
+                  "start": 308,
+                  "end": 318,
+                  "ctxt": 0
+                },
+                "object": {
+                  "type": "Identifier",
+                  "span": {
+                    "start": 308,
+                    "end": 311,
+                    "ctxt": 0
+                  },
+                  "value": "foo",
+                  "optional": false
+                },
+                "property": {
+                  "type": "Computed",
+                  "span": {
+                    "start": 313,
+                    "end": 318,
+                    "ctxt": 0
+                  },
+                  "expression": {
+                    "type": "StringLiteral",
+                    "span": {
+                      "start": 314,
+                      "end": 317,
+                      "ctxt": 0
+                    },
+                    "value": "m",
+                    "raw": "\"m\""
+                  }
+                }
+              }
+            },
+            "typeAnnotation": {
+              "type": "TsKeywordType",
+              "span": {
+                "start": 322,
+                "end": 325,
+                "ctxt": 0
+              },
+              "kind": "any"
+            }
+          }
+        },
+        "property": {
+          "type": "Identifier",
+          "span": {
+            "start": 327,
+            "end": 333,
+            "ctxt": 0
+          },
+          "value": "length",
+          "optional": false
+        }
+      }
+    },
+    {
+      "type": "ExpressionStatement",
+      "span": {
+        "start": 335,
+        "end": 360,
+        "ctxt": 0
+      },
+      "expression": {
+        "type": "MemberExpression",
+        "span": {
+          "start": 335,
+          "end": 359,
+          "ctxt": 0
+        },
+        "object": {
+          "type": "ParenthesisExpression",
+          "span": {
+            "start": 335,
+            "end": 352,
+            "ctxt": 0
+          },
+          "expression": {
+            "type": "TsTypeAssertion",
+            "span": {
+              "start": 336,
+              "end": 351,
+              "ctxt": 0
+            },
+            "expression": {
+              "type": "OptionalChainingExpression",
+              "span": {
+                "start": 341,
+                "end": 351,
+                "ctxt": 0
+              },
+              "questionDotToken": {
+                "start": 344,
+                "end": 345,
+                "ctxt": 0
+              },
+              "base": {
+                "type": "MemberExpression",
+                "span": {
+                  "start": 341,
+                  "end": 351,
+                  "ctxt": 0
+                },
+                "object": {
+                  "type": "Identifier",
+                  "span": {
+                    "start": 341,
+                    "end": 344,
+                    "ctxt": 0
+                  },
+                  "value": "foo",
+                  "optional": false
+                },
+                "property": {
+                  "type": "Computed",
+                  "span": {
+                    "start": 346,
+                    "end": 351,
+                    "ctxt": 0
+                  },
+                  "expression": {
+                    "type": "StringLiteral",
+                    "span": {
+                      "start": 347,
+                      "end": 350,
+                      "ctxt": 0
+                    },
+                    "value": "m",
+                    "raw": "\"m\""
+                  }
+                }
+              }
+            },
+            "typeAnnotation": {
+              "type": "TsKeywordType",
+              "span": {
+                "start": 337,
+                "end": 340,
+                "ctxt": 0
+              },
+              "kind": "any"
+            }
+          }
+        },
+        "property": {
+          "type": "Identifier",
+          "span": {
+            "start": 353,
+            "end": 359,
+            "ctxt": 0
+          },
+          "value": "length",
+          "optional": false
+        }
+      }
     }
   ],
   "interpreter": null
diff --git a/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.ts b/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.ts
index 3140c2d6ca..cd92258d3e 100644
--- a/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.ts
+++ b/crates/swc_ecma_parser/tests/tsc/optionalChainingInTypeAssertions.ts
@@ -11,3 +11,9 @@ const foo = new Foo();
 
 /*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
 /*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();
+
+// https://github.com/microsoft/TypeScript/issues/50148
+(foo?.m as any).length;
+(<any>foo?.m).length;
+(foo?.["m"] as any).length;
+(<any>foo?.["m"]).length;
\ No newline at end of file