better-regex.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. 'use strict';
  2. const cleanRegexp = require('clean-regexp');
  3. const {optimize} = require('regexp-tree');
  4. const quoteString = require('./utils/quote-string.js');
  5. const {newExpressionSelector} = require('./selectors/index.js');
  6. const MESSAGE_ID = 'better-regex';
  7. const messages = {
  8. [MESSAGE_ID]: '{{original}} can be optimized to {{optimized}}.',
  9. };
  10. const newRegExp = [
  11. newExpressionSelector({name: 'RegExp', minimumArguments: 1}),
  12. '[arguments.0.type="Literal"]',
  13. ].join('');
  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. data: {
  36. original,
  37. error: error.message,
  38. },
  39. message: 'Problem parsing {{original}}: {{error}}',
  40. };
  41. }
  42. if (original === optimized) {
  43. return;
  44. }
  45. return {
  46. node,
  47. messageId: MESSAGE_ID,
  48. data: {
  49. original,
  50. optimized,
  51. },
  52. fix: fixer => fixer.replaceText(node, optimized),
  53. };
  54. },
  55. [newRegExp]: node => {
  56. const [patternNode, flagsNode] = node.arguments;
  57. if (typeof patternNode.value !== 'string') {
  58. return;
  59. }
  60. const oldPattern = patternNode.value;
  61. const flags = (
  62. flagsNode
  63. && flagsNode.type === 'Literal'
  64. && typeof flagsNode.value === 'string'
  65. )
  66. ? flagsNode.value
  67. : '';
  68. const newPattern = cleanRegexp(oldPattern, flags);
  69. if (oldPattern !== newPattern) {
  70. return {
  71. node,
  72. messageId: MESSAGE_ID,
  73. data: {
  74. original: oldPattern,
  75. optimized: newPattern,
  76. },
  77. fix: fixer => fixer.replaceText(
  78. patternNode,
  79. quoteString(newPattern, patternNode.raw.charAt(0)),
  80. ),
  81. };
  82. }
  83. },
  84. };
  85. };
  86. const schema = [
  87. {
  88. type: 'object',
  89. additionalProperties: false,
  90. properties: {
  91. sortCharacterClasses: {
  92. type: 'boolean',
  93. default: true,
  94. },
  95. },
  96. },
  97. ];
  98. /** @type {import('eslint').Rule.RuleModule} */
  99. module.exports = {
  100. create,
  101. meta: {
  102. type: 'suggestion',
  103. docs: {
  104. description: 'Improve regexes by making them shorter, consistent, and safer.',
  105. },
  106. fixable: 'code',
  107. schema,
  108. messages,
  109. },
  110. };