Skip to content

Commit

Permalink
fix(eslint-plugin): [prefer-regexp-exec] factor in union types
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh Goldberg committed May 23, 2021
1 parent 21d1b62 commit c79e813
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 45 deletions.
129 changes: 84 additions & 45 deletions packages/eslint-plugin/src/rules/prefer-regexp-exec.ts
Expand Up @@ -2,6 +2,8 @@ import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as tsutils from 'tsutils';
import * as ts from 'typescript';
import {
createRule,
getParserServices,
Expand All @@ -10,6 +12,13 @@ import {
getWrappingFixer,
} from '../util';

enum ArgumentType {
Both,
Other,
RegExp,
String,
}

export default createRule({
name: 'prefer-regexp-exec',
defaultOptions: [],
Expand Down Expand Up @@ -37,25 +46,41 @@ export default createRule({
const sourceCode = context.getSourceCode();

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

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

function collectArgumentTypes(types: ts.Type[]): ArgumentType {
let result = ArgumentType.Other;

for (const type of types) {
if (isRegExpType(type)) {
if (result === ArgumentType.Other) {
result = ArgumentType.RegExp;
} else if (result === ArgumentType.String) {
result = ArgumentType.Both;
}
} else if (isStringType(type)) {
if (result === ArgumentType.Other) {
result = ArgumentType.String;
} else if (result === ArgumentType.RegExp) {
result = ArgumentType.Both;
}
}
}

return result;
}

return {
Expand All @@ -67,7 +92,13 @@ export default createRule({
const argumentNode = callNode.arguments[0];
const argumentValue = getStaticValue(argumentNode, globalScope);

if (!isStringType(objectNode)) {
if (
!isStringType(
typeChecker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(objectNode),
),
)
) {
return;
}

Expand Down Expand Up @@ -97,38 +128,46 @@ export default createRule({
});
}

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})`,
}),
});
}
const argumentType = typeChecker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(argumentNode),
);
const argumentTypes = collectArgumentTypes(
tsutils.unionTypeParts(argumentType),
);
switch (argumentTypes) {
case ArgumentType.Both:
case ArgumentType.Other:
return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
});

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})`,
}),
});
}
case ArgumentType.RegExp:
return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
fix: getWrappingFixer({
sourceCode,
node: callNode,
innerNode: [objectNode, argumentNode],
wrap: (objectCode, argumentCode) =>
`${argumentCode}.exec(${objectCode})`,
}),
});

return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
});
case ArgumentType.String:
return context.report({
node: memberNode.property,
messageId: 'regExpExecOverStringMatch',
fix: getWrappingFixer({
sourceCode,
node: callNode,
innerNode: [objectNode, argumentNode],
wrap: (objectCode, argumentCode) =>
`RegExp(${argumentCode}).exec(${objectCode})`,
}),
});
}
},
};
},
Expand Down
42 changes: 42 additions & 0 deletions packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts
Expand Up @@ -227,5 +227,47 @@ function f<T extends 'a' | 'b'>(s: T) {
}
`,
},
{
code: `
const matchers = ['package-lock.json', /regexp/];
const file = '';
matchers.some(matcher => !!file.match(matcher));
`,
errors: [
{
messageId: 'regExpExecOverStringMatch',
line: 4,
column: 33,
},
],
},
{
code: `
const matchers = [/regexp/, 'package-lock.json'];
const file = '';
matchers.some(matcher => !!file.match(matcher));
`,
errors: [
{
messageId: 'regExpExecOverStringMatch',
line: 4,
column: 33,
},
],
},
{
code: `
const matchers = [{ match: (s: RegExp) => false }];
const file = '';
matchers.some(matcher => !!file.match(matcher));
`,
errors: [
{
messageId: 'regExpExecOverStringMatch',
line: 4,
column: 33,
},
],
},
],
});

0 comments on commit c79e813

Please sign in to comment.