Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [ban-types] handle empty type literal {} (#1348)
  • Loading branch information
a-tarasyuk authored and bradzacher committed Dec 20, 2019
1 parent e51048c commit 1c0ce9b
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 30 deletions.
7 changes: 7 additions & 0 deletions packages/eslint-plugin/docs/rules/ban-types.md
Expand Up @@ -31,6 +31,8 @@ class Foo<F = string> extends Bar<string> implements Baz<string> {

## Options

The banned type can either be a type name literal (`Foo`), a type name with generic parameter instantiations(s) (`Foo<Bar>`), or the empty object literal (`{}`).

```CJSON
{
"@typescript-eslint/ban-types": ["error", {
Expand All @@ -46,6 +48,11 @@ class Foo<F = string> extends Bar<string> implements Baz<string> {
"message": "Use string instead",
"fixWith": "string"
}
"{}": {
"message": "Use object instead",
"fixWith": "object"
}
}
}]
}
Expand Down
85 changes: 55 additions & 30 deletions 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(
Expand Down Expand Up @@ -106,28 +112,47 @@ export default util.createRule<Options, MessageIds>({
},
},
],
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);
},
};
},
Expand Down
117 changes: 117 additions & 0 deletions packages/eslint-plugin/tests/rules/ban-types.test.ts
Expand Up @@ -27,6 +27,8 @@ const options: InferOptionsTypeFromRule<typeof rule> = [
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,
Expand Down Expand Up @@ -295,5 +297,120 @@ let b: Foo<NS.Good>;
],
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',
},
},
},
],
},
],
});

0 comments on commit 1c0ce9b

Please sign in to comment.