Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f61d421

Browse files
ldrickbradzacher
authored andcommitted
feat(eslint-plugin): add prefer-regexp-exec rule (typescript-eslint#305)
1 parent eb613ca commit f61d421

File tree

6 files changed

+361
-53
lines changed

6 files changed

+361
-53
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
162162
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
163163
| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: | |
164164
| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope. (`no-unbound-method` from TSLint) | :heavy_check_mark: | | :thought_balloon: |
165+
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce to use `RegExp#exec` over `String#match` | | | :thought_balloon: |
165166
| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one. (`unified-signatures` from TSLint) | | | |
166167

167168
<!-- end rule list -->
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Enforce to use `RegExp#exec` over `String#match` (prefer-regexp-exec)
2+
3+
`RegExp#exec` is faster than `String#match` and both work the same when not using the `/g` flag.
4+
5+
## Rule Details
6+
7+
This rule is aimed at enforcing the more performant way of applying regular expressions on strings.
8+
9+
From [`String#match` on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match):
10+
11+
> If the regular expression does not include the g flag, returns the same result as RegExp.exec().
12+
13+
From [Stack Overflow](https://stackoverflow.com/questions/9214754/what-is-the-difference-between-regexp-s-exec-function-and-string-s-match-fun)
14+
15+
> `RegExp.prototype.exec` is a lot faster than `String.prototype.match`, but that’s because they are not exactly the same thing, they are different.
16+
17+
Examples of **incorrect** code for this rule:
18+
19+
```ts
20+
'something'.match(/thing/);
21+
22+
'some things are just things'.match(/thing/);
23+
24+
const text = 'something';
25+
const search = /thing/;
26+
text.match(search);
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```ts
32+
/thing/.exec('something');
33+
34+
'some things are just things'.match(/thing/g);
35+
36+
const text = 'something';
37+
const search = /thing/;
38+
search.exec(text);
39+
```
40+
41+
## Options
42+
43+
There are no options.
44+
45+
```json
46+
{
47+
"@typescript-eslint/prefer-regexp-exec": "error"
48+
}
49+
```
50+
51+
## When Not To Use It
52+
53+
If you prefer consistent use of `String#match` for both, with `g` flag and without it, you can turn this rule off.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { TSESTree } from '@typescript-eslint/typescript-estree';
2+
import { createRule, getParserServices, getTypeName } from '../util';
3+
import { getStaticValue } from 'eslint-utils';
4+
5+
export default createRule({
6+
name: 'prefer-regexp-exec',
7+
defaultOptions: [],
8+
9+
meta: {
10+
type: 'suggestion',
11+
docs: {
12+
description:
13+
'Prefer RegExp#exec() over String#match() if no global flag is provided.',
14+
category: 'Best Practices',
15+
recommended: false,
16+
},
17+
messages: {
18+
regExpExecOverStringMatch: 'Use the `RegExp#exec()` method instead.',
19+
},
20+
schema: [],
21+
},
22+
23+
create(context) {
24+
const globalScope = context.getScope();
25+
const service = getParserServices(context);
26+
const typeChecker = service.program.getTypeChecker();
27+
28+
/**
29+
* Check if a given node is a string.
30+
* @param node The node to check.
31+
*/
32+
function isStringType(node: TSESTree.Node): boolean {
33+
const objectType = typeChecker.getTypeAtLocation(
34+
service.esTreeNodeToTSNodeMap.get(node),
35+
);
36+
return getTypeName(typeChecker, objectType) === 'string';
37+
}
38+
39+
return {
40+
"CallExpression[arguments.length=1] > MemberExpression.callee[property.name='match'][computed=false]"(
41+
node: TSESTree.MemberExpression,
42+
) {
43+
const callNode = node.parent as TSESTree.CallExpression;
44+
const arg = callNode.arguments[0];
45+
const evaluated = getStaticValue(arg, globalScope);
46+
47+
// Don't report regular expressions with global flag.
48+
if (
49+
evaluated &&
50+
evaluated.value instanceof RegExp &&
51+
evaluated.value.flags.includes('g')
52+
) {
53+
return;
54+
}
55+
56+
if (isStringType(node.object)) {
57+
context.report({
58+
node: callNode,
59+
messageId: 'regExpExecOverStringMatch',
60+
});
61+
return;
62+
}
63+
},
64+
};
65+
},
66+
});

packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
getStaticValue,
66
} from 'eslint-utils';
77
import { RegExpParser, AST as RegExpAST } from 'regexpp';
8-
import ts from 'typescript';
9-
import { createRule, getParserServices } from '../util';
8+
import { createRule, getParserServices, getTypeName } from '../util';
109

1110
const EQ_OPERATORS = /^[=!]=/;
1211
const regexpp = new RegExpParser();
@@ -35,65 +34,17 @@ export default createRule({
3534
const globalScope = context.getScope();
3635
const sourceCode = context.getSourceCode();
3736
const service = getParserServices(context);
38-
const types = service.program.getTypeChecker();
39-
40-
/**
41-
* Get the type name of a given type.
42-
* @param type The type to get.
43-
*/
44-
function getTypeName(type: ts.Type): string {
45-
// It handles `string` and string literal types as string.
46-
if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
47-
return 'string';
48-
}
49-
50-
// If the type is a type parameter which extends primitive string types,
51-
// but it was not recognized as a string like. So check the constraint
52-
// type of the type parameter.
53-
if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) {
54-
// `type.getConstraint()` method doesn't return the constraint type of
55-
// the type parameter for some reason. So this gets the constraint type
56-
// via AST.
57-
const node = type.symbol.declarations[0] as ts.TypeParameterDeclaration;
58-
if (node.constraint != null) {
59-
return getTypeName(types.getTypeFromTypeNode(node.constraint));
60-
}
61-
}
62-
63-
// If the type is a union and all types in the union are string like,
64-
// return `string`. For example:
65-
// - `"a" | "b"` is string.
66-
// - `string | string[]` is not string.
67-
if (
68-
type.isUnion() &&
69-
type.types.map(getTypeName).every(t => t === 'string')
70-
) {
71-
return 'string';
72-
}
73-
74-
// If the type is an intersection and a type in the intersection is string
75-
// like, return `string`. For example: `string & {__htmlEscaped: void}`
76-
if (
77-
type.isIntersection() &&
78-
type.types.map(getTypeName).some(t => t === 'string')
79-
) {
80-
return 'string';
81-
}
82-
83-
return types.typeToString(type);
84-
}
37+
const typeChecker = service.program.getTypeChecker();
8538

8639
/**
8740
* Check if a given node is a string.
8841
* @param node The node to check.
8942
*/
9043
function isStringType(node: TSESTree.Node): boolean {
91-
const objectType = types.getTypeAtLocation(
44+
const objectType = typeChecker.getTypeAtLocation(
9245
service.esTreeNodeToTSNodeMap.get(node),
9346
);
94-
const typeName = getTypeName(objectType);
95-
96-
return typeName === 'string';
47+
return getTypeName(typeChecker, objectType) === 'string';
9748
}
9849

9950
/**

packages/eslint-plugin/src/util/types.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,63 @@ export function containsTypeByName(
4141
);
4242
}
4343

44+
/**
45+
* Get the type name of a given type.
46+
* @param typeChecker The context sensitive TypeScript TypeChecker.
47+
* @param type The type to get the name of.
48+
*/
49+
export function getTypeName(
50+
typeChecker: ts.TypeChecker,
51+
type: ts.Type,
52+
): string {
53+
// It handles `string` and string literal types as string.
54+
if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
55+
return 'string';
56+
}
57+
58+
// If the type is a type parameter which extends primitive string types,
59+
// but it was not recognized as a string like. So check the constraint
60+
// type of the type parameter.
61+
if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) {
62+
// `type.getConstraint()` method doesn't return the constraint type of
63+
// the type parameter for some reason. So this gets the constraint type
64+
// via AST.
65+
const node = type.symbol.declarations[0] as ts.TypeParameterDeclaration;
66+
if (node.constraint != null) {
67+
return getTypeName(
68+
typeChecker,
69+
typeChecker.getTypeFromTypeNode(node.constraint),
70+
);
71+
}
72+
}
73+
74+
// If the type is a union and all types in the union are string like,
75+
// return `string`. For example:
76+
// - `"a" | "b"` is string.
77+
// - `string | string[]` is not string.
78+
if (
79+
type.isUnion() &&
80+
type.types
81+
.map(value => getTypeName(typeChecker, value))
82+
.every(t => t === 'string')
83+
) {
84+
return 'string';
85+
}
86+
87+
// If the type is an intersection and a type in the intersection is string
88+
// like, return `string`. For example: `string & {__htmlEscaped: void}`
89+
if (
90+
type.isIntersection() &&
91+
type.types
92+
.map(value => getTypeName(typeChecker, value))
93+
.some(t => t === 'string')
94+
) {
95+
return 'string';
96+
}
97+
98+
return typeChecker.typeToString(type);
99+
}
100+
44101
/**
45102
* Resolves the given node's type. Will resolve to the type's generic constraint, if it has one.
46103
*/

0 commit comments

Comments
 (0)