From c68e033f423d1143330e8e21104d5de10185e9a8 Mon Sep 17 00:00:00 2001 From: Ankeet Maini Date: Mon, 19 Aug 2019 18:57:34 +0530 Subject: [PATCH] feat(eslint-plugin): [no-type-alias] support tuples (#775) --- .../eslint-plugin/docs/rules/no-type-alias.md | 76 +++++ .../eslint-plugin/src/rules/no-type-alias.ts | 145 +++++----- .../tests/rules/no-type-alias.test.ts | 261 ++++++++++++++++++ .../src/ts-estree/ts-estree.ts | 2 +- 4 files changed, 408 insertions(+), 76 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-type-alias.md b/packages/eslint-plugin/docs/rules/no-type-alias.md index 63656dfada8..46230f3d329 100644 --- a/packages/eslint-plugin/docs/rules/no-type-alias.md +++ b/packages/eslint-plugin/docs/rules/no-type-alias.md @@ -86,6 +86,7 @@ or more of the following you may pass an object with the options set as follows: - `allowCallbacks` set to `"always"` will allow you to use type aliases with callbacks (Defaults to `"never"`) - `allowLiterals` set to `"always"` will allow you to use type aliases with literal objects (Defaults to `"never"`) - `allowMappedTypes` set to `"always"` will allow you to use type aliases as mapping tools (Defaults to `"never"`) +- `allowTupleTypes` set to `"always"` will allow you to use type aliases with tuples (Defaults to `"never"`) ### allowAliases @@ -453,6 +454,81 @@ type Foo = { readonly [P in keyof T]: T[P] } & type Foo = { [P in keyof T]?: T[P] } & { [P in keyof U]?: U[P] }; ``` +### allowTupleTypes + +This applies to tuple types (`type Foo = [number]`). + +The setting accepts the following options: + +- `"always"` or `"never"` to active or deactivate the feature. +- `"in-unions"`, allows tuples in union statements, e.g. `type Foo = [string] | [string, string];` +- `"in-intersections"`, allows tuples in intersection statements, e.g. `type Foo = [string] & [string, string];` +- `"in-unions-and-intersections"`, allows tuples in union and/or intersection statements. + +Examples of **correct** code for the `{ "allowTupleTypes": "always" }` options: + +```ts +type Foo = [number]; + +type Foo = [number] | [number, number]; + +type Foo = [number] & [number, number]; + +type Foo = [number] | [number, number] & [string, string]; +``` + +Examples of **incorrect** code for the `{ "allowTupleTypes": "in-unions" }` option: + +```ts +type Foo = [number]; + +type Foo = [number] & [number, number]; + +type Foo = [string] & [number]; +``` + +Examples of **correct** code for the `{ "allowTupleTypes": "in-unions" }` option: + +```ts +type Foo = [number] | [number, number]; + +type Foo = [string] | [number]; +``` + +Examples of **incorrect** code for the `{ "allowTupleTypes": "in-intersections" }` option: + +```ts +type Foo = [number]; + +type Foo = [number] | [number, number]; + +type Foo = [string] | [number]; +``` + +Examples of **correct** code for the `{ "allowTupleTypes": "in-intersections" }` option: + +```ts +type Foo = [number] & [number, number]; + +type Foo = [string] & [number]; +``` + +Examples of **incorrect** code for the `{ "allowTupleTypes": "in-unions-and-intersections" }` option: + +```ts +type Foo = [number]; + +type Foo = [string]; +``` + +Examples of **correct** code for the `{ "allowLiterals": "in-unions-and-intersections" }` option: + +```ts +type Foo = [number] & [number, number]; + +type Foo = [string] | [number]; +``` + ## When Not To Use It When you can't express some shape with an interface or you need to use a union, tuple type, diff --git a/packages/eslint-plugin/src/rules/no-type-alias.ts b/packages/eslint-plugin/src/rules/no-type-alias.ts index 1648b89f0df..68d537cc09b 100644 --- a/packages/eslint-plugin/src/rules/no-type-alias.ts +++ b/packages/eslint-plugin/src/rules/no-type-alias.ts @@ -4,27 +4,27 @@ import { } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; +type Values = + | 'always' + | 'never' + | 'in-unions' + | 'in-intersections' + | 'in-unions-and-intersections'; +const enumValues: Values[] = [ + 'always', + 'never', + 'in-unions', + 'in-intersections', + 'in-unions-and-intersections', +]; + type Options = [ { - allowAliases?: - | 'always' - | 'never' - | 'in-unions' - | 'in-intersections' - | 'in-unions-and-intersections'; + allowAliases?: Values; allowCallbacks?: 'always' | 'never'; - allowLiterals?: - | 'always' - | 'never' - | 'in-unions' - | 'in-intersections' - | 'in-unions-and-intersections'; - allowMappedTypes?: - | 'always' - | 'never' - | 'in-unions' - | 'in-intersections' - | 'in-unions-and-intersections'; + allowLiterals?: Values; + allowMappedTypes?: Values; + allowTupleTypes?: Values; }, ]; type MessageIds = 'noTypeAlias' | 'noCompositionAlias'; @@ -57,34 +57,19 @@ export default util.createRule({ type: 'object', properties: { allowAliases: { - enum: [ - 'always', - 'never', - 'in-unions', - 'in-intersections', - 'in-unions-and-intersections', - ], + enum: enumValues, }, allowCallbacks: { enum: ['always', 'never'], }, allowLiterals: { - enum: [ - 'always', - 'never', - 'in-unions', - 'in-intersections', - 'in-unions-and-intersections', - ], + enum: enumValues, }, allowMappedTypes: { - enum: [ - 'always', - 'never', - 'in-unions', - 'in-intersections', - 'in-unions-and-intersections', - ], + enum: enumValues, + }, + allowTupleTypes: { + enum: enumValues, }, }, additionalProperties: false, @@ -97,11 +82,20 @@ export default util.createRule({ allowCallbacks: 'never', allowLiterals: 'never', allowMappedTypes: 'never', + allowTupleTypes: 'never', }, ], create( context, - [{ allowAliases, allowCallbacks, allowLiterals, allowMappedTypes }], + [ + { + allowAliases, + allowCallbacks, + allowLiterals, + allowMappedTypes, + allowTupleTypes, + }, + ], ) { const unions = ['always', 'in-unions', 'in-unions-and-intersections']; const intersections = [ @@ -180,6 +174,36 @@ export default util.createRule({ }); } + const isValidTupleType = (type: TypeWithLabel) => { + if (type.node.type === AST_NODE_TYPES.TSTupleType) { + return true; + } + if (type.node.type === AST_NODE_TYPES.TSTypeOperator) { + if ( + ['keyof', 'readonly'].includes(type.node.operator) && + type.node.typeAnnotation && + type.node.typeAnnotation.type === AST_NODE_TYPES.TSTupleType + ) { + return true; + } + } + return false; + }; + + const checkAndReport = ( + optionValue: Values, + isTopLevel: boolean, + type: TypeWithLabel, + label: string, + ) => { + if ( + optionValue === 'never' || + !isSupportedComposition(isTopLevel, type.compositionType, optionValue) + ) { + reportError(type.node, type.compositionType, isTopLevel, label); + } + }; + /** * Validates the node looking for aliases, callbacks and literals. * @param node the node to be validated. @@ -198,48 +222,19 @@ export default util.createRule({ } } else if (type.node.type === AST_NODE_TYPES.TSTypeLiteral) { // literal object type - if ( - allowLiterals === 'never' || - !isSupportedComposition( - isTopLevel, - type.compositionType, - allowLiterals!, - ) - ) { - reportError(type.node, type.compositionType, isTopLevel, 'Literals'); - } + checkAndReport(allowLiterals!, isTopLevel, type, 'Literals'); } else if (type.node.type === AST_NODE_TYPES.TSMappedType) { // mapped type - if ( - allowMappedTypes === 'never' || - !isSupportedComposition( - isTopLevel, - type.compositionType, - allowMappedTypes!, - ) - ) { - reportError( - type.node, - type.compositionType, - isTopLevel, - 'Mapped types', - ); - } + checkAndReport(allowMappedTypes!, isTopLevel, type, 'Mapped types'); + } else if (isValidTupleType(type)) { + // tuple types + checkAndReport(allowTupleTypes!, isTopLevel, type, 'Tuple Types'); } else if ( type.node.type.endsWith('Keyword') || aliasTypes.has(type.node.type) ) { // alias / keyword - if ( - allowAliases === 'never' || - !isSupportedComposition( - isTopLevel, - type.compositionType, - allowAliases!, - ) - ) { - reportError(type.node, type.compositionType, isTopLevel, 'Aliases'); - } + checkAndReport(allowAliases!, isTopLevel, type, 'Aliases'); } else { // unhandled type - shouldn't happen reportError(type.node, type.compositionType, isTopLevel, 'Unhandled'); diff --git a/packages/eslint-plugin/tests/rules/no-type-alias.test.ts b/packages/eslint-plugin/tests/rules/no-type-alias.test.ts index 3c3d83520e8..d17348af746 100644 --- a/packages/eslint-plugin/tests/rules/no-type-alias.test.ts +++ b/packages/eslint-plugin/tests/rules/no-type-alias.test.ts @@ -376,6 +376,69 @@ type Foo = { code: 'type Foo = typeof bar | typeof baz;', options: [{ allowAliases: 'in-unions' }], }, + { + code: 'type Foo = keyof [string]', + options: [{ allowTupleTypes: 'always' }], + }, + { + code: 'type Foo = [string] | [number, number];', + options: [{ allowTupleTypes: 'always' }], + }, + { + code: 'type Foo = [string] | [number, number];', + options: [{ allowTupleTypes: 'in-unions' }], + }, + { + code: 'type Foo = [string] & [number, number];', + options: [{ allowTupleTypes: 'in-intersections' }], + }, + { + code: + 'type Foo = [string] & [number, number] | [number, number, number];', + options: [{ allowTupleTypes: 'in-unions-and-intersections' }], + }, + { + code: 'type Foo = readonly [string] | [number, number];', + options: [{ allowTupleTypes: 'always' }], + }, + { + code: 'type Foo = readonly [string] | readonly [number, number];', + options: [{ allowTupleTypes: 'always' }], + }, + { + code: 'type Foo = readonly [string] | [number, number];', + options: [{ allowTupleTypes: 'in-unions' }], + }, + { + code: 'type Foo = [string] & readonly [number, number];', + options: [{ allowTupleTypes: 'in-intersections' }], + }, + { + code: + 'type Foo = [string] & [number, number] | readonly [number, number, number];', + options: [{ allowTupleTypes: 'in-unions-and-intersections' }], + }, + { + code: 'type Foo = keyof [string] | [number, number];', + options: [{ allowTupleTypes: 'always' }], + }, + { + code: 'type Foo = keyof [string] | keyof [number, number];', + options: [{ allowTupleTypes: 'always' }], + }, + { + code: 'type Foo = keyof [string] | [number, number];', + options: [{ allowTupleTypes: 'in-unions' }], + }, + { + code: 'type Foo = [string] & keyof [number, number];', + options: [{ allowTupleTypes: 'in-intersections' }], + }, + { + code: + 'type Foo = [string] & [number, number] | keyof [number, number, number];', + options: [{ allowTupleTypes: 'in-unions-and-intersections' }], + }, ], invalid: [ { @@ -2915,5 +2978,203 @@ type Foo = { }, ], }, + { + code: 'type Foo = [number] | [number, number]', + options: [{ allowTupleTypes: 'never' }], + errors: [ + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 12, + }, + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 23, + }, + ], + }, + { + code: 'type Foo = [number] & [number, number]', + options: [{ allowTupleTypes: 'in-unions' }], + errors: [ + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'intersection', + typeName: 'Tuple Types', + }, + line: 1, + column: 12, + }, + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'intersection', + typeName: 'Tuple Types', + }, + line: 1, + column: 23, + }, + ], + }, + { + code: 'type Foo = [number] | [number, number]', + options: [{ allowTupleTypes: 'in-intersections' }], + errors: [ + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 12, + }, + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 23, + }, + ], + }, + { + code: 'type Foo = [number];', + options: [{ allowTupleTypes: 'in-intersections' }], + errors: [ + { + messageId: 'noTypeAlias', + }, + ], + }, + { + code: 'type Foo = [number];', + options: [{ allowTupleTypes: 'in-unions' }], + errors: [ + { + messageId: 'noTypeAlias', + }, + ], + }, + { + code: 'type Foo = [number];', + options: [{ allowTupleTypes: 'in-unions-and-intersections' }], + errors: [ + { + messageId: 'noTypeAlias', + }, + ], + }, + { + code: 'type Foo = readonly [number] | keyof [number, number]', + options: [{ allowTupleTypes: 'never' }], + errors: [ + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 12, + }, + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 32, + }, + ], + }, + { + code: 'type Foo = keyof [number] & [number, number]', + options: [{ allowTupleTypes: 'in-unions' }], + errors: [ + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'intersection', + typeName: 'Tuple Types', + }, + line: 1, + column: 12, + }, + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'intersection', + typeName: 'Tuple Types', + }, + line: 1, + column: 29, + }, + ], + }, + { + code: 'type Foo = [number] | readonly [number, number]', + options: [{ allowTupleTypes: 'in-intersections' }], + errors: [ + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 12, + }, + { + messageId: 'noCompositionAlias', + data: { + compositionType: 'union', + typeName: 'Tuple Types', + }, + line: 1, + column: 23, + }, + ], + }, + { + code: 'type Foo = readonly [number];', + options: [{ allowTupleTypes: 'in-intersections' }], + errors: [ + { + messageId: 'noTypeAlias', + }, + ], + }, + { + code: 'type Foo = keyof [number];', + options: [{ allowTupleTypes: 'in-unions' }], + errors: [ + { + messageId: 'noTypeAlias', + }, + ], + }, + { + code: 'type Foo = readonly [number];', + options: [{ allowTupleTypes: 'in-unions-and-intersections' }], + errors: [ + { + messageId: 'noTypeAlias', + }, + ], + }, ], }); diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 7276f8d93e3..51529a470cb 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -1324,7 +1324,7 @@ export interface TSTypeLiteral extends BaseNode { export interface TSTypeOperator extends BaseNode { type: AST_NODE_TYPES.TSTypeOperator; operator: 'keyof' | 'unique' | 'readonly'; - typeAnnotation?: TSTypeAnnotation; + typeAnnotation?: TypeNode; } export interface TSTypeParameter extends BaseNode {