From b735a485e77bcc791e4c4c6b8716801d94e98b2c Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 6 Jun 2020 01:12:14 +0200 Subject: [PATCH] Update: add enforceForFunctionPrototypeMethods option to no-extra-parens (#12895) --- docs/rules/no-extra-parens.md | 19 +- lib/rules/no-extra-parens.js | 30 ++- tests/lib/rules/no-extra-parens.js | 331 +++++++++++++++++++++++++++++ 3 files changed, 377 insertions(+), 3 deletions(-) diff --git a/docs/rules/no-extra-parens.md b/docs/rules/no-extra-parens.md index 4cadaa757e4..68b986eff6c 100644 --- a/docs/rules/no-extra-parens.md +++ b/docs/rules/no-extra-parens.md @@ -7,7 +7,7 @@ This rule restricts the use of parentheses to only where they are necessary. This rule always ignores extra parentheses around the following: * RegExp literals such as `(/abc/).test(var)` to avoid conflicts with the [wrap-regex](wrap-regex.md) rule -* immediately-invoked function expressions (also known as IIFEs) such as `var x = (function () {})();` and `((function foo() {return 1;})())` to avoid conflicts with the [wrap-iife](wrap-iife.md) rule +* immediately-invoked function expressions (also known as IIFEs) such as `var x = (function () {})();` and `var x = (function () {}());` to avoid conflicts with the [wrap-iife](wrap-iife.md) rule * arrow function arguments to avoid conflicts with the [arrow-parens](arrow-parens.md) rule ## Options @@ -26,6 +26,7 @@ This rule has an object option for exceptions to the `"all"` option: * `"enforceForArrowConditionals": false` allows extra parentheses around ternary expressions which are the body of an arrow function * `"enforceForSequenceExpressions": false` allows extra parentheses around sequence expressions * `"enforceForNewInMemberExpressions": false` allows extra parentheses around `new` expressions in member expressions +* `"enforceForFunctionPrototypeMethods": false` allows extra parentheses around immediate `.call` and `.apply` method calls on function expressions and around function expressions in the same context. ### all @@ -222,6 +223,22 @@ const quux = (new Bar())[baz]; (new Bar()).doSomething(); ``` +### enforceForFunctionPrototypeMethods + +Examples of **correct** code for this rule with the `"all"` and `{ "enforceForFunctionPrototypeMethods": false }` options: + +```js +/* eslint no-extra-parens: ["error", "all", { "enforceForFunctionPrototypeMethods": false }] */ + +const foo = (function () {}).call(); + +const bar = (function () {}).apply(); + +const baz = (function () {}.call()); + +const quux = (function () {}.apply()); +``` + ### functions Examples of **incorrect** code for this rule with the `"functions"` option: diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index dea1835b903..bae1a498cf0 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -51,7 +51,8 @@ module.exports = { ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] }, enforceForArrowConditionals: { type: "boolean" }, enforceForSequenceExpressions: { type: "boolean" }, - enforceForNewInMemberExpressions: { type: "boolean" } + enforceForNewInMemberExpressions: { type: "boolean" }, + enforceForFunctionPrototypeMethods: { type: "boolean" } }, additionalProperties: false } @@ -83,12 +84,28 @@ module.exports = { context.options[1].enforceForSequenceExpressions === false; const IGNORE_NEW_IN_MEMBER_EXPR = ALL_NODES && context.options[1] && context.options[1].enforceForNewInMemberExpressions === false; + const IGNORE_FUNCTION_PROTOTYPE_METHODS = ALL_NODES && context.options[1] && + context.options[1].enforceForFunctionPrototypeMethods === false; const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" }); const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" }); let reportsBuffer; + /** + * Determines whether the given node is a `call` or `apply` method call, invoked directly on a `FunctionExpression` node. + * Example: function(){}.call() + * @param {ASTNode} node The node to be checked. + * @returns {boolean} True if the node is an immediate `call` or `apply` method call. + * @private + */ + function isImmediateFunctionPrototypeMethodCall(node) { + return node.type === "CallExpression" && + node.callee.type === "MemberExpression" && + node.callee.object.type === "FunctionExpression" && + ["call", "apply"].includes(astUtils.getStaticPropertyName(node.callee)); + } + /** * Determines if this rule should be enforced for a node given the current configuration. * @param {ASTNode} node The node to be checked. @@ -125,6 +142,10 @@ module.exports = { return false; } + if (isImmediateFunctionPrototypeMethodCall(node) && IGNORE_FUNCTION_PROTOTYPE_METHODS) { + return false; + } + return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"; } @@ -929,7 +950,12 @@ module.exports = { LogicalExpression: checkBinaryLogical, MemberExpression(node) { - const nodeObjHasExcessParens = hasExcessParens(node.object); + const nodeObjHasExcessParens = hasExcessParens(node.object) && + !( + isImmediateFunctionPrototypeMethodCall(node.parent) && + node.parent.callee === node && + IGNORE_FUNCTION_PROTOTYPE_METHODS + ); if ( nodeObjHasExcessParens && diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 7dac725f568..91099f8cba0 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -508,6 +508,27 @@ ruleTester.run("no-extra-parens", rule, { { code: "(new foo.bar()).baz()", options: ["all", { enforceForNewInMemberExpressions: false }] }, { code: "((new foo.bar())).baz()", options: ["all", { enforceForNewInMemberExpressions: false }] }, + // ["all", { enforceForFunctionPrototypeMethods: false }] + { code: "var foo = (function(){}).call()", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){}).apply()", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){}.call())", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){}.apply())", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){}).call(arg)", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){}.apply(arg))", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){}['call']())", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = (function(){})[`apply`]()", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = ((function(){})).call()", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = ((function(){}).apply())", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = ((function(){}.call()))", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = ((((function(){})).apply()))", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "foo((function(){}).call().bar)", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "foo = (function(){}).call()()", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "foo = (function(){}.call())()", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = { bar: (function(){}.call()) }", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "var foo = { [(function(){}.call())]: bar }", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "if((function(){}).call()){}", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + { code: "while((function(){}.apply())){}", options: ["all", { enforceForFunctionPrototypeMethods: false }] }, + "let a = [ ...b ]", "let a = { ...b }", { @@ -1303,6 +1324,316 @@ ruleTester.run("no-extra-parens", rule, { ] }, + // enforceForFunctionPrototypeMethods + { + code: "var foo = (function(){}).call()", + output: "var foo = function(){}.call()", + options: ["all"], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}.apply())", + output: "var foo = function(){}.apply()", + options: ["all"], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (function(){}).apply()", + output: "var foo = function(){}.apply()", + options: ["all", {}], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}.call())", + output: "var foo = function(){}.call()", + options: ["all", {}], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (function(){}).call()", + output: "var foo = function(){}.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}).apply()", + output: "var foo = function(){}.apply()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}.call())", + output: "var foo = function(){}.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (function(){}.apply())", + output: "var foo = function(){}.apply()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (function(){}.call)()", // removing these parens does not cause any conflicts with wrap-iife + output: "var foo = function(){}.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "MemberExpression" + } + ] + }, + { + code: "var foo = (function(){}.apply)()", // removing these parens does not cause any conflicts with wrap-iife + output: "var foo = function(){}.apply()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "MemberExpression" + } + ] + }, + { + code: "var foo = (function(){}).call", + output: "var foo = function(){}.call", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}.call)", + output: "var foo = function(){}.call", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "MemberExpression" + } + ] + }, + { + code: "var foo = new (function(){}).call()", + output: "var foo = new function(){}.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (new function(){}.call())", + output: "var foo = new function(){}.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "NewExpression" + } + ] + }, + { + code: "var foo = (function(){})[call]()", + output: "var foo = function(){}[call]()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}[apply]())", + output: "var foo = function(){}[apply]()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (function(){}).bar()", + output: "var foo = function(){}.bar()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}.bar())", + output: "var foo = function(){}.bar()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (function(){}).call.call()", + output: "var foo = function(){}.call.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "FunctionExpression" + } + ] + }, + { + code: "var foo = (function(){}.call.call())", + output: "var foo = function(){}.call.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (call())", + output: "var foo = call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (apply())", + output: "var foo = apply()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = (bar).call()", + output: "var foo = bar.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "Identifier" + } + ] + }, + { + code: "var foo = (bar.call())", + output: "var foo = bar.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "((() => {}).call())", + output: "(() => {}).call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "CallExpression" + } + ] + }, + { + code: "var foo = function(){}.call((a.b))", + output: "var foo = function(){}.call(a.b)", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "MemberExpression" + } + ] + }, + { + code: "var foo = function(){}.call((a).b)", + output: "var foo = function(){}.call(a.b)", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "Identifier" + } + ] + }, + { + code: "var foo = function(){}[('call')]()", + output: "var foo = function(){}['call']()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + errors: [ + { + messageId: "unexpected", + type: "Literal" + } + ] + }, + // https://github.com/eslint/eslint/issues/8175 invalid( "let a = [...(b)]",