Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [prefer-regexp-exec] add autofix #3207

Merged
merged 5 commits into from Apr 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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