Skip to content

Commit

Permalink
fix(eslint-plugin): [non-nullable-type-assertion-style] fix false pos…
Browse files Browse the repository at this point in the history
…itive when asserting to a generic type that might be nullish (#4509)
  • Loading branch information
djcsdy authored Feb 5, 2022
1 parent 5ab1d57 commit 4209362
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default util.createRule({
fixable: 'code',
messages: {
preferNonNullAssertion:
'Use a ! assertion to more succintly remove null and undefined from the type.',
'Use a ! assertion to more succinctly remove null and undefined from the type.',
},
schema: [],
type: 'suggestion',
Expand All @@ -43,22 +43,42 @@ export default util.createRule({
return tsutils.unionTypeParts(type);
};

const couldBeNullish = (type: ts.Type): boolean => {
if (type.flags & ts.TypeFlags.TypeParameter) {
const constraint = type.getConstraint();
return constraint == null || couldBeNullish(constraint);
} else if (tsutils.isUnionType(type)) {
for (const part of type.types) {
if (couldBeNullish(part)) {
return true;
}
}
return false;
} else {
return (
(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0
);
}
};

const sameTypeWithoutNullish = (
assertedTypes: ts.Type[],
originalTypes: ts.Type[],
): boolean => {
const nonNullishOriginalTypes = originalTypes.filter(
type =>
type.flags !== ts.TypeFlags.Null &&
type.flags !== ts.TypeFlags.Undefined,
(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) === 0,
);

if (nonNullishOriginalTypes.length === originalTypes.length) {
return false;
}

for (const assertedType of assertedTypes) {
if (!nonNullishOriginalTypes.includes(assertedType)) {
if (
couldBeNullish(assertedType) ||
!nonNullishOriginalTypes.includes(assertedType)
) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const ruleTester = new RuleTester({
parserOptions: {
sourceType: 'module',
tsconfigRootDir: rootDir,
project: './tsconfig.json',
project: './tsconfig.noUncheckedIndexedAccess.json',
},
parser: '@typescript-eslint/parser',
});
Expand Down Expand Up @@ -61,6 +61,35 @@ const x = 1 as 1;
declare function foo<T = any>(): T;
const bar = foo() as number;
`,
`
function first<T>(array: ArrayLike<T>): T | null {
return array.length > 0 ? (array[0] as T) : null;
}
`,
`
function first<T extends string | null>(array: ArrayLike<T>): T | null {
return array.length > 0 ? (array[0] as T) : null;
}
`,
`
function first<T extends string | undefined>(array: ArrayLike<T>): T | null {
return array.length > 0 ? (array[0] as T) : null;
}
`,
`
function first<T extends string | null | undefined>(
array: ArrayLike<T>,
): T | null {
return array.length > 0 ? (array[0] as T) : null;
}
`,
`
type A = 'a' | 'A';
type B = 'b' | 'B';
function first<T extends A | B | null>(array: ArrayLike<T>): T | null {
return array.length > 0 ? (array[0] as T) : null;
}
`,
],

invalid: [
Expand Down Expand Up @@ -199,5 +228,26 @@ declare const x: T;
const y = x!;
`,
},
{
code: `
function first<T extends string | number>(array: ArrayLike<T>): T | null {
return array.length > 0 ? (array[0] as T) : null;
}
`,
errors: [
{
column: 30,
line: 3,
messageId: 'preferNonNullAssertion',
},
],
// Output is not expected to match required formatting due to excess parentheses
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
output: `
function first<T extends string | number>(array: ArrayLike<T>): T | null {
return array.length > 0 ? (array[0]!) : null;
}
`,
},
],
});

0 comments on commit 4209362

Please sign in to comment.