From 03dca0c4fe550b24466b7a8b6b2aede75ab50b4d Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Sat, 7 May 2022 17:44:21 +0800 Subject: [PATCH] feat(eslint-plugin): new rule consistent-generic-constructors --- .../rules/consistent-generic-constructors.ts | 87 ++++++++++++ .../consistent-generic-constructors.test.ts | 132 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/consistent-generic-constructors.ts create mode 100644 packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts diff --git a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts new file mode 100644 index 00000000000..17d019cd85a --- /dev/null +++ b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts @@ -0,0 +1,87 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import { createRule } from '../util'; + +type MessageIds = 'preferLHS' | 'preferRHS'; +type Options = ['lhs' | 'rhs']; + +export default createRule({ + name: 'consistent-generic-constructors', + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce specifying generic type arguments on LHS or RHS of constructor call', + recommended: false, + }, + messages: { + preferLHS: + 'The generic type arguments should be specified on the left-hand side of the constructor call.', + preferRHS: + 'The generic type arguments should be specified on the right-hand side of the constructor call.', + }, + fixable: 'code', + schema: [ + { + enum: ['lhs', 'rhs'], + }, + ], + }, + defaultOptions: ['rhs'], + create(context, [mode]) { + return { + VariableDeclarator(node: TSESTree.VariableDeclarator): void { + const sourceCode = context.getSourceCode(); + const lhs = node.id.typeAnnotation?.typeAnnotation; + const rhs = node.init; + if ( + !rhs || + rhs.type !== AST_NODE_TYPES.NewExpression || + rhs.callee.type !== AST_NODE_TYPES.Identifier + ) { + return; + } + if ( + lhs && + (lhs.type !== AST_NODE_TYPES.TSTypeReference || + lhs.typeName.type !== AST_NODE_TYPES.Identifier) + ) { + return; + } + if (mode === 'lhs' && !lhs && rhs.typeParameters) { + context.report({ + node, + messageId: 'preferLHS', + fix(fixer) { + const { typeParameters, callee } = rhs; + const typeAnnotation = + sourceCode.getText(callee) + sourceCode.getText(typeParameters); + return [ + fixer.remove(typeParameters!), + fixer.insertTextAfter(node.id, ': ' + typeAnnotation), + ]; + }, + }); + } else if ( + mode === 'rhs' && + lhs?.typeParameters && + !rhs.typeParameters && + (lhs.typeName as TSESTree.Identifier).name === rhs.callee.name + ) { + context.report({ + node, + messageId: 'preferRHS', + fix(fixer) { + return [ + fixer.remove(lhs.parent!), + fixer.insertTextAfter( + rhs.callee, + sourceCode.getText(lhs.typeParameters), + ), + ]; + }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts b/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts new file mode 100644 index 00000000000..16038b88534 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts @@ -0,0 +1,132 @@ +import rule from '../../src/rules/consistent-generic-constructors'; +import { RuleTester, noFormat } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('consistent-generic-constructors', rule, { + valid: [ + // default: rhs + 'const a = new Foo();', + 'const a = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Bar = new Foo();', + // lhs + { + code: 'const a = new Foo();', + options: ['lhs'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['lhs'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['lhs'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['lhs'], + }, + { + code: 'const a: Bar = new Foo();', + options: ['lhs'], + }, + { + code: 'const a: Bar = new Foo();', + options: ['lhs'], + }, + ], + invalid: [ + { + code: 'const a: Foo = new Foo();', + errors: [ + { + messageId: 'preferRHS', + }, + ], + output: 'const a = new Foo();', + }, + { + code: 'const a: Map = new Map();', + errors: [ + { + messageId: 'preferRHS', + }, + ], + output: 'const a = new Map();', + }, + { + code: noFormat`const a: Map = new Map();`, + errors: [ + { + messageId: 'preferRHS', + }, + ], + output: noFormat`const a = new Map();`, + }, + { + code: noFormat`const a: Map< string, number > = new Map();`, + errors: [ + { + messageId: 'preferRHS', + }, + ], + output: noFormat`const a = new Map< string, number >();`, + }, + { + code: noFormat`const a: Map = new Map ();`, + errors: [ + { + messageId: 'preferRHS', + }, + ], + output: noFormat`const a = new Map ();`, + }, + { + code: 'const a = new Foo();', + options: ['lhs'], + errors: [ + { + messageId: 'preferLHS', + }, + ], + output: 'const a: Foo = new Foo();', + }, + { + code: 'const a = new Map();', + options: ['lhs'], + errors: [ + { + messageId: 'preferLHS', + }, + ], + output: 'const a: Map = new Map();', + }, + { + code: noFormat`const a = new Map ();`, + options: ['lhs'], + errors: [ + { + messageId: 'preferLHS', + }, + ], + output: noFormat`const a: Map = new Map ();`, + }, + { + code: noFormat`const a = new Map< string, number >();`, + options: ['lhs'], + errors: [ + { + messageId: 'preferLHS', + }, + ], + output: noFormat`const a: Map< string, number > = new Map();`, + }, + ], +});