Skip to content

Commit

Permalink
feat(eslint-plugin): add consistent-type-definitions rule
Browse files Browse the repository at this point in the history
  • Loading branch information
otofu-square committed Apr 26, 2019
1 parent 8e2d2f5 commit 02c1e55
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 0 deletions.
105 changes: 105 additions & 0 deletions packages/eslint-plugin/src/rules/consistent-type-definisions.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-definisions',
meta: {
type: 'suggestion',
docs: {
description:
'Consistent with type definition either `interface` or `type`',
category: 'Stylistic Issues',
recommended: false,
tslintName: 'consistent-type-definisions',
},
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;
},
});
}
},
};
},
});
153 changes: 153 additions & 0 deletions packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts
@@ -0,0 +1,153 @@
import rule from '../../src/rules/consistent-type-definisions';
import { RuleTester } from '../RuleTester';

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

ruleTester.run('consistent-type-definisions', rule, {
valid: [
`var foo = { };`,
`type U = string;`,
`type V = { x: number; } | { y: string; };`,
`
type Record<T, U> = {
[K in T]: U;
}
`,
],
invalid: [
{
code: `type T = { x: number; }`,
output: `interface T { x: number; }`,
errors: [
{
messageId: 'interfaceOverType',
line: 1,
column: 6,
},
],
},
{
code: `type T={ x: number; }`,
output: `interface T { x: number; }`,
errors: [
{
messageId: 'interfaceOverType',
line: 1,
column: 6,
},
],
},
{
code: `type T= { x: number; }`,
output: `interface T { x: number; }`,
errors: [
{
messageId: 'interfaceOverType',
line: 1,
column: 6,
},
],
},
{
code: `
export type W<T> = {
x: T,
};
`,
output: `
export interface W<T> {
x: T,
}
`,
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 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: `interface T { x: number; }`,
output: `type T = { x: number; }`,
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,
},
],
},
],
});

0 comments on commit 02c1e55

Please sign in to comment.