Skip to content

Commit 85b7de5

Browse files
scottohararafaelss95
authored andcommitted
feat(eslint-plugin): [no-type-alias]: add allowGenerics option (typescript-eslint#3865)
1 parent 25a42c0 commit 85b7de5

File tree

5 files changed

+137
-5
lines changed

5 files changed

+137
-5
lines changed

Diff for: packages/eslint-plugin/docs/rules/no-type-alias.md

+23
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ or more of the following you may pass an object with the options set as follows:
8989
- `allowLiterals` set to `"always"` will allow you to use type aliases with literal objects (Defaults to `"never"`)
9090
- `allowMappedTypes` set to `"always"` will allow you to use type aliases as mapping tools (Defaults to `"never"`)
9191
- `allowTupleTypes` set to `"always"` will allow you to use type aliases with tuples (Defaults to `"never"`)
92+
- `allowGenerics` set to `"always"` will allow you to use type aliases with generics (Defaults to `"never"`)
9293

9394
### `allowAliases`
9495

@@ -555,6 +556,28 @@ type Foo = [number] & [number, number];
555556
type Foo = [string] | [number];
556557
```
557558

559+
### `allowGenerics`
560+
561+
This applies to generic types, including TypeScript provided global utility types (`type Foo = Record<string, number>`).
562+
563+
The setting accepts the following options:
564+
565+
- `"always"` or `"never"` to active or deactivate the feature.
566+
567+
Examples of **correct** code for the `{ "allowGenerics": "always" }` options:
568+
569+
```ts
570+
type Foo = Bar<string>;
571+
572+
type Foo = Record<string, number>;
573+
574+
type Foo = Readonly<Bar>;
575+
576+
type Foo = Partial<Bar>;
577+
578+
type Foo = Omit<Bar, 'a' | 'b'>;
579+
```
580+
558581
## When Not To Use It
559582

560583
When you can't express some shape with an interface or you need to use a union, tuple type,

Diff for: packages/eslint-plugin/src/rules/no-type-alias.ts

+17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Options = [
2727
allowLiterals?: Values;
2828
allowMappedTypes?: Values;
2929
allowTupleTypes?: Values;
30+
allowGenerics?: 'always' | 'never';
3031
},
3132
];
3233
type MessageIds = 'noTypeAlias' | 'noCompositionAlias';
@@ -79,6 +80,9 @@ export default util.createRule<Options, MessageIds>({
7980
allowTupleTypes: {
8081
enum: enumValues,
8182
},
83+
allowGenerics: {
84+
enum: ['always', 'never'],
85+
},
8286
},
8387
additionalProperties: false,
8488
},
@@ -93,6 +97,7 @@ export default util.createRule<Options, MessageIds>({
9397
allowLiterals: 'never',
9498
allowMappedTypes: 'never',
9599
allowTupleTypes: 'never',
100+
allowGenerics: 'never',
96101
},
97102
],
98103
create(
@@ -106,6 +111,7 @@ export default util.createRule<Options, MessageIds>({
106111
allowLiterals,
107112
allowMappedTypes,
108113
allowTupleTypes,
114+
allowGenerics,
109115
},
110116
],
111117
) {
@@ -203,6 +209,13 @@ export default util.createRule<Options, MessageIds>({
203209
return false;
204210
};
205211

212+
const isValidGeneric = (type: TypeWithLabel): boolean => {
213+
return (
214+
type.node.type === AST_NODE_TYPES.TSTypeReference &&
215+
type.node.typeParameters !== undefined
216+
);
217+
};
218+
206219
const checkAndReport = (
207220
optionValue: Values,
208221
isTopLevel: boolean,
@@ -260,6 +273,10 @@ export default util.createRule<Options, MessageIds>({
260273
} else if (isValidTupleType(type)) {
261274
// tuple types
262275
checkAndReport(allowTupleTypes!, isTopLevel, type, 'Tuple Types');
276+
} else if (isValidGeneric(type)) {
277+
if (allowGenerics === 'never') {
278+
reportError(type.node, type.compositionType, isTopLevel, 'Generics');
279+
}
263280
} else if (
264281
// eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
265282
type.node.type.endsWith('Keyword') ||

Diff for: packages/eslint-plugin/src/rules/prefer-regexp-exec.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,31 @@ export default createRule({
7575
return result;
7676
}
7777

78+
function isLikelyToContainGlobalFlag(
79+
node: TSESTree.CallExpressionArgument,
80+
): boolean {
81+
if (
82+
node.type === AST_NODE_TYPES.CallExpression ||
83+
node.type === AST_NODE_TYPES.NewExpression
84+
) {
85+
const [, flags] = node.arguments;
86+
return (
87+
flags.type === AST_NODE_TYPES.Literal &&
88+
typeof flags.value === 'string' &&
89+
flags.value.includes('g')
90+
);
91+
}
92+
93+
return node.type === AST_NODE_TYPES.Identifier;
94+
}
95+
7896
return {
7997
"CallExpression[arguments.length=1] > MemberExpression.callee[property.name='match'][computed=false]"(
8098
memberNode: TSESTree.MemberExpression,
8199
): void {
82100
const objectNode = memberNode.object;
83101
const callNode = memberNode.parent as TSESTree.CallExpression;
84-
const argumentNode = callNode.arguments[0];
102+
const [argumentNode] = callNode.arguments;
85103
const argumentValue = getStaticValue(argumentNode, globalScope);
86104

87105
if (
@@ -96,9 +114,10 @@ export default createRule({
96114

97115
// Don't report regular expressions with global flag.
98116
if (
99-
argumentValue &&
100-
argumentValue.value instanceof RegExp &&
101-
argumentValue.value.flags.includes('g')
117+
(!argumentValue && isLikelyToContainGlobalFlag(argumentNode)) ||
118+
(argumentValue &&
119+
argumentValue.value instanceof RegExp &&
120+
argumentValue.value.flags.includes('g'))
102121
) {
103122
return;
104123
}

Diff for: packages/eslint-plugin/tests/rules/no-type-alias.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,10 @@ type KeyNames = keyof typeof SCALARS;
481481
code: 'type Foo = new (bar: number) => string | null;',
482482
options: [{ allowConstructors: 'always' }],
483483
},
484+
{
485+
code: 'type Foo = Record<string, number>;',
486+
options: [{ allowGenerics: 'always' }],
487+
},
484488
],
485489
invalid: [
486490
{
@@ -3329,5 +3333,18 @@ type Foo<T> = {
33293333
},
33303334
],
33313335
},
3336+
{
3337+
code: 'type Foo = Record<string, number>;',
3338+
errors: [
3339+
{
3340+
messageId: 'noTypeAlias',
3341+
data: {
3342+
alias: 'generics',
3343+
},
3344+
line: 1,
3345+
column: 12,
3346+
},
3347+
],
3348+
},
33323349
],
33333350
});

Diff for: packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import rule from '../../src/rules/prefer-regexp-exec';
2-
import { RuleTester, getFixturesRootDir } from '../RuleTester';
2+
import { getFixturesRootDir, RuleTester } from '../RuleTester';
33

44
const rootPath = getFixturesRootDir();
55

@@ -56,6 +56,24 @@ const matchers = [{ match: (s: RegExp) => false }];
5656
const file = '';
5757
matchers.some(matcher => !!file.match(matcher));
5858
`,
59+
// https://github.com/typescript-eslint/typescript-eslint/issues/3477
60+
`
61+
function test(pattern: string) {
62+
'hello hello'.match(RegExp(pattern, 'g'))?.reduce(() => []);
63+
}
64+
`,
65+
// https://github.com/typescript-eslint/typescript-eslint/issues/3477
66+
`
67+
function test(pattern: string) {
68+
'hello hello'.match(new RegExp(pattern, 'gi'))?.reduce(() => []);
69+
}
70+
`,
71+
// https://github.com/typescript-eslint/typescript-eslint/issues/3477
72+
`
73+
const matchCount = (str: string, re: RegExp) => {
74+
return (str.match(re) || []).length;
75+
};
76+
`,
5977
],
6078
invalid: [
6179
{
@@ -174,6 +192,44 @@ function f<T extends 'a' | 'b'>(s: T) {
174192
output: `
175193
function f<T extends 'a' | 'b'>(s: T) {
176194
/thing/.exec(s);
195+
}
196+
`,
197+
},
198+
{
199+
code: `
200+
const text = 'something';
201+
const search = new RegExp('test', '');
202+
text.match(search);
203+
`,
204+
errors: [
205+
{
206+
messageId: 'regExpExecOverStringMatch',
207+
line: 4,
208+
column: 6,
209+
},
210+
],
211+
output: `
212+
const text = 'something';
213+
const search = new RegExp('test', '');
214+
search.exec(text);
215+
`,
216+
},
217+
{
218+
code: `
219+
function test(pattern: string) {
220+
'check'.match(new RegExp(pattern, undefined));
221+
}
222+
`,
223+
errors: [
224+
{
225+
messageId: 'regExpExecOverStringMatch',
226+
line: 3,
227+
column: 11,
228+
},
229+
],
230+
output: `
231+
function test(pattern: string) {
232+
new RegExp(pattern, undefined).exec('check');
177233
}
178234
`,
179235
},

0 commit comments

Comments
 (0)