diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 5de189bd8..914cde909 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -325,7 +325,7 @@ export default (function log() {}).toString(); export default (function log() {} as typeof console.log); ``` -### TypeScript: Keep necessary parentheses around non-null assertions. ([#6136] by [@sosukesuzuki], [#6140] by [@thorn0]) +### TypeScript: Keep necessary parentheses around non-null assertions. ([#6136] by [@sosukesuzuki], [#6140] by [@thorn0], [#6148] by [@bakkot]) Previously, Prettier removed necessary parentheses around non-null assertions if the result of the assertion expression was called as a constructor. @@ -338,7 +338,29 @@ const b = new (c()!)(); const b = new c()!(); // Output (Prettier master) -const b = new (c()!)(); +const b = new (c())!(); +``` + +### Javascript: Address parentheses bugs for edge cases with call and new. ([#6148] by [@bakkot]) + +Adding all and only the necessary parentheses when mixing `new` with function calls is tricky. This change fixes two issues where necessary parentheses were omitted and one when redundant parentheses were added. + + +```ts +// Input +new (x()``.y)(); +new (x()!.y)(); +new e[f().x].y() + +// Output (Prettier stable) +new x()``.y(); +new x()!.y(); +new e[(f()).x].y(); + +// Output (Prettier master) +new (x())``.y(); +new (x())!.y(); +new e[f().x].y(); ``` [#5979]: https://github.com/prettier/prettier/pull/5979 @@ -357,6 +379,7 @@ const b = new (c()!)(); [#6133]: https://github.com/prettier/prettier/pull/6133 [#6136]: https://github.com/prettier/prettier/pull/6136 [#6140]: https://github.com/prettier/prettier/pull/6140 +[#6148]: https://github.com/prettier/prettier/pull/6148 [@belochub]: https://github.com/belochub [@brainkim]: https://github.com/brainkim [@duailibe]: https://github.com/duailibe @@ -366,3 +389,4 @@ const b = new (c()!)(); [@jwbay]: https://github.com/jwbay [@sosukesuzuki]: https://github.com/sosukesuzuki [@thorn0]: https://github.com/thorn0 +[@bakkot]: https://github.com/bakkot diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 74a3ad0e3..09967d9d7 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -216,9 +216,16 @@ function needsParens(path, options) { case "CallExpression": { let firstParentNotMemberExpression = parent; let i = 0; + // tagged templates are basically member expressions from a grammar perspective + // see https://tc39.github.io/ecma262/#prod-MemberExpression + // so are typescript's non-null assertions, though there's no grammar to point to while ( firstParentNotMemberExpression && - firstParentNotMemberExpression.type === "MemberExpression" + ((firstParentNotMemberExpression.type === "MemberExpression" && + firstParentNotMemberExpression.object === + path.getParentNode(i - 1)) || + firstParentNotMemberExpression.type === "TaggedTemplateExpression" || + firstParentNotMemberExpression.type === "TSNonNullExpression") ) { firstParentNotMemberExpression = path.getParentNode(++i); } @@ -725,13 +732,6 @@ function needsParens(path, options) { return false; } return true; - - case "TSNonNullExpression": { - return ( - node.expression.type === "CallExpression" && - parent.type === "NewExpression" - ); - } } return false; diff --git a/tests/new_expression/__snapshots__/jsfmt.spec.js.snap b/tests/new_expression/__snapshots__/jsfmt.spec.js.snap index 971368b51..15e40b214 100644 --- a/tests/new_expression/__snapshots__/jsfmt.spec.js.snap +++ b/tests/new_expression/__snapshots__/jsfmt.spec.js.snap @@ -29,11 +29,17 @@ printWidth: 80 new (memoize.Cache || MapCache) new (typeof this == "function" ? this : Dict()) new (createObj()).prop(a()); +new (x()\`\`.y)(); +new e[f().x].y(); +new e[f()].y(); =====================================output===================================== new (memoize.Cache || MapCache)(); new (typeof this == "function" ? this : Dict())(); new (createObj()).prop(a()); +new (x())\`\`.y(); +new e[f().x].y(); +new e[f()].y(); ================================================================================ `; diff --git a/tests/new_expression/new_expression.js b/tests/new_expression/new_expression.js index 144275d08..c85c355a8 100644 --- a/tests/new_expression/new_expression.js +++ b/tests/new_expression/new_expression.js @@ -1,3 +1,6 @@ new (memoize.Cache || MapCache) new (typeof this == "function" ? this : Dict()) new (createObj()).prop(a()); +new (x()``.y)(); +new e[f().x].y(); +new e[f()].y(); diff --git a/tests/typescript_non_null/__snapshots__/jsfmt.spec.js.snap b/tests/typescript_non_null/__snapshots__/jsfmt.spec.js.snap index 62a40c40d..39235371d 100644 --- a/tests/typescript_non_null/__snapshots__/jsfmt.spec.js.snap +++ b/tests/typescript_non_null/__snapshots__/jsfmt.spec.js.snap @@ -62,6 +62,7 @@ const b = c!(); // parens are necessary if the expression result is called as a constructor const c = new (d()!)(); const c = new (d()!); +const c = new (d()!.e)(); =====================================output===================================== (a ? b : c)![tokenKey]; @@ -80,8 +81,9 @@ const a = b()!(); // parens aren't necessary const b = c!(); // parens are necessary if the expression result is called as a constructor -const c = new (d()!)(); -const c = new (d()!)(); +const c = new (d())!(); +const c = new (d())!(); +const c = new (d())!.e(); ================================================================================ `; diff --git a/tests/typescript_non_null/parens.ts b/tests/typescript_non_null/parens.ts index b6d1721ec..b96c63dda 100644 --- a/tests/typescript_non_null/parens.ts +++ b/tests/typescript_non_null/parens.ts @@ -16,3 +16,4 @@ const b = c!(); // parens are necessary if the expression result is called as a constructor const c = new (d()!)(); const c = new (d()!); +const c = new (d()!.e)();