Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add type-name-prefix rule #1329

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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 | | | |
Expand Down
60 changes: 60 additions & 0 deletions 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.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
55 changes: 55 additions & 0 deletions 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<Options, PrefixWithT>({
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 });
},
};
},
});
88 changes: 88 additions & 0 deletions 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,
},
],
},
],
});