From 7c4e7ed6758c427f7cfd75853f9293ec7b94f58b Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 2 Oct 2020 00:36:40 -0400 Subject: [PATCH 1/4] feat(eslint-plugin): new rule: non-nullable-type-assertion-style --- packages/eslint-plugin/README.md | 1 + .../non-nullable-type-assertion-style.md | 27 ++++ packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../non-nullable-type-assertion-style.ts | 104 +++++++++++++ .../non-nullable-type-assertion-style.test.ts | 139 ++++++++++++++++++ .../eslint-plugin/typings/typescript.d.ts | 9 ++ 7 files changed, 283 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md create mode 100644 packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts create mode 100644 packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 63b5b6e7f321..389bd35f25ff 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -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 | | | | diff --git a/packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md b/packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md new file mode 100644 index 000000000000..473f83b0a9bf --- /dev/null +++ b/packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md @@ -0,0 +1,27 @@ +# 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 definitely = maybe as string; +const alsoDefinitely = maybe; +``` + +Examples of **correct** code for this rule: + +```ts +const maybe = Math.random() > 0.5 ? '' : undefined; + +const definitely = maybe!; +const alsoDefinitely = maybe!; +``` + +## When Not To Use It + +If you don't mind having unnecessarily verbose type casts, you can avoid this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 7e457d7aa1f5..5752136e6161 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -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', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index fa8dba93ed1d..84e760fac2c7 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -74,6 +74,7 @@ import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; +import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style'; import preferAsConst from './prefer-as-const'; import preferEnumInitializers from './prefer-enum-initializers'; import preferForOf from './prefer-for-of'; @@ -180,6 +181,7 @@ export default { 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, + 'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle, 'prefer-as-const': preferAsConst, 'prefer-enum-initializers': preferEnumInitializers, 'prefer-for-of': preferForOf, diff --git a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts new file mode 100644 index 000000000000..e1c5bbbd939f --- /dev/null +++ b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts @@ -0,0 +1,104 @@ +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, + suggestion: 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, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts new file mode 100644 index 000000000000..77cbb2b1dc06 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts @@ -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!; + `, + }, + ], +}); diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts index 73304155ee74..3c18282814e5 100644 --- a/packages/eslint-plugin/typings/typescript.d.ts +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -25,6 +25,15 @@ declare module 'typescript' { } interface Type { + /** + * Private, unique identifier for the type as cached by the TypeScript program. + * + * @remarks + * This should rarely be used, and then only to check for reference equality + * between types (since we don't have an available isAssignableTo API). + */ + id?: string; + /** * If the type is `any`, and this is set to "error", then TS was unable to resolve the type */ From c584e2c9992766e614a5dffe13c7e096f96c20a9 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 25 Oct 2020 23:12:11 -0400 Subject: [PATCH 2/4] Separate ternary --- .../src/rules/non-nullable-type-assertion-style.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts index e1c5bbbd939f..de0905d26f2b 100644 --- a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts +++ b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts @@ -35,12 +35,13 @@ export default util.createRule({ parserServices.esTreeNodeToTSNodeMap.get(node), ); - return tsutils.isTypeFlagSet( - type, - ts.TypeFlags.Any | ts.TypeFlags.Unknown, - ) - ? undefined - : tsutils.unionTypeParts(type); + if ( + tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown) + ) { + return undefined; + } + + return tsutils.unionTypeParts(type); }; const sameTypeWithoutNullish = ( From 157964c4bd715da2de2fcd829c5986fce5bd9c87 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 25 Oct 2020 23:39:31 -0400 Subject: [PATCH 3/4] Remove .id --- .../src/rules/non-nullable-type-assertion-style.ts | 8 ++------ packages/eslint-plugin/typings/typescript.d.ts | 9 --------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts index de0905d26f2b..9bb099ef88cc 100644 --- a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts +++ b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts @@ -48,24 +48,20 @@ export default util.createRule({ 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)) { + if (!nonNullishOriginalTypes.includes(assertedType)) { return false; } } for (const originalType of nonNullishOriginalTypes) { - if (!assertedTypeIds.has(originalType.id)) { + if (!assertedTypes.includes(originalType)) { return false; } } diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts index 3c18282814e5..73304155ee74 100644 --- a/packages/eslint-plugin/typings/typescript.d.ts +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -25,15 +25,6 @@ declare module 'typescript' { } interface Type { - /** - * Private, unique identifier for the type as cached by the TypeScript program. - * - * @remarks - * This should rarely be used, and then only to check for reference equality - * between types (since we don't have an available isAssignableTo API). - */ - id?: string; - /** * If the type is `any`, and this is set to "error", then TS was unable to resolve the type */ From a8033310448c3377e1f262d2131d444f563b61f6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 12 Dec 2020 14:05:18 -0500 Subject: [PATCH 4/4] Added test cases around NonNullable --- .../non-nullable-type-assertion-style.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts index 77cbb2b1dc06..45ca1774d301 100644 --- a/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts +++ b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts @@ -39,6 +39,18 @@ type Type = { value: string }; declare const original: Type | number; const cast = original as Type; `, + ` +type T = string; +declare const x: T | number; + +const y = x as NonNullable; + `, + ` +type T = string | null; +declare const x: T | number; + +const y = x as NonNullable; + `, ], invalid: [ @@ -135,5 +147,47 @@ declare const maybe: Interface | undefined; const bar = maybe!; `, }, + { + code: ` +type T = string | null; +declare const x: T; + +const y = x as NonNullable; + `, + errors: [ + { + column: 11, + line: 5, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +type T = string | null; +declare const x: T; + +const y = x!; + `, + }, + { + code: ` +type T = string | null | undefined; +declare const x: T; + +const y = x as NonNullable; + `, + errors: [ + { + column: 11, + line: 5, + messageId: 'preferNonNullAssertion', + }, + ], + output: ` +type T = string | null | undefined; +declare const x: T; + +const y = x!; + `, + }, ], });