better-regex.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. 'use strict';
  2. const cleanRegexp = require('clean-regexp');
  3. const {optimize} = require('regexp-tree');
  4. const escapeString = require('./utils/escape-string.js');
  5. const {newExpressionSelector} = require('./selectors/index.js');
  6. const {isStringLiteral} = require('./ast/index.js');
  7. const MESSAGE_ID = 'better-regex';
  8. const MESSAGE_ID_PARSE_ERROR = 'better-regex/parse-error';
  9. const messages = {
  10. [MESSAGE_ID]: '{{original}} can be optimized to {{optimized}}.',
  11. [MESSAGE_ID_PARSE_ERROR]: 'Problem parsing {{original}}: {{error}}',
  12. };
  13. const newRegExp = newExpressionSelector({name: 'RegExp', minimumArguments: 1});
  14. /** @param {import('eslint').Rule.RuleContext} context */
  15. const create = context => {
  16. const {sortCharacterClasses} = context.options[0] || {};
  17. const ignoreList = [];
  18. if (sortCharacterClasses === false) {
  19. ignoreList.push('charClassClassrangesMerge');
  20. }
  21. return {
  22. 'Literal[regex]'(node) {
  23. const {raw: original, regex} = node;
  24. // Regular Expressions with `u` flag are not well handled by `regexp-tree`
  25. // https://github.com/DmitrySoshnikov/regexp-tree/issues/162
  26. if (regex.flags.includes('u')) {
  27. return;
  28. }
  29. let optimized = original;
  30. try {
  31. optimized = optimize(original, undefined, {blacklist: ignoreList}).toString();
  32. } catch (error) {
  33. return {
  34. node,
  35. messageId: MESSAGE_ID_PARSE_ERROR,
  36. data: {
  37. original,
  38. error: error.message,
  39. },
  40. };
  41. }
  42. if (original === optimized) {
  43. return;
  44. }
  45. const problem = {
  46. node,
  47. messageId: MESSAGE_ID,
  48. data: {
  49. original,
  50. optimized,
  51. },
  52. };
  53. if (
  54. node.parent.type === 'MemberExpression'
  55. && node.parent.object === node
  56. && !node.parent.optional
  57. && !node.parent.computed
  58. && node.parent.property.type === 'Identifier'
  59. && (
  60. node.parent.property.name === 'toString'
  61. || node.parent.property.name === 'source'
  62. )
  63. ) {
  64. return problem;
  65. }
  66. return Object.assign(problem, {
  67. fix: fixer => fixer.replaceText(node, optimized),
  68. });
  69. },
  70. [newRegExp](node) {
  71. const [patternNode, flagsNode] = node.arguments;
  72. if (!isStringLiteral(patternNode)) {
  73. return;
  74. }
  75. const oldPattern = patternNode.value;
  76. const flags = isStringLiteral(flagsNode)
  77. ? flagsNode.value
  78. : '';
  79. const newPattern = cleanRegexp(oldPattern, flags);
  80. if (oldPattern !== newPattern) {
  81. return {
  82. node,
  83. messageId: MESSAGE_ID,
  84. data: {
  85. original: oldPattern,
  86. optimized: newPattern,
  87. },
  88. fix: fixer => fixer.replaceText(
  89. patternNode,
  90. escapeString(newPattern, patternNode.raw.charAt(0)),
  91. ),
  92. };
  93. }
  94. },
  95. };
  96. };
  97. const schema = [
  98. {
  99. type: 'object',
  100. additionalProperties: false,
  101. properties: {
  102. sortCharacterClasses: {
  103. type: 'boolean',
  104. default: true,
  105. },
  106. },
  107. },
  108. ];
  109. /** @type {import('eslint').Rule.RuleModule} */
  110. module.exports = {
  111. create,
  112. meta: {
  113. type: 'suggestion',
  114. docs: {
  115. description: 'Improve regexes by making them shorter, consistent, and safer.',
  116. },
  117. fixable: 'code',
  118. schema,
  119. messages,
  120. },
  121. };