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 |
|
14 | //------------------------------------------------------------------------------
|
15 | // Rule Definition
|
16 | //------------------------------------------------------------------------------
|
17 |
|
18 | module.exports = {
|
19 | meta: {
|
20 | type: "layout",
|
21 |
|
22 | docs: {
|
23 | description: "require parentheses around immediate `function` invocations",
|
24 | category: "Best Practices",
|
25 | recommended: false,
|
26 | url: "https://eslint.org/docs/rules/wrap-iife"
|
27 | },
|
28 |
|
29 | schema: [
|
30 | {
|
31 | enum: ["outside", "inside", "any"]
|
32 | },
|
33 | {
|
34 | type: "object",
|
35 | properties: {
|
36 | functionPrototypeMethods: {
|
37 | type: "boolean",
|
38 | default: false
|
39 | }
|
40 | },
|
41 | additionalProperties: false
|
42 | }
|
43 | ],
|
44 |
|
45 | fixable: "code",
|
46 | messages: {
|
47 | wrapInvocation: "Wrap an immediate function invocation in parentheses.",
|
48 | wrapExpression: "Wrap only the function expression in parens.",
|
49 | moveInvocation: "Move the invocation into the parens that contain the function."
|
50 | }
|
51 | },
|
52 |
|
53 | create(context) {
|
54 |
|
55 | const style = context.options[0] || "outside";
|
56 | const includeFunctionPrototypeMethods = context.options[1] && context.options[1].functionPrototypeMethods;
|
57 |
|
58 | const sourceCode = context.getSourceCode();
|
59 |
|
60 | /**
|
61 | * Check if the node is wrapped in ()
|
62 | * @param {ASTNode} node node to evaluate
|
63 | * @returns {boolean} True if it is wrapped
|
64 | * @private
|
65 | */
|
66 | function wrapped(node) {
|
67 | return astUtils.isParenthesised(sourceCode, node);
|
68 | }
|
69 |
|
70 | /**
|
71 | * Get the function node from an IIFE
|
72 | * @param {ASTNode} node node to evaluate
|
73 | * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist
|
74 | */
|
75 | function getFunctionNodeFromIIFE(node) {
|
76 | const callee = node.callee;
|
77 |
|
78 | if (callee.type === "FunctionExpression") {
|
79 | return callee;
|
80 | }
|
81 |
|
82 | if (includeFunctionPrototypeMethods &&
|
83 | callee.type === "MemberExpression" &&
|
84 | callee.object.type === "FunctionExpression" &&
|
85 | (astUtils.getStaticPropertyName(callee) === "call" || astUtils.getStaticPropertyName(callee) === "apply")
|
86 | ) {
|
87 | return callee.object;
|
88 | }
|
89 |
|
90 | return null;
|
91 | }
|
92 |
|
93 |
|
94 | return {
|
95 | CallExpression(node) {
|
96 | const innerNode = getFunctionNodeFromIIFE(node);
|
97 |
|
98 | if (!innerNode) {
|
99 | return;
|
100 | }
|
101 |
|
102 | const callExpressionWrapped = wrapped(node),
|
103 | functionExpressionWrapped = wrapped(innerNode);
|
104 |
|
105 | if (!callExpressionWrapped && !functionExpressionWrapped) {
|
106 | context.report({
|
107 | node,
|
108 | messageId: "wrapInvocation",
|
109 | fix(fixer) {
|
110 | const nodeToSurround = style === "inside" ? innerNode : node;
|
111 |
|
112 | return fixer.replaceText(nodeToSurround, `(${sourceCode.getText(nodeToSurround)})`);
|
113 | }
|
114 | });
|
115 | } else if (style === "inside" && !functionExpressionWrapped) {
|
116 | context.report({
|
117 | node,
|
118 | messageId: "wrapExpression",
|
119 | fix(fixer) {
|
120 |
|
121 | /*
|
122 | * The outer call expression will always be wrapped at this point.
|
123 | * Replace the range between the end of the function expression and the end of the call expression.
|
124 | * for example, in `(function(foo) {}(bar))`, the range `(bar))` should get replaced with `)(bar)`.
|
125 | * Replace the parens from the outer expression, and parenthesize the function expression.
|
126 | */
|
127 | const parenAfter = sourceCode.getTokenAfter(node);
|
128 |
|
129 | return fixer.replaceTextRange(
|
130 | [innerNode.range[1], parenAfter.range[1]],
|
131 | `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}`
|
132 | );
|
133 | }
|
134 | });
|
135 | } else if (style === "outside" && !callExpressionWrapped) {
|
136 | context.report({
|
137 | node,
|
138 | messageId: "moveInvocation",
|
139 | fix(fixer) {
|
140 |
|
141 | /*
|
142 | * The inner function expression will always be wrapped at this point.
|
143 | * It's only necessary to replace the range between the end of the function expression
|
144 | * and the call expression. For example, in `(function(foo) {})(bar)`, the range `)(bar)`
|
145 | * should get replaced with `(bar))`.
|
146 | */
|
147 | const parenAfter = sourceCode.getTokenAfter(innerNode);
|
148 |
|
149 | return fixer.replaceTextRange(
|
150 | [parenAfter.range[0], node.range[1]],
|
151 | `${sourceCode.getText().slice(parenAfter.range[1], node.range[1])})`
|
152 | );
|
153 | }
|
154 | });
|
155 | }
|
156 | }
|
157 | };
|
158 |
|
159 | }
|
160 | };
|