diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 503c06e6894..7241964834c 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -211,6 +211,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | | [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/type-name-prefix`](./docs/rules/type-name-prefix.md) | Require that type names should or should not prefixed with `T` | | | | | [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | diff --git a/packages/eslint-plugin/docs/rules/type-name-prefix.md b/packages/eslint-plugin/docs/rules/type-name-prefix.md new file mode 100644 index 00000000000..4226b78e6c0 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/type-name-prefix.md @@ -0,0 +1,60 @@ +# Require that type names be prefixed with `T` (type-name-prefix) + +Type often represent an important software contract, so it can be helpful to prefix their names with `T`, same as +interfaces with `I`. The unprefixed name is then available for a class or functions that provides a standard +implementation of the type or interface. + +## Rule Details + +This rule enforces whether or not the `T` prefix is required for type names. + +## Options + +This rule has an object option: + +- `{ "prefixWithT": "never" }`: (default) disallows all types being prefixed with `T` +- `{ "prefixWithT": "always" }`: requires all types be prefixed with `T` + +## Examples + +prefixWithT + +### never + +**Configuration:** `{ "prefixWithT": "never" }` + +The following patterns are considered warnings: + +```ts +type TAlign = 'left' | 'right'; +type TType = 'primary' | 'secondary'; +``` + +The following patterns are not warnings: + +```ts +type Align = 'left' | 'right'; +type Type = 'primary' | 'secondary'; +``` + +### always + +**Configuration:** `{ "prefixWithT": "always" }` + +The following patterns are considered warnings: + +```ts +type Align = 'left' | 'right'; +type Type = 'primary' | 'secondary'; +``` + +The following patterns are not warnings: + +```ts +type TAlign = 'left' | 'right'; +type TType = 'primary' | 'secondary'; +``` + +## When Not To Use It + +If you do not want to enforce type name prefixing. diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index c3ee46bc95a..3adad2cfc6a 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -88,6 +88,7 @@ "@typescript-eslint/strict-boolean-expressions": "error", "@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/type-name-prefix": "error", "@typescript-eslint/typedef": "error", "@typescript-eslint/unbound-method": "error", "@typescript-eslint/unified-signatures": "error" diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index a4dc193e939..8e76c0594aa 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -69,6 +69,7 @@ import spaceBeforeFunctionParen from './space-before-function-paren'; import strictBooleanExpressions from './strict-boolean-expressions'; import tripleSlashReference from './triple-slash-reference'; import typeAnnotationSpacing from './type-annotation-spacing'; +import typeNamePrefix from './type-name-prefix'; import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; @@ -145,6 +146,7 @@ export default { 'strict-boolean-expressions': strictBooleanExpressions, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, + 'type-name-prefix': typeNamePrefix, typedef: typedef, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, diff --git a/packages/eslint-plugin/src/rules/type-name-prefix.ts b/packages/eslint-plugin/src/rules/type-name-prefix.ts new file mode 100644 index 00000000000..1f6a5c6d502 --- /dev/null +++ b/packages/eslint-plugin/src/rules/type-name-prefix.ts @@ -0,0 +1,55 @@ +import * as util from '../util'; + +type Options = [{ prefixWithT: PrefixWithT }]; +type PrefixWithT = 'always' | 'never'; + +export default util.createRule({ + name: 'type-name-prefix', + meta: { + type: 'suggestion', + docs: { + description: + 'Require that type names should or should not prefixed with `T`', + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + always: 'Type name must be prefixed with "T".', + never: 'Type name must not be prefixed with "T".', + }, + schema: [ + { + oneOf: [ + { + type: 'object', + properties: { + prefixWithT: { + type: 'string', + enum: ['never', 'always'], + }, + }, + additionalProperties: false, + requiresTypeChecking: true, + }, + ], + }, + ], + }, + defaultOptions: [{ prefixWithT: 'never' }], + create(context, [options]) { + const isPrefixed = (name: string): boolean => /^T[A-Z0-9]/.test(name); + + const rule = options.prefixWithT; + const ensureRule = + rule === 'never' + ? (name: string): boolean => !isPrefixed(name) + : (name: string): boolean => isPrefixed(name); + + return { + TSTypeAliasDeclaration(node): void { + ensureRule(node.id.name) || + context.report({ node: node.id, messageId: rule }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/type-name-prefix.test.ts b/packages/eslint-plugin/tests/rules/type-name-prefix.test.ts new file mode 100644 index 00000000000..93f1c9f3fff --- /dev/null +++ b/packages/eslint-plugin/tests/rules/type-name-prefix.test.ts @@ -0,0 +1,88 @@ +import rule from '../../src/rules/type-name-prefix'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('type-name-prefix', rule, { + valid: [ + `type Color = "white";`, + { + code: `type TColor = "white";`, + options: [{ prefixWithT: 'always' }], + }, + { + code: `type TThreshold = 100 | 50;`, + options: [{ prefixWithT: 'always' }], + }, + { + code: `type T20x = 200 | 201;`, + options: [{ prefixWithT: 'always' }], + }, + { + code: `type Color = "white";`, + options: [{ prefixWithT: 'never' }], + }, + { + code: `type Threshold = 100 | 50;`, + options: [{ prefixWithT: 'never' }], + }, + ], + invalid: [ + { + code: `type TColor = "white";`, + errors: [ + { + messageId: 'never', + line: 1, + column: 6, + }, + ], + }, + { + code: `type Color = "white";`, + options: [{ prefixWithT: 'always' }], + errors: [ + { + messageId: 'always', + line: 1, + column: 6, + }, + ], + }, + { + code: `type Threshold = 100 | 50;`, + options: [{ prefixWithT: 'always' }], + errors: [ + { + messageId: 'always', + line: 1, + column: 6, + }, + ], + }, + { + code: `type TColor = "white";`, + options: [{ prefixWithT: 'never' }], + errors: [ + { + messageId: 'never', + line: 1, + column: 6, + }, + ], + }, + { + code: `type TThreshold = 100 | 50;`, + options: [{ prefixWithT: 'never' }], + errors: [ + { + messageId: 'never', + line: 1, + column: 6, + }, + ], + }, + ], +});