Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit be3a224

Browse files
feat(eslint-plugin): [prefer-nullish-coalescing] add option ignoreBooleanCoercion (typescript-eslint#9924)
1 parent 8c6a721 commit be3a224

File tree

5 files changed

+575
-28
lines changed

5 files changed

+575
-28
lines changed

packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,28 @@ foo ?? 'a string';
172172

173173
Also, if you would like to ignore all primitives types, you can set `ignorePrimitives: true`. It is equivalent to `ignorePrimitives: { string: true, number: true, bigint: true, boolean: true }`.
174174

175+
### `ignoreBooleanCoercion`
176+
177+
Whether to ignore expressions that coerce a value into a boolean: `Boolean(...)`.
178+
179+
Incorrect code for `ignoreBooleanCoercion: false`, and correct code for `ignoreBooleanCoercion: true`:
180+
181+
```ts option='{ "ignoreBooleanCoercion": true }' showPlaygroundButton
182+
let a: string | true | undefined;
183+
let b: string | boolean | undefined;
184+
185+
const x = Boolean(a || b);
186+
```
187+
188+
Correct code for `ignoreBooleanCoercion: false`:
189+
190+
```ts option='{ "ignoreBooleanCoercion": false }' showPlaygroundButton
191+
let a: string | true | undefined;
192+
let b: string | boolean | undefined;
193+
194+
const x = Boolean(a ?? b);
195+
```
196+
175197
### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing`
176198

177199
{/* insert option description */}

packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
export type Options = [
2222
{
2323
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
24+
ignoreBooleanCoercion?: boolean;
2425
ignoreConditionalTests?: boolean;
2526
ignoreMixedLogicalExpressions?: boolean;
2627
ignorePrimitives?:
@@ -71,6 +72,11 @@ export default createRule<Options, MessageIds>({
7172
description:
7273
'Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.',
7374
},
75+
ignoreBooleanCoercion: {
76+
type: 'boolean',
77+
description:
78+
'Whether to ignore arguments to the `Boolean` constructor',
79+
},
7480
ignoreConditionalTests: {
7581
type: 'boolean',
7682
description:
@@ -126,6 +132,7 @@ export default createRule<Options, MessageIds>({
126132
defaultOptions: [
127133
{
128134
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
135+
ignoreBooleanCoercion: false,
129136
ignoreConditionalTests: true,
130137
ignoreMixedLogicalExpressions: false,
131138
ignorePrimitives: {
@@ -142,6 +149,7 @@ export default createRule<Options, MessageIds>({
142149
[
143150
{
144151
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
152+
ignoreBooleanCoercion,
145153
ignoreConditionalTests,
146154
ignoreMixedLogicalExpressions,
147155
ignorePrimitives,
@@ -431,46 +439,102 @@ export default createRule<Options, MessageIds>({
431439
'LogicalExpression[operator = "||"]'(
432440
node: TSESTree.LogicalExpression,
433441
): void {
442+
if (
443+
ignoreBooleanCoercion === true &&
444+
isBooleanConstructorContext(node, context)
445+
) {
446+
return;
447+
}
448+
434449
checkAssignmentOrLogicalExpression(node, 'or', '');
435450
},
436451
};
437452
},
438453
});
439454

440455
function isConditionalTest(node: TSESTree.Node): boolean {
441-
const parents = new Set<TSESTree.Node | null>([node]);
442-
let current = node.parent;
443-
while (current) {
444-
parents.add(current);
456+
const parent = node.parent;
457+
if (parent == null) {
458+
return false;
459+
}
445460

446-
if (
447-
(current.type === AST_NODE_TYPES.ConditionalExpression ||
448-
current.type === AST_NODE_TYPES.DoWhileStatement ||
449-
current.type === AST_NODE_TYPES.IfStatement ||
450-
current.type === AST_NODE_TYPES.ForStatement ||
451-
current.type === AST_NODE_TYPES.WhileStatement) &&
452-
parents.has(current.test)
453-
) {
454-
return true;
455-
}
461+
if (parent.type === AST_NODE_TYPES.LogicalExpression) {
462+
return isConditionalTest(parent);
463+
}
456464

457-
if (
458-
[
459-
AST_NODE_TYPES.ArrowFunctionExpression,
460-
AST_NODE_TYPES.FunctionExpression,
461-
].includes(current.type)
462-
) {
463-
/**
464-
* This is a weird situation like:
465-
* `if (() => a || b) {}`
466-
* `if (function () { return a || b }) {}`
467-
*/
468-
return false;
469-
}
465+
if (
466+
parent.type === AST_NODE_TYPES.ConditionalExpression &&
467+
(parent.consequent === node || parent.alternate === node)
468+
) {
469+
return isConditionalTest(parent);
470+
}
471+
472+
if (
473+
parent.type === AST_NODE_TYPES.SequenceExpression &&
474+
parent.expressions.at(-1) === node
475+
) {
476+
return isConditionalTest(parent);
477+
}
470478

471-
current = current.parent;
479+
if (
480+
(parent.type === AST_NODE_TYPES.ConditionalExpression ||
481+
parent.type === AST_NODE_TYPES.DoWhileStatement ||
482+
parent.type === AST_NODE_TYPES.IfStatement ||
483+
parent.type === AST_NODE_TYPES.ForStatement ||
484+
parent.type === AST_NODE_TYPES.WhileStatement) &&
485+
parent.test === node
486+
) {
487+
return true;
488+
}
489+
490+
return false;
491+
}
492+
493+
function isBooleanConstructorContext(
494+
node: TSESTree.Node,
495+
context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
496+
): boolean {
497+
const parent = node.parent;
498+
if (parent == null) {
499+
return false;
500+
}
501+
502+
if (parent.type === AST_NODE_TYPES.LogicalExpression) {
503+
return isBooleanConstructorContext(parent, context);
504+
}
505+
506+
if (
507+
parent.type === AST_NODE_TYPES.ConditionalExpression &&
508+
(parent.consequent === node || parent.alternate === node)
509+
) {
510+
return isBooleanConstructorContext(parent, context);
511+
}
512+
513+
if (
514+
parent.type === AST_NODE_TYPES.SequenceExpression &&
515+
parent.expressions.at(-1) === node
516+
) {
517+
return isBooleanConstructorContext(parent, context);
472518
}
473519

520+
return isBuiltInBooleanCall(parent, context);
521+
}
522+
523+
function isBuiltInBooleanCall(
524+
node: TSESTree.Node,
525+
context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
526+
): boolean {
527+
if (
528+
node.type === AST_NODE_TYPES.CallExpression &&
529+
node.callee.type === AST_NODE_TYPES.Identifier &&
530+
// eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
531+
node.callee.name === 'Boolean' &&
532+
node.arguments[0]
533+
) {
534+
const scope = context.sourceCode.getScope(node);
535+
const variable = scope.set.get(AST_TOKEN_TYPES.Boolean);
536+
return variable == null || variable.defs.length === 0;
537+
}
474538
return false;
475539
}
476540

packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-nullish-coalescing.shot

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)