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

Skip to content

Commit 96ef1e7

Browse files
Retsambradzacher
andcommitted
feat(eslint-plugin): [no-unnec-cond] support nullish coalescing (typescript-eslint#1148)
Co-authored-by: Brad Zacher <[email protected]>
1 parent e350a21 commit 96ef1e7

File tree

2 files changed

+96
-7
lines changed

2 files changed

+96
-7
lines changed

packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ const isPossiblyFalsy = (type: ts.Type): boolean =>
3131
const isPossiblyTruthy = (type: ts.Type): boolean =>
3232
unionTypeParts(type).some(type => !isFalsyType(type));
3333

34+
// Nullish utilities
35+
const nullishFlag = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
36+
const isNullishType = (type: ts.Type): boolean =>
37+
isTypeFlagSet(type, nullishFlag);
38+
39+
const isPossiblyNullish = (type: ts.Type): boolean =>
40+
unionTypeParts(type).some(isNullishType);
41+
42+
const isAlwaysNullish = (type: ts.Type): boolean =>
43+
unionTypeParts(type).every(isNullishType);
44+
3445
// isLiteralType only covers numbers and strings, this is a more exhaustive check.
3546
const isLiteral = (type: ts.Type): boolean =>
3647
isBooleanLiteralType(type, true) ||
@@ -51,6 +62,8 @@ export type Options = [
5162
export type MessageId =
5263
| 'alwaysTruthy'
5364
| 'alwaysFalsy'
65+
| 'neverNullish'
66+
| 'alwaysNullish'
5467
| 'literalBooleanExpression'
5568
| 'never';
5669
export default createRule<Options, MessageId>({
@@ -81,6 +94,10 @@ export default createRule<Options, MessageId>({
8194
messages: {
8295
alwaysTruthy: 'Unnecessary conditional, value is always truthy.',
8396
alwaysFalsy: 'Unnecessary conditional, value is always falsy.',
97+
neverNullish:
98+
'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.',
99+
alwaysNullish:
100+
'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`',
84101
literalBooleanExpression:
85102
'Unnecessary conditional, both sides of the expression are literal values',
86103
never: 'Unnecessary conditional, value is `never`',
@@ -120,12 +137,35 @@ export default createRule<Options, MessageId>({
120137
) {
121138
return;
122139
}
123-
if (isTypeFlagSet(type, TypeFlags.Never)) {
124-
context.report({ node, messageId: 'never' });
125-
} else if (!isPossiblyTruthy(type)) {
126-
context.report({ node, messageId: 'alwaysFalsy' });
127-
} else if (!isPossiblyFalsy(type)) {
128-
context.report({ node, messageId: 'alwaysTruthy' });
140+
const messageId = isTypeFlagSet(type, TypeFlags.Never)
141+
? 'never'
142+
: !isPossiblyTruthy(type)
143+
? 'alwaysFalsy'
144+
: !isPossiblyFalsy(type)
145+
? 'alwaysTruthy'
146+
: undefined;
147+
148+
if (messageId) {
149+
context.report({ node, messageId });
150+
}
151+
}
152+
153+
function checkNodeForNullish(node: TSESTree.Node): void {
154+
const type = getNodeType(node);
155+
// Conditional is always necessary if it involves `any` or `unknown`
156+
if (isTypeFlagSet(type, TypeFlags.Any | TypeFlags.Unknown)) {
157+
return;
158+
}
159+
const messageId = isTypeFlagSet(type, TypeFlags.Never)
160+
? 'never'
161+
: !isPossiblyNullish(type)
162+
? 'neverNullish'
163+
: isAlwaysNullish(type)
164+
? 'alwaysNullish'
165+
: undefined;
166+
167+
if (messageId) {
168+
context.report({ node, messageId });
129169
}
130170
}
131171

@@ -178,6 +218,10 @@ export default createRule<Options, MessageId>({
178218
function checkLogicalExpressionForUnnecessaryConditionals(
179219
node: TSESTree.LogicalExpression,
180220
): void {
221+
if (node.operator === '??') {
222+
checkNodeForNullish(node.left);
223+
return;
224+
}
181225
checkNode(node.left);
182226
if (!ignoreRhs) {
183227
checkNode(node.right);

packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,23 @@ function test<T>(t: T | []) {
8888
function test(a: string) {
8989
return a === "a"
9090
}`,
91-
91+
// Nullish coalescing operator
92+
`
93+
function test(a: string | null) {
94+
return a ?? "default";
95+
}`,
96+
`
97+
function test(a: string | undefined) {
98+
return a ?? "default";
99+
}`,
100+
`
101+
function test(a: string | null | undefined) {
102+
return a ?? "default";
103+
}`,
104+
`
105+
function test(a: unknown) {
106+
return a ?? "default";
107+
}`,
92108
// Supports ignoring the RHS
93109
{
94110
code: `
@@ -187,6 +203,35 @@ if (x === Foo.a) {}
187203
`,
188204
errors: [ruleError(8, 5, 'literalBooleanExpression')],
189205
},
206+
// Nullish coalescing operator
207+
{
208+
code: `
209+
function test(a: string) {
210+
return a ?? 'default';
211+
}`,
212+
errors: [ruleError(3, 10, 'neverNullish')],
213+
},
214+
{
215+
code: `
216+
function test(a: string | false) {
217+
return a ?? 'default';
218+
}`,
219+
errors: [ruleError(3, 10, 'neverNullish')],
220+
},
221+
{
222+
code: `
223+
function test(a: null) {
224+
return a ?? 'default';
225+
}`,
226+
errors: [ruleError(3, 10, 'alwaysNullish')],
227+
},
228+
{
229+
code: `
230+
function test(a: never) {
231+
return a ?? 'default';
232+
}`,
233+
errors: [ruleError(3, 10, 'never')],
234+
},
190235

191236
// Still errors on in the expected locations when ignoring RHS
192237
{

0 commit comments

Comments
 (0)