prefer-regexp-test.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. 'use strict';
  2. const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils');
  3. const {checkVueTemplate} = require('./utils/rule.js');
  4. const {methodCallSelector} = require('./selectors/index.js');
  5. const {isRegexLiteral, isNewExpression} = require('./ast/index.js');
  6. const {isBooleanNode} = require('./utils/boolean.js');
  7. const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
  8. const REGEXP_EXEC = 'regexp-exec';
  9. const STRING_MATCH = 'string-match';
  10. const SUGGESTION = 'suggestion';
  11. const messages = {
  12. [REGEXP_EXEC]: 'Prefer `.test(…)` over `.exec(…)`.',
  13. [STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.',
  14. [SUGGESTION]: 'Switch to `RegExp#test(…)`.',
  15. };
  16. const cases = [
  17. {
  18. type: REGEXP_EXEC,
  19. selector: methodCallSelector({
  20. method: 'exec',
  21. argumentsLength: 1,
  22. }),
  23. getNodes: node => ({
  24. stringNode: node.arguments[0],
  25. methodNode: node.callee.property,
  26. regexpNode: node.callee.object,
  27. }),
  28. fix: (fixer, {methodNode}) => fixer.replaceText(methodNode, 'test'),
  29. },
  30. {
  31. type: STRING_MATCH,
  32. selector: methodCallSelector({
  33. method: 'match',
  34. argumentsLength: 1,
  35. }),
  36. getNodes: node => ({
  37. stringNode: node.callee.object,
  38. methodNode: node.callee.property,
  39. regexpNode: node.arguments[0],
  40. }),
  41. * fix(fixer, {stringNode, methodNode, regexpNode}, sourceCode) {
  42. yield fixer.replaceText(methodNode, 'test');
  43. let stringText = sourceCode.getText(stringNode);
  44. if (
  45. !isParenthesized(regexpNode, sourceCode)
  46. // Only `SequenceExpression` need add parentheses
  47. && stringNode.type === 'SequenceExpression'
  48. ) {
  49. stringText = `(${stringText})`;
  50. }
  51. yield fixer.replaceText(regexpNode, stringText);
  52. let regexpText = sourceCode.getText(regexpNode);
  53. if (
  54. !isParenthesized(stringNode, sourceCode)
  55. && shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode)
  56. ) {
  57. regexpText = `(${regexpText})`;
  58. }
  59. // The nodes that pass `isBooleanNode` cannot have an ASI problem.
  60. yield fixer.replaceText(stringNode, regexpText);
  61. },
  62. },
  63. ];
  64. const isRegExpNode = node => isRegexLiteral(node) || isNewExpression(node, {name: 'RegExp'});
  65. const isRegExpWithoutGlobalFlag = (node, scope) => {
  66. const staticResult = getStaticValue(node, scope);
  67. // Don't know if there is `g` flag
  68. if (!staticResult) {
  69. return false;
  70. }
  71. const {value} = staticResult;
  72. return (
  73. Object.prototype.toString.call(value) === '[object RegExp]'
  74. && !value.global
  75. );
  76. };
  77. /** @param {import('eslint').Rule.RuleContext} context */
  78. const create = context => Object.fromEntries(
  79. cases.map(checkCase => [
  80. checkCase.selector,
  81. node => {
  82. if (!isBooleanNode(node)) {
  83. return;
  84. }
  85. const {type, getNodes, fix} = checkCase;
  86. const nodes = getNodes(node);
  87. const {methodNode, regexpNode} = nodes;
  88. if (regexpNode.type === 'Literal' && !regexpNode.regex) {
  89. return;
  90. }
  91. const problem = {
  92. node: type === REGEXP_EXEC ? methodNode : node,
  93. messageId: type,
  94. };
  95. const fixFunction = fixer => fix(fixer, nodes, context.getSourceCode());
  96. if (
  97. isRegExpNode(regexpNode)
  98. || isRegExpWithoutGlobalFlag(regexpNode, context.getScope())
  99. ) {
  100. problem.fix = fixFunction;
  101. } else {
  102. problem.suggest = [
  103. {
  104. messageId: SUGGESTION,
  105. fix: fixFunction,
  106. },
  107. ];
  108. }
  109. return problem;
  110. },
  111. ]),
  112. );
  113. /** @type {import('eslint').Rule.RuleModule} */
  114. module.exports = {
  115. create: checkVueTemplate(create),
  116. meta: {
  117. type: 'suggestion',
  118. docs: {
  119. description: 'Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`.',
  120. },
  121. fixable: 'code',
  122. hasSuggestions: true,
  123. messages,
  124. },
  125. };