From 92e98deed6290479b54f768b396ddfbc9e00b55b Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 24 Jul 2019 17:14:54 -0700 Subject: [PATCH] feat(eslint-plugin)!: add rule `consistent-type-assertions` (#731) BREAKING CHANGE: Merges both no-angle-bracket-type-assertion and no-object-literal-type-assertion into one rule --- packages/eslint-plugin/README.md | 3 +- packages/eslint-plugin/ROADMAP.md | 7 +- .../docs/rules/consistent-type-assertions.md | 79 +++++ .../rules/no-angle-bracket-type-assertion.md | 32 -- .../rules/no-object-literal-type-assertion.md | 33 -- packages/eslint-plugin/src/configs/all.json | 3 +- packages/eslint-plugin/src/configs/base.json | 4 +- .../src/configs/recommended.json | 3 +- .../src/rules/consistent-type-assertions.ts | 158 +++++++++ packages/eslint-plugin/src/rules/index.ts | 6 +- .../rules/no-angle-bracket-type-assertion.ts | 34 -- .../rules/no-object-literal-type-assertion.ts | 91 ------ .../rules/consistent-type-assertions.test.ts | 302 ++++++++++++++++++ .../no-angle-bracket-type-assertion.test.ts | 164 ---------- .../no-object-literal-type-assertion.test.ts | 100 ------ .../eslint-utils/batchedSingleLineTests.ts | 13 +- .../src/ts-estree/ts-estree.ts | 2 +- 17 files changed, 563 insertions(+), 471 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/consistent-type-assertions.md delete mode 100644 packages/eslint-plugin/docs/rules/no-angle-bracket-type-assertion.md delete mode 100644 packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md create mode 100644 packages/eslint-plugin/src/rules/consistent-type-assertions.ts delete mode 100644 packages/eslint-plugin/src/rules/no-angle-bracket-type-assertion.ts delete mode 100644 packages/eslint-plugin/src/rules/no-object-literal-type-assertion.ts create mode 100644 packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts delete mode 100644 packages/eslint-plugin/tests/rules/no-angle-bracket-type-assertion.test.ts delete mode 100644 packages/eslint-plugin/tests/rules/no-object-literal-type-assertion.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 167bb2b4933..f33823200e7 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -130,6 +130,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Enforces that types will not to be used | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | | | [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | | +| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions. | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | :heavy_check_mark: | | | @@ -140,7 +141,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility | | | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | -| [`@typescript-eslint/no-angle-bracket-type-assertion`](./docs/rules/no-angle-bracket-type-assertion.md) | Enforces the use of `as Type` assertions instead of `` assertions | :heavy_check_mark: | | | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | | | | | [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | | | @@ -155,7 +155,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | | | :thought_balloon: | | [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | | | [`@typescript-eslint/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallows non-null assertions using the `!` postfix operator | :heavy_check_mark: | | | -| [`@typescript-eslint/no-object-literal-type-assertion`](./docs/rules/no-object-literal-type-assertion.md) | Forbids an object literal to appear in a type assertion expression | :heavy_check_mark: | | | | [`@typescript-eslint/no-parameter-properties`](./docs/rules/no-parameter-properties.md) | Disallow the use of parameter properties in class constructors | :heavy_check_mark: | | | | [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | | | [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index eff7e0656f9..cdad2d46f1b 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -71,7 +71,7 @@ | [`no-invalid-this`] | 🌟 | [`no-invalid-this`][no-invalid-this] | | [`no-misused-new`] | βœ… | [`@typescript-eslint/no-misused-new`] | | [`no-null-keyword`] | πŸ”Œ | [`no-null/no-null`] (doesn’t handle `null` type) | -| [`no-object-literal-type-assertion`] | βœ… | [`@typescript-eslint/no-object-literal-type-assertion`] | +| [`no-object-literal-type-assertion`] | βœ… | [`@typescript-eslint/consistent-type-assertions`] | | [`no-return-await`] | 🌟 | [`no-return-await`][no-return-await] | | [`no-shadowed-variable`] | 🌟 | [`no-shadow`][no-shadow] | | [`no-sparse-arrays`] | 🌟 | [`no-sparse-arrays`][no-sparse-arrays] | @@ -155,7 +155,7 @@ | [`newline-before-return`] | 🌟 | [`padding-line-between-statements`][padding-line-between-statements] [1] | | [`newline-per-chained-call`] | 🌟 | [`newline-per-chained-call`][newline-per-chained-call] | | [`new-parens`] | 🌟 | [`new-parens`][new-parens] | -| [`no-angle-bracket-type-assertion`] | βœ… | [`@typescript-eslint/no-angle-bracket-type-assertion`] | +| [`no-angle-bracket-type-assertion`] | βœ… | [`@typescript-eslint/consistent-type-assertions`] | | [`no-boolean-literal-compare`] | πŸ›‘ | N/A | | [`no-consecutive-blank-lines`] | 🌟 | [`no-multiple-empty-lines`][no-multiple-empty-lines] | | [`no-irregular-whitespace`] | 🌟 | [`no-irregular-whitespace`][no-irregular-whitespace] with `skipStrings: false` | @@ -579,6 +579,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/await-thenable`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/await-thenable.md [`@typescript-eslint/ban-types`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md [`@typescript-eslint/ban-ts-ignore`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-ts-ignore.md +[`@typescript-eslint/consistent-type-assertions`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md [`@typescript-eslint/consistent-type-definitions`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-definitions.md [`@typescript-eslint/explicit-member-accessibility`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md [`@typescript-eslint/member-ordering`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-ordering.md @@ -597,7 +598,6 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/typedef`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/typedef.md [`@typescript-eslint/unified-signatures`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unified-signatures.md [`@typescript-eslint/no-misused-new`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-new.md -[`@typescript-eslint/no-object-literal-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md [`@typescript-eslint/no-this-alias`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-this-alias.md [`@typescript-eslint/no-extraneous-class`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-extraneous-class.md [`@typescript-eslint/no-unused-vars`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md @@ -608,7 +608,6 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/array-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md [`@typescript-eslint/class-name-casing`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/class-name-casing.md [`@typescript-eslint/interface-name-prefix`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/interface-name-prefix.md -[`@typescript-eslint/no-angle-bracket-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-angle-bracket-type-assertion.md [`@typescript-eslint/no-parameter-properties`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-parameter-properties.md [`@typescript-eslint/member-delimiter-style`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md [`@typescript-eslint/prefer-for-of`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-for-of.md diff --git a/packages/eslint-plugin/docs/rules/consistent-type-assertions.md b/packages/eslint-plugin/docs/rules/consistent-type-assertions.md new file mode 100644 index 00000000000..8e619c430ea --- /dev/null +++ b/packages/eslint-plugin/docs/rules/consistent-type-assertions.md @@ -0,0 +1,79 @@ +# Enforces consistent usage of type assertions. (consistent-type-assertions) + +## Rule Details + +This rule aims to standardise the use of type assertion style across the codebase. + +Type assertions are also commonly referred as "type casting" in TypeScript (even though it is technically slightly different to what is understood by type casting in other languages), so you can think of type assertions and type casting referring to the same thing. It is essentially you saying to the TypeScript compiler, "in this case, I know better than you!". + +## Options + +```ts +type Options = + | { + assertionStyle: 'as' | 'angle-bracket'; + objectLiteralTypeAssertions: 'allow' | 'allow-as-parameter' | 'never'; + } + | { + assertionStyle: 'never'; + }; + +const defaultOptions: Options = { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow', +}; +``` + +### assertionStyle + +This option defines the expected assertion style. Valid values for `assertionStyle` are: + +- `as` will enforce that you always use `... as foo`. +- `angle-bracket` will enforce that you always use `...` +- `never` will enforce that you do not do any type assertions. + +Most code bases will want to enforce not using `angle-bracket` style because it conflicts with JSX syntax, and is confusing when paired with with generic syntax. + +Some codebases like to go for an extra level of type safety, and ban assertions altogether via the `never` option. + +### objectLiteralTypeAssertions + +Always prefer `const x: T = { ... };` to `const x = { ... } as T;` (or similar with angle brackets). The type assertion in the latter case is either unnecessary or will probably hide an error. + +The compiler will warn for excess properties with this syntax, but not missing _required_ fields. For example: `const x: { foo: number } = {};` will fail to compile, but `const x = {} as { foo: number }` will succeed. + +The const assertion `const x = { foo: 1 } as const`, introduced in TypeScript 3.4, is considered beneficial and is ignored by this option. + +Examples of **incorrect** code for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }` (and for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' }`) + +```ts +const x = { ... } as T; +``` + +Examples of **correct** code for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }`. + +```ts +const x: T = { ... }; +const y = { ... } as any; +const z = { ... } as unknown; +``` + +Examples of **correct** code for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' }`. + +```ts +const x: T = { ... }; +const y = { ... } as any; +const z = { ... } as unknown; +foo({ ... } as T); +new Clazz({ ... } as T); +function foo() { throw { bar: 5 } as Foo } +``` + +## When Not To Use It + +If you do not want to enforce consistent type assertions. + +## Compatibility + +- TSLint: [no-angle-bracket-type-assertion](https://palantir.github.io/tslint/rules/no-angle-bracket-type-assertion/) +- TSLint: [no-object-literal-type-assertion](https://palantir.github.io/tslint/rules/no-object-literal-type-assertion/) diff --git a/packages/eslint-plugin/docs/rules/no-angle-bracket-type-assertion.md b/packages/eslint-plugin/docs/rules/no-angle-bracket-type-assertion.md deleted file mode 100644 index 68cf1591a75..00000000000 --- a/packages/eslint-plugin/docs/rules/no-angle-bracket-type-assertion.md +++ /dev/null @@ -1,32 +0,0 @@ -# Enforces the use of `as Type` assertions instead of `` assertions (no-angle-bracket-type-assertion) - -TypeScript disallows the use of `` assertions in `.tsx` because of the similarity with -JSX's syntax, which makes it impossible to parse. - -## Rule Details - -This rule aims to standardise the use of type assertion style across the codebase - -The following patterns are considered warnings: - -```ts -const foo = bar; -``` - -The following patterns are not warnings: - -```ts -const foo = bar as Foo; -``` - -## When Not To Use It - -If your codebase does not include `.tsx` files, then you will not need this rule. - -## Further Reading - -- [Typescript and JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) - -## Compatibility - -- TSLint: [no-angle-bracket-type-assertion](https://palantir.github.io/tslint/rules/no-angle-bracket-type-assertion/) diff --git a/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md b/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md deleted file mode 100644 index 044c68aa57f..00000000000 --- a/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md +++ /dev/null @@ -1,33 +0,0 @@ -# Forbids an object literal to appear in a type assertion expression (no-object-literal-type-assertion) - -Always prefer `const x: T = { ... };` to `const x = { ... } as T;`. Casting to `any` and `unknown` is still allowed, and const assertions (`as const`) are still allowed. - -## Rule Details - -Examples of **incorrect** code for this rule. - -```ts -const x = { ... } as T; -``` - -Examples of **correct** code for this rule. - -```ts -const x: T = { ... }; -const y = { ... } as any; -const z = { ... } as unknown; -``` - -## Options - -```cjson -{ - "@typescript-eslint/no-object-literal-type-assertion": ["error", { - allowAsParameter: false // Allow type assertion in call and new expression, default false - }] -} -``` - -## Compatibility - -- TSLint: [no-object-literal-type-assertion](https://palantir.github.io/tslint/rules/no-object-literal-type-assertion/) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index dac2e6b3f6b..00bdf366600 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -9,6 +9,7 @@ "camelcase": "off", "@typescript-eslint/camelcase": "error", "@typescript-eslint/class-name-casing": "error", + "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-member-accessibility": "error", @@ -21,7 +22,6 @@ "@typescript-eslint/member-delimiter-style": "error", "@typescript-eslint/member-naming": "error", "@typescript-eslint/member-ordering": "error", - "@typescript-eslint/no-angle-bracket-type-assertion": "error", "no-array-constructor": "off", "@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-empty-function": "error", @@ -39,7 +39,6 @@ "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-non-null-assertion": "error", - "@typescript-eslint/no-object-literal-type-assertion": "error", "@typescript-eslint/no-parameter-properties": "error", "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-this-alias": "error", diff --git a/packages/eslint-plugin/src/configs/base.json b/packages/eslint-plugin/src/configs/base.json index 9b6931ad616..6f56100a6ae 100644 --- a/packages/eslint-plugin/src/configs/base.json +++ b/packages/eslint-plugin/src/configs/base.json @@ -3,5 +3,7 @@ "parserOptions": { "sourceType": "module" }, - "plugins": ["@typescript-eslint"] + "plugins": [ + "@typescript-eslint" + ] } diff --git a/packages/eslint-plugin/src/configs/recommended.json b/packages/eslint-plugin/src/configs/recommended.json index 471d3df02a9..121b85bf17a 100644 --- a/packages/eslint-plugin/src/configs/recommended.json +++ b/packages/eslint-plugin/src/configs/recommended.json @@ -11,13 +11,13 @@ "camelcase": "off", "@typescript-eslint/camelcase": "error", "@typescript-eslint/class-name-casing": "error", + "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/explicit-member-accessibility": "error", "indent": "off", "@typescript-eslint/indent": "error", "@typescript-eslint/interface-name-prefix": "error", "@typescript-eslint/member-delimiter-style": "error", - "@typescript-eslint/no-angle-bracket-type-assertion": "error", "no-array-constructor": "off", "@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-empty-interface": "error", @@ -26,7 +26,6 @@ "@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-non-null-assertion": "error", - "@typescript-eslint/no-object-literal-type-assertion": "error", "@typescript-eslint/no-parameter-properties": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "warn", diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts new file mode 100644 index 00000000000..6380f9f0003 --- /dev/null +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -0,0 +1,158 @@ +import * as util from '../util'; +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; + +// intentionally mirroring the options +type MessageIds = + | 'as' + | 'angle-bracket' + | 'never' + | 'unexpectedObjectTypeAssertion'; +// https://github.com/prettier/prettier/issues/4794 +type OptUnion = + | { + assertionStyle: 'as' | 'angle-bracket'; + objectLiteralTypeAssertions?: 'allow' | 'allow-as-parameter' | 'never'; + } + | { + assertionStyle: 'never'; + }; +type Options = [OptUnion]; + +export default util.createRule({ + name: 'consistent-type-assertions', + meta: { + type: 'suggestion', + docs: { + category: 'Best Practices', + description: 'Enforces consistent usage of type assertions.', + recommended: 'error', + }, + messages: { + as: "Use 'as {{cast}}' instead of '<{{cast}}>'.", + 'angle-bracket': "Use '<{{cast}}>' instead of 'as {{cast}}'.", + never: 'Do not use any type assertions.', + unexpectedObjectTypeAssertion: 'Always prefer const x: T = { ... }.', + }, + schema: [ + { + oneOf: [ + { + type: 'object', + properties: { + assertionStyle: { + enum: ['never'], + }, + }, + additionalProperties: false, + required: ['assertionStyle'], + }, + { + type: 'object', + properties: { + assertionStyle: { + enum: ['as', 'angle-bracket'], + }, + objectLiteralTypeAssertions: { + enum: ['allow', 'allow-as-parameter', 'never'], + }, + }, + additionalProperties: false, + required: ['assertionStyle'], + }, + ], + }, + ], + }, + defaultOptions: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow', + }, + ], + create(context, [options]) { + const sourceCode = context.getSourceCode(); + + function reportIncorrectAssertionType( + node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, + ) { + const messageId = options.assertionStyle; + context.report({ + node, + messageId, + data: + messageId !== 'never' + ? { cast: sourceCode.getText(node.typeAnnotation) } + : {}, + }); + } + + function checkType(node: TSESTree.TypeNode) { + switch (node.type) { + case AST_NODE_TYPES.TSAnyKeyword: + case AST_NODE_TYPES.TSUnknownKeyword: + return false; + case AST_NODE_TYPES.TSTypeReference: + // Ignore `as const` and `` + return ( + node.typeName.type === AST_NODE_TYPES.Identifier && + node.typeName.name !== 'const' + ); + default: + return true; + } + } + + function checkExpression( + node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, + ) { + if ( + options.assertionStyle === 'never' || + options.objectLiteralTypeAssertions === 'allow' || + node.expression.type !== AST_NODE_TYPES.ObjectExpression + ) { + return; + } + if ( + options.objectLiteralTypeAssertions === 'allow-as-parameter' && + node.parent && + (node.parent.type === AST_NODE_TYPES.NewExpression || + node.parent.type === AST_NODE_TYPES.CallExpression || + node.parent.type === AST_NODE_TYPES.ThrowStatement) + ) { + return; + } + + if ( + checkType(node.typeAnnotation) && + node.expression.type === AST_NODE_TYPES.ObjectExpression + ) { + context.report({ + node, + messageId: 'unexpectedObjectTypeAssertion', + }); + } + } + + return { + TSTypeAssertion(node) { + if (options.assertionStyle !== 'angle-bracket') { + reportIncorrectAssertionType(node); + return; + } + + checkExpression(node); + }, + TSAsExpression(node) { + if (options.assertionStyle !== 'as') { + reportIncorrectAssertionType(node); + return; + } + + checkExpression(node); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 1d1bc59a846..3f10d73426e 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -5,6 +5,7 @@ import banTsIgnore from './ban-ts-ignore'; import banTypes from './ban-types'; import camelcase from './camelcase'; import classNameCasing from './class-name-casing'; +import consistentTypeAssertions from './consistent-type-assertions'; import consistentTypeDefinitions from './consistent-type-definitions'; import explicitFunctionReturnType from './explicit-function-return-type'; import explicitMemberAccessibility from './explicit-member-accessibility'; @@ -15,7 +16,6 @@ import interfaceNamePrefix from './interface-name-prefix'; import memberDelimiterStyle from './member-delimiter-style'; import memberNaming from './member-naming'; import memberOrdering from './member-ordering'; -import noAngleBracketTypeAssertion from './no-angle-bracket-type-assertion'; import noArrayConstructor from './no-array-constructor'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; @@ -30,7 +30,6 @@ import noMisusedNew from './no-misused-new'; import noMisusedPromises from './no-misused-promises'; import noNamespace from './no-namespace'; import noNonNullAssertion from './no-non-null-assertion'; -import noObjectLiteralTypeAssertion from './no-object-literal-type-assertion'; import noParameterProperties from './no-parameter-properties'; import noRequireImports from './no-require-imports'; import noThisAlias from './no-this-alias'; @@ -68,6 +67,7 @@ export default { 'ban-types': banTypes, camelcase: camelcase, 'class-name-casing': classNameCasing, + 'consistent-type-assertions': consistentTypeAssertions, 'consistent-type-definitions': consistentTypeDefinitions, 'explicit-function-return-type': explicitFunctionReturnType, 'explicit-member-accessibility': explicitMemberAccessibility, @@ -78,7 +78,6 @@ export default { 'member-delimiter-style': memberDelimiterStyle, 'member-naming': memberNaming, 'member-ordering': memberOrdering, - 'no-angle-bracket-type-assertion': noAngleBracketTypeAssertion, 'no-array-constructor': noArrayConstructor, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, @@ -93,7 +92,6 @@ export default { 'no-misused-promises': noMisusedPromises, 'no-namespace': noNamespace, 'no-non-null-assertion': noNonNullAssertion, - 'no-object-literal-type-assertion': noObjectLiteralTypeAssertion, 'no-parameter-properties': noParameterProperties, 'no-require-imports': noRequireImports, 'no-this-alias': noThisAlias, diff --git a/packages/eslint-plugin/src/rules/no-angle-bracket-type-assertion.ts b/packages/eslint-plugin/src/rules/no-angle-bracket-type-assertion.ts deleted file mode 100644 index 82848a07196..00000000000 --- a/packages/eslint-plugin/src/rules/no-angle-bracket-type-assertion.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as util from '../util'; - -export default util.createRule({ - name: 'no-angle-bracket-type-assertion', - meta: { - type: 'problem', - docs: { - description: - 'Enforces the use of `as Type` assertions instead of `` assertions', - category: 'Stylistic Issues', - recommended: 'error', - }, - messages: { - preferAs: - "Prefer 'as {{cast}}' instead of '<{{cast}}>' when doing type assertions.", - }, - schema: [], - }, - defaultOptions: [], - create(context) { - const sourceCode = context.getSourceCode(); - return { - TSTypeAssertion(node) { - context.report({ - node, - messageId: 'preferAs', - data: { - cast: sourceCode.getText(node.typeAnnotation), - }, - }); - }, - }; - }, -}); diff --git a/packages/eslint-plugin/src/rules/no-object-literal-type-assertion.ts b/packages/eslint-plugin/src/rules/no-object-literal-type-assertion.ts deleted file mode 100644 index f4089655337..00000000000 --- a/packages/eslint-plugin/src/rules/no-object-literal-type-assertion.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import * as util from '../util'; - -type Options = [ - { - allowAsParameter?: boolean; - } -]; -type MessageIds = 'unexpectedTypeAssertion'; - -export default util.createRule({ - name: 'no-object-literal-type-assertion', - meta: { - type: 'problem', - docs: { - description: - 'Forbids an object literal to appear in a type assertion expression', - category: 'Stylistic Issues', - recommended: 'error', - }, - messages: { - unexpectedTypeAssertion: - 'Type assertion on object literals is forbidden, use a type annotation instead.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allowAsParameter: { - type: 'boolean', - }, - }, - }, - ], - }, - defaultOptions: [ - { - allowAsParameter: false, - }, - ], - create(context, [{ allowAsParameter }]) { - /** - * Check whatever node should be reported - * @param node the node to be evaluated. - */ - function checkType(node: TSESTree.TypeNode): boolean { - switch (node.type) { - case AST_NODE_TYPES.TSAnyKeyword: - case AST_NODE_TYPES.TSUnknownKeyword: - return false; - case AST_NODE_TYPES.TSTypeReference: - // Ignore `as const` and `` (#166) - return ( - node.typeName.type === AST_NODE_TYPES.Identifier && - node.typeName.name !== 'const' - ); - default: - return true; - } - } - - return { - 'TSTypeAssertion, TSAsExpression'( - node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, - ) { - if ( - allowAsParameter && - node.parent && - (node.parent.type === AST_NODE_TYPES.NewExpression || - node.parent.type === AST_NODE_TYPES.CallExpression) - ) { - return; - } - - if ( - checkType(node.typeAnnotation) && - node.expression.type === AST_NODE_TYPES.ObjectExpression - ) { - context.report({ - node, - messageId: 'unexpectedTypeAssertion', - }); - } - }, - }; - }, -}); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts new file mode 100644 index 00000000000..b631fdda9d4 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -0,0 +1,302 @@ +import rule from '../../src/rules/consistent-type-assertions'; +import { RuleTester, batchedSingleLineTests } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const ANGLE_BRACKET_TESTS = ` +const x = new Generic(); +const x = b; +const x = [1]; +const x = [1]; +const x = ('string'); +`; +const AS_TESTS = ` +const x = new Generic() as Foo; +const x = b as A; +const x = [1] as readonly number[]; +const x = [1] as const; +const x = ('string') as a | b; +`; +const OBJECT_LITERAL_AS_CASTS = ` +const x = {} as Foo; +`; +const OBJECT_LITERAL_ANGLE_BRACKET_CASTS = ` +const x = >{}; +`; +const OBJECT_LITERAL_ARGUMENT_AS_CASTS = ` +print({ bar: 5 } as Foo) +new print({ bar: 5 } as Foo) +function foo() { throw { bar: 5 } as Foo } +`; +const OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS = ` +print({ bar: 5 }) +new print({ bar: 5 }) +function foo() { throw { bar: 5 } } +`; + +ruleTester.run('consistent-type-assertions', rule, { + valid: [ + ...batchedSingleLineTests({ + code: AS_TESTS, + options: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow', + }, + ], + }), + ...batchedSingleLineTests({ + code: ANGLE_BRACKET_TESTS, + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow', + }, + ], + }), + ...batchedSingleLineTests({ + code: `${OBJECT_LITERAL_AS_CASTS.trimRight()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, + options: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow', + }, + ], + }), + ...batchedSingleLineTests({ + code: `${OBJECT_LITERAL_ANGLE_BRACKET_CASTS.trimRight()}${OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS}`, + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow', + }, + ], + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_ARGUMENT_AS_CASTS, + options: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow-as-parameter', + }, + ], + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS, + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow-as-parameter', + }, + ], + }), + ], + invalid: [ + ...batchedSingleLineTests({ + code: AS_TESTS, + options: [ + { + assertionStyle: 'angle-bracket', + }, + ], + errors: [ + { + messageId: 'angle-bracket', + line: 2, + }, + { + messageId: 'angle-bracket', + line: 3, + }, + { + messageId: 'angle-bracket', + line: 4, + }, + { + messageId: 'angle-bracket', + line: 5, + }, + { + messageId: 'angle-bracket', + line: 6, + }, + ], + }), + ...batchedSingleLineTests({ + code: ANGLE_BRACKET_TESTS, + options: [ + { + assertionStyle: 'as', + }, + ], + errors: [ + { + messageId: 'as', + line: 2, + }, + { + messageId: 'as', + line: 3, + }, + { + messageId: 'as', + line: 4, + }, + { + messageId: 'as', + line: 5, + }, + { + messageId: 'as', + line: 6, + }, + ], + }), + ...batchedSingleLineTests({ + code: AS_TESTS, + options: [ + { + assertionStyle: 'never', + }, + ], + errors: [ + { + messageId: 'never', + line: 2, + }, + { + messageId: 'never', + line: 3, + }, + { + messageId: 'never', + line: 4, + }, + { + messageId: 'never', + line: 5, + }, + { + messageId: 'never', + line: 6, + }, + ], + }), + ...batchedSingleLineTests({ + code: ANGLE_BRACKET_TESTS, + options: [ + { + assertionStyle: 'never', + }, + ], + errors: [ + { + messageId: 'never', + line: 2, + }, + { + messageId: 'never', + line: 3, + }, + { + messageId: 'never', + line: 4, + }, + { + messageId: 'never', + line: 5, + }, + { + messageId: 'never', + line: 6, + }, + ], + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_AS_CASTS, + options: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow-as-parameter', + }, + ], + errors: [ + { + messageId: 'unexpectedObjectTypeAssertion', + line: 2, + }, + ], + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_ANGLE_BRACKET_CASTS, + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow-as-parameter', + }, + ], + errors: [ + { + messageId: 'unexpectedObjectTypeAssertion', + line: 2, + }, + ], + }), + ...batchedSingleLineTests({ + code: `${OBJECT_LITERAL_AS_CASTS.trimRight()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, + options: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'never', + }, + ], + errors: [ + { + messageId: 'unexpectedObjectTypeAssertion', + line: 2, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 3, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 4, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 5, + }, + ], + }), + ...batchedSingleLineTests({ + code: `${OBJECT_LITERAL_ANGLE_BRACKET_CASTS.trimRight()}${OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS}`, + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'never', + }, + ], + errors: [ + { + messageId: 'unexpectedObjectTypeAssertion', + line: 2, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 3, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 4, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 5, + }, + ], + }), + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-angle-bracket-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-angle-bracket-type-assertion.test.ts deleted file mode 100644 index 1d8bd4b2321..00000000000 --- a/packages/eslint-plugin/tests/rules/no-angle-bracket-type-assertion.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import rule from '../../src/rules/no-angle-bracket-type-assertion'; -import { RuleTester } from '../RuleTester'; - -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', -}); - -ruleTester.run('no-angle-bracket-type-assertion', rule, { - valid: [ - ` -interface Foo { - bar : number; - bas : string; -} - -class Generic implements Foo {} - -const foo = {} as Foo; -const bar = new Generic() as Foo; - `, - 'const array : Array = [];', - ` -class A {} -class B extends A {} - -const b : B = new B(); -const a : A = b as A; - `, - ` -type A = { - num: number -}; - -const b = { - num: 5 -}; - -const a: A = b as A; - `, - 'const a : number = 5 as number', - ` -const a : number = 5; -const b : number = a as number; - `, - 'const a : Array = [1] as Array;', - ], - invalid: [ - { - code: ` -interface Foo { - bar : number; - bas : string; -} - -class Generic implements Foo {} - -const foo = {}; -const bar = new Generic(); - `, - errors: [ - { - messageId: 'preferAs', - data: { - cast: 'Foo', - }, - line: 9, - column: 13, - }, - { - messageId: 'preferAs', - data: { - cast: 'Foo', - }, - line: 10, - column: 13, - }, - ], - }, - { - code: 'const a : number = 5', - errors: [ - { - messageId: 'preferAs', - data: { - cast: 'number', - }, - line: 1, - column: 20, - }, - ], - }, - { - code: ` -const a : number = 5; -const b : number = a; - `, - errors: [ - { - messageId: 'preferAs', - data: { - cast: 'number', - }, - line: 3, - column: 20, - }, - ], - }, - { - code: 'const a : Array = >[1];', - errors: [ - { - messageId: 'preferAs', - data: { - cast: 'Array', - }, - line: 1, - column: 27, - }, - ], - }, - { - code: ` -class A {} -class B extends A {} - -const b : B = new B(); -const a : A = b; - `, - errors: [ - { - messageId: 'preferAs', - data: { - cast: 'A', - }, - line: 6, - column: 15, - }, - ], - }, - { - code: ` -type A = { - num: number -}; - -const b = { - num: 5 -}; - -const a: A = b; - `, - errors: [ - { - messageId: 'preferAs', - data: { - cast: 'A', - }, - line: 10, - column: 14, - }, - ], - }, - ], -}); diff --git a/packages/eslint-plugin/tests/rules/no-object-literal-type-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-object-literal-type-assertion.test.ts deleted file mode 100644 index 336df4c29a1..00000000000 --- a/packages/eslint-plugin/tests/rules/no-object-literal-type-assertion.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import rule from '../../src/rules/no-object-literal-type-assertion'; -import { RuleTester } from '../RuleTester'; - -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - ecmaFeatures: { - jsx: false, - }, - }, -}); - -ruleTester.run('no-object-literal-type-assertion', rule, { - valid: [ - ` x;`, - `x as T;`, - `const foo = bar;`, - `const foo: baz = bar;`, - `const x: T = {};`, - `const foo = { bar: { } };`, - // Allow cast to 'any' - `const foo = {} as any;`, - `const foo = {};`, - // Allow cast to 'unknown' - `const foo = {} as unknown;`, - `const foo = {};`, - `const foo = {} as const;`, - `const foo = {};`, - { - code: `print({ bar: 5 } as Foo)`, - options: [ - { - allowAsParameter: true, - }, - ], - }, - { - code: `new print({ bar: 5 } as Foo)`, - options: [ - { - allowAsParameter: true, - }, - ], - }, - ], - invalid: [ - { - code: ` ({});`, - errors: [ - { - messageId: 'unexpectedTypeAssertion', - line: 1, - column: 1, - }, - ], - }, - { - code: `({}) as T;`, - errors: [ - { - messageId: 'unexpectedTypeAssertion', - line: 1, - column: 1, - }, - ], - }, - { - code: `const x = {} as T;`, - errors: [ - { - messageId: 'unexpectedTypeAssertion', - line: 1, - column: 11, - }, - ], - }, - { - code: `print({ bar: 5 } as Foo)`, - errors: [ - { - messageId: 'unexpectedTypeAssertion', - line: 1, - column: 7, - }, - ], - }, - { - code: `new print({ bar: 5 } as Foo)`, - errors: [ - { - messageId: 'unexpectedTypeAssertion', - line: 1, - column: 11, - }, - ], - }, - ], -}); diff --git a/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts b/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts index 0812adade32..567378fdd5d 100644 --- a/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts +++ b/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts @@ -36,6 +36,10 @@ function batchedSingleLineTests< ): (ValidTestCase | InvalidTestCase)[] { // eslint counts lines from 1 const lineOffset = options.code[0] === '\n' ? 2 : 1; + const output = + 'output' in options && options.output + ? options.output.trim().split('\n') + : null; return options.code .trim() .split('\n') @@ -45,7 +49,7 @@ function batchedSingleLineTests< 'errors' in options ? options.errors.filter(e => e.line === lineNum) : []; - return { + const returnVal = { ...options, code, errors: errors.map(e => ({ @@ -53,6 +57,13 @@ function batchedSingleLineTests< line: 1, })), }; + if (output && output[i]) { + return { + ...returnVal, + output: output[i], + }; + } + return returnVal; }); } diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 8eed48390d0..f079fd68d47 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -1312,7 +1312,7 @@ export interface TSTypeAnnotation extends BaseNode { export interface TSTypeAssertion extends BaseNode { type: AST_NODE_TYPES.TSTypeAssertion; typeAnnotation: TypeNode; - expression: UnaryExpression; + expression: Expression; } export interface TSTypeLiteral extends BaseNode {