Skip to content

Commit

Permalink
feat(eslint-plugin): [prefer-regexp-exec] add autofix (#3207)
Browse files Browse the repository at this point in the history
  • Loading branch information
phaux committed Apr 10, 2021
1 parent a5836be commit e2cbeef
Show file tree
Hide file tree
Showing 6 changed files with 632 additions and 99 deletions.
2 changes: 1 addition & 1 deletion packages/eslint-plugin/README.md
Expand Up @@ -162,7 +162,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-readonly-parameter-types`](./docs/rules/prefer-readonly-parameter-types.md) | Requires that function parameters are typed as readonly to prevent accidental mutation of inputs | | | :thought_balloon: |
| [`@typescript-eslint/prefer-reduce-type-parameter`](./docs/rules/prefer-reduce-type-parameter.md) | Prefer using type parameter when calling `Array#reduce` instead of casting | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :white_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :white_check_mark: | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-ts-expect-error`](./docs/rules/prefer-ts-expect-error.md) | Recommends using `@ts-expect-error` over `@ts-ignore` | | :wrench: | |
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: |
Expand Down
96 changes: 80 additions & 16 deletions packages/eslint-plugin/src/rules/prefer-regexp-exec.ts
@@ -1,9 +1,13 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import {
createRule,
getParserServices,
getStaticValue,
getTypeName,
getWrappingFixer,
} from '../util';

export default createRule({
Expand All @@ -12,6 +16,7 @@ export default createRule({

meta: {
type: 'suggestion',
fixable: 'code',
docs: {
description:
'Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided',
Expand All @@ -27,44 +32,103 @@ export default createRule({

create(context) {
const globalScope = context.getScope();
const service = getParserServices(context);
const typeChecker = service.program.getTypeChecker();
const parserServices = getParserServices(context);
const typeChecker = parserServices.program.getTypeChecker();
const sourceCode = context.getSourceCode();

/**
* Check if a given node is a string.
* @param node The node to check.
*/
function isStringType(node: TSESTree.LeftHandSideExpression): boolean {
function isStringType(node: TSESTree.Expression): boolean {
const objectType = typeChecker.getTypeAtLocation(
service.esTreeNodeToTSNodeMap.get(node),
parserServices.esTreeNodeToTSNodeMap.get(node),
);
return getTypeName(typeChecker, objectType) === 'string';
}

/**
* Check if a given node is a RegExp.
* @param node The node to check.
*/
function isRegExpType(node: TSESTree.Expression): boolean {
const objectType = typeChecker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node),
);
return getTypeName(typeChecker, objectType) === 'RegExp';
}

return {
"CallExpression[arguments.length=1] > MemberExpression.callee[property.name='match'][computed=false]"(
node: TSESTree.MemberExpression,
memberNode: TSESTree.MemberExpression,
): void {
const callNode = node.parent as TSESTree.CallExpression;
const arg = callNode.arguments[0];
const evaluated = getStaticValue(arg, globalScope);
const objectNode = memberNode.object;
const callNode = memberNode.parent as TSESTree.CallExpression;
const argumentNode = callNode.arguments[0];
const argumentValue = getStaticValue(argumentNode, globalScope);

if (!isStringType(objectNode)) {
return;
}

// Don't report regular expressions with global flag.
if (
evaluated &&
evaluated.value instanceof RegExp &&
evaluated.value.flags.includes('g')
argumentValue &&
argumentValue.value instanceof RegExp &&
argumentValue.value.flags.includes('g')
) {
return;
}

if (isStringType(node.object)) {
context.report({
node: callNode,
if (
argumentNode.type === AST_NODE_TYPES.Literal &&
typeof argumentNode.value == 'string'
) {
const regExp = RegExp(argumentNode.value);
return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
fix: getWrappingFixer({
sourceCode,
node: callNode,
innerNode: [objectNode],
wrap: objectCode => `${regExp.toString()}.exec(${objectCode})`,
}),
});
}

if (isRegExpType(argumentNode)) {
return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
fix: getWrappingFixer({
sourceCode,
node: callNode,
innerNode: [objectNode, argumentNode],
wrap: (objectCode, argumentCode) =>
`${argumentCode}.exec(${objectCode})`,
}),
});
return;
}

if (isStringType(argumentNode)) {
return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
fix: getWrappingFixer({
sourceCode,
node: callNode,
innerNode: [objectNode, argumentNode],
wrap: (objectCode, argumentCode) =>
`RegExp(${argumentCode}).exec(${objectCode})`,
}),
});
}

return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
});
},
};
},
Expand Down

0 comments on commit e2cbeef

Please sign in to comment.