diff --git a/lib/rules/wrap-iife.js b/lib/rules/wrap-iife.js index 5e590be13e2..896aed63de5 100644 --- a/lib/rules/wrap-iife.js +++ b/lib/rules/wrap-iife.js @@ -10,6 +10,21 @@ //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); +const eslintUtils = require("eslint-utils"); + +//---------------------------------------------------------------------- +// Helpers +//---------------------------------------------------------------------- + +/** + * Check if the given node is callee of a `NewExpression` node + * @param {ASTNode} node node to check + * @returns {boolean} True if the node is callee of a `NewExpression` node + * @private + */ +function isCalleeOfNewExpression(node) { + return node.parent.type === "NewExpression" && node.parent.callee === node; +} //------------------------------------------------------------------------------ // Rule Definition @@ -58,15 +73,25 @@ module.exports = { const sourceCode = context.getSourceCode(); /** - * Check if the node is wrapped in () + * Check if the node is wrapped in any (). All parens count: grouping parens and parens for constructs such as if() * @param {ASTNode} node node to evaluate - * @returns {boolean} True if it is wrapped + * @returns {boolean} True if it is wrapped in any parens * @private */ - function wrapped(node) { + function isWrappedInAnyParens(node) { return astUtils.isParenthesised(sourceCode, node); } + /** + * Check if the node is wrapped in grouping (). Parens for constructs such as if() don't count + * @param {ASTNode} node node to evaluate + * @returns {boolean} True if it is wrapped in grouping parens + * @private + */ + function isWrappedInGroupingParens(node) { + return eslintUtils.isParenthesized(1, node, sourceCode); + } + /** * Get the function node from an IIFE * @param {ASTNode} node node to evaluate @@ -99,10 +124,10 @@ module.exports = { return; } - const callExpressionWrapped = wrapped(node), - functionExpressionWrapped = wrapped(innerNode); + const isCallExpressionWrapped = isWrappedInAnyParens(node), + isFunctionExpressionWrapped = isWrappedInAnyParens(innerNode); - if (!callExpressionWrapped && !functionExpressionWrapped) { + if (!isCallExpressionWrapped && !isFunctionExpressionWrapped) { context.report({ node, messageId: "wrapInvocation", @@ -112,27 +137,39 @@ module.exports = { return fixer.replaceText(nodeToSurround, `(${sourceCode.getText(nodeToSurround)})`); } }); - } else if (style === "inside" && !functionExpressionWrapped) { + } else if (style === "inside" && !isFunctionExpressionWrapped) { context.report({ node, messageId: "wrapExpression", fix(fixer) { + // The outer call expression will always be wrapped at this point. + + if (isWrappedInGroupingParens(node) && !isCalleeOfNewExpression(node)) { + + /* + * Parenthesize the function expression and remove unnecessary grouping parens around the call expression. + * Replace the range between the end of the function expression and the end of the call expression. + * for example, in `(function(foo) {}(bar))`, the range `(bar))` should get replaced with `)(bar)`. + */ + + const parenAfter = sourceCode.getTokenAfter(node); + + return fixer.replaceTextRange( + [innerNode.range[1], parenAfter.range[1]], + `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}` + ); + } + /* - * The outer call expression will always be wrapped at this point. - * Replace the range between the end of the function expression and the end of the call expression. - * for example, in `(function(foo) {}(bar))`, the range `(bar))` should get replaced with `)(bar)`. - * Replace the parens from the outer expression, and parenthesize the function expression. + * Call expression is wrapped in mandatory parens such as if(), or in necessary grouping parens. + * These parens cannot be removed, so just parenthesize the function expression. */ - const parenAfter = sourceCode.getTokenAfter(node); - return fixer.replaceTextRange( - [innerNode.range[1], parenAfter.range[1]], - `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}` - ); + return fixer.replaceText(innerNode, `(${sourceCode.getText(innerNode)})`); } }); - } else if (style === "outside" && !callExpressionWrapped) { + } else if (style === "outside" && !isCallExpressionWrapped) { context.report({ node, messageId: "moveInvocation", diff --git a/tests/lib/rules/wrap-iife.js b/tests/lib/rules/wrap-iife.js index 6e9a998331c..d18948f429a 100644 --- a/tests/lib/rules/wrap-iife.js +++ b/tests/lib/rules/wrap-iife.js @@ -64,6 +64,137 @@ ruleTester.run("wrap-iife", rule, { code: "var a = function(){return 1;};", options: ["any"] }, + { + code: "var a = ((function(){return 1;})());", // always allows existing extra parens (parens both inside and outside) + options: ["any"] + }, + { + code: "var a = ((function(){return 1;})());", // always allows existing extra parens (parens both inside and outside) + options: ["inside"] + }, + { + code: "var a = ((function(){return 1;})());", // always allows existing extra parens (parens both inside and outside) + options: ["outside"] + }, + { + code: "if (function (){}()) {}", + options: ["any"] + }, + { + code: "while (function (){}()) {}", + options: ["any"] + }, + { + code: "do {} while (function (){}())", + options: ["any"] + }, + { + code: "switch (function (){}()) {}", + options: ["any"] + }, + { + code: "with (function (){}()) {}", + options: ["any"] + }, + { + code: "foo(function (){}());", + options: ["any"] + }, + { + code: "new foo(function (){}());", + options: ["any"] + }, + { + code: "import(function (){}());", + options: ["any"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "if ((function (){})()) {}", + options: ["any"] + }, + { + code: "while (((function (){})())) {}", + options: ["any"] + }, + { + code: "if (function (){}()) {}", + options: ["outside"] + }, + { + code: "while (function (){}()) {}", + options: ["outside"] + }, + { + code: "do {} while (function (){}())", + options: ["outside"] + }, + { + code: "switch (function (){}()) {}", + options: ["outside"] + }, + { + code: "with (function (){}()) {}", + options: ["outside"] + }, + { + code: "foo(function (){}());", + options: ["outside"] + }, + { + code: "new foo(function (){}());", + options: ["outside"] + }, + { + code: "import(function (){}());", + options: ["outside"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "if ((function (){})()) {}", + options: ["outside"] + }, + { + code: "while (((function (){})())) {}", + options: ["outside"] + }, + { + code: "if ((function (){})()) {}", + options: ["inside"] + }, + { + code: "while ((function (){})()) {}", + options: ["inside"] + }, + { + code: "do {} while ((function (){})())", + options: ["inside"] + }, + { + code: "switch ((function (){})()) {}", + options: ["inside"] + }, + { + code: "with ((function (){})()) {}", + options: ["inside"] + }, + { + code: "foo((function (){})());", + options: ["inside"] + }, + { + code: "new foo((function (){})());", + options: ["inside"] + }, + { + code: "import((function (){})());", + options: ["inside"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "while (((function (){})())) {}", + options: ["inside"] + }, { code: "window.bar = (function() { return 3; }.call(this, arg1));", options: ["outside", { functionPrototypeMethods: true }] @@ -84,6 +215,10 @@ ruleTester.run("wrap-iife", rule, { code: "window.bar = function() { return 3; }.call(this, arg1);", options: ["inside"] }, + { + code: "window.bar = function() { return 3; }.call(this, arg1);", + options: ["inside", {}] + }, { code: "window.bar = function() { return 3; }.call(this, arg1);", options: ["inside", { functionPrototypeMethods: false }] @@ -107,6 +242,137 @@ ruleTester.run("wrap-iife", rule, { { code: "var a = function(){return 1;}.bind(this).apply(that);", options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "var a = ((function(){return 1;}).call());", // always allows existing extra parens (parens both inside and outside) + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "var a = ((function(){return 1;}).call());", // always allows existing extra parens (parens both inside and outside) + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "var a = ((function(){return 1;}).call());", // always allows existing extra parens (parens both inside and outside) + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "if (function (){}.call()) {}", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "while (function (){}.call()) {}", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "do {} while (function (){}.call())", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "switch (function (){}.call()) {}", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "with (function (){}.call()) {}", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "foo(function (){}.call())", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "new foo(function (){}.call())", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "import(function (){}.call())", + options: ["any", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "if ((function (){}).call()) {}", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "while (((function (){}).call())) {}", + options: ["any", { functionPrototypeMethods: true }] + }, + { + code: "if (function (){}.call()) {}", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "while (function (){}.call()) {}", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "do {} while (function (){}.call())", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "switch (function (){}.call()) {}", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "with (function (){}.call()) {}", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "foo(function (){}.call())", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "new foo(function (){}.call())", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "import(function (){}.call())", + options: ["outside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "if ((function (){}).call()) {}", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "while (((function (){}).call())) {}", + options: ["outside", { functionPrototypeMethods: true }] + }, + { + code: "if ((function (){}).call()) {}", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "while ((function (){}).call()) {}", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "do {} while ((function (){}).call())", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "switch ((function (){}).call()) {}", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "with ((function (){}).call()) {}", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "foo((function (){}).call())", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "new foo((function (){}).call())", + options: ["inside", { functionPrototypeMethods: true }] + }, + { + code: "import((function (){}).call())", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "if (((function (){}).call())) {}", + options: ["inside", { functionPrototypeMethods: true }] } ], invalid: [ @@ -142,6 +408,79 @@ ruleTester.run("wrap-iife", rule, { options: ["inside"], errors: [wrapExpressionError] }, + { + code: "new foo((function (){}()))", + output: "new foo((function (){})())", + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "new (function (){}())", + output: "new ((function (){})())", // wrap function expression, but don't remove necessary grouping parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "new (function (){}())()", + output: "new ((function (){})())()", // wrap function expression, but don't remove necessary grouping parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "if (function (){}()) {}", + output: "if ((function (){})()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "if ((function (){}())) {}", + output: "if ((function (){})()) {}", // wrap function expression and remove unnecessary grouping parens aroung the call expression + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "while (function (){}()) {}", + output: "while ((function (){})()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "do {} while (function (){}())", + output: "do {} while ((function (){})())", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "switch (function (){}()) {}", + output: "switch ((function (){})()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "with (function (){}()) {}", + output: "with ((function (){})()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "foo(function (){}())", + output: "foo((function (){})())", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "new foo(function (){}())", + output: "new foo((function (){})())", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + errors: [wrapExpressionError] + }, + { + code: "import(function (){}())", + output: "import((function (){})())", // wrap function expression, but don't remove mandatory parens + options: ["inside"], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapExpressionError] + }, { // Ensure all comments get preserved when autofixing. @@ -197,6 +536,73 @@ ruleTester.run("wrap-iife", rule, { output: "window.bar = (function() { return 3; }.call(this, arg1));", options: ["outside", { functionPrototypeMethods: true }], errors: [moveInvocationError] + }, + { + code: "new (function (){}.call())", + output: "new ((function (){}).call())", // wrap function expression, but don't remove necessary grouping parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "new (function (){}.call())()", + output: "new ((function (){}).call())()", // wrap function expression, but don't remove necessary grouping parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "if (function (){}.call()) {}", + output: "if ((function (){}).call()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "if ((function (){}.call())) {}", + output: "if ((function (){}).call()) {}", // wrap function expression and remove unnecessary grouping parens aroung the call expression + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "while (function (){}.call()) {}", + output: "while ((function (){}).call()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "do {} while (function (){}.call())", + output: "do {} while ((function (){}).call())", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "switch (function (){}.call()) {}", + output: "switch ((function (){}).call()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "with (function (){}.call()) {}", + output: "with ((function (){}).call()) {}", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "foo(function (){}.call())", + output: "foo((function (){}).call())", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "new foo(function (){}.call())", + output: "new foo((function (){}).call())", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + errors: [wrapExpressionError] + }, + { + code: "import(function (){}.call())", + output: "import((function (){}).call())", // wrap function expression, but don't remove mandatory parens + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapExpressionError] } ] });