From 02c1e55b6fa32229e56364ff33c43aae40d4a3a5 Mon Sep 17 00:00:00 2001 From: otofu-square Date: Fri, 26 Apr 2019 10:17:28 +0900 Subject: [PATCH] feat(eslint-plugin): add consistent-type-definitions rule --- .../src/rules/consistent-type-definisions.ts | 105 ++++++++++++ .../rules/consistent-type-definitions.test.ts | 153 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/consistent-type-definisions.ts create mode 100644 packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts diff --git a/packages/eslint-plugin/src/rules/consistent-type-definisions.ts b/packages/eslint-plugin/src/rules/consistent-type-definisions.ts new file mode 100644 index 000000000000..0b1b300c6ab7 --- /dev/null +++ b/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; + }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts new file mode 100644 index 000000000000..21479df92d71 --- /dev/null +++ b/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 = { + [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 = { + x: T, +}; +`, + output: ` +export interface W { + 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, C { x: number; };`, + output: `type A = { x: number; } & B & C;`, + 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 { + x: T, +}; +`, + output: ` +export type W = { + x: T, +}; +`, + options: ['type'], + errors: [ + { + messageId: 'typeOverInterface', + line: 2, + column: 18, + }, + ], + }, + ], +});