UNPKG

13.7 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5}) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
7 o[k2] = m[k];
8}));
9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
11}) : function(o, v) {
12 o["default"] = v;
13});
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22const utils_1 = require("@typescript-eslint/utils");
23const tsutils = __importStar(require("tsutils"));
24const ts = __importStar(require("typescript"));
25const util = __importStar(require("../util"));
26exports.default = util.createRule({
27 name: 'no-confusing-void-expression',
28 meta: {
29 docs: {
30 description: 'Requires expressions of type void to appear in statement position',
31 recommended: false,
32 requiresTypeChecking: true,
33 },
34 messages: {
35 invalidVoidExpr: 'Placing a void expression inside another expression is forbidden. ' +
36 'Move it to its own statement instead.',
37 invalidVoidExprWrapVoid: 'Void expressions used inside another expression ' +
38 'must be moved to its own statement ' +
39 'or marked explicitly with the `void` operator.',
40 invalidVoidExprArrow: 'Returning a void expression from an arrow function shorthand is forbidden. ' +
41 'Please add braces to the arrow function.',
42 invalidVoidExprArrowWrapVoid: 'Void expressions returned from an arrow function shorthand ' +
43 'must be marked explicitly with the `void` operator.',
44 invalidVoidExprReturn: 'Returning a void expression from a function is forbidden. ' +
45 'Please move it before the `return` statement.',
46 invalidVoidExprReturnLast: 'Returning a void expression from a function is forbidden. ' +
47 'Please remove the `return` statement.',
48 invalidVoidExprReturnWrapVoid: 'Void expressions returned from a function ' +
49 'must be marked explicitly with the `void` operator.',
50 voidExprWrapVoid: 'Mark with an explicit `void` operator.',
51 },
52 schema: [
53 {
54 type: 'object',
55 properties: {
56 ignoreArrowShorthand: { type: 'boolean' },
57 ignoreVoidOperator: { type: 'boolean' },
58 },
59 additionalProperties: false,
60 },
61 ],
62 type: 'problem',
63 fixable: 'code',
64 hasSuggestions: true,
65 },
66 defaultOptions: [{}],
67 create(context, [options]) {
68 return {
69 'AwaitExpression, CallExpression, TaggedTemplateExpression'(node) {
70 const parserServices = util.getParserServices(context);
71 const checker = parserServices.program.getTypeChecker();
72 const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
73 const type = util.getConstrainedTypeAtLocation(checker, tsNode);
74 if (!tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) {
75 // not a void expression
76 return;
77 }
78 const invalidAncestor = findInvalidAncestor(node);
79 if (invalidAncestor == null) {
80 // void expression is in valid position
81 return;
82 }
83 const sourceCode = context.getSourceCode();
84 const wrapVoidFix = (fixer) => {
85 const nodeText = sourceCode.getText(node);
86 const newNodeText = `void ${nodeText}`;
87 return fixer.replaceText(node, newNodeText);
88 };
89 if (invalidAncestor.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
90 // handle arrow function shorthand
91 if (options.ignoreVoidOperator) {
92 // handle wrapping with `void`
93 return context.report({
94 node,
95 messageId: 'invalidVoidExprArrowWrapVoid',
96 fix: wrapVoidFix,
97 });
98 }
99 // handle wrapping with braces
100 const arrowFunction = invalidAncestor;
101 return context.report({
102 node,
103 messageId: 'invalidVoidExprArrow',
104 fix(fixer) {
105 const arrowBody = arrowFunction.body;
106 const arrowBodyText = sourceCode.getText(arrowBody);
107 const newArrowBodyText = `{ ${arrowBodyText}; }`;
108 if (util.isParenthesized(arrowBody, sourceCode)) {
109 const bodyOpeningParen = sourceCode.getTokenBefore(arrowBody, util.isOpeningParenToken);
110 const bodyClosingParen = sourceCode.getTokenAfter(arrowBody, util.isClosingParenToken);
111 return fixer.replaceTextRange([bodyOpeningParen.range[0], bodyClosingParen.range[1]], newArrowBodyText);
112 }
113 return fixer.replaceText(arrowBody, newArrowBodyText);
114 },
115 });
116 }
117 if (invalidAncestor.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
118 // handle return statement
119 if (options.ignoreVoidOperator) {
120 // handle wrapping with `void`
121 return context.report({
122 node,
123 messageId: 'invalidVoidExprReturnWrapVoid',
124 fix: wrapVoidFix,
125 });
126 }
127 const returnStmt = invalidAncestor;
128 if (isFinalReturn(returnStmt)) {
129 // remove the `return` keyword
130 return context.report({
131 node,
132 messageId: 'invalidVoidExprReturnLast',
133 fix(fixer) {
134 const returnValue = returnStmt.argument;
135 const returnValueText = sourceCode.getText(returnValue);
136 let newReturnStmtText = `${returnValueText};`;
137 if (isPreventingASI(returnValue, sourceCode)) {
138 // put a semicolon at the beginning of the line
139 newReturnStmtText = `;${newReturnStmtText}`;
140 }
141 return fixer.replaceText(returnStmt, newReturnStmtText);
142 },
143 });
144 }
145 // move before the `return` keyword
146 return context.report({
147 node,
148 messageId: 'invalidVoidExprReturn',
149 fix(fixer) {
150 var _a;
151 const returnValue = returnStmt.argument;
152 const returnValueText = sourceCode.getText(returnValue);
153 let newReturnStmtText = `${returnValueText}; return;`;
154 if (isPreventingASI(returnValue, sourceCode)) {
155 // put a semicolon at the beginning of the line
156 newReturnStmtText = `;${newReturnStmtText}`;
157 }
158 if (((_a = returnStmt.parent) === null || _a === void 0 ? void 0 : _a.type) !== utils_1.AST_NODE_TYPES.BlockStatement) {
159 // e.g. `if (cond) return console.error();`
160 // add braces if not inside a block
161 newReturnStmtText = `{ ${newReturnStmtText} }`;
162 }
163 return fixer.replaceText(returnStmt, newReturnStmtText);
164 },
165 });
166 }
167 // handle generic case
168 if (options.ignoreVoidOperator) {
169 // this would be reported by this rule btw. such irony
170 return context.report({
171 node,
172 messageId: 'invalidVoidExprWrapVoid',
173 suggest: [{ messageId: 'voidExprWrapVoid', fix: wrapVoidFix }],
174 });
175 }
176 context.report({
177 node,
178 messageId: 'invalidVoidExpr',
179 });
180 },
181 };
182 /**
183 * Inspects the void expression's ancestors and finds closest invalid one.
184 * By default anything other than an ExpressionStatement is invalid.
185 * Parent expressions which can be used for their short-circuiting behavior
186 * are ignored and their parents are checked instead.
187 * @param node The void expression node to check.
188 * @returns Invalid ancestor node if it was found. `null` otherwise.
189 */
190 function findInvalidAncestor(node) {
191 const parent = util.nullThrows(node.parent, util.NullThrowsReasons.MissingParent);
192 if (parent.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
193 // e.g. `{ console.log("foo"); }`
194 // this is always valid
195 return null;
196 }
197 if (parent.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
198 if (parent.right === node) {
199 // e.g. `x && console.log(x)`
200 // this is valid only if the next ancestor is valid
201 return findInvalidAncestor(parent);
202 }
203 }
204 if (parent.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
205 if (parent.consequent === node || parent.alternate === node) {
206 // e.g. `cond ? console.log(true) : console.log(false)`
207 // this is valid only if the next ancestor is valid
208 return findInvalidAncestor(parent);
209 }
210 }
211 if (parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
212 // e.g. `() => console.log("foo")`
213 // this is valid with an appropriate option
214 if (options.ignoreArrowShorthand) {
215 return null;
216 }
217 }
218 if (parent.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
219 if (parent.operator === 'void') {
220 // e.g. `void console.log("foo")`
221 // this is valid with an appropriate option
222 if (options.ignoreVoidOperator) {
223 return null;
224 }
225 }
226 }
227 if (parent.type === utils_1.AST_NODE_TYPES.ChainExpression) {
228 // e.g. `console?.log('foo')`
229 return findInvalidAncestor(parent);
230 }
231 // any other parent is invalid
232 return parent;
233 }
234 /** Checks whether the return statement is the last statement in a function body. */
235 function isFinalReturn(node) {
236 // the parent must be a block
237 const block = util.nullThrows(node.parent, util.NullThrowsReasons.MissingParent);
238 if (block.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
239 // e.g. `if (cond) return;` (not in a block)
240 return false;
241 }
242 // the block's parent must be a function
243 const blockParent = util.nullThrows(block.parent, util.NullThrowsReasons.MissingParent);
244 if (![
245 utils_1.AST_NODE_TYPES.FunctionDeclaration,
246 utils_1.AST_NODE_TYPES.FunctionExpression,
247 utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
248 ].includes(blockParent.type)) {
249 // e.g. `if (cond) { return; }`
250 // not in a top-level function block
251 return false;
252 }
253 // must be the last child of the block
254 if (block.body.indexOf(node) < block.body.length - 1) {
255 // not the last statement in the block
256 return false;
257 }
258 return true;
259 }
260 /**
261 * Checks whether the given node, if placed on its own line,
262 * would prevent automatic semicolon insertion on the line before.
263 *
264 * This happens if the line begins with `(`, `[` or `` ` ``
265 */
266 function isPreventingASI(node, sourceCode) {
267 const startToken = util.nullThrows(sourceCode.getFirstToken(node), util.NullThrowsReasons.MissingToken('first token', node.type));
268 return ['(', '[', '`'].includes(startToken.value);
269 }
270 },
271});
272//# sourceMappingURL=no-confusing-void-expression.js.map
\No newline at end of file