prefer-string-replace-all.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. 'use strict';
  2. const {getStaticValue} = require('@eslint-community/eslint-utils');
  3. const {parse: parseRegExp} = require('regjsparser');
  4. const escapeString = require('./utils/escape-string.js');
  5. const {methodCallSelector} = require('./selectors/index.js');
  6. const {isRegexLiteral, isNewExpression} = require('./ast/index.js');
  7. const MESSAGE_ID_USE_REPLACE_ALL = 'method';
  8. const MESSAGE_ID_USE_STRING = 'pattern';
  9. const messages = {
  10. [MESSAGE_ID_USE_REPLACE_ALL]: 'Prefer `String#replaceAll()` over `String#replace()`.',
  11. [MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.',
  12. };
  13. const selector = methodCallSelector({
  14. methods: ['replace', 'replaceAll'],
  15. argumentsLength: 2,
  16. });
  17. function getPatternReplacement(node) {
  18. if (!isRegexLiteral(node)) {
  19. return;
  20. }
  21. const {pattern, flags} = node.regex;
  22. if (flags.replace('u', '') !== 'g') {
  23. return;
  24. }
  25. let tree;
  26. try {
  27. tree = parseRegExp(pattern, flags, {
  28. unicodePropertyEscape: true,
  29. namedGroups: true,
  30. lookbehind: true,
  31. });
  32. } catch {
  33. return;
  34. }
  35. const parts = tree.type === 'alternative' ? tree.body : [tree];
  36. if (parts.some(part => part.type !== 'value')) {
  37. return;
  38. }
  39. // TODO: Preserve escape
  40. const string = String.fromCodePoint(...parts.map(part => part.codePoint));
  41. return escapeString(string);
  42. }
  43. const isRegExpWithGlobalFlag = (node, scope) => {
  44. if (isRegexLiteral(node)) {
  45. return node.regex.flags.includes('g');
  46. }
  47. if (
  48. isNewExpression(node, {name: 'RegExp'})
  49. && node.arguments[0]?.type !== 'SpreadElement'
  50. && node.arguments[1]?.type === 'Literal'
  51. && typeof node.arguments[1].value === 'string'
  52. ) {
  53. return node.arguments[1].value.includes('g');
  54. }
  55. const staticResult = getStaticValue(node, scope);
  56. // Don't know if there is `g` flag
  57. if (!staticResult) {
  58. return false;
  59. }
  60. const {value} = staticResult;
  61. return (
  62. Object.prototype.toString.call(value) === '[object RegExp]'
  63. && value.global
  64. );
  65. };
  66. /** @param {import('eslint').Rule.RuleContext} context */
  67. const create = context => ({
  68. [selector](node) {
  69. const {
  70. arguments: [pattern],
  71. callee: {property},
  72. } = node;
  73. if (!isRegExpWithGlobalFlag(pattern, context.getScope())) {
  74. return;
  75. }
  76. const methodName = property.name;
  77. const patternReplacement = getPatternReplacement(pattern);
  78. if (methodName === 'replaceAll') {
  79. if (!patternReplacement) {
  80. return;
  81. }
  82. return {
  83. node: pattern,
  84. messageId: MESSAGE_ID_USE_STRING,
  85. data: {
  86. // Show `This pattern can be replaced with a string literal.` for long strings
  87. replacement: patternReplacement.length < 20 ? patternReplacement : 'a string literal',
  88. },
  89. /** @param {import('eslint').Rule.RuleFixer} fixer */
  90. fix: fixer => fixer.replaceText(pattern, patternReplacement),
  91. };
  92. }
  93. return {
  94. node: property,
  95. messageId: MESSAGE_ID_USE_REPLACE_ALL,
  96. /** @param {import('eslint').Rule.RuleFixer} fixer */
  97. * fix(fixer) {
  98. yield fixer.insertTextAfter(property, 'All');
  99. if (!patternReplacement) {
  100. return;
  101. }
  102. yield fixer.replaceText(pattern, patternReplacement);
  103. },
  104. };
  105. },
  106. });
  107. /** @type {import('eslint').Rule.RuleModule} */
  108. module.exports = {
  109. create,
  110. meta: {
  111. type: 'suggestion',
  112. docs: {
  113. description: 'Prefer `String#replaceAll()` over regex searches with the global flag.',
  114. },
  115. fixable: 'code',
  116. messages,
  117. },
  118. };