Skip to content

Commit 4e99961

Browse files
authored
feat(eslint-plugin): add no-non-null-asserted-nullish-coalescing rule (typescript-eslint#3349)
1 parent 0bb96ca commit 4e99961

File tree

6 files changed

+577
-84
lines changed

6 files changed

+577
-84
lines changed

packages/eslint-plugin/README.md

Lines changed: 85 additions & 84 deletions
Large diffs are not rendered by default.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Disallows using a non-null assertion in the left operand of the nullish coalescing operator (`no-non-null-asserted-nullish-coalescing`)
2+
3+
## Rule Details
4+
5+
The nullish coalescing operator is designed to provide a default value when dealing with `null` or `undefined`.
6+
Using non-null assertions in the left operand of the nullish coalescing operator is redundant.
7+
8+
Examples of **incorrect** code for this rule:
9+
10+
```ts
11+
/* eslint @typescript-eslint/no-non-null-asserted-nullish-coalescing: "error" */
12+
13+
foo! ?? bar;
14+
foo.bazz! ?? bar;
15+
foo!.bazz! ?? bar;
16+
foo()! ?? bar;
17+
18+
let x!: string;
19+
x! ?? '';
20+
21+
let x: string;
22+
x = foo();
23+
x! ?? '';
24+
```
25+
26+
Examples of **correct** code for this rule:
27+
28+
```ts
29+
/* eslint @typescript-eslint/no-non-null-asserted-nullish-coalescing: "error" */
30+
31+
foo ?? bar;
32+
foo ?? bar!;
33+
foo!.bazz ?? bar;
34+
foo!.bazz ?? bar!;
35+
foo() ?? bar;
36+
37+
// This is considered correct code because because there's no way for the user to satisfy it.
38+
let x: string;
39+
x! ?? '';
40+
```
41+
42+
## When Not To Use It
43+
44+
If you are not using TypeScript 3.7 (or greater), then you will not need to use this rule, as the nullish coalescing operator is not supported.
45+
46+
## Further Reading
47+
48+
- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html)
49+
- [Nullish Coalescing Proposal](https://github.com/tc39/proposal-nullish-coalescing)

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export = {
8282
'@typescript-eslint/no-misused-new': 'error',
8383
'@typescript-eslint/no-misused-promises': 'error',
8484
'@typescript-eslint/no-namespace': 'error',
85+
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
8586
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
8687
'@typescript-eslint/no-non-null-assertion': 'error',
8788
'@typescript-eslint/no-parameter-properties': 'error',

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import noMeaninglessVoidOperator from './no-meaningless-void-operator';
5454
import noMisusedNew from './no-misused-new';
5555
import noMisusedPromises from './no-misused-promises';
5656
import noNamespace from './no-namespace';
57+
import noNonNullAssertedNullishCoalescing from './no-non-null-asserted-nullish-coalescing';
5758
import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chain';
5859
import noNonNullAssertion from './no-non-null-assertion';
5960
import noParameterProperties from './no-parameter-properties';
@@ -175,6 +176,7 @@ export default {
175176
'no-misused-new': noMisusedNew,
176177
'no-misused-promises': noMisusedPromises,
177178
'no-namespace': noNamespace,
179+
'no-non-null-asserted-nullish-coalescing': noNonNullAssertedNullishCoalescing,
178180
'no-non-null-asserted-optional-chain': noNonNullAssertedOptionalChain,
179181
'no-non-null-assertion': noNonNullAssertion,
180182
'no-parameter-properties': noParameterProperties,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {
2+
ASTUtils,
3+
TSESTree,
4+
TSESLint,
5+
} from '@typescript-eslint/experimental-utils';
6+
import { Definition, DefinitionType } from '@typescript-eslint/scope-manager';
7+
import * as util from '../util';
8+
9+
function hasAssignmentBeforeNode(
10+
variable: TSESLint.Scope.Variable,
11+
node: TSESTree.Node,
12+
): boolean {
13+
return (
14+
variable.references.some(
15+
ref => ref.isWrite() && ref.identifier.range[1] < node.range[1],
16+
) ||
17+
variable.defs.some(
18+
def =>
19+
isDefinitionWithAssignment(def) && def.node.range[1] < node.range[1],
20+
)
21+
);
22+
}
23+
24+
function isDefinitionWithAssignment(definition: Definition): boolean {
25+
if (definition.type !== DefinitionType.Variable) {
26+
return false;
27+
}
28+
29+
const variableDeclarator = definition.node;
30+
return (
31+
variableDeclarator.definite === true || variableDeclarator.init !== null
32+
);
33+
}
34+
35+
export default util.createRule({
36+
name: 'no-non-null-asserted-nullish-coalescing',
37+
meta: {
38+
type: 'problem',
39+
docs: {
40+
description:
41+
'Disallows using a non-null assertion in the left operand of the nullish coalescing operator',
42+
category: 'Possible Errors',
43+
recommended: false,
44+
suggestion: true,
45+
},
46+
messages: {
47+
noNonNullAssertedNullishCoalescing:
48+
'The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed.',
49+
suggestRemovingNonNull: 'Remove the non-null assertion.',
50+
},
51+
schema: [],
52+
},
53+
defaultOptions: [],
54+
create(context) {
55+
return {
56+
'LogicalExpression[operator = "??"] > TSNonNullExpression.left'(
57+
node: TSESTree.TSNonNullExpression,
58+
): void {
59+
if (node.expression.type === TSESTree.AST_NODE_TYPES.Identifier) {
60+
const scope = context.getScope();
61+
const identifier = node.expression;
62+
const variable = ASTUtils.findVariable(scope, identifier.name);
63+
if (variable && !hasAssignmentBeforeNode(variable, node)) {
64+
return;
65+
}
66+
}
67+
68+
const sourceCode = context.getSourceCode();
69+
70+
context.report({
71+
node,
72+
messageId: 'noNonNullAssertedNullishCoalescing',
73+
/*
74+
Use a suggestion instead of a fixer, because this can break type checks.
75+
The resulting type of the nullish coalesce is only influenced by the right operand if the left operand can be `null` or `undefined`.
76+
After removing the non-null assertion the type of the left operand might contain `null` or `undefined` and then the type of the right operand
77+
might change the resulting type of the nullish coalesce.
78+
See the following example:
79+
80+
function test(x?: string): string {
81+
const bar = x! ?? false; // type analysis reports `bar` has type `string`
82+
// x ?? false; // type analysis reports `bar` has type `string | false`
83+
return bar;
84+
}
85+
*/
86+
suggest: [
87+
{
88+
messageId: 'suggestRemovingNonNull',
89+
fix(fixer): TSESLint.RuleFix {
90+
const exclamationMark = util.nullThrows(
91+
sourceCode.getLastToken(
92+
node,
93+
ASTUtils.isNonNullAssertionPunctuator,
94+
),
95+
util.NullThrowsReasons.MissingToken(
96+
'!',
97+
'Non-null Assertion',
98+
),
99+
);
100+
return fixer.remove(exclamationMark);
101+
},
102+
},
103+
],
104+
});
105+
},
106+
};
107+
},
108+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy