Skip to content

Commit b16409a

Browse files
authored
fix(eslint-plugin): [NUTA] false positive for null assign to undefined (typescript-eslint#536)
Fixes typescript-eslint#529
1 parent 6489293 commit b16409a

File tree

3 files changed

+89
-23
lines changed

3 files changed

+89
-23
lines changed

packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,43 @@ export default util.createRule<Options, MessageIds>({
188188
} else {
189189
// we know it's a nullable type
190190
// so figure out if the variable is used in a place that accepts nullable types
191+
191192
const contextualType = getContextualType(checker, originalNode);
192-
if (contextualType && util.isNullableType(contextualType)) {
193-
context.report({
194-
node,
195-
messageId: 'contextuallyUnnecessary',
196-
fix(fixer) {
197-
return fixer.removeRange([
198-
originalNode.expression.end,
199-
originalNode.end,
200-
]);
201-
},
202-
});
193+
if (contextualType) {
194+
// in strict mode you can't assign null to undefined, so we have to make sure that
195+
// the two types share a nullable type
196+
const typeIncludesUndefined = util.isTypeFlagSet(
197+
type,
198+
ts.TypeFlags.Undefined,
199+
);
200+
const typeIncludesNull = util.isTypeFlagSet(
201+
type,
202+
ts.TypeFlags.Null,
203+
);
204+
205+
const contextualTypeIncludesUndefined = util.isTypeFlagSet(
206+
contextualType,
207+
ts.TypeFlags.Undefined,
208+
);
209+
const contextualTypeIncludesNull = util.isTypeFlagSet(
210+
contextualType,
211+
ts.TypeFlags.Null,
212+
);
213+
if (
214+
(typeIncludesUndefined && contextualTypeIncludesUndefined) ||
215+
(typeIncludesNull && contextualTypeIncludesNull)
216+
) {
217+
context.report({
218+
node,
219+
messageId: 'contextuallyUnnecessary',
220+
fix(fixer) {
221+
return fixer.removeRange([
222+
originalNode.expression.end,
223+
originalNode.end,
224+
]);
225+
},
226+
});
227+
}
203228
}
204229
}
205230
},

packages/eslint-plugin/src/util/types.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
isTypeFlagSet,
32
isTypeReference,
43
isUnionOrIntersectionType,
54
unionTypeParts,
@@ -115,18 +114,24 @@ export function getConstrainedTypeAtLocation(
115114
* Checks if the given type is (or accepts) nullable
116115
* @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter)
117116
*/
118-
export function isNullableType(type: ts.Type, isReceiver?: boolean): boolean {
119-
let flags: ts.TypeFlags = 0;
120-
for (const t of unionTypeParts(type)) {
121-
flags |= t.flags;
122-
}
117+
export function isNullableType(
118+
type: ts.Type,
119+
{
120+
isReceiver = false,
121+
allowUndefined = true,
122+
}: { isReceiver?: boolean; allowUndefined?: boolean } = {},
123+
): boolean {
124+
const flags = getTypeFlags(type);
123125

124-
flags =
125-
isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)
126-
? -1
127-
: flags;
126+
if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
127+
return true;
128+
}
128129

129-
return (flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
130+
if (allowUndefined) {
131+
return (flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
132+
} else {
133+
return (flags & ts.TypeFlags.Null) !== 0;
134+
}
130135
}
131136

132137
/**
@@ -138,3 +143,32 @@ export function getDeclaration(
138143
): ts.Declaration {
139144
return checker.getSymbolAtLocation(node)!.declarations![0];
140145
}
146+
147+
/**
148+
* Gets all of the type flags in a type, iterating through unions automatically
149+
*/
150+
export function getTypeFlags(type: ts.Type): ts.TypeFlags {
151+
let flags: ts.TypeFlags = 0;
152+
for (const t of unionTypeParts(type)) {
153+
flags |= t.flags;
154+
}
155+
return flags;
156+
}
157+
158+
/**
159+
* Checks if the given type is (or accepts) the given flags
160+
* @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter)
161+
*/
162+
export function isTypeFlagSet(
163+
type: ts.Type,
164+
flagsToCheck: ts.TypeFlags,
165+
isReceiver?: boolean,
166+
): boolean {
167+
const flags = getTypeFlags(type);
168+
169+
if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
170+
return true;
171+
}
172+
173+
return (flags & flagsToCheck) !== 0;
174+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'path';
22
import rule from '../../src/rules/no-unnecessary-type-assertion';
33
import { RuleTester } from '../RuleTester';
44

5-
const rootDir = path.join(process.cwd(), 'tests/fixtures');
5+
const rootDir = path.resolve(__dirname, '../fixtures/');
66
const ruleTester = new RuleTester({
77
parserOptions: {
88
ecmaVersion: 2015,
@@ -88,6 +88,13 @@ class Foo {
8888
prop: number = x!;
8989
}
9090
`,
91+
// https://github.com/typescript-eslint/typescript-eslint/issues/529
92+
`
93+
declare function foo(str?: string): void;
94+
declare const str: string | null;
95+
96+
foo(str!);
97+
`,
9198
],
9299

93100
invalid: [

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