Skip to content

Commit 923f61d

Browse files
fix: false positive with assignment in no-extra-parens (#16872)
* fix: false positive with assignment in `no-extra-parens` Fixes #16850 * Update lib/rules/no-extra-parens.js Updated as suggested Co-authored-by: Milos Djermanovic <[email protected]> * check assignment operator --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent 9dbe06d commit 923f61d

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

lib/rules/no-extra-parens.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,38 @@ module.exports = {
766766
return false;
767767
}
768768

769+
/**
770+
* Checks if the left-hand side of an assignment is an identifier, the operator is one of
771+
* `=`, `&&=`, `||=` or `??=` and the right-hand side is an anonymous class or function.
772+
*
773+
* As per https://tc39.es/ecma262/#sec-assignment-operators-runtime-semantics-evaluation, an
774+
* assignment involving one of the operators `=`, `&&=`, `||=` or `??=` where the right-hand
775+
* side is an anonymous class or function and the left-hand side is an *unparenthesized*
776+
* identifier has different semantics than other assignments.
777+
* Specifically, when an expression like `foo = function () {}` is evaluated, `foo.name`
778+
* will be set to the string "foo", i.e. the identifier name. The same thing does not happen
779+
* when evaluating `(foo) = function () {}`.
780+
* Since the parenthesizing of the identifier in the left-hand side is significant in this
781+
* special case, the parentheses, if present, should not be flagged as unnecessary.
782+
* @param {ASTNode} node an AssignmentExpression node.
783+
* @returns {boolean} `true` if the left-hand side of the assignment is an identifier, the
784+
* operator is one of `=`, `&&=`, `||=` or `??=` and the right-hand side is an anonymous
785+
* class or function; otherwise, `false`.
786+
*/
787+
function isAnonymousFunctionAssignmentException({ left, operator, right }) {
788+
if (left.type === "Identifier" && ["=", "&&=", "||=", "??="].includes(operator)) {
789+
const rhsType = right.type;
790+
791+
if (rhsType === "ArrowFunctionExpression") {
792+
return true;
793+
}
794+
if ((rhsType === "FunctionExpression" || rhsType === "ClassExpression") && !right.id) {
795+
return true;
796+
}
797+
}
798+
return false;
799+
}
800+
769801
return {
770802
ArrayExpression(node) {
771803
node.elements
@@ -804,7 +836,8 @@ module.exports = {
804836
},
805837

806838
AssignmentExpression(node) {
807-
if (canBeAssignmentTarget(node.left) && hasExcessParens(node.left)) {
839+
if (canBeAssignmentTarget(node.left) && hasExcessParens(node.left) &&
840+
(!isAnonymousFunctionAssignmentException(node) || isParenthesisedTwice(node.left))) {
808841
report(node.left);
809842
}
810843

tests/lib/rules/no-extra-parens.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,18 @@ ruleTester.run("no-extra-parens", rule, {
771771
{
772772
code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));",
773773
options: ["all", { allowParensAfterCommentPattern: "@type" }]
774+
},
775+
776+
// https://github.com/eslint/eslint/issues/16850
777+
"(a) = function () {};",
778+
"(a) = () => {};",
779+
"(a) = class {};",
780+
"(a) ??= function () {};",
781+
"(a) &&= class extends SuperClass {};",
782+
"(a) ||= async () => {}",
783+
{
784+
code: "((a)) = function () {};",
785+
options: ["functions"]
774786
}
775787
],
776788

@@ -3410,6 +3422,28 @@ ruleTester.run("no-extra-parens", rule, {
34103422
options: ["all"],
34113423
parserOptions: { ecmaVersion: 2020 },
34123424
errors: [{ messageId: "unexpected" }]
3413-
}
3425+
},
3426+
3427+
// https://github.com/eslint/eslint/issues/16850
3428+
invalid("(a) = function foo() {};", "a = function foo() {};", "Identifier"),
3429+
invalid("(a) = class Bar {};", "a = class Bar {};", "Identifier"),
3430+
invalid("(a.b) = function () {};", "a.b = function () {};", "MemberExpression"),
3431+
{
3432+
code: "(newClass) = [(one)] = class { static * [Symbol.iterator]() { yield 1; } };",
3433+
output: "newClass = [one] = class { static * [Symbol.iterator]() { yield 1; } };",
3434+
errors: [
3435+
{ messageId: "unexpected", type: "Identifier" },
3436+
{ messageId: "unexpected", type: "Identifier" }
3437+
]
3438+
},
3439+
invalid("((a)) = () => {};", "(a) = () => {};", "Identifier"),
3440+
invalid("(a) = (function () {})();", "a = (function () {})();", "Identifier"),
3441+
...["**=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", ">>>=", "&=", "^=", "|="].map(
3442+
operator => invalid(
3443+
`(a) ${operator} function () {};`,
3444+
`a ${operator} function () {};`,
3445+
"Identifier"
3446+
)
3447+
)
34143448
]
34153449
});

0 commit comments

Comments
 (0)