Repository: prettier/prettier (JavaScript) — Bug Fix

Issue:
Brackets around property accessor on `let` are removed; changes program syntax
**Prettier 2.8.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEA3AhgJwAQBs4zYC82A2gIwC6A3ADpQAU+MpADJcdgEwCUdUIADQgIABxgBLaAGdkoLJggB3AApYEslOlxL0AT1nCARpnRgA1gQDK6ALZwAMhKhxkAM23S4x0xeuizZwBzZBhMAFdvEDhbIzgAE3iEh3QoIPD0ILgAMQhMW3QYSTTkEHRwmAghEAALGFtcAHUaiXhpALA4Kw1WiVRWvVKwaUMQZy9MGBVTIIL3TyiAK2kADytg-ABFcIh4edwvYQDMCdKjdDjcatFMZxhGiXiYGuQADlYjxS9G01FSm7gE1QrmEAEcdvBpmJNGVpABaFwJBLVTBwcESVHTTJzJAeA5RLy2CShCIEjZwACCRVuRgqcBUcEwThc+0OIGk5O2u1cuIWwhgFweTxeSC4-NMElwwQAwhBbDjotIAKzVcJeAAqF00eLZqEiAEkoElYFYwLdxBSjVYYHp8Ky4ABfB1AA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
var let = [1];
(let[0] = 2);

```

**Output:**
<!-- prettier-ignore -->
```jsx
var let = [1];
let[0] = 2;

```

**Second Output:**
<!-- prettier-ignore -->
```jsx
SyntaxError: Unexpected token (2:5)
  1 | var let = [1];
> 2 | let[0] = 2;
    |     ^
  3 |
```

**Expected behavior:**
Do not remove the brackets, as they are necessary to make the assignment not destructuring. This is a very niche bug, but it seems like a fairly trivial oversight.

For context, I'm documenting expression statements at https://github.com/mdn/content/pull/22994, and this tripped me up when I was trying to document a workaround.


Discussion:
Which style do you think we should use?

`(let[a][1] = 2);`

or

`(let)[a][1] = 2;`

I prefer the second one.
I don't have strong opinions, and I can't think of other similar cases that I can follow. I'd say the second one, since that seems like less work in terms of emitting. Grouping `let` also makes it obvious that it's a primary expression on its own, instead of a keyword.

_Edit._ Ah, a precedent would be:

```js
// Before:
(function () {}());

// After:
(function () {})();
```

Since we always group the smallest primary expression, seems the second option is indeed better.

_Edit 2._ But we are also inconsistent here 😥

```js
// Before:
({}).toString();

// After:
({}.toString());
```

I don't know if we want to "fix" this case, but that just means we can do whatever we want 😄

Resolving patch:
diff --git a/changelog_unreleased/javascript/14000.md b/changelog_unreleased/javascript/14000.md
new file mode 100644
index 000000000000..a8388fda0f1a
--- /dev/null
+++ b/changelog_unreleased/javascript/14000.md
@@ -0,0 +1,19 @@
+#### Fix missing parentheses when an expression statement starts with `let[` (#14000 by @fisker, @thorn0)
+
+<!-- prettier-ignore -->
+```jsx
+// Input
+(let[0] = 2);
+
+// Prettier stable
+let[0] = 2;
+
+// Prettier stable (second format)
+SyntaxError: Unexpected token (1:5)
+> 1 | let[0] = 2;
+    |     ^
+  2 |
+
+// Prettier main
+(let)[0] = 2;
+```
diff --git a/package.json b/package.json
index d87d6b49792f..5c6e20827e5d 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
     "@glimmer/syntax": "0.84.2",
     "@iarna/toml": "2.2.5",
     "@typescript-eslint/typescript-estree": "5.45.0",
-    "acorn": "8.8.0",
+    "acorn": "8.8.1",
     "acorn-jsx": "5.3.2",
     "angular-estree-parser": "2.5.1",
     "angular-html-parser": "1.8.0",
@@ -44,7 +44,7 @@
     "editorconfig": "0.15.3",
     "editorconfig-to-prettier": "0.2.0",
     "escape-string-regexp": "5.0.0",
-    "espree": "9.4.0",
+    "espree": "9.4.1",
     "esutils": "2.0.3",
     "fast-glob": "3.2.11",
     "fast-json-stable-stringify": "2.1.0",
diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js
index 94d95febc44a..7a31778038dc 100644
--- a/src/language-js/needs-parens.js
+++ b/src/language-js/needs-parens.js
@@ -77,6 +77,39 @@ function needsParens(path, options) {
       return true;
     }
 
+    // `(let)[a] = 1`
+    if (
+      name === "object" &&
+      node.name === "let" &&
+      parent.type === "MemberExpression" &&
+      parent.computed &&
+      !parent.optional
+    ) {
+      const statement = path.findAncestor(
+        (node) =>
+          node.type === "ExpressionStatement" ||
+          node.type === "ForStatement" ||
+          node.type === "ForInStatement" ||
+          node.type === "ForOfStatement"
+      );
+      const expression = !statement
+        ? undefined
+        : statement.type === "ExpressionStatement"
+        ? statement.expression
+        : statement.type === "ForStatement"
+        ? statement.init
+        : statement.left;
+      if (
+        expression &&
+        startsWithNoLookaheadToken(
+          expression,
+          (leftmostNode) => leftmostNode === node
+        )
+      ) {
+        return true;
+      }
+    }
+
     return false;
   }
 
@@ -157,7 +190,11 @@ function needsParens(path, options) {
       if (
         startsWithNoLookaheadToken(
           node,
-          /* forbidFunctionClassAndDoExpr */ true
+          (node) =>
+            node.type === "ObjectExpression" ||
+            node.type === "FunctionExpression" ||
+            node.type === "ClassExpression" ||
+            node.type === "DoExpression"
         )
       ) {
         return true;
@@ -170,7 +207,7 @@ function needsParens(path, options) {
         node.type !== "SequenceExpression" && // these have parens added anyway
         startsWithNoLookaheadToken(
           node,
-          /* forbidFunctionClassAndDoExpr */ false
+          (node) => node.type === "ObjectExpression"
         )
       ) {
         return true;
diff --git a/src/language-js/print/function.js b/src/language-js/print/function.js
index 8263a11f0886..6323c639da45 100644
--- a/src/language-js/print/function.js
+++ b/src/language-js/print/function.js
@@ -373,7 +373,10 @@ function printArrowFunction(path, options, print, args) {
   // a <= a ? a : a
   const shouldAddParens =
     node.body.type === "ConditionalExpression" &&
-    !startsWithNoLookaheadToken(node.body, /* forbidFunctionAndClass */ false);
+    !startsWithNoLookaheadToken(
+      node.body,
+      (node) => node.type === "ObjectExpression"
+    );
 
   return group([
     ...parts,
diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js
index 097010953a13..15fd6069b7e9 100644
--- a/src/language-js/utils/index.js
+++ b/src/language-js/utils/index.js
@@ -939,74 +939,59 @@ function shouldPrintComma(options, level = "es5") {
 }
 
 /**
- * Tests if an expression starts with `{`, or (if forbidFunctionClassAndDoExpr
- * holds) `function`, `class`, or `do {}`. Will be overzealous if there's
- * already necessary grouping parentheses.
+ * Tests if the leftmost node of the expression matches the predicate. E.g.,
+ * used to check whether an expression statement needs to be wrapped in extra
+ * parentheses because it starts with:
+ *
+ * - `{`
+ * - `function`, `class`, or `do {}`
+ * - `let[`
+ *
+ * Will be overzealous if there already are necessary grouping parentheses.
  *
  * @param {Node} node
- * @param {boolean} forbidFunctionClassAndDoExpr
+ * @param {(leftmostNode: Node) => boolean} predicate
  * @returns {boolean}
  */
-function startsWithNoLookaheadToken(node, forbidFunctionClassAndDoExpr) {
-  node = getLeftMost(node);
+function startsWithNoLookaheadToken(node, predicate) {
   switch (node.type) {
-    case "FunctionExpression":
-    case "ClassExpression":
-    case "DoExpression":
-      return forbidFunctionClassAndDoExpr;
-    case "ObjectExpression":
-      return true;
+    case "BinaryExpression":
+    case "LogicalExpression":
+    case "AssignmentExpression":
+    case "NGPipeExpression":
+      return startsWithNoLookaheadToken(node.left, predicate);
     case "MemberExpression":
     case "OptionalMemberExpression":
-      return startsWithNoLookaheadToken(
-        node.object,
-        forbidFunctionClassAndDoExpr
-      );
+      return startsWithNoLookaheadToken(node.object, predicate);
     case "TaggedTemplateExpression":
       if (node.tag.type === "FunctionExpression") {
         // IIFEs are always already parenthesized
         return false;
       }
-      return startsWithNoLookaheadToken(node.tag, forbidFunctionClassAndDoExpr);
+      return startsWithNoLookaheadToken(node.tag, predicate);
     case "CallExpression":
     case "OptionalCallExpression":
       if (node.callee.type === "FunctionExpression") {
         // IIFEs are always already parenthesized
         return false;
       }
-      return startsWithNoLookaheadToken(
-        node.callee,
-        forbidFunctionClassAndDoExpr
-      );
+      return startsWithNoLookaheadToken(node.callee, predicate);
     case "ConditionalExpression":
-      return startsWithNoLookaheadToken(
-        node.test,
-        forbidFunctionClassAndDoExpr
-      );
+      return startsWithNoLookaheadToken(node.test, predicate);
     case "UpdateExpression":
       return (
-        !node.prefix &&
-        startsWithNoLookaheadToken(node.argument, forbidFunctionClassAndDoExpr)
+        !node.prefix && startsWithNoLookaheadToken(node.argument, predicate)
       );
     case "BindExpression":
-      return (
-        node.object &&
-        startsWithNoLookaheadToken(node.object, forbidFunctionClassAndDoExpr)
-      );
+      return node.object && startsWithNoLookaheadToken(node.object, predicate);
     case "SequenceExpression":
-      return startsWithNoLookaheadToken(
-        node.expressions[0],
-        forbidFunctionClassAndDoExpr
-      );
+      return startsWithNoLookaheadToken(node.expressions[0], predicate);
     case "TSSatisfiesExpression":
     case "TSAsExpression":
     case "TSNonNullExpression":
-      return startsWithNoLookaheadToken(
-        node.expression,
-        forbidFunctionClassAndDoExpr
-      );
+      return startsWithNoLookaheadToken(node.expression, predicate);
     default:
-      return false;
+      return predicate(node);
   }
 }
 
@@ -1092,13 +1077,6 @@ function getPrecedence(operator) {
   return PRECEDENCE.get(operator);
 }
 
-function getLeftMost(node) {
-  while (node.left) {
-    node = node.left;
-  }
-  return node;
-}
-
 function isBitwiseOperator(operator) {
   return (
     Boolean(bitshiftOperators[operator]) ||
diff --git a/yarn.lock b/yarn.lock
index 172f1f94fa58..c9930b25eae4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1811,10 +1811,10 @@ acorn-walk@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
-acorn@8.8.0, acorn@^8.2.4, acorn@^8.8.0:
-  version "8.8.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
-  integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+acorn@8.8.1, acorn@^8.2.4, acorn@^8.8.0:
+  version "8.8.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
+  integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
 
 acorn@^7.1.1:
   version "7.4.1"
@@ -3176,7 +3176,16 @@ esm-utils@3.0.0:
   dependencies:
     url-or-path "2.1.0"
 
-espree@9.4.0, espree@^9.4.0:
+espree@9.4.1:
+  version "9.4.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd"
+  integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==
+  dependencies:
+    acorn "^8.8.0"
+    acorn-jsx "^5.3.2"
+    eslint-visitor-keys "^3.3.0"
+
+espree@^9.4.0:
   version "9.4.0"
   resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a"
   integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==


Test patch:
diff --git a/tests/config/format-test.js b/tests/config/format-test.js
index 3574fddf1e40..c4e6710b3953 100644
--- a/tests/config/format-test.js
+++ b/tests/config/format-test.js
@@ -41,6 +41,10 @@ const unstableTests = new Map(
     "js/comments/html-like/comment.js",
     "js/for/continue-and-break-comment-without-blocks.js",
     "typescript/satisfies-operators/comments-unstable.ts",
+    [
+      "js/identifier/parentheses/let-in-assignment.js",
+      (options) => options.semi === false,
+    ],
   ].map((fixture) => {
     const [file, isUnstable = () => true] = Array.isArray(fixture)
       ? fixture
diff --git a/tests/format/js/identifier/parentheses/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/identifier/parentheses/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..6d0419713d22
--- /dev/null
+++ b/tests/format/js/identifier/parentheses/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,481 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`const.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+const [a = ((let)[0] = 1)] = 2;
+
+=====================================output=====================================
+const [a = (let[0] = 1)] = 2
+
+================================================================================
+`;
+
+exports[`const.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const [a = ((let)[0] = 1)] = 2;
+
+=====================================output=====================================
+const [a = (let[0] = 1)] = 2;
+
+================================================================================
+`;
+
+exports[`let.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+let.a = 1;
+
+let.a[0] = 1;
+
+(let[a] = 1);
+
+(let[a].b.c.e = 1);
+
+foo[let[a]] = 1;
+
+(let)[let[a]] = 1;
+
+(let[a] ??= 1);
+
+foo = let[a];
+
+let()[a] = 1;
+
+foo(let)[a] = 1;
+
+foo(let[a])[a] = 1;
+
+(let[0] = 1);
+
+(let["a"] = 1);
+
+let = 1;
+
+var let = 1;
+
+[let[a]] = 1;
+
+({a: let[a]} = 1);
+
+alert(let[0] = 1);
+
+(let[0] = 1) || 2;
+
+((let[0] = 1), 2);
+
+((let[0] = 1) ? a : b);
+
+if (let[0] = 1);
+
+while (let[0] = 1);
+
+do{} while (let[0] = 1);
+
+var a = (let[0] = 1);
+
+(let[0] = 1) instanceof a;
+
+void (let[0] = 1);
+
+(let[0] = 1)();
+
+new (let[0] = 1)();
+
+((let)[0] = 1)\`\`;
+
+((let)[0] = 1).toString;
+
+((let)[0] = 1)?.toString;
+
+[...(let[0] = 1)];
+
+foo = () => (let[0] = 1);
+
+function * foo() {yield (let[0] = 1)}
+
+async function foo() {await (let[0] = 1)}
+
+function foo() {return (let[0] = 1)}
+
+while (true) (let[0] = 1);
+
+throw (let[0] = 1);
+
+({foo: (let[0] = 1)});
+
+[(let[0] = 1)];
+
+for ((let[0] = 1);;);
+for ((let)[0] in {});
+for ((let)[0] of []);
+
+switch (let[0] = 1) {}
+
+switch (foo) {
+  case let[0] = 1:
+}
+
+with (let[0] = 1);
+
+(let[x]).foo();
+
+let.let[x].foo();
+
+a = let[x].foo();
+
+(let)[2];
+
+a[1] + (let[2] = 2);
+
+=====================================output=====================================
+let.a = 1
+
+let.a[0] = 1
+
+;(let)[a] = 1
+
+;(let)[a].b.c.e = 1
+
+foo[let[a]] = 1
+
+;(let)[let[a]] = 1
+
+;(let)[a] ??= 1
+
+foo = let[a]
+
+let()[a] = 1
+
+foo(let)[a] = 1
+
+foo(let[a])[a] = 1
+
+;(let)[0] = 1
+
+;(let)["a"] = 1
+
+let = 1
+
+var let = 1
+
+;[let[a]] = 1
+
+;({ a: let[a] } = 1)
+
+alert((let[0] = 1))
+
+;((let)[0] = 1) || 2
+
+;((let)[0] = 1), 2
+
+;((let)[0] = 1) ? a : b
+
+if ((let[0] = 1));
+
+while ((let[0] = 1));
+
+do {} while ((let[0] = 1))
+
+var a = (let[0] = 1)
+
+;((let)[0] = 1) instanceof a
+
+void (let[0] = 1)
+
+;((let)[0] = 1)()
+
+new (let[0] = 1)()
+
+;((let)[0] = 1)\`\`
+
+;((let)[0] = 1).toString
+
+;((let)[0] = 1)?.toString
+
+;[...(let[0] = 1)]
+
+foo = () => (let[0] = 1)
+
+function* foo() {
+  yield (let[0] = 1)
+}
+
+async function foo() {
+  await (let[0] = 1)
+}
+
+function foo() {
+  return (let[0] = 1)
+}
+
+while (true) (let)[0] = 1
+
+throw (let[0] = 1)
+
+;({ foo: (let[0] = 1) })
+
+;[(let[0] = 1)]
+
+for ((let)[0] = 1; ; );
+for ((let)[0] in {});
+for ((let)[0] of []);
+
+switch ((let[0] = 1)) {
+}
+
+switch (foo) {
+  case (let[0] = 1):
+}
+
+with ((let[0] = 1));
+
+;(let)[x].foo()
+
+let.let[x].foo()
+
+a = let[x].foo()
+
+;(let)[2]
+
+a[1] + (let[2] = 2)
+
+================================================================================
+`;
+
+exports[`let.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+let.a = 1;
+
+let.a[0] = 1;
+
+(let[a] = 1);
+
+(let[a].b.c.e = 1);
+
+foo[let[a]] = 1;
+
+(let)[let[a]] = 1;
+
+(let[a] ??= 1);
+
+foo = let[a];
+
+let()[a] = 1;
+
+foo(let)[a] = 1;
+
+foo(let[a])[a] = 1;
+
+(let[0] = 1);
+
+(let["a"] = 1);
+
+let = 1;
+
+var let = 1;
+
+[let[a]] = 1;
+
+({a: let[a]} = 1);
+
+alert(let[0] = 1);
+
+(let[0] = 1) || 2;
+
+((let[0] = 1), 2);
+
+((let[0] = 1) ? a : b);
+
+if (let[0] = 1);
+
+while (let[0] = 1);
+
+do{} while (let[0] = 1);
+
+var a = (let[0] = 1);
+
+(let[0] = 1) instanceof a;
+
+void (let[0] = 1);
+
+(let[0] = 1)();
+
+new (let[0] = 1)();
+
+((let)[0] = 1)\`\`;
+
+((let)[0] = 1).toString;
+
+((let)[0] = 1)?.toString;
+
+[...(let[0] = 1)];
+
+foo = () => (let[0] = 1);
+
+function * foo() {yield (let[0] = 1)}
+
+async function foo() {await (let[0] = 1)}
+
+function foo() {return (let[0] = 1)}
+
+while (true) (let[0] = 1);
+
+throw (let[0] = 1);
+
+({foo: (let[0] = 1)});
+
+[(let[0] = 1)];
+
+for ((let[0] = 1);;);
+for ((let)[0] in {});
+for ((let)[0] of []);
+
+switch (let[0] = 1) {}
+
+switch (foo) {
+  case let[0] = 1:
+}
+
+with (let[0] = 1);
+
+(let[x]).foo();
+
+let.let[x].foo();
+
+a = let[x].foo();
+
+(let)[2];
+
+a[1] + (let[2] = 2);
+
+=====================================output=====================================
+let.a = 1;
+
+let.a[0] = 1;
+
+(let)[a] = 1;
+
+(let)[a].b.c.e = 1;
+
+foo[let[a]] = 1;
+
+(let)[let[a]] = 1;
+
+(let)[a] ??= 1;
+
+foo = let[a];
+
+let()[a] = 1;
+
+foo(let)[a] = 1;
+
+foo(let[a])[a] = 1;
+
+(let)[0] = 1;
+
+(let)["a"] = 1;
+
+let = 1;
+
+var let = 1;
+
+[let[a]] = 1;
+
+({ a: let[a] } = 1);
+
+alert((let[0] = 1));
+
+((let)[0] = 1) || 2;
+
+((let)[0] = 1), 2;
+
+((let)[0] = 1) ? a : b;
+
+if ((let[0] = 1));
+
+while ((let[0] = 1));
+
+do {} while ((let[0] = 1));
+
+var a = (let[0] = 1);
+
+((let)[0] = 1) instanceof a;
+
+void (let[0] = 1);
+
+((let)[0] = 1)();
+
+new (let[0] = 1)();
+
+((let)[0] = 1)\`\`;
+
+((let)[0] = 1).toString;
+
+((let)[0] = 1)?.toString;
+
+[...(let[0] = 1)];
+
+foo = () => (let[0] = 1);
+
+function* foo() {
+  yield (let[0] = 1);
+}
+
+async function foo() {
+  await (let[0] = 1);
+}
+
+function foo() {
+  return (let[0] = 1);
+}
+
+while (true) (let)[0] = 1;
+
+throw (let[0] = 1);
+
+({ foo: (let[0] = 1) });
+
+[(let[0] = 1)];
+
+for ((let)[0] = 1; ; );
+for ((let)[0] in {});
+for ((let)[0] of []);
+
+switch ((let[0] = 1)) {
+}
+
+switch (foo) {
+  case (let[0] = 1):
+}
+
+with ((let[0] = 1));
+
+(let)[x].foo();
+
+let.let[x].foo();
+
+a = let[x].foo();
+
+(let)[2];
+
+a[1] + (let[2] = 2);
+
+================================================================================
+`;
diff --git a/tests/format/js/identifier/parentheses/const.js b/tests/format/js/identifier/parentheses/const.js
new file mode 100644
index 000000000000..8fa111711c35
--- /dev/null
+++ b/tests/format/js/identifier/parentheses/const.js
@@ -0,0 +1,1 @@
+const [a = ((let)[0] = 1)] = 2;
diff --git a/tests/format/js/identifier/parentheses/jsfmt.spec.js b/tests/format/js/identifier/parentheses/jsfmt.spec.js
new file mode 100644
index 000000000000..9556557681db
--- /dev/null
+++ b/tests/format/js/identifier/parentheses/jsfmt.spec.js
@@ -0,0 +1,14 @@
+run_spec(__dirname, [
+  "babel",
+  //  "flow",
+  "typescript",
+]);
+run_spec(
+  __dirname,
+  [
+    "babel",
+    // "flow",
+    "typescript",
+  ],
+  { semi: false }
+);
diff --git a/tests/format/js/identifier/parentheses/let.js b/tests/format/js/identifier/parentheses/let.js
new file mode 100644
index 000000000000..2f0cc8a37b9a
--- /dev/null
+++ b/tests/format/js/identifier/parentheses/let.js
@@ -0,0 +1,103 @@
+let.a = 1;
+
+let.a[0] = 1;
+
+(let[a] = 1);
+
+(let[a].b.c.e = 1);
+
+foo[let[a]] = 1;
+
+(let)[let[a]] = 1;
+
+(let[a] ??= 1);
+
+foo = let[a];
+
+let()[a] = 1;
+
+foo(let)[a] = 1;
+
+foo(let[a])[a] = 1;
+
+(let[0] = 1);
+
+(let["a"] = 1);
+
+let = 1;
+
+var let = 1;
+
+[let[a]] = 1;
+
+({a: let[a]} = 1);
+
+alert(let[0] = 1);
+
+(let[0] = 1) || 2;
+
+((let[0] = 1), 2);
+
+((let[0] = 1) ? a : b);
+
+if (let[0] = 1);
+
+while (let[0] = 1);
+
+do{} while (let[0] = 1);
+
+var a = (let[0] = 1);
+
+(let[0] = 1) instanceof a;
+
+void (let[0] = 1);
+
+(let[0] = 1)();
+
+new (let[0] = 1)();
+
+((let)[0] = 1)``;
+
+((let)[0] = 1).toString;
+
+((let)[0] = 1)?.toString;
+
+[...(let[0] = 1)];
+
+foo = () => (let[0] = 1);
+
+function * foo() {yield (let[0] = 1)}
+
+async function foo() {await (let[0] = 1)}
+
+function foo() {return (let[0] = 1)}
+
+while (true) (let[0] = 1);
+
+throw (let[0] = 1);
+
+({foo: (let[0] = 1)});
+
+[(let[0] = 1)];
+
+for ((let[0] = 1);;);
+for ((let)[0] in {});
+for ((let)[0] of []);
+
+switch (let[0] = 1) {}
+
+switch (foo) {
+  case let[0] = 1:
+}
+
+with (let[0] = 1);
+
+(let[x]).foo();
+
+let.let[x].foo();
+
+a = let[x].foo();
+
+(let)[2];
+
+a[1] + (let[2] = 2);
diff --git a/tests/format/misc/errors/js/assignment/__snapshots__/jsfmt.spec.js.snap b/tests/format/misc/errors/js/assignment/__snapshots__/jsfmt.spec.js.snap
index 1ece57b5bafd..f4b45b707e0f 100644
--- a/tests/format/misc/errors/js/assignment/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/misc/errors/js/assignment/__snapshots__/jsfmt.spec.js.snap
@@ -6,26 +6,110 @@ exports[`snippet: #0 [acorn] format 1`] = `
     | ^"
 `;
 
+exports[`snippet: #0 [acorn] format 2`] = `
+"The keyword 'let' is reserved (1:1)
+> 1 | let?.()[a] =1
+    | ^"
+`;
+
+exports[`snippet: #0 [acorn] format 3`] = `
+"The keyword 'let' is reserved (1:21)
+> 1 | class Foo extends ((let)[0] = 1) {}
+    |                     ^"
+`;
+
 exports[`snippet: #0 [babel] format 1`] = `
 "Invalid parenthesized assignment pattern. (1:1)
 > 1 | ({}) = x;
     | ^"
 `;
 
+exports[`snippet: #0 [babel] format 2`] = `
+"Invalid left-hand side in assignment expression. (1:1)
+> 1 | let?.()[a] =1
+    | ^"
+`;
+
 exports[`snippet: #0 [babel-ts] format 1`] = `
 "Invalid parenthesized assignment pattern. (1:1)
 > 1 | ({}) = x;
     | ^"
 `;
 
+exports[`snippet: #0 [babel-ts] format 2`] = `
+"Invalid left-hand side in assignment expression. (1:1)
+> 1 | let?.()[a] =1
+    | ^"
+`;
+
 exports[`snippet: #0 [espree] format 1`] = `
 "Assigning to rvalue (1:1)
 > 1 | ({}) = x;
     | ^"
 `;
 
+exports[`snippet: #0 [espree] format 2`] = `
+"The keyword 'let' is reserved (1:1)
+> 1 | let?.()[a] =1
+    | ^"
+`;
+
+exports[`snippet: #0 [espree] format 3`] = `
+"The keyword 'let' is reserved (1:21)
+> 1 | class Foo extends ((let)[0] = 1) {}
+    |                     ^"
+`;
+
+exports[`snippet: #0 [espree] format 4`] = `
+"The keyword 'let' is reserved (1:18)
+> 1 | export default ((let)[0] = 1);
+    |                  ^"
+`;
+
 exports[`snippet: #0 [meriyah] format 1`] = `
 "Invalid left-hand side in assignment (1:6)
 > 1 | ({}) = x;
     |      ^"
 `;
+
+exports[`snippet: #0 [meriyah] format 2`] = `
+"The identifier 'let' must not be in expression position in strict mode (1:24)
+> 1 | class Foo extends ((let)[0] = 1) {}
+    |                        ^"
+`;
+
+exports[`snippet: #0 [meriyah] format 3`] = `
+"The identifier 'let' must not be in expression position in strict mode (1:21)
+> 1 | export default ((let)[0] = 1);
+    |                     ^"
+`;
+
+exports[`snippet: #1 [acorn] format 1`] = `
+"The keyword 'let' is reserved (1:1)
+> 1 | let?.[a] = 1
+    | ^"
+`;
+
+exports[`snippet: #1 [babel] format 1`] = `
+"Invalid left-hand side in assignment expression. (1:1)
+> 1 | let?.[a] = 1
+    | ^"
+`;
+
+exports[`snippet: #1 [babel-ts] format 1`] = `
+"Invalid left-hand side in assignment expression. (1:1)
+> 1 | let?.[a] = 1
+    | ^"
+`;
+
+exports[`snippet: #1 [espree] format 1`] = `
+"The keyword 'let' is reserved (1:1)
+> 1 | let?.[a] = 1
+    | ^"
+`;
+
+exports[`snippet: #1 [meriyah] format 1`] = `
+"\`let\` declaration not allowed here and \`let\` cannot be a regular var name in strict mode (1:5)
+> 1 | let?.[a] = 1
+    |     ^"
+`;
diff --git a/tests/format/misc/errors/js/assignment/jsfmt.spec.js b/tests/format/misc/errors/js/assignment/jsfmt.spec.js
index b83f8d1c7169..29f1195c4852 100644
--- a/tests/format/misc/errors/js/assignment/jsfmt.spec.js
+++ b/tests/format/misc/errors/js/assignment/jsfmt.spec.js
@@ -1,7 +1,31 @@
 run_spec(
   {
     dirname: __dirname,
-    snippets: ["({}) = x;"],
+    snippets: ["({}) = x;", "let?.[a] = 1"],
   },
   ["babel", "babel-ts", "acorn", "espree", "meriyah"]
 );
+
+run_spec(
+  {
+    dirname: __dirname,
+    snippets: ["let?.()[a] =1"],
+  },
+  ["babel", "babel-ts", "acorn", "espree"]
+);
+
+run_spec(
+  {
+    dirname: __dirname,
+    snippets: ["class Foo extends ((let)[0] = 1) {}"],
+  },
+  ["acorn", "espree", "meriyah"]
+);
+
+run_spec(
+  {
+    dirname: __dirname,
+    snippets: ["export default ((let)[0] = 1);"],
+  },
+  ["espree", "meriyah"]
+);