1 | /**
|
2 | * @fileoverview Rule to flag when IIFE is not wrapped in parens
|
3 | * @author Ilya Volodin
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("./utils/ast-utils");
|
13 | const eslintUtils = require("eslint-utils");
|
14 |
|
15 | //----------------------------------------------------------------------
|
16 | // Helpers
|
17 | //----------------------------------------------------------------------
|
18 |
|
19 | /**
|
20 | * Check if the given node is callee of a `NewExpression` node
|
21 | * @param {ASTNode} node node to check
|
22 | * @returns {boolean} True if the node is callee of a `NewExpression` node
|
23 | * @private
|
24 | */
|
25 | function isCalleeOfNewExpression(node) {
|
26 | const maybeCallee = node.parent.type === "ChainExpression"
|
27 | ? node.parent
|
28 | : node;
|
29 |
|
30 | return (
|
31 | maybeCallee.parent.type === "NewExpression" &&
|
32 | maybeCallee.parent.callee === maybeCallee
|
33 | );
|
34 | }
|
35 |
|
36 | //------------------------------------------------------------------------------
|
37 | // Rule Definition
|
38 | //------------------------------------------------------------------------------
|
39 |
|
40 | module.exports = {
|
41 | meta: {
|
42 | type: "layout",
|
43 |
|
44 | docs: {
|
45 | description: "require parentheses around immediate `function` invocations",
|
46 | category: "Best Practices",
|
47 | recommended: false,
|
48 | url: "https://eslint.org/docs/rules/wrap-iife"
|
49 | },
|
50 |
|
51 | schema: [
|
52 | {
|
53 | enum: ["outside", "inside", "any"]
|
54 | },
|
55 | {
|
56 | type: "object",
|
57 | properties: {
|
58 | functionPrototypeMethods: {
|
59 | type: "boolean",
|
60 | default: false
|
61 | }
|
62 | },
|
63 | additionalProperties: false
|
64 | }
|
65 | ],
|
66 |
|
67 | fixable: "code",
|
68 | messages: {
|
69 | wrapInvocation: "Wrap an immediate function invocation in parentheses.",
|
70 | wrapExpression: "Wrap only the function expression in parens.",
|
71 | moveInvocation: "Move the invocation into the parens that contain the function."
|
72 | }
|
73 | },
|
74 |
|
75 | create(context) {
|
76 |
|
77 | const style = context.options[0] || "outside";
|
78 | const includeFunctionPrototypeMethods = context.options[1] && context.options[1].functionPrototypeMethods;
|
79 |
|
80 | const sourceCode = context.getSourceCode();
|
81 |
|
82 | /**
|
83 | * Check if the node is wrapped in any (). All parens count: grouping parens and parens for constructs such as if()
|
84 | * @param {ASTNode} node node to evaluate
|
85 | * @returns {boolean} True if it is wrapped in any parens
|
86 | * @private
|
87 | */
|
88 | function isWrappedInAnyParens(node) {
|
89 | return astUtils.isParenthesised(sourceCode, node);
|
90 | }
|
91 |
|
92 | /**
|
93 | * Check if the node is wrapped in grouping (). Parens for constructs such as if() don't count
|
94 | * @param {ASTNode} node node to evaluate
|
95 | * @returns {boolean} True if it is wrapped in grouping parens
|
96 | * @private
|
97 | */
|
98 | function isWrappedInGroupingParens(node) {
|
99 | return eslintUtils.isParenthesized(1, node, sourceCode);
|
100 | }
|
101 |
|
102 | /**
|
103 | * Get the function node from an IIFE
|
104 | * @param {ASTNode} node node to evaluate
|
105 | * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist
|
106 | */
|
107 | function getFunctionNodeFromIIFE(node) {
|
108 | const callee = astUtils.skipChainExpression(node.callee);
|
109 |
|
110 | if (callee.type === "FunctionExpression") {
|
111 | return callee;
|
112 | }
|
113 |
|
114 | if (includeFunctionPrototypeMethods &&
|
115 | callee.type === "MemberExpression" &&
|
116 | callee.object.type === "FunctionExpression" &&
|
117 | (astUtils.getStaticPropertyName(callee) === "call" || astUtils.getStaticPropertyName(callee) === "apply")
|
118 | ) {
|
119 | return callee.object;
|
120 | }
|
121 |
|
122 | return null;
|
123 | }
|
124 |
|
125 |
|
126 | return {
|
127 | CallExpression(node) {
|
128 | const innerNode = getFunctionNodeFromIIFE(node);
|
129 |
|
130 | if (!innerNode) {
|
131 | return;
|
132 | }
|
133 |
|
134 | const isCallExpressionWrapped = isWrappedInAnyParens(node),
|
135 | isFunctionExpressionWrapped = isWrappedInAnyParens(innerNode);
|
136 |
|
137 | if (!isCallExpressionWrapped && !isFunctionExpressionWrapped) {
|
138 | context.report({
|
139 | node,
|
140 | messageId: "wrapInvocation",
|
141 | fix(fixer) {
|
142 | const nodeToSurround = style === "inside" ? innerNode : node;
|
143 |
|
144 | return fixer.replaceText(nodeToSurround, `(${sourceCode.getText(nodeToSurround)})`);
|
145 | }
|
146 | });
|
147 | } else if (style === "inside" && !isFunctionExpressionWrapped) {
|
148 | context.report({
|
149 | node,
|
150 | messageId: "wrapExpression",
|
151 | fix(fixer) {
|
152 |
|
153 | // The outer call expression will always be wrapped at this point.
|
154 |
|
155 | if (isWrappedInGroupingParens(node) && !isCalleeOfNewExpression(node)) {
|
156 |
|
157 | /*
|
158 | * Parenthesize the function expression and remove unnecessary grouping parens around the call expression.
|
159 | * Replace the range between the end of the function expression and the end of the call expression.
|
160 | * for example, in `(function(foo) {}(bar))`, the range `(bar))` should get replaced with `)(bar)`.
|
161 | */
|
162 |
|
163 | const parenAfter = sourceCode.getTokenAfter(node);
|
164 |
|
165 | return fixer.replaceTextRange(
|
166 | [innerNode.range[1], parenAfter.range[1]],
|
167 | `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}`
|
168 | );
|
169 | }
|
170 |
|
171 | /*
|
172 | * Call expression is wrapped in mandatory parens such as if(), or in necessary grouping parens.
|
173 | * These parens cannot be removed, so just parenthesize the function expression.
|
174 | */
|
175 |
|
176 | return fixer.replaceText(innerNode, `(${sourceCode.getText(innerNode)})`);
|
177 | }
|
178 | });
|
179 | } else if (style === "outside" && !isCallExpressionWrapped) {
|
180 | context.report({
|
181 | node,
|
182 | messageId: "moveInvocation",
|
183 | fix(fixer) {
|
184 |
|
185 | /*
|
186 | * The inner function expression will always be wrapped at this point.
|
187 | * It's only necessary to replace the range between the end of the function expression
|
188 | * and the call expression. For example, in `(function(foo) {})(bar)`, the range `)(bar)`
|
189 | * should get replaced with `(bar))`.
|
190 | */
|
191 | const parenAfter = sourceCode.getTokenAfter(innerNode);
|
192 |
|
193 | return fixer.replaceTextRange(
|
194 | [parenAfter.range[0], node.range[1]],
|
195 | `${sourceCode.getText().slice(parenAfter.range[1], node.range[1])})`
|
196 | );
|
197 | }
|
198 | });
|
199 | }
|
200 | }
|
201 | };
|
202 |
|
203 | }
|
204 | };
|