Skip to content

Commit

Permalink
feat(eslint-plugin): new rule: non-nullable-type-assertion-style
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Oct 2, 2020
1 parent 33522b4 commit ed64bf9
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | |
| [`@typescript-eslint/non-nullable-type-assertion-style`](./docs/rules/non-nullable-type-assertion-style.md) | Prefers a non-null assertion over explicit type cast when possible | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Prefer initializing each enums member value | | | |
| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Prefers a non-null assertion over explicit type cast when possible (`non-nullable-type-assertion-style`)

This rule detects when an `as` cast is doing the same job as a `!` would, and suggests fixing the code to be an `!`.

## Rule Details

Examples of **incorrect** code for this rule:

```ts
const maybe = Math.random() > 0.5 ? '' : undefined;
const bar = maybe as string;
```

Examples of **correct** code for this rule:

```ts
const maybe = Math.random() > 0.5 ? '' : undefined;
const bar = maybe!;
```

## When Not To Use It

If you don't mind having unnecessarily verbose type casts, you can avoid this rule.
1 change: 1 addition & 0 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"ts-simple-type": "^1.0.7",
"tsutils": "^3.17.1"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export = {
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/non-nullable-type-assertion-style': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/prefer-enum-initializers': 'error',
'@typescript-eslint/prefer-for-of': 'error',
Expand Down
103 changes: 103 additions & 0 deletions packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import * as tsutils from 'tsutils';
import * as ts from 'typescript';

import * as util from '../util';

export default util.createRule({
name: 'non-nullable-type-assertion-style',
meta: {
docs: {
category: 'Best Practices',
description:
'Prefers a non-null assertion over explicit type cast when possible',
recommended: false,
requiresTypeChecking: true,
},
fixable: 'code',
messages: {
preferNonNullAssertion:
'Use a ! assertion to more succintly remove null and undefined from the type.',
},
schema: [],
type: 'suggestion',
},
defaultOptions: [],

create(context) {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const sourceCode = context.getSourceCode();

const getTypesIfNotLoose = (node: TSESTree.Node): ts.Type[] | undefined => {
const type = checker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node),
);

return tsutils.isTypeFlagSet(
type,
ts.TypeFlags.Any | ts.TypeFlags.Unknown,
)
? undefined
: tsutils.unionTypeParts(type);
};

const sameTypeWithoutNullish = (
assertedTypes: ts.Type[],
originalTypes: ts.Type[],
): boolean => {
const assertedTypeIds = new Set(assertedTypes.map(type => type.id));
const nonNullishOriginalTypes = originalTypes.filter(
type =>
type.flags !== ts.TypeFlags.Null &&
type.flags !== ts.TypeFlags.Undefined,
);
const nonNullishOriginalTypeIds = new Set(
nonNullishOriginalTypes.map(type => type.id),
);

for (const assertedType of assertedTypes) {
if (!nonNullishOriginalTypeIds.has(assertedType.id)) {
return false;
}
}

for (const originalType of nonNullishOriginalTypes) {
if (!assertedTypeIds.has(originalType.id)) {
return false;
}
}

return true;
};

return {
'TSAsExpression, TSTypeAssertion'(
node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression,
): void {
const originalTypes = getTypesIfNotLoose(node.expression);
if (!originalTypes) {
return;
}

const assertedTypes = getTypesIfNotLoose(node.typeAnnotation);
if (!assertedTypes) {
return;
}

if (sameTypeWithoutNullish(assertedTypes, originalTypes)) {
context.report({
fix(fixer) {
return fixer.replaceText(
node,
`${sourceCode.getText(node.expression)}!`,
);
},
messageId: 'preferNonNullAssertion',
node,
});
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import path from 'path';
import rule from '../../src/rules/non-nullable-type-assertion-style';
import { RuleTester } from '../RuleTester';

const rootDir = path.resolve(__dirname, '../fixtures/');
const ruleTester = new RuleTester({
parserOptions: {
sourceType: 'module',
tsconfigRootDir: rootDir,
project: './tsconfig.json',
},
parser: '@typescript-eslint/parser',
});

ruleTester.run('non-nullable-type-assertion-style', rule, {
valid: [
`
declare const original: number | string;
const cast = original as string;
`,
`
declare const original: number | undefined;
const cast = original as string | number | undefined;
`,
`
declare const original: number | any;
const cast = original as string | number | undefined;
`,
`
declare const original: number | undefined;
const cast = original as any;
`,
`
declare const original: number | null | undefined;
const cast = original as number | null;
`,
`
type Type = { value: string };
declare const original: Type | number;
const cast = original as Type;
`,
],

invalid: [
{
code: `
declare const maybe: string | undefined;
const bar = maybe as string;
`,
errors: [
{
column: 13,
line: 3,
messageId: 'preferNonNullAssertion',
},
],
output: `
declare const maybe: string | undefined;
const bar = maybe!;
`,
},
{
code: `
declare const maybe: string | null;
const bar = maybe as string;
`,
errors: [
{
column: 13,
line: 3,
messageId: 'preferNonNullAssertion',
},
],
output: `
declare const maybe: string | null;
const bar = maybe!;
`,
},
{
code: `
declare const maybe: string | null | undefined;
const bar = maybe as string;
`,
errors: [
{
column: 13,
line: 3,
messageId: 'preferNonNullAssertion',
},
],
output: `
declare const maybe: string | null | undefined;
const bar = maybe!;
`,
},
{
code: `
type Type = { value: string };
declare const maybe: Type | undefined;
const bar = maybe as Type;
`,
errors: [
{
column: 13,
line: 4,
messageId: 'preferNonNullAssertion',
},
],
output: `
type Type = { value: string };
declare const maybe: Type | undefined;
const bar = maybe!;
`,
},
{
code: `
interface Interface {
value: string;
}
declare const maybe: Interface | undefined;
const bar = maybe as Interface;
`,
errors: [
{
column: 13,
line: 6,
messageId: 'preferNonNullAssertion',
},
],
output: `
interface Interface {
value: string;
}
declare const maybe: Interface | undefined;
const bar = maybe!;
`,
},
],
});
5 changes: 5 additions & 0 deletions packages/eslint-plugin/typings/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ declare module 'typescript' {
}

interface Type {
/**
* Private, unique identifier for the node -- use rarely, if at all!
*/
id?: string;

/**
* If the type is `any`, and this is set to "error", then TS was unable to resolve the type
*/
Expand Down

0 comments on commit ed64bf9

Please sign in to comment.