1 | "use strict";
|
2 | var __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 | }));
|
9 | var __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 | });
|
14 | var __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 | };
|
21 | Object.defineProperty(exports, "__esModule", { value: true });
|
22 | const utils_1 = require("@typescript-eslint/utils");
|
23 | const ts = __importStar(require("typescript"));
|
24 | const tsutils_1 = require("tsutils");
|
25 | const util_1 = require("../util");
|
26 |
|
27 |
|
28 | const isTruthyLiteral = (type) => (0, tsutils_1.isBooleanLiteralType)(type, true) || ((0, tsutils_1.isLiteralType)(type) && !!type.value);
|
29 | const isPossiblyFalsy = (type) => (0, tsutils_1.unionTypeParts)(type)
|
30 |
|
31 |
|
32 | .filter(t => !isTruthyLiteral(t))
|
33 | .some(type => (0, util_1.isTypeFlagSet)(type, ts.TypeFlags.PossiblyFalsy));
|
34 | const isPossiblyTruthy = (type) => (0, tsutils_1.unionTypeParts)(type).some(type => !(0, tsutils_1.isFalsyType)(type));
|
35 |
|
36 | const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
|
37 | const isNullishType = (type) => (0, util_1.isTypeFlagSet)(type, nullishFlag);
|
38 | const isPossiblyNullish = (type) => (0, tsutils_1.unionTypeParts)(type).some(isNullishType);
|
39 | const isAlwaysNullish = (type) => (0, tsutils_1.unionTypeParts)(type).every(isNullishType);
|
40 |
|
41 | const 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);
|
47 | exports.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 |
|
122 | node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
123 | node.computed &&
|
124 |
|
125 | (nodeIsArrayType(node.object) ||
|
126 |
|
127 | (nodeIsTupleType(node.object) &&
|
128 |
|
129 | node.property.type !== utils_1.AST_NODE_TYPES.Literal)));
|
130 | }
|
131 | |
132 |
|
133 |
|
134 |
|
135 | function checkNode(node, isUnaryNotArgument = false) {
|
136 |
|
137 | if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
138 | node.operator === '!') {
|
139 | return checkNode(node.argument, true);
|
140 | }
|
141 |
|
142 |
|
143 |
|
144 | if (isArrayIndexExpression(node)) {
|
145 | return;
|
146 | }
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
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 |
|
159 |
|
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 |
|
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 |
|
191 |
|
192 |
|
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 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
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 |
|
238 | if (isStrictNullChecks) {
|
239 | const UNDEFINED = ts.TypeFlags.Undefined;
|
240 | const NULL = ts.TypeFlags.Null;
|
241 | const isComparable = (type, flag) => {
|
242 |
|
243 | flag |=
|
244 | ts.TypeFlags.Any |
|
245 | ts.TypeFlags.Unknown |
|
246 | ts.TypeFlags.TypeParameter;
|
247 |
|
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 |
|
266 |
|
267 | function checkLogicalExpressionForUnnecessaryConditionals(node) {
|
268 | if (node.operator === '??') {
|
269 | checkNodeForNullish(node.left);
|
270 | return;
|
271 | }
|
272 |
|
273 |
|
274 | checkNode(node.left);
|
275 | }
|
276 | |
277 |
|
278 |
|
279 | function checkIfLoopIsNecessaryConditional(node) {
|
280 | if (node.test === null) {
|
281 |
|
282 | return;
|
283 | }
|
284 | |
285 |
|
286 |
|
287 |
|
288 |
|
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 |
|
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 |
|
310 | (nodeIsArrayType(callee.object) || nodeIsTupleType(callee.object)));
|
311 | }
|
312 | function checkCallExpression(node) {
|
313 |
|
314 | if (isArrayPredicateFunction(node) && node.arguments.length) {
|
315 | const callback = node.arguments[0];
|
316 |
|
317 | if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
318 | callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
319 | callback.body) {
|
320 |
|
321 |
|
322 | if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
323 | return checkNode(callback.body);
|
324 | }
|
325 |
|
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 |
|
333 |
|
334 |
|
335 | }
|
336 |
|
337 | const returnTypes = (0, tsutils_1.getCallSignaturesOfType)(getNodeType(callback)).map(sig => sig.getReturnType());
|
338 | if (returnTypes.length === 0) {
|
339 |
|
340 | return;
|
341 | }
|
342 |
|
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 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
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 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
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 |
|
430 |
|
431 | if (!node.optional) {
|
432 | return;
|
433 | }
|
434 |
|
435 |
|
436 |
|
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 |
|
\ | No newline at end of file |