UNPKG

23.4 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 ts = __importStar(require("typescript"));
24const tsutils_1 = require("tsutils");
25const util_1 = require("../util");
26// Truthiness utilities
27// #region
28const isTruthyLiteral = (type) => (0, tsutils_1.isBooleanLiteralType)(type, true) || ((0, tsutils_1.isLiteralType)(type) && !!type.value);
29const isPossiblyFalsy = (type) => (0, tsutils_1.unionTypeParts)(type)
30 // PossiblyFalsy flag includes literal values, so exclude ones that
31 // are definitely truthy
32 .filter(t => !isTruthyLiteral(t))
33 .some(type => (0, util_1.isTypeFlagSet)(type, ts.TypeFlags.PossiblyFalsy));
34const isPossiblyTruthy = (type) => (0, tsutils_1.unionTypeParts)(type).some(type => !(0, tsutils_1.isFalsyType)(type));
35// Nullish utilities
36const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
37const isNullishType = (type) => (0, util_1.isTypeFlagSet)(type, nullishFlag);
38const isPossiblyNullish = (type) => (0, tsutils_1.unionTypeParts)(type).some(isNullishType);
39const isAlwaysNullish = (type) => (0, tsutils_1.unionTypeParts)(type).every(isNullishType);
40// isLiteralType only covers numbers and strings, this is a more exhaustive check.
41const isLiteral = (type) => (0, tsutils_1.isBooleanLiteralType)(type, true) ||
42 (0, tsutils_1.isBooleanLiteralType)(type, false) ||
43 type.flags === ts.TypeFlags.Undefined ||
44 type.flags === ts.TypeFlags.Null ||
45 type.flags === ts.TypeFlags.Void ||
46 (0, tsutils_1.isLiteralType)(type);
47exports.default = (0, util_1.createRule)({
48 name: 'no-unnecessary-condition',
49 meta: {
50 type: 'suggestion',
51 docs: {
52 description: 'Prevents conditionals where the type is always truthy or always falsy',
53 recommended: false,
54 requiresTypeChecking: true,
55 },
56 schema: [
57 {
58 type: 'object',
59 properties: {
60 allowConstantLoopConditions: {
61 type: 'boolean',
62 },
63 allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
64 type: 'boolean',
65 },
66 },
67 additionalProperties: false,
68 },
69 ],
70 fixable: 'code',
71 messages: {
72 alwaysTruthy: 'Unnecessary conditional, value is always truthy.',
73 alwaysFalsy: 'Unnecessary conditional, value is always falsy.',
74 alwaysTruthyFunc: 'This callback should return a conditional, but return is always truthy.',
75 alwaysFalsyFunc: 'This callback should return a conditional, but return is always falsy.',
76 neverNullish: 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.',
77 alwaysNullish: 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.',
78 literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values.',
79 noOverlapBooleanExpression: 'Unnecessary conditional, the types have no overlap.',
80 never: 'Unnecessary conditional, value is `never`.',
81 neverOptionalChain: 'Unnecessary optional chain on a non-nullish value.',
82 noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
83 },
84 },
85 defaultOptions: [
86 {
87 allowConstantLoopConditions: false,
88 allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
89 },
90 ],
91 create(context, [{ allowConstantLoopConditions, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, },]) {
92 const service = (0, util_1.getParserServices)(context);
93 const checker = service.program.getTypeChecker();
94 const sourceCode = context.getSourceCode();
95 const compilerOptions = service.program.getCompilerOptions();
96 const isStrictNullChecks = (0, tsutils_1.isStrictCompilerOptionEnabled)(compilerOptions, 'strictNullChecks');
97 if (!isStrictNullChecks &&
98 allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true) {
99 context.report({
100 loc: {
101 start: { line: 0, column: 0 },
102 end: { line: 0, column: 0 },
103 },
104 messageId: 'noStrictNullCheck',
105 });
106 }
107 function getNodeType(node) {
108 const tsNode = service.esTreeNodeToTSNodeMap.get(node);
109 return (0, util_1.getConstrainedTypeAtLocation)(checker, tsNode);
110 }
111 function nodeIsArrayType(node) {
112 const nodeType = getNodeType(node);
113 return checker.isArrayType(nodeType);
114 }
115 function nodeIsTupleType(node) {
116 const nodeType = getNodeType(node);
117 return checker.isTupleType(nodeType);
118 }
119 function isArrayIndexExpression(node) {
120 return (
121 // Is an index signature
122 node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
123 node.computed &&
124 // ...into an array type
125 (nodeIsArrayType(node.object) ||
126 // ... or a tuple type
127 (nodeIsTupleType(node.object) &&
128 // Exception: literal index into a tuple - will have a sound type
129 node.property.type !== utils_1.AST_NODE_TYPES.Literal)));
130 }
131 /**
132 * Checks if a conditional node is necessary:
133 * if the type of the node is always true or always false, it's not necessary.
134 */
135 function checkNode(node, isUnaryNotArgument = false) {
136 // Check if the node is Unary Negation expression and handle it
137 if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
138 node.operator === '!') {
139 return checkNode(node.argument, true);
140 }
141 // Since typescript array index signature types don't represent the
142 // possibility of out-of-bounds access, if we're indexing into an array
143 // just skip the check, to avoid false positives
144 if (isArrayIndexExpression(node)) {
145 return;
146 }
147 // When checking logical expressions, only check the right side
148 // as the left side has been checked by checkLogicalExpressionForUnnecessaryConditionals
149 //
150 // Unless the node is nullish coalescing, as it's common to use patterns like `nullBool ?? true` to to strict
151 // boolean checks if we inspect the right here, it'll usually be a constant condition on purpose.
152 // In this case it's better to inspect the type of the expression as a whole.
153 if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
154 node.operator !== '??') {
155 return checkNode(node.right);
156 }
157 const type = getNodeType(node);
158 // Conditional is always necessary if it involves:
159 // `any` or `unknown` or a naked type parameter
160 if ((0, tsutils_1.unionTypeParts)(type).some(part => (0, util_1.isTypeAnyType)(part) ||
161 (0, util_1.isTypeUnknownType)(part) ||
162 (0, util_1.isTypeFlagSet)(part, ts.TypeFlags.TypeParameter))) {
163 return;
164 }
165 let messageId = null;
166 if ((0, util_1.isTypeFlagSet)(type, ts.TypeFlags.Never)) {
167 messageId = 'never';
168 }
169 else if (!isPossiblyTruthy(type)) {
170 messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
171 }
172 else if (!isPossiblyFalsy(type)) {
173 messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
174 }
175 if (messageId) {
176 context.report({ node, messageId });
177 }
178 }
179 function checkNodeForNullish(node) {
180 const type = getNodeType(node);
181 // Conditional is always necessary if it involves `any` or `unknown`
182 if ((0, util_1.isTypeAnyType)(type) || (0, util_1.isTypeUnknownType)(type)) {
183 return;
184 }
185 let messageId = null;
186 if ((0, util_1.isTypeFlagSet)(type, ts.TypeFlags.Never)) {
187 messageId = 'never';
188 }
189 else if (!isPossiblyNullish(type)) {
190 // Since typescript array index signature types don't represent the
191 // possibility of out-of-bounds access, if we're indexing into an array
192 // just skip the check, to avoid false positives
193 if (!isArrayIndexExpression(node) &&
194 !(node.type === utils_1.AST_NODE_TYPES.ChainExpression &&
195 node.expression.type !== utils_1.AST_NODE_TYPES.TSNonNullExpression &&
196 optionChainContainsOptionArrayIndex(node.expression))) {
197 messageId = 'neverNullish';
198 }
199 }
200 else if (isAlwaysNullish(type)) {
201 messageId = 'alwaysNullish';
202 }
203 if (messageId) {
204 context.report({ node, messageId });
205 }
206 }
207 /**
208 * Checks that a binary expression is necessarily conditional, reports otherwise.
209 * If both sides of the binary expression are literal values, it's not a necessary condition.
210 *
211 * NOTE: It's also unnecessary if the types that don't overlap at all
212 * but that case is handled by the Typescript compiler itself.
213 * Known exceptions:
214 * * https://github.com/microsoft/TypeScript/issues/32627
215 * * https://github.com/microsoft/TypeScript/issues/37160 (handled)
216 */
217 const BOOL_OPERATORS = new Set([
218 '<',
219 '>',
220 '<=',
221 '>=',
222 '==',
223 '===',
224 '!=',
225 '!==',
226 ]);
227 function checkIfBinaryExpressionIsNecessaryConditional(node) {
228 if (!BOOL_OPERATORS.has(node.operator)) {
229 return;
230 }
231 const leftType = getNodeType(node.left);
232 const rightType = getNodeType(node.right);
233 if (isLiteral(leftType) && isLiteral(rightType)) {
234 context.report({ node, messageId: 'literalBooleanExpression' });
235 return;
236 }
237 // Workaround for https://github.com/microsoft/TypeScript/issues/37160
238 if (isStrictNullChecks) {
239 const UNDEFINED = ts.TypeFlags.Undefined;
240 const NULL = ts.TypeFlags.Null;
241 const isComparable = (type, flag) => {
242 // Allow comparison to `any`, `unknown` or a naked type parameter.
243 flag |=
244 ts.TypeFlags.Any |
245 ts.TypeFlags.Unknown |
246 ts.TypeFlags.TypeParameter;
247 // Allow loose comparison to nullish values.
248 if (node.operator === '==' || node.operator === '!=') {
249 flag |= NULL | UNDEFINED;
250 }
251 return (0, util_1.isTypeFlagSet)(type, flag);
252 };
253 if ((leftType.flags === UNDEFINED &&
254 !isComparable(rightType, UNDEFINED)) ||
255 (rightType.flags === UNDEFINED &&
256 !isComparable(leftType, UNDEFINED)) ||
257 (leftType.flags === NULL && !isComparable(rightType, NULL)) ||
258 (rightType.flags === NULL && !isComparable(leftType, NULL))) {
259 context.report({ node, messageId: 'noOverlapBooleanExpression' });
260 return;
261 }
262 }
263 }
264 /**
265 * Checks that a logical expression contains a boolean, reports otherwise.
266 */
267 function checkLogicalExpressionForUnnecessaryConditionals(node) {
268 if (node.operator === '??') {
269 checkNodeForNullish(node.left);
270 return;
271 }
272 // Only checks the left side, since the right side might not be "conditional" at all.
273 // The right side will be checked if the LogicalExpression is used in a conditional context
274 checkNode(node.left);
275 }
276 /**
277 * Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
278 */
279 function checkIfLoopIsNecessaryConditional(node) {
280 if (node.test === null) {
281 // e.g. `for(;;)`
282 return;
283 }
284 /**
285 * Allow:
286 * while (true) {}
287 * for (;true;) {}
288 * do {} while (true)
289 */
290 if (allowConstantLoopConditions &&
291 (0, tsutils_1.isBooleanLiteralType)(getNodeType(node.test), true)) {
292 return;
293 }
294 checkNode(node.test);
295 }
296 const ARRAY_PREDICATE_FUNCTIONS = new Set([
297 'filter',
298 'find',
299 'some',
300 'every',
301 ]);
302 function isArrayPredicateFunction(node) {
303 const { callee } = node;
304 return (
305 // looks like `something.filter` or `something.find`
306 callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
307 callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
308 ARRAY_PREDICATE_FUNCTIONS.has(callee.property.name) &&
309 // and the left-hand side is an array, according to the types
310 (nodeIsArrayType(callee.object) || nodeIsTupleType(callee.object)));
311 }
312 function checkCallExpression(node) {
313 // If this is something like arr.filter(x => /*condition*/), check `condition`
314 if (isArrayPredicateFunction(node) && node.arguments.length) {
315 const callback = node.arguments[0];
316 // Inline defined functions
317 if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
318 callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
319 callback.body) {
320 // Two special cases, where we can directly check the node that's returned:
321 // () => something
322 if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
323 return checkNode(callback.body);
324 }
325 // () => { return something; }
326 const callbackBody = callback.body.body;
327 if (callbackBody.length === 1 &&
328 callbackBody[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
329 callbackBody[0].argument) {
330 return checkNode(callbackBody[0].argument);
331 }
332 // Potential enhancement: could use code-path analysis to check
333 // any function with a single return statement
334 // (Value to complexity ratio is dubious however)
335 }
336 // Otherwise just do type analysis on the function as a whole.
337 const returnTypes = (0, tsutils_1.getCallSignaturesOfType)(getNodeType(callback)).map(sig => sig.getReturnType());
338 /* istanbul ignore if */ if (returnTypes.length === 0) {
339 // Not a callable function
340 return;
341 }
342 // Predicate is always necessary if it involves `any` or `unknown`
343 if (returnTypes.some(t => (0, util_1.isTypeAnyType)(t) || (0, util_1.isTypeUnknownType)(t))) {
344 return;
345 }
346 if (!returnTypes.some(isPossiblyFalsy)) {
347 return context.report({
348 node: callback,
349 messageId: 'alwaysTruthyFunc',
350 });
351 }
352 if (!returnTypes.some(isPossiblyTruthy)) {
353 return context.report({
354 node: callback,
355 messageId: 'alwaysFalsyFunc',
356 });
357 }
358 }
359 }
360 // Recursively searches an optional chain for an array index expression
361 // Has to search the entire chain, because an array index will "infect" the rest of the types
362 // Example:
363 // ```
364 // [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
365 // ?.x // type is {y: "z"}
366 // ?.y // This access is considered "unnecessary" according to the types
367 // ```
368 function optionChainContainsOptionArrayIndex(node) {
369 const lhsNode = node.type === utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object;
370 if (node.optional && isArrayIndexExpression(lhsNode)) {
371 return true;
372 }
373 if (lhsNode.type === utils_1.AST_NODE_TYPES.MemberExpression ||
374 lhsNode.type === utils_1.AST_NODE_TYPES.CallExpression) {
375 return optionChainContainsOptionArrayIndex(lhsNode);
376 }
377 return false;
378 }
379 function isNullablePropertyType(objType, propertyType) {
380 if (propertyType.isUnion()) {
381 return propertyType.types.some(type => isNullablePropertyType(objType, type));
382 }
383 if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
384 const propType = (0, util_1.getTypeOfPropertyOfName)(checker, objType, propertyType.value.toString());
385 if (propType) {
386 return (0, util_1.isNullableType)(propType, { allowUndefined: true });
387 }
388 }
389 const typeName = (0, util_1.getTypeName)(checker, propertyType);
390 return !!((typeName === 'string' &&
391 checker.getIndexInfoOfType(objType, ts.IndexKind.String)) ||
392 (typeName === 'number' &&
393 checker.getIndexInfoOfType(objType, ts.IndexKind.Number)));
394 }
395 // Checks whether a member expression is nullable or not regardless of it's previous node.
396 // Example:
397 // ```
398 // // 'bar' is nullable if 'foo' is null.
399 // // but this function checks regardless of 'foo' type, so returns 'true'.
400 // declare const foo: { bar : { baz: string } } | null
401 // foo?.bar;
402 // ```
403 function isNullableOriginFromPrev(node) {
404 const prevType = getNodeType(node.object);
405 const property = node.property;
406 if (prevType.isUnion() && (0, util_1.isIdentifier)(property)) {
407 const isOwnNullable = prevType.types.some(type => {
408 if (node.computed) {
409 const propertyType = getNodeType(node.property);
410 return isNullablePropertyType(type, propertyType);
411 }
412 const propType = (0, util_1.getTypeOfPropertyOfName)(checker, type, property.name);
413 return propType && (0, util_1.isNullableType)(propType, { allowUndefined: true });
414 });
415 return (!isOwnNullable && (0, util_1.isNullableType)(prevType, { allowUndefined: true }));
416 }
417 return false;
418 }
419 function isOptionableExpression(node) {
420 const type = getNodeType(node);
421 const isOwnNullable = node.type === utils_1.AST_NODE_TYPES.MemberExpression
422 ? !isNullableOriginFromPrev(node)
423 : true;
424 return ((0, util_1.isTypeAnyType)(type) ||
425 (0, util_1.isTypeUnknownType)(type) ||
426 ((0, util_1.isNullableType)(type, { allowUndefined: true }) && isOwnNullable));
427 }
428 function checkOptionalChain(node, beforeOperator, fix) {
429 // We only care if this step in the chain is optional. If just descend
430 // from an optional chain, then that's fine.
431 if (!node.optional) {
432 return;
433 }
434 // Since typescript array index signature types don't represent the
435 // possibility of out-of-bounds access, if we're indexing into an array
436 // just skip the check, to avoid false positives
437 if (optionChainContainsOptionArrayIndex(node)) {
438 return;
439 }
440 const nodeToCheck = node.type === utils_1.AST_NODE_TYPES.CallExpression ? node.callee : node.object;
441 if (isOptionableExpression(nodeToCheck)) {
442 return;
443 }
444 const questionDotOperator = (0, util_1.nullThrows)(sourceCode.getTokenAfter(beforeOperator, token => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '?.'), util_1.NullThrowsReasons.MissingToken('operator', node.type));
445 context.report({
446 node,
447 loc: questionDotOperator.loc,
448 messageId: 'neverOptionalChain',
449 fix(fixer) {
450 return fixer.replaceText(questionDotOperator, fix);
451 },
452 });
453 }
454 function checkOptionalMemberExpression(node) {
455 checkOptionalChain(node, node.object, node.computed ? '' : '.');
456 }
457 function checkOptionalCallExpression(node) {
458 checkOptionalChain(node, node.callee, '');
459 }
460 return {
461 BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional,
462 CallExpression: checkCallExpression,
463 ConditionalExpression: (node) => checkNode(node.test),
464 DoWhileStatement: checkIfLoopIsNecessaryConditional,
465 ForStatement: checkIfLoopIsNecessaryConditional,
466 IfStatement: (node) => checkNode(node.test),
467 LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
468 WhileStatement: checkIfLoopIsNecessaryConditional,
469 'MemberExpression[optional = true]': checkOptionalMemberExpression,
470 'CallExpression[optional = true]': checkOptionalCallExpression,
471 };
472 },
473});
474//# sourceMappingURL=no-unnecessary-condition.js.map
\No newline at end of file