UNPKG

33.9 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: 'strict-boolean-expressions',
28 meta: {
29 type: 'suggestion',
30 fixable: 'code',
31 hasSuggestions: true,
32 docs: {
33 description: 'Restricts the types allowed in boolean expressions',
34 recommended: false,
35 requiresTypeChecking: true,
36 },
37 schema: [
38 {
39 type: 'object',
40 properties: {
41 allowString: { type: 'boolean' },
42 allowNumber: { type: 'boolean' },
43 allowNullableObject: { type: 'boolean' },
44 allowNullableBoolean: { type: 'boolean' },
45 allowNullableString: { type: 'boolean' },
46 allowNullableNumber: { type: 'boolean' },
47 allowAny: { type: 'boolean' },
48 allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
49 type: 'boolean',
50 },
51 },
52 additionalProperties: false,
53 },
54 ],
55 messages: {
56 conditionErrorOther: 'Unexpected value in conditional. ' +
57 'A boolean expression is required.',
58 conditionErrorAny: 'Unexpected any value in conditional. ' +
59 'An explicit comparison or type cast is required.',
60 conditionErrorNullish: 'Unexpected nullish value in conditional. ' +
61 'The condition is always false.',
62 conditionErrorNullableBoolean: 'Unexpected nullable boolean value in conditional. ' +
63 'Please handle the nullish case explicitly.',
64 conditionErrorString: 'Unexpected string value in conditional. ' +
65 'An explicit empty string check is required.',
66 conditionErrorNullableString: 'Unexpected nullable string value in conditional. ' +
67 'Please handle the nullish/empty cases explicitly.',
68 conditionErrorNumber: 'Unexpected number value in conditional. ' +
69 'An explicit zero/NaN check is required.',
70 conditionErrorNullableNumber: 'Unexpected nullable number value in conditional. ' +
71 'Please handle the nullish/zero/NaN cases explicitly.',
72 conditionErrorObject: 'Unexpected object value in conditional. ' +
73 'The condition is always true.',
74 conditionErrorNullableObject: 'Unexpected nullable object value in conditional. ' +
75 'An explicit null check is required.',
76 noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
77 conditionFixDefaultFalse: 'Explicitly treat nullish value the same as false (`value ?? false`)',
78 conditionFixDefaultEmptyString: 'Explicitly treat nullish value the same as an empty string (`value ?? ""`)',
79 conditionFixDefaultZero: 'Explicitly treat nullish value the same as 0 (`value ?? 0`)',
80 conditionFixCompareNullish: 'Change condition to check for null/undefined (`value != null`)',
81 conditionFixCastBoolean: 'Explicitly cast value to a boolean (`Boolean(value)`)',
82 conditionFixCompareTrue: 'Change condition to check if true (`value === true`)',
83 conditionFixCompareFalse: 'Change condition to check if false (`value === false`)',
84 conditionFixCompareStringLength: "Change condition to check string's length (`value.length !== 0`)",
85 conditionFixCompareEmptyString: 'Change condition to check for empty string (`value !== ""`)',
86 conditionFixCompareZero: 'Change condition to check for 0 (`value !== 0`)',
87 conditionFixCompareNaN: 'Change condition to check for NaN (`!Number.isNaN(value)`)',
88 },
89 },
90 defaultOptions: [
91 {
92 allowString: true,
93 allowNumber: true,
94 allowNullableObject: true,
95 allowNullableBoolean: false,
96 allowNullableString: false,
97 allowNullableNumber: false,
98 allowAny: false,
99 allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
100 },
101 ],
102 create(context, [options]) {
103 const parserServices = util.getParserServices(context);
104 const typeChecker = parserServices.program.getTypeChecker();
105 const compilerOptions = parserServices.program.getCompilerOptions();
106 const sourceCode = context.getSourceCode();
107 const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks');
108 if (!isStrictNullChecks &&
109 options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true) {
110 context.report({
111 loc: {
112 start: { line: 0, column: 0 },
113 end: { line: 0, column: 0 },
114 },
115 messageId: 'noStrictNullCheck',
116 });
117 }
118 const checkedNodes = new Set();
119 return {
120 ConditionalExpression: checkTestExpression,
121 DoWhileStatement: checkTestExpression,
122 ForStatement: checkTestExpression,
123 IfStatement: checkTestExpression,
124 WhileStatement: checkTestExpression,
125 'LogicalExpression[operator!="??"]': checkNode,
126 'UnaryExpression[operator="!"]': checkUnaryLogicalExpression,
127 };
128 function checkTestExpression(node) {
129 if (node.test == null) {
130 return;
131 }
132 checkNode(node.test, true);
133 }
134 function checkUnaryLogicalExpression(node) {
135 checkNode(node.argument, true);
136 }
137 /**
138 * This function analyzes the type of a node and checks if it is allowed in a boolean context.
139 * It can recurse when checking nested logical operators, so that only the outermost operands are reported.
140 * The right operand of a logical expression is ignored unless it's a part of a test expression (if/while/ternary/etc).
141 * @param node The AST node to check.
142 * @param isTestExpr Whether the node is a descendant of a test expression.
143 */
144 function checkNode(node, isTestExpr = false) {
145 // prevent checking the same node multiple times
146 if (checkedNodes.has(node)) {
147 return;
148 }
149 checkedNodes.add(node);
150 // for logical operator, we check its operands
151 if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
152 node.operator !== '??') {
153 checkNode(node.left, isTestExpr);
154 // we ignore the right operand when not in a context of a test expression
155 if (isTestExpr) {
156 checkNode(node.right, isTestExpr);
157 }
158 return;
159 }
160 const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
161 const type = util.getConstrainedTypeAtLocation(typeChecker, tsNode);
162 const types = inspectVariantTypes(tsutils.unionTypeParts(type));
163 const is = (...wantedTypes) => types.size === wantedTypes.length &&
164 wantedTypes.every(type => types.has(type));
165 // boolean
166 if (is('boolean') || is('truthy boolean')) {
167 // boolean is always okay
168 return;
169 }
170 // never
171 if (is('never')) {
172 // never is always okay
173 return;
174 }
175 // nullish
176 if (is('nullish')) {
177 // condition is always false
178 context.report({ node, messageId: 'conditionErrorNullish' });
179 return;
180 }
181 // Known edge case: boolean `true` and nullish values are always valid boolean expressions
182 if (is('nullish', 'truthy boolean')) {
183 return;
184 }
185 // nullable boolean
186 if (is('nullish', 'boolean')) {
187 if (!options.allowNullableBoolean) {
188 if (isLogicalNegationExpression(node.parent)) {
189 // if (!nullableBoolean)
190 context.report({
191 node,
192 messageId: 'conditionErrorNullableBoolean',
193 suggest: [
194 {
195 messageId: 'conditionFixDefaultFalse',
196 fix: util.getWrappingFixer({
197 sourceCode,
198 node,
199 wrap: code => `${code} ?? false`,
200 }),
201 },
202 {
203 messageId: 'conditionFixCompareFalse',
204 fix: util.getWrappingFixer({
205 sourceCode,
206 node: node.parent,
207 innerNode: node,
208 wrap: code => `${code} === false`,
209 }),
210 },
211 ],
212 });
213 }
214 else {
215 // if (nullableBoolean)
216 context.report({
217 node,
218 messageId: 'conditionErrorNullableBoolean',
219 suggest: [
220 {
221 messageId: 'conditionFixDefaultFalse',
222 fix: util.getWrappingFixer({
223 sourceCode,
224 node,
225 wrap: code => `${code} ?? false`,
226 }),
227 },
228 {
229 messageId: 'conditionFixCompareTrue',
230 fix: util.getWrappingFixer({
231 sourceCode,
232 node,
233 wrap: code => `${code} === true`,
234 }),
235 },
236 ],
237 });
238 }
239 }
240 return;
241 }
242 // Known edge case: truthy primitives and nullish values are always valid boolean expressions
243 if ((options.allowNumber && is('nullish', 'truthy number')) ||
244 (options.allowString && is('nullish', 'truthy string'))) {
245 return;
246 }
247 // string
248 if (is('string') || is('truthy string')) {
249 if (!options.allowString) {
250 if (isLogicalNegationExpression(node.parent)) {
251 // if (!string)
252 context.report({
253 node,
254 messageId: 'conditionErrorString',
255 suggest: [
256 {
257 messageId: 'conditionFixCompareStringLength',
258 fix: util.getWrappingFixer({
259 sourceCode,
260 node: node.parent,
261 innerNode: node,
262 wrap: code => `${code}.length === 0`,
263 }),
264 },
265 {
266 messageId: 'conditionFixCompareEmptyString',
267 fix: util.getWrappingFixer({
268 sourceCode,
269 node: node.parent,
270 innerNode: node,
271 wrap: code => `${code} === ""`,
272 }),
273 },
274 {
275 messageId: 'conditionFixCastBoolean',
276 fix: util.getWrappingFixer({
277 sourceCode,
278 node: node.parent,
279 innerNode: node,
280 wrap: code => `!Boolean(${code})`,
281 }),
282 },
283 ],
284 });
285 }
286 else {
287 // if (string)
288 context.report({
289 node,
290 messageId: 'conditionErrorString',
291 suggest: [
292 {
293 messageId: 'conditionFixCompareStringLength',
294 fix: util.getWrappingFixer({
295 sourceCode,
296 node,
297 wrap: code => `${code}.length > 0`,
298 }),
299 },
300 {
301 messageId: 'conditionFixCompareEmptyString',
302 fix: util.getWrappingFixer({
303 sourceCode,
304 node,
305 wrap: code => `${code} !== ""`,
306 }),
307 },
308 {
309 messageId: 'conditionFixCastBoolean',
310 fix: util.getWrappingFixer({
311 sourceCode,
312 node,
313 wrap: code => `Boolean(${code})`,
314 }),
315 },
316 ],
317 });
318 }
319 }
320 return;
321 }
322 // nullable string
323 if (is('nullish', 'string')) {
324 if (!options.allowNullableString) {
325 if (isLogicalNegationExpression(node.parent)) {
326 // if (!nullableString)
327 context.report({
328 node,
329 messageId: 'conditionErrorNullableString',
330 suggest: [
331 {
332 messageId: 'conditionFixCompareNullish',
333 fix: util.getWrappingFixer({
334 sourceCode,
335 node: node.parent,
336 innerNode: node,
337 wrap: code => `${code} == null`,
338 }),
339 },
340 {
341 messageId: 'conditionFixDefaultEmptyString',
342 fix: util.getWrappingFixer({
343 sourceCode,
344 node,
345 wrap: code => `${code} ?? ""`,
346 }),
347 },
348 {
349 messageId: 'conditionFixCastBoolean',
350 fix: util.getWrappingFixer({
351 sourceCode,
352 node: node.parent,
353 innerNode: node,
354 wrap: code => `!Boolean(${code})`,
355 }),
356 },
357 ],
358 });
359 }
360 else {
361 // if (nullableString)
362 context.report({
363 node,
364 messageId: 'conditionErrorNullableString',
365 suggest: [
366 {
367 messageId: 'conditionFixCompareNullish',
368 fix: util.getWrappingFixer({
369 sourceCode,
370 node,
371 wrap: code => `${code} != null`,
372 }),
373 },
374 {
375 messageId: 'conditionFixDefaultEmptyString',
376 fix: util.getWrappingFixer({
377 sourceCode,
378 node,
379 wrap: code => `${code} ?? ""`,
380 }),
381 },
382 {
383 messageId: 'conditionFixCastBoolean',
384 fix: util.getWrappingFixer({
385 sourceCode,
386 node,
387 wrap: code => `Boolean(${code})`,
388 }),
389 },
390 ],
391 });
392 }
393 }
394 return;
395 }
396 // number
397 if (is('number') || is('truthy number')) {
398 if (!options.allowNumber) {
399 if (isArrayLengthExpression(node, typeChecker, parserServices)) {
400 if (isLogicalNegationExpression(node.parent)) {
401 // if (!array.length)
402 context.report({
403 node,
404 messageId: 'conditionErrorNumber',
405 fix: util.getWrappingFixer({
406 sourceCode,
407 node: node.parent,
408 innerNode: node,
409 wrap: code => `${code} === 0`,
410 }),
411 });
412 }
413 else {
414 // if (array.length)
415 context.report({
416 node,
417 messageId: 'conditionErrorNumber',
418 fix: util.getWrappingFixer({
419 sourceCode,
420 node,
421 wrap: code => `${code} > 0`,
422 }),
423 });
424 }
425 }
426 else if (isLogicalNegationExpression(node.parent)) {
427 // if (!number)
428 context.report({
429 node,
430 messageId: 'conditionErrorNumber',
431 suggest: [
432 {
433 messageId: 'conditionFixCompareZero',
434 fix: util.getWrappingFixer({
435 sourceCode,
436 node: node.parent,
437 innerNode: node,
438 // TODO: we have to compare to 0n if the type is bigint
439 wrap: code => `${code} === 0`,
440 }),
441 },
442 {
443 // TODO: don't suggest this for bigint because it can't be NaN
444 messageId: 'conditionFixCompareNaN',
445 fix: util.getWrappingFixer({
446 sourceCode,
447 node: node.parent,
448 innerNode: node,
449 wrap: code => `Number.isNaN(${code})`,
450 }),
451 },
452 {
453 messageId: 'conditionFixCastBoolean',
454 fix: util.getWrappingFixer({
455 sourceCode,
456 node: node.parent,
457 innerNode: node,
458 wrap: code => `!Boolean(${code})`,
459 }),
460 },
461 ],
462 });
463 }
464 else {
465 // if (number)
466 context.report({
467 node,
468 messageId: 'conditionErrorNumber',
469 suggest: [
470 {
471 messageId: 'conditionFixCompareZero',
472 fix: util.getWrappingFixer({
473 sourceCode,
474 node,
475 wrap: code => `${code} !== 0`,
476 }),
477 },
478 {
479 messageId: 'conditionFixCompareNaN',
480 fix: util.getWrappingFixer({
481 sourceCode,
482 node,
483 wrap: code => `!Number.isNaN(${code})`,
484 }),
485 },
486 {
487 messageId: 'conditionFixCastBoolean',
488 fix: util.getWrappingFixer({
489 sourceCode,
490 node,
491 wrap: code => `Boolean(${code})`,
492 }),
493 },
494 ],
495 });
496 }
497 }
498 return;
499 }
500 // nullable number
501 if (is('nullish', 'number')) {
502 if (!options.allowNullableNumber) {
503 if (isLogicalNegationExpression(node.parent)) {
504 // if (!nullableNumber)
505 context.report({
506 node,
507 messageId: 'conditionErrorNullableNumber',
508 suggest: [
509 {
510 messageId: 'conditionFixCompareNullish',
511 fix: util.getWrappingFixer({
512 sourceCode,
513 node: node.parent,
514 innerNode: node,
515 wrap: code => `${code} == null`,
516 }),
517 },
518 {
519 messageId: 'conditionFixDefaultZero',
520 fix: util.getWrappingFixer({
521 sourceCode,
522 node,
523 wrap: code => `${code} ?? 0`,
524 }),
525 },
526 {
527 messageId: 'conditionFixCastBoolean',
528 fix: util.getWrappingFixer({
529 sourceCode,
530 node: node.parent,
531 innerNode: node,
532 wrap: code => `!Boolean(${code})`,
533 }),
534 },
535 ],
536 });
537 }
538 else {
539 // if (nullableNumber)
540 context.report({
541 node,
542 messageId: 'conditionErrorNullableNumber',
543 suggest: [
544 {
545 messageId: 'conditionFixCompareNullish',
546 fix: util.getWrappingFixer({
547 sourceCode,
548 node,
549 wrap: code => `${code} != null`,
550 }),
551 },
552 {
553 messageId: 'conditionFixDefaultZero',
554 fix: util.getWrappingFixer({
555 sourceCode,
556 node,
557 wrap: code => `${code} ?? 0`,
558 }),
559 },
560 {
561 messageId: 'conditionFixCastBoolean',
562 fix: util.getWrappingFixer({
563 sourceCode,
564 node,
565 wrap: code => `Boolean(${code})`,
566 }),
567 },
568 ],
569 });
570 }
571 }
572 return;
573 }
574 // object
575 if (is('object')) {
576 // condition is always true
577 context.report({ node, messageId: 'conditionErrorObject' });
578 return;
579 }
580 // nullable object
581 if (is('nullish', 'object')) {
582 if (!options.allowNullableObject) {
583 if (isLogicalNegationExpression(node.parent)) {
584 // if (!nullableObject)
585 context.report({
586 node,
587 messageId: 'conditionErrorNullableObject',
588 fix: util.getWrappingFixer({
589 sourceCode,
590 node: node.parent,
591 innerNode: node,
592 wrap: code => `${code} == null`,
593 }),
594 });
595 }
596 else {
597 // if (nullableObject)
598 context.report({
599 node,
600 messageId: 'conditionErrorNullableObject',
601 fix: util.getWrappingFixer({
602 sourceCode,
603 node,
604 wrap: code => `${code} != null`,
605 }),
606 });
607 }
608 }
609 return;
610 }
611 // any
612 if (is('any')) {
613 if (!options.allowAny) {
614 context.report({
615 node,
616 messageId: 'conditionErrorAny',
617 suggest: [
618 {
619 messageId: 'conditionFixCastBoolean',
620 fix: util.getWrappingFixer({
621 sourceCode,
622 node,
623 wrap: code => `Boolean(${code})`,
624 }),
625 },
626 ],
627 });
628 }
629 return;
630 }
631 // other
632 context.report({ node, messageId: 'conditionErrorOther' });
633 }
634 /**
635 * Check union variants for the types we care about
636 */
637 function inspectVariantTypes(types) {
638 const variantTypes = new Set();
639 if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike))) {
640 variantTypes.add('nullish');
641 }
642 const booleans = types.filter(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike));
643 // If incoming type is either "true" or "false", there will be one type
644 // object with intrinsicName set accordingly
645 // If incoming type is boolean, there will be two type objects with
646 // intrinsicName set "true" and "false" each because of tsutils.unionTypeParts()
647 if (booleans.length === 1) {
648 tsutils.isBooleanLiteralType(booleans[0], true)
649 ? variantTypes.add('truthy boolean')
650 : variantTypes.add('boolean');
651 }
652 else if (booleans.length === 2) {
653 variantTypes.add('boolean');
654 }
655 const strings = types.filter(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.StringLike));
656 if (strings.length) {
657 if (strings.some(type => type.isStringLiteral() && type.value !== '')) {
658 variantTypes.add('truthy string');
659 }
660 else {
661 variantTypes.add('string');
662 }
663 }
664 const numbers = types.filter(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike));
665 if (numbers.length) {
666 if (numbers.some(type => type.isNumberLiteral() && type.value !== 0)) {
667 variantTypes.add('truthy number');
668 }
669 else {
670 variantTypes.add('number');
671 }
672 }
673 if (types.some(type => !tsutils.isTypeFlagSet(type, ts.TypeFlags.Null |
674 ts.TypeFlags.Undefined |
675 ts.TypeFlags.VoidLike |
676 ts.TypeFlags.BooleanLike |
677 ts.TypeFlags.StringLike |
678 ts.TypeFlags.NumberLike |
679 ts.TypeFlags.BigIntLike |
680 ts.TypeFlags.TypeParameter |
681 ts.TypeFlags.Any |
682 ts.TypeFlags.Unknown |
683 ts.TypeFlags.Never))) {
684 variantTypes.add('object');
685 }
686 if (types.some(type => util.isTypeFlagSet(type, ts.TypeFlags.TypeParameter |
687 ts.TypeFlags.Any |
688 ts.TypeFlags.Unknown))) {
689 variantTypes.add('any');
690 }
691 if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Never))) {
692 variantTypes.add('never');
693 }
694 return variantTypes;
695 }
696 },
697});
698function isLogicalNegationExpression(node) {
699 return node.type === utils_1.AST_NODE_TYPES.UnaryExpression && node.operator === '!';
700}
701function isArrayLengthExpression(node, typeChecker, parserServices) {
702 if (node.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
703 return false;
704 }
705 if (node.computed) {
706 return false;
707 }
708 if (node.property.name !== 'length') {
709 return false;
710 }
711 const objectTsNode = parserServices.esTreeNodeToTSNodeMap.get(node.object);
712 const objectType = util.getConstrainedTypeAtLocation(typeChecker, objectTsNode);
713 return util.isTypeArrayTypeOrUnionOfArrayTypes(objectType, typeChecker);
714}
715//# sourceMappingURL=strict-boolean-expressions.js.map
\No newline at end of file