From 1c0ce9b6110c1d93cd64a3465a7f18bc2f7c034b Mon Sep 17 00:00:00 2001 From: Alexander T Date: Fri, 20 Dec 2019 02:42:29 +0200 Subject: [PATCH] feat(eslint-plugin): [ban-types] handle empty type literal {} (#1348) --- .../eslint-plugin/docs/rules/ban-types.md | 7 ++ packages/eslint-plugin/src/rules/ban-types.ts | 85 ++++++++----- .../tests/rules/ban-types.test.ts | 117 ++++++++++++++++++ 3 files changed, 179 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/ban-types.md b/packages/eslint-plugin/docs/rules/ban-types.md index 4da27a93041..d3478062998 100644 --- a/packages/eslint-plugin/docs/rules/ban-types.md +++ b/packages/eslint-plugin/docs/rules/ban-types.md @@ -31,6 +31,8 @@ class Foo extends Bar implements Baz { ## Options +The banned type can either be a type name literal (`Foo`), a type name with generic parameter instantiations(s) (`Foo`), or the empty object literal (`{}`). + ```CJSON { "@typescript-eslint/ban-types": ["error", { @@ -46,6 +48,11 @@ class Foo extends Bar implements Baz { "message": "Use string instead", "fixWith": "string" } + + "{}": { + "message": "Use object instead", + "fixWith": "object" + } } }] } diff --git a/packages/eslint-plugin/src/rules/ban-types.ts b/packages/eslint-plugin/src/rules/ban-types.ts index a8c2cec6c06..d15d822de06 100644 --- a/packages/eslint-plugin/src/rules/ban-types.ts +++ b/packages/eslint-plugin/src/rules/ban-types.ts @@ -1,26 +1,32 @@ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; +type Types = Record< + string, + | string + | null + | { + message: string; + fixWith?: string; + } +>; + type Options = [ { - types: Record< - string, - | string - | null - | { - message: string; - fixWith?: string; - } - >; + types: Types; }, ]; type MessageIds = 'bannedTypeMessage'; +function removeSpaces(str: string): string { + return str.replace(/ /g, ''); +} + function stringifyTypeName( - node: TSESTree.EntityName, + node: TSESTree.EntityName | TSESTree.TSTypeLiteral, sourceCode: TSESLint.SourceCode, ): string { - return sourceCode.getText(node).replace(/ /g, ''); + return removeSpaces(sourceCode.getText(node)); } function getCustomMessage( @@ -106,28 +112,47 @@ export default util.createRule({ }, }, ], - create(context, [{ types: bannedTypes }]) { - return { - TSTypeReference({ typeName }): void { - const name = stringifyTypeName(typeName, context.getSourceCode()); + create(context, [{ types }]) { + const bannedTypes: Types = Object.keys(types).reduce( + (res, type) => ({ ...res, [removeSpaces(type)]: types[type] }), + {}, + ); - if (name in bannedTypes) { - const bannedType = bannedTypes[name]; - const customMessage = getCustomMessage(bannedType); - const fixWith = - bannedType && typeof bannedType === 'object' && bannedType.fixWith; + function checkBannedTypes( + typeNode: TSESTree.EntityName | TSESTree.TSTypeLiteral, + ): void { + const name = stringifyTypeName(typeNode, context.getSourceCode()); - context.report({ - node: typeName, - messageId: 'bannedTypeMessage', - data: { - name: name, - customMessage, - }, - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - fix: fixWith ? fixer => fixer.replaceText(typeName, fixWith) : null, - }); + if (name in bannedTypes) { + const bannedType = bannedTypes[name]; + const customMessage = getCustomMessage(bannedType); + const fixWith = + bannedType && typeof bannedType === 'object' && bannedType.fixWith; + + context.report({ + node: typeNode, + messageId: 'bannedTypeMessage', + data: { + name, + customMessage, + }, + fix: fixWith + ? (fixer): TSESLint.RuleFix => fixer.replaceText(typeNode, fixWith) + : null, + }); + } + } + + return { + TSTypeLiteral(node): void { + if (node.members.length) { + return; } + + checkBannedTypes(node); + }, + TSTypeReference({ typeName }): void { + checkBannedTypes(typeName); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/ban-types.test.ts b/packages/eslint-plugin/tests/rules/ban-types.test.ts index 73427fcc806..416c71a1e7a 100644 --- a/packages/eslint-plugin/tests/rules/ban-types.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-types.test.ts @@ -27,6 +27,8 @@ const options: InferOptionsTypeFromRule = [ ruleTester.run('ban-types', rule, { valid: [ 'let f = Object();', // Should not fail if there is no options set + 'let f: {} = {};', + 'let f: { x: number, y: number } = { x: 1, y: 1 };', { code: 'let f = Object();', options, @@ -295,5 +297,120 @@ let b: Foo; ], options, }, + { + code: `let foo: {} = {};`, + output: `let foo: object = {};`, + options: [ + { + types: { + '{}': { + message: 'Use object instead.', + fixWith: 'object', + }, + }, + }, + ], + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: '{}', + customMessage: ' Use object instead.', + }, + line: 1, + column: 10, + }, + ], + }, + { + code: ` +let foo: {} = {}; +let bar: { } = {}; + `, + output: ` +let foo: object = {}; +let bar: object = {}; + `, + options: [ + { + types: { + '{ }': { + message: 'Use object instead.', + fixWith: 'object', + }, + }, + }, + ], + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: '{}', + customMessage: ' Use object instead.', + }, + line: 2, + column: 10, + }, + { + messageId: 'bannedTypeMessage', + data: { + name: '{}', + customMessage: ' Use object instead.', + }, + line: 3, + column: 10, + }, + ], + }, + { + code: 'let a: NS.Bad;', + output: 'let a: NS.Good;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'NS.Bad', + customMessage: ' Use NS.Good instead.', + }, + line: 1, + column: 8, + }, + ], + options: [ + { + types: { + ' NS.Bad ': { + message: 'Use NS.Good instead.', + fixWith: 'NS.Good', + }, + }, + }, + ], + }, + { + code: 'let a: Foo< F >;', + output: 'let a: Foo< T >;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'F', + customMessage: ' Use T instead.', + }, + line: 1, + column: 15, + }, + ], + options: [ + { + types: { + ' F ': { + message: 'Use T instead.', + fixWith: 'T', + }, + }, + }, + ], + }, ], });