wrap-iife.js 5.91 KB
Newer Older
liang ce committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
/**
 * @fileoverview Rule to flag when IIFE is not wrapped in parens
 * @author Ilya Volodin
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require("../util/ast-utils");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        type: "layout",

        docs: {
            description: "require parentheses around immediate `function` invocations",
            category: "Best Practices",
            recommended: false,
            url: "https://eslint.org/docs/rules/wrap-iife"
        },

        schema: [
            {
                enum: ["outside", "inside", "any"]
            },
            {
                type: "object",
                properties: {
                    functionPrototypeMethods: {
                        type: "boolean",
                        default: false
                    }
                },
                additionalProperties: false
            }
        ],

        fixable: "code",
        messages: {
            wrapInvocation: "Wrap an immediate function invocation in parentheses.",
            wrapExpression: "Wrap only the function expression in parens.",
            moveInvocation: "Move the invocation into the parens that contain the function."
        }
    },

    create(context) {

        const style = context.options[0] || "outside";
        const includeFunctionPrototypeMethods = context.options[1] && context.options[1].functionPrototypeMethods;

        const sourceCode = context.getSourceCode();

        /**
         * Check if the node is wrapped in ()
         * @param {ASTNode} node node to evaluate
         * @returns {boolean} True if it is wrapped
         * @private
         */
        function wrapped(node) {
            return astUtils.isParenthesised(sourceCode, node);
        }

        /**
         * Get the function node from an IIFE
         * @param {ASTNode} node node to evaluate
         * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist
         */
        function getFunctionNodeFromIIFE(node) {
            const callee = node.callee;

            if (callee.type === "FunctionExpression") {
                return callee;
            }

            if (includeFunctionPrototypeMethods &&
                callee.type === "MemberExpression" &&
                callee.object.type === "FunctionExpression" &&
                (astUtils.getStaticPropertyName(callee) === "call" || astUtils.getStaticPropertyName(callee) === "apply")
            ) {
                return callee.object;
            }

            return null;
        }


        return {
            CallExpression(node) {
                const innerNode = getFunctionNodeFromIIFE(node);

                if (!innerNode) {
                    return;
                }

                const callExpressionWrapped = wrapped(node),
                    functionExpressionWrapped = wrapped(innerNode);

                if (!callExpressionWrapped && !functionExpressionWrapped) {
                    context.report({
                        node,
                        messageId: "wrapInvocation",
                        fix(fixer) {
                            const nodeToSurround = style === "inside" ? innerNode : node;

                            return fixer.replaceText(nodeToSurround, `(${sourceCode.getText(nodeToSurround)})`);
                        }
                    });
                } else if (style === "inside" && !functionExpressionWrapped) {
                    context.report({
                        node,
                        messageId: "wrapExpression",
                        fix(fixer) {

                            /*
                             * 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.
                             */
                            const parenAfter = sourceCode.getTokenAfter(node);

                            return fixer.replaceTextRange(
                                [innerNode.range[1], parenAfter.range[1]],
                                `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}`
                            );
                        }
                    });
                } else if (style === "outside" && !callExpressionWrapped) {
                    context.report({
                        node,
                        messageId: "moveInvocation",
                        fix(fixer) {

                            /*
                             * The inner function expression will always be wrapped at this point.
                             * It's only necessary to replace the range between the end of the function expression
                             * and the call expression. For example, in `(function(foo) {})(bar)`, the range `)(bar)`
                             * should get replaced with `(bar))`.
                             */
                            const parenAfter = sourceCode.getTokenAfter(innerNode);

                            return fixer.replaceTextRange(
                                [parenAfter.range[0], node.range[1]],
                                `${sourceCode.getText().slice(parenAfter.range[1], node.range[1])})`
                            );
                        }
                    });
                }
            }
        };

    }
};