From 89ab7ac7ac1d6d4aa06d99fde3c98709b73b7552 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 23 May 2021 09:15:34 -0400 Subject: [PATCH] fix(eslint-plugin): [prefer-regexp-exec] factor in union types --- .../src/rules/prefer-regexp-exec.ts | 129 ++++++++++++------ .../tests/rules/prefer-regexp-exec.test.ts | 14 ++ 2 files changed, 98 insertions(+), 45 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index 636fe2b3825a..b7fc74069c61 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -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, @@ -10,6 +12,13 @@ import { getWrappingFixer, } from '../util'; +enum ArgumentType { + Both, + Other, + RegExp, + String, +} + export default createRule({ name: 'prefer-regexp-exec', defaultOptions: [], @@ -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 { @@ -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; } @@ -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})`, + }), + }); + } }, }; }, diff --git a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts index 243c4e065b98..a7fd0ee67c8f 100644 --- a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts @@ -227,5 +227,19 @@ function f(s: T) { } `, }, + { + code: ` +const matchers = ['package-lock.json', /regexp/]; +const file = ''; +matchers.some(matcher => !!file.match(matcher)); + `, + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 4, + column: 33, + }, + ], + }, ], });