Skip to content

Commit

Permalink
feat(eslint-plugin): [prefer-nullish-coalescing] add `ignorePrimitive…
Browse files Browse the repository at this point in the history
…s` option (#6487)

* issue-4906 - prefer-nullish-coalescing enhancment

* remove comment

* add doc

* CR: sort keys

* CR: import once

* CR: remove unncessary "as const"

* CR: Fix markdown lint rule

* CR: Use binary OR to simplify loop

* CR: More positive test cases
  - Literal types (true, 1, etc.)
  - Template literals (`hello${string}`)
  - Bigints (1n)
  - Combinations of the types (boolean | string | ...), etc.

* CR: mixed

* CR: remove implied line

* CR: Move the note up

* prettier fixz

* Fix coverage, ts didn't know it has a default so it's not optional

* CR1: Remove redundant check

* CR2: Remove redundant check for the type-system

* Add bigint option

* markdown too

* Fixed union test and JSON.stringify(ignorePrimitives)

* flattening map

* flattening map 2

* add examples to docs
  • Loading branch information
Omri Luzon committed Jul 8, 2023
1 parent b1a23a9 commit 6edaa04
Show file tree
Hide file tree
Showing 3 changed files with 525 additions and 8 deletions.
23 changes: 23 additions & 0 deletions packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md
Expand Up @@ -131,6 +131,29 @@ a ?? (b && c && d);

**_NOTE:_** Errors for this specific case will be presented as suggestions (see below), instead of fixes. This is because it is not always safe to automatically convert `||` to `??` within a mixed logical expression, as we cannot tell the intended precedence of the operator. Note that by design, `??` requires parentheses when used with `&&` or `||` in the same expression.

### `ignorePrimitives`

If you would like to ignore certain primitive types that can be falsy then you may pass an object containing a boolean value for each primitive:

- `string: true`, ignores `null` or `undefined` unions with `string` (default: false).
- `number: true`, ignores `null` or `undefined` unions with `number` (default: false).
- `bigint: true`, ignores `null` or `undefined` unions with `bigint` (default: false).
- `boolean: true`, ignores `null` or `undefined` unions with `boolean` (default: false).

Incorrect code for `ignorePrimitives: { string: true }`, and correct code for `ignorePrimitives: { string: false }`:

```ts
const foo: string | undefined = 'bar';
foo || 'a string';
```

Correct code for `ignorePrimitives: { string: true }`:

```ts
const foo: string | undefined = 'bar';
foo ?? 'a string';
```

## When Not To Use It

If you are not using TypeScript 3.7 (or greater), then you will not be able to use this rule, as the operator is not supported.
Expand Down
54 changes: 46 additions & 8 deletions packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts
Expand Up @@ -7,10 +7,16 @@ import * as util from '../util';

export type Options = [
{
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
ignoreConditionalTests?: boolean;
ignoreTernaryTests?: boolean;
ignoreMixedLogicalExpressions?: boolean;
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
ignorePrimitives?: {
bigint?: boolean;
boolean?: boolean;
number?: boolean;
string?: boolean;
};
ignoreTernaryTests?: boolean;
},
];

Expand Down Expand Up @@ -44,16 +50,25 @@ export default util.createRule<Options, MessageIds>({
{
type: 'object',
properties: {
ignoreConditionalTests: {
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
type: 'boolean',
},
ignoreTernaryTests: {
ignoreConditionalTests: {
type: 'boolean',
},
ignoreMixedLogicalExpressions: {
type: 'boolean',
},
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
ignorePrimitives: {
type: 'object',
properties: {
bigint: { type: 'boolean' },
boolean: { type: 'boolean' },
number: { type: 'boolean' },
string: { type: 'boolean' },
},
},
ignoreTernaryTests: {
type: 'boolean',
},
},
Expand All @@ -63,20 +78,27 @@ export default util.createRule<Options, MessageIds>({
},
defaultOptions: [
{
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
ignoreConditionalTests: true,
ignoreTernaryTests: true,
ignoreMixedLogicalExpressions: true,
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
ignorePrimitives: {
bigint: false,
boolean: false,
number: false,
string: false,
},
},
],
create(
context,
[
{
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
ignoreConditionalTests,
ignoreTernaryTests,
ignoreMixedLogicalExpressions,
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
ignorePrimitives,
ignoreTernaryTests,
},
],
) {
Expand Down Expand Up @@ -279,6 +301,22 @@ export default util.createRule<Options, MessageIds>({
return;
}

const ignorableFlags = [
ignorePrimitives!.bigint && ts.TypeFlags.BigInt,
ignorePrimitives!.boolean && ts.TypeFlags.BooleanLiteral,
ignorePrimitives!.number && ts.TypeFlags.Number,
ignorePrimitives!.string && ts.TypeFlags.String,
]
.filter((flag): flag is number => flag !== undefined)
.reduce((previous, flag) => previous | flag, 0);
if (
(type as ts.UnionOrIntersectionType).types.some(t =>
tsutils.isTypeFlagSet(t, ignorableFlags),
)
) {
return;
}

const barBarOperator = util.nullThrows(
sourceCode.getTokenAfter(
node.left,
Expand Down

0 comments on commit 6edaa04

Please sign in to comment.