]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag when IIFE is not wrapped in parens | |
3 | * @author Ilya Volodin | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
f2a92ac6 | 13 | const eslintUtils = require("@eslint-community/eslint-utils"); |
eb39fafa DC |
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) { | |
6f036462 TL |
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 | ); | |
eb39fafa DC |
34 | } |
35 | ||
36 | //------------------------------------------------------------------------------ | |
37 | // Rule Definition | |
38 | //------------------------------------------------------------------------------ | |
39 | ||
34eeec05 | 40 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
41 | module.exports = { |
42 | meta: { | |
43 | type: "layout", | |
44 | ||
45 | docs: { | |
8f9d1d4d | 46 | description: "Require parentheses around immediate `function` invocations", |
eb39fafa | 47 | recommended: false, |
f2a92ac6 | 48 | url: "https://eslint.org/docs/latest/rules/wrap-iife" |
eb39fafa DC |
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 | ||
f2a92ac6 | 80 | const sourceCode = context.sourceCode; |
eb39fafa DC |
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) { | |
6f036462 | 108 | const callee = astUtils.skipChainExpression(node.callee); |
eb39fafa DC |
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 | }; |