8000 feat(eslint-plugin): [ban-types] handle empty type literal {} (#1348) · sstephens/typescript-eslint@1c0ce9b · GitHub
[go: up one dir, main page]

Skip to content

Commit 1c0ce9b

Browse files
a-tarasyukbradzacher
authored andcommitted
feat(eslint-plugin): [ban-types] handle empty type literal {} (typescript-eslint#1348)
1 parent e51048c commit 1c0ce9b

File tree

3 files changed

+179
-30
lines changed

3 files changed

+179
-30
lines changed

packages/eslint-plugin/docs/rules/ban-types.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class Foo<F = string> extends Bar<string> implements Baz<string> {
3131

3232
## Options
3333

34+
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 (`{}`).
35+
3436
```CJSON
3537
{
3638
"@typescript-eslint/ban-types": ["error", {
@@ -46,6 +48,11 @@ class Foo<F = string> extends Bar<string> implements Baz<string> {
4648
"message": "Use string instead",
4749
"fixWith": "string"
4850
}
51+
52+
"{}": {
53+
"message": "Use object instead",
54+
"fixWith": "object"
55+
}
4956
}
5057
}]
5158
}

packages/eslint-plugin/src/rules/ban-types.ts

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
22
import * as util from '../util';
33

4+
type Types = Record<
5+
string,
6+
| string
7+
| null
8+
| {
9+
message: string;
10+
fixWith?: string;
11+
}
12+
>;
13+
414
type Options = [
515
{
6-
types: Record<
7-
string,
8-
| string
9-
| null
10-
| {
11-
message: string;
12-
fixWith?: string;
13-
}
14-
>;
16+
types: Types;
1517
},
1618
];
1719
type MessageIds = 'bannedTypeMessage';
1820

21+
function removeSpaces(str: string): string {
22+
return str.replace(/ /g, '');
23+
}
24+
1925
function stringifyTypeName(
20-
node: TSESTree.EntityName,
26+
node: TSESTree.EntityName | TSESTree.TSTypeLiteral,
2127
sourceCode: TSESLint.SourceCode,
2228
): string {
23-
return sourceCode.getText(node).replace(/ /g, '');
29+
return removeSpaces(sourceCode.getText(node));
2430
}
2531

2632
function getCustomMessage(
@@ -106,28 +112,47 @@ export default util.createRule<Options, MessageIds>({
106112
},
107113
},
108114
],
109-
create(context, [{ types: bannedTypes }]) {
110-
return {
111-
TSTypeReference({ typeName }): void {
112-
const name = stringifyTypeName(typeName, context.getSourceCode());
115+
create(context, [{ types }]) {
116+
const bannedTypes: Types = Object.keys(types).reduce(
117+
(res, type) => ({ ...res, [removeSpaces(type)]: types[type] }),
118+
{},
119+
);
113120

114-
if (name in bannedTypes) {
115-
const bannedType = bannedTypes[name];
116-
const customMessage = getCustomMessage(bannedType);
117-
const fixWith =
118-
bannedType && typeof bannedType === 'object' && bannedType.fixWith;
121+
function checkBannedTypes(
122+
typeNode: TSESTree.EntityName | TSESTree.TSTypeLiteral,
123+
): void {
124+
const name = stringifyTypeName(typeNode, context.getSourceCode());
119125

120-
context.report({
121-
node: typeName,
122-
messageId: 'bannedTypeMessage',
123-
data: {
124-
name: name,
125-
customMessage,
126-
},
127-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
128-
fix: fixWith ? fixer => fixer.replaceText(typeName, fixWith) : null,
129-
});
126+
if (name in bannedTypes) {
127+
const bannedType = bannedTypes[name];
128+
const customMessage = getCustomMessage(bannedType);
129+
const fixWith =
130+
bannedType && typeof bannedType === 'object' && bannedType.fixWith;
131+
132+
context.report({
133+
node: typeNode,
134+
messageId: 'bannedTypeMessage',
135+
data: {
136+
name,
137+
customMessage,
138+
},
139+
fix: fixWith
140+
? (fixer): TSESLint.RuleFix => fixer.replaceText(typeNode, fixWith)
141+
: null,
142+
});
143+
}
144+
}
145+
146+
return {
147+
TSTypeLiteral(node): void {
148+
if (node.members.length) {
149+
return;
130150
}
151+
152+
checkBannedTypes(node);
153+
},
154+
TSTypeReference({ typeName }): void {
155+
checkBannedTypes(typeName);
131156
},
132157
};
133158
},

packages/eslint-plugin/tests/rules/ban-types.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const options: InferOptionsTypeFromRule<typeof rule> = [
2727
ruleTester.run('ban-types', rule, {
2828
valid: [
2929
'let f = Object();', // Should not fail if there is no options set
30+
'let f: {} = {};',
31+
'let f: { x: number, y: number } = { x: 1, y: 1 };',
3032
{
3133
code: 'let f = Object();',
3234
options,
@@ -295,5 +297,120 @@ let b: Foo<NS.Good>;
295297
],
296298
options,
297299
},
300+
{
301+
code: `let foo: {} = {};`,
302+
output: `let foo: object = {};`,
303+
options: [
304+
{
305+
types: {
306+
'{}': {
307+
message: 'Use object instead.',
308+
fixWith: 'object',
309+
},
310+
},
311+
},
312+
],
313+
errors: [
314+
{
315+
messageId: 'bannedTypeMessage',
316+
data: {
317+
name: '{}',
318+
customMessage: ' Use object instead.',
319+
},
320+
line: 1,
321+
column: 10,
322+
},
323+
],
324+
},
325+
{
326+
code: `
327+
let foo: {} = {};
328+
let bar: { } = {};
329+
`,
330+
output: `
331+
let foo: object = {};
332+
let bar: object = {};
333+
`,
334+
options: [
335+
{
336+
types: {
337+
'{ }': {
338+
message: 'Use object instead.',
339+
fixWith: 'object',
340+
},
341+
},
342+
},
343+
],
344+
errors: [
345+
{
346+
messageId: 'bannedTypeMessage',
347+
data: {
348+
name: '{}',
349+
customMessage: ' Use object instead.',
350+
},
351+
line: 2,
352+
column: 10,
353+
},
354+
{
355+
messageId: 'bannedTypeMessage',
356+
data: {
357+
name: '{}',
358+
customMessage: ' Use object instead.',
359+
},
360+
line: 3,
361+
column: 10,
362+
},
363+
],
364+
},
365+
{
366+
code: 'let a: NS.Bad;',
367+
output: 'let a: NS.Good;',
368+
errors: [
369+
{
370+
messageId: 'bannedTypeMessage',
371+
data: {
372+
name: 'NS.Bad',
373+
customMessage: ' Use NS.Good instead.',
374+
},
375+
line: 1,
376+
column: 8,
377+
},
378+
],
379+
options: [
380+
{
381+
types: {
382+
' NS.Bad ': {
383+
message: 'Use NS.Good instead.',
384+
fixWith: 'NS.Good',
385+
},
386+
},
387+
},
388+
],
389+
},
390+
{
391+
code: 'let a: Foo< F >;',
392+
output: 'let a: Foo< T >;',
393+
errors: [
394+
{
395+
messageId: 'bannedTypeMessage',
396+
A463 data: {
397+
name: 'F',
398+
customMessage: ' Use T instead.',
399+
},
400+
line: 1,
401+
column: 15,
402+
},
403+
],
404+
options: [
405+
{
406+
types: {
407+
' F ': {
408+
message: 'Use T instead.',
409+
fixWith: 'T',
410+
},
411+
},
412+
},
413+
],
414+
},
298415
],
299416
});

0 commit comments

Comments
 (0)
0