no-useless-backreference.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. /**
  2. * @fileoverview Rule to disallow useless backreferences in regular expressions
  3. * @author Milos Djermanovic
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("eslint-utils");
  10. const { RegExpParser, visitRegExpAST } = require("regexpp");
  11. const lodash = require("lodash");
  12. //------------------------------------------------------------------------------
  13. // Helpers
  14. //------------------------------------------------------------------------------
  15. const parser = new RegExpParser();
  16. /**
  17. * Finds the path from the given `regexpp` AST node to the root node.
  18. * @param {regexpp.Node} node Node.
  19. * @returns {regexpp.Node[]} Array that starts with the given node and ends with the root node.
  20. */
  21. function getPathToRoot(node) {
  22. const path = [];
  23. let current = node;
  24. do {
  25. path.push(current);
  26. current = current.parent;
  27. } while (current);
  28. return path;
  29. }
  30. /**
  31. * Determines whether the given `regexpp` AST node is a lookaround node.
  32. * @param {regexpp.Node} node Node.
  33. * @returns {boolean} `true` if it is a lookaround node.
  34. */
  35. function isLookaround(node) {
  36. return node.type === "Assertion" &&
  37. (node.kind === "lookahead" || node.kind === "lookbehind");
  38. }
  39. /**
  40. * Determines whether the given `regexpp` AST node is a negative lookaround node.
  41. * @param {regexpp.Node} node Node.
  42. * @returns {boolean} `true` if it is a negative lookaround node.
  43. */
  44. function isNegativeLookaround(node) {
  45. return isLookaround(node) && node.negate;
  46. }
  47. //------------------------------------------------------------------------------
  48. // Rule Definition
  49. //------------------------------------------------------------------------------
  50. module.exports = {
  51. meta: {
  52. type: "problem",
  53. docs: {
  54. description: "disallow useless backreferences in regular expressions",
  55. category: "Possible Errors",
  56. recommended: false,
  57. url: "https://eslint.org/docs/rules/no-useless-backreference"
  58. },
  59. schema: [],
  60. messages: {
  61. nested: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
  62. forward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
  63. backward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
  64. disjunctive: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
  65. intoNegativeLookaround: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround."
  66. }
  67. },
  68. create(context) {
  69. /**
  70. * Checks and reports useless backreferences in the given regular expression.
  71. * @param {ASTNode} node Node that represents regular expression. A regex literal or RegExp constructor call.
  72. * @param {string} pattern Regular expression pattern.
  73. * @param {string} flags Regular expression flags.
  74. * @returns {void}
  75. */
  76. function checkRegex(node, pattern, flags) {
  77. let regExpAST;
  78. try {
  79. regExpAST = parser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
  80. } catch {
  81. // Ignore regular expressions with syntax errors
  82. return;
  83. }
  84. visitRegExpAST(regExpAST, {
  85. onBackreferenceEnter(bref) {
  86. const group = bref.resolved,
  87. brefPath = getPathToRoot(bref),
  88. groupPath = getPathToRoot(group);
  89. let messageId = null;
  90. if (brefPath.includes(group)) {
  91. // group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
  92. messageId = "nested";
  93. } else {
  94. // Start from the root to find the lowest common ancestor.
  95. let i = brefPath.length - 1,
  96. j = groupPath.length - 1;
  97. do {
  98. i--;
  99. j--;
  100. } while (brefPath[i] === groupPath[j]);
  101. const indexOfLowestCommonAncestor = j + 1,
  102. groupCut = groupPath.slice(0, indexOfLowestCommonAncestor),
  103. commonPath = groupPath.slice(indexOfLowestCommonAncestor),
  104. lowestCommonLookaround = commonPath.find(isLookaround),
  105. isMatchingBackward = lowestCommonLookaround && lowestCommonLookaround.kind === "lookbehind";
  106. if (!isMatchingBackward && bref.end <= group.start) {
  107. // bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
  108. messageId = "forward";
  109. } else if (isMatchingBackward && group.end <= bref.start) {
  110. // the opposite of the previous when the regex is matching backward in a lookbehind context.
  111. messageId = "backward";
  112. } else if (lodash.last(groupCut).type === "Alternative") {
  113. // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
  114. messageId = "disjunctive";
  115. } else if (groupCut.some(isNegativeLookaround)) {
  116. // group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
  117. messageId = "intoNegativeLookaround";
  118. }
  119. }
  120. if (messageId) {
  121. context.report({
  122. node,
  123. messageId,
  124. data: {
  125. bref: bref.raw,
  126. group: group.raw
  127. }
  128. });
  129. }
  130. }
  131. });
  132. }
  133. return {
  134. "Literal[regex]"(node) {
  135. const { pattern, flags } = node.regex;
  136. checkRegex(node, pattern, flags);
  137. },
  138. Program() {
  139. const scope = context.getScope(),
  140. tracker = new ReferenceTracker(scope),
  141. traceMap = {
  142. RegExp: {
  143. [CALL]: true,
  144. [CONSTRUCT]: true
  145. }
  146. };
  147. for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
  148. const [patternNode, flagsNode] = node.arguments,
  149. pattern = getStringIfConstant(patternNode, scope),
  150. flags = getStringIfConstant(flagsNode, scope);
  151. if (typeof pattern === "string") {
  152. checkRegex(node, pattern, flags || "");
  153. }
  154. }
  155. }
  156. };
  157. }
  158. };