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(eslint-plugin): add consistent-type-definitions rule #463

Merged
merged 21 commits into from Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7448441
feat(eslint-plugin): add consistent-type-definitions rule
otofu-square Apr 26, 2019
770b656
fix(eslint-plugin): deprecate prefer-interface rule
otofu-square Apr 27, 2019
7c26569
fix(eslint-plugin): fix wrong typing for `replacedBy` in meta
otofu-square Apr 27, 2019
0427c58
refactor(eslint-plugin): modify tests to check fixer removes semi
otofu-square Apr 30, 2019
2f5fdea
Merge branch 'master' into prefer-type-alias
otofu-square May 8, 2019
3a03995
fix(eslint-plugin): fix typos
otofu-square May 8, 2019
f843fa8
fix(eslint-plugin): convert arrow function to method
otofu-square May 8, 2019
0e5d28d
fix(eslint-plugin): improve test for consistent-type-definitions
otofu-square May 8, 2019
533c4a3
Merge branch 'master' into prefer-type-alias
otofu-square May 9, 2019
177973f
docs(eslint-plugin): write a doc for consistent-type-definitions
otofu-square May 9, 2019
4ebaf22
docs(eslint-plugin): fix docs
otofu-square May 9, 2019
ce3f4e0
Merge branch 'master' into prefer-type-alias
otofu-square May 10, 2019
b0d705b
docs(eslint-plugin): improve consistent-type-definitions doc
otofu-square May 10, 2019
9908a89
Merge branch 'master' into prefer-type-alias
otofu-square Jun 17, 2019
8443aa4
fix(eslint-plugin): use typings in @typescript-eslint/experimental-utils
otofu-square Jun 17, 2019
9ba3a90
fix(eslint-plugin): remove unnecessary `tslintName` property
otofu-square Jun 18, 2019
5d619ad
Merge branch 'master' into prefer-type-alias
otofu-square Jun 18, 2019
97ecab4
docs: ixed documentation and checker for deprecated rules
bradzacher Jun 20, 2019
83da20e
Update all.json
bradzacher Jun 20, 2019
16abfd5
Update all.json
bradzacher Jun 20, 2019
838b487
Merge branch 'master' into prefer-type-alias
bradzacher Jun 20, 2019
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
2 changes: 1 addition & 1 deletion packages/eslint-plugin/README.md
Expand Up @@ -117,6 +117,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 (`ban-types` from TSLint) | :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 (`class-name` from TSLint) | :heavy_check_mark: | | |
| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `type` or `interface` | :heavy_check_mark: | :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 (`member-access` from TSLint) | :heavy_check_mark: | | |
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Spacing between function identifiers and their invocations | | :wrench: | |
Expand Down Expand Up @@ -152,7 +153,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@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. | | | |
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures (`callable-types` from TSLint) | | :wrench: | |
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method. | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) (`interface-over-type-literal` from TSLint) | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules. (`no-internal-module` from TSLint) | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | | :wrench: | |
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | | | :thought_balloon: |
Expand Down
105 changes: 105 additions & 0 deletions packages/eslint-plugin/src/rules/consistent-type-definitions.ts
@@ -0,0 +1,105 @@
import { TSESTree } from '@typescript-eslint/typescript-estree';
import { RuleFix } from 'ts-eslint';
import * as util from '../util';

export default util.createRule({
name: 'consistent-type-definitions',
meta: {
type: 'suggestion',
docs: {
description:
'Consistent with type definition either `interface` or `type`',
category: 'Stylistic Issues',
recommended: 'error',
tslintName: 'consistent-type-definitions',
},
messages: {
interfaceOverType: 'Use an `interface` instead of a `type`',
typeOverInterface: 'Use a `type` instead of an `interface`',
},
schema: [
{
enum: ['interface', 'type'],
},
],
fixable: 'code',
},
defaultOptions: ['interface'],
create(context, [option]) {
const sourceCode = context.getSourceCode();

return {
// VariableDeclaration with kind type has only one VariableDeclarator
"TSTypeAliasDeclaration[typeAnnotation.type='TSTypeLiteral']"(
node: TSESTree.TSTypeAliasDeclaration,
) {
if (option === 'interface') {
context.report({
node: node.id,
messageId: 'interfaceOverType',
fix(fixer) {
const typeNode = node.typeParameters || node.id;
const fixes: RuleFix[] = [];

const firstToken = sourceCode.getFirstToken(node);
if (firstToken) {
fixes.push(fixer.replaceText(firstToken, 'interface'));
fixes.push(
fixer.replaceTextRange(
[typeNode.range[1], node.typeAnnotation.range[0]],
' ',
),
);
}

const afterToken = sourceCode.getTokenAfter(node.typeAnnotation);
if (
afterToken &&
afterToken.type === 'Punctuator' &&
afterToken.value === ';'
) {
fixes.push(fixer.remove(afterToken));
}

return fixes;
},
});
}
},
TSInterfaceDeclaration(node) {
if (option === 'type') {
context.report({
node: node.id,
messageId: 'typeOverInterface',
fix(fixer) {
const typeNode = node.typeParameters || node.id;
const fixes: RuleFix[] = [];

const firstToken = sourceCode.getFirstToken(node);
if (firstToken) {
fixes.push(fixer.replaceText(firstToken, 'type'));
fixes.push(
fixer.replaceTextRange(
[typeNode.range[1], node.body.range[0]],
' = ',
),
);
}

if (node.extends) {
node.extends.forEach(heritage => {
const typeIdentifier = sourceCode.getText(heritage);
fixes.push(
fixer.insertTextAfter(node.body, ` & ${typeIdentifier}`),
);
});
}

return fixes;
},
});
}
},
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/prefer-interface.ts
Expand Up @@ -18,6 +18,8 @@ export default util.createRule({
interfaceOverType: 'Use an interface instead of a type literal.',
},
schema: [],
deprecated: true,
replacedBy: ['consistent-type-definitions'],
},
defaultOptions: [],
create(context) {
Expand Down
209 changes: 209 additions & 0 deletions packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts
@@ -0,0 +1,209 @@
import rule from '../../src/rules/consistent-type-definitions';
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('consistent-type-definitions', rule, {
valid: [
{
code: `var foo = { };`,
options: ['interface'],
},
{
code: `interface A {}`,
options: ['interface'],
},
{
code: `interface A extends B { x: number; }`,
options: ['interface'],
},
{
code: `type U = string;`,
options: ['interface'],
},
{
code: `type V = { x: number; } | { y: string; };`,
options: ['interface'],
},
{
code: `
type Record<T, U> = {
[K in T]: U;
}
`,
options: ['interface'],
},
{
code: `type V = { x: number; } | { y: string; };`,
options: ['interface'],
},
{
code: `type T = { x: number; }`,
options: ['type'],
},
{
code: `type T = { x: number; }`,
options: ['type'],
},
{
code: `type T = { x: number; }`,
options: ['type'],
},
{
code: `type A = { x: number; } & B & C;`,
options: ['type'],
},
{
code: `type A = { x: number; } & B<T1> & C<T2>;`,
options: ['type'],
},
{
code: `
export type W<T> = {
x: T,
};
`,
options: ['type'],
},
],
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
invalid: [
{
code: `type T = { x: number; };`,
output: `interface T { x: number; }`,
options: ['interface'],
errors: [
{
messageId: 'interfaceOverType',
line: 1,
column: 6,
},
],
},
{
code: `type T={ x: number; };`,
output: `interface T { x: number; }`,
options: ['interface'],
errors: [
{
messageId: 'interfaceOverType',
line: 1,
column: 6,
},
],
},
{
code: `type T= { x: number; };`,
output: `interface T { x: number; }`,
options: ['interface'],
errors: [
{
messageId: 'interfaceOverType',
line: 1,
column: 6,
},
],
},
{
code: `
export type W<T> = {
x: T,
};
`,
output: `
export interface W<T> {
x: T,
}
`,
options: ['interface'],
errors: [
{
messageId: 'interfaceOverType',
line: 2,
column: 13,
},
],
},
{
code: `interface T { x: number; }`,
output: `type T = { x: number; }`,
options: ['type'],
errors: [
{
messageId: 'typeOverInterface',
line: 1,
column: 11,
},
],
},
{
code: `interface T{ x: number; }`,
output: `type T = { x: number; }`,
options: ['type'],
errors: [
{
messageId: 'typeOverInterface',
line: 1,
column: 11,
},
],
},
{
code: `interface T { x: number; }`,
output: `type T = { x: number; }`,
options: ['type'],
errors: [
{
messageId: 'typeOverInterface',
line: 1,
column: 11,
},
],
},
{
code: `interface A extends B, C { x: number; };`,
output: `type A = { x: number; } & B & C;`,
options: ['type'],
errors: [
{
messageId: 'typeOverInterface',
line: 1,
column: 11,
},
],
},
{
code: `interface A extends B<T1>, C<T2> { x: number; };`,
output: `type A = { x: number; } & B<T1> & C<T2>;`,
options: ['type'],
errors: [
{
messageId: 'typeOverInterface',
line: 1,
column: 11,
},
],
},
{
code: `
export interface W<T> {
x: T,
};
`,
output: `
export type W<T> = {
x: T,
};
`,
options: ['type'],
errors: [
{
messageId: 'typeOverInterface',
line: 2,
column: 18,
},
],
},
],
});
2 changes: 1 addition & 1 deletion packages/eslint-plugin/typings/ts-eslint.d.ts
Expand Up @@ -262,7 +262,7 @@ declare module 'ts-eslint' {
/**
* The name of the rule this rule was replaced by, if it was deprecated.
*/
replacedBy?: string;
replacedBy?: string[];
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
/**
* The options schema. Supply an empty array if there are no options.
*/
Expand Down