no-useless-backreference.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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 {
  10. CALL,
  11. CONSTRUCT,
  12. ReferenceTracker,
  13. getStringIfConstant,
  14. } = require("@eslint-community/eslint-utils");
  15. const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
  16. //------------------------------------------------------------------------------
  17. // Helpers
  18. //------------------------------------------------------------------------------
  19. const parser = new RegExpParser();
  20. /**
  21. * Finds the path from the given `regexpp` AST node to the root node.
  22. * @param {regexpp.Node} node Node.
  23. * @returns {regexpp.Node[]} Array that starts with the given node and ends with the root node.
  24. */
  25. function getPathToRoot(node) {
  26. const path = [];
  27. let current = node;
  28. do {
  29. path.push(current);
  30. current = current.parent;
  31. } while (current);
  32. return path;
  33. }
  34. /**
  35. * Determines whether the given `regexpp` AST node is a lookaround node.
  36. * @param {regexpp.Node} node Node.
  37. * @returns {boolean} `true` if it is a lookaround node.
  38. */
  39. function isLookaround(node) {
  40. return (
  41. node.type === "Assertion" &&
  42. (node.kind === "lookahead" || node.kind === "lookbehind")
  43. );
  44. }
  45. /**
  46. * Determines whether the given `regexpp` AST node is a negative lookaround node.
  47. * @param {regexpp.Node} node Node.
  48. * @returns {boolean} `true` if it is a negative lookaround node.
  49. */
  50. function isNegativeLookaround(node) {
  51. return isLookaround(node) && node.negate;
  52. }
  53. //------------------------------------------------------------------------------
  54. // Rule Definition
  55. //------------------------------------------------------------------------------
  56. /** @type {import('../types').Rule.RuleModule} */
  57. module.exports = {
  58. meta: {
  59. type: "problem",
  60. docs: {
  61. description:
  62. "Disallow useless backreferences in regular expressions",
  63. recommended: true,
  64. url: "https://eslint.org/docs/latest/rules/no-useless-backreference",
  65. },
  66. schema: [],
  67. messages: {
  68. nested: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} from within that group.",
  69. forward:
  70. "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which appears later in the pattern.",
  71. backward:
  72. "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which appears before in the same lookbehind.",
  73. disjunctive:
  74. "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which is in another alternative.",
  75. intoNegativeLookaround:
  76. "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which is in a negative lookaround.",
  77. },
  78. },
  79. create(context) {
  80. const sourceCode = context.sourceCode;
  81. /**
  82. * Checks and reports useless backreferences in the given regular expression.
  83. * @param {ASTNode} node Node that represents regular expression. A regex literal or RegExp constructor call.
  84. * @param {string} pattern Regular expression pattern.
  85. * @param {string} flags Regular expression flags.
  86. * @returns {void}
  87. */
  88. function checkRegex(node, pattern, flags) {
  89. let regExpAST;
  90. try {
  91. regExpAST = parser.parsePattern(pattern, 0, pattern.length, {
  92. unicode: flags.includes("u"),
  93. unicodeSets: flags.includes("v"),
  94. });
  95. } catch {
  96. // Ignore regular expressions with syntax errors
  97. return;
  98. }
  99. visitRegExpAST(regExpAST, {
  100. onBackreferenceEnter(bref) {
  101. const groups = [bref.resolved].flat(),
  102. brefPath = getPathToRoot(bref);
  103. const problems = groups.map(group => {
  104. const groupPath = getPathToRoot(group);
  105. if (brefPath.includes(group)) {
  106. // group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
  107. return {
  108. messageId: "nested",
  109. group,
  110. };
  111. }
  112. // Start from the root to find the lowest common ancestor.
  113. let i = brefPath.length - 1,
  114. j = groupPath.length - 1;
  115. do {
  116. i--;
  117. j--;
  118. } while (brefPath[i] === groupPath[j]);
  119. const indexOfLowestCommonAncestor = j + 1,
  120. groupCut = groupPath.slice(
  121. 0,
  122. indexOfLowestCommonAncestor,
  123. ),
  124. commonPath = groupPath.slice(
  125. indexOfLowestCommonAncestor,
  126. ),
  127. lowestCommonLookaround =
  128. commonPath.find(isLookaround),
  129. isMatchingBackward =
  130. lowestCommonLookaround &&
  131. lowestCommonLookaround.kind === "lookbehind";
  132. if (groupCut.at(-1).type === "Alternative") {
  133. // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
  134. return {
  135. messageId: "disjunctive",
  136. group,
  137. };
  138. }
  139. if (!isMatchingBackward && bref.end <= group.start) {
  140. // bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
  141. return {
  142. messageId: "forward",
  143. group,
  144. };
  145. }
  146. if (isMatchingBackward && group.end <= bref.start) {
  147. // the opposite of the previous when the regex is matching backward in a lookbehind context.
  148. return {
  149. messageId: "backward",
  150. group,
  151. };
  152. }
  153. if (groupCut.some(isNegativeLookaround)) {
  154. // group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
  155. return {
  156. messageId: "intoNegativeLookaround",
  157. group,
  158. };
  159. }
  160. return null;
  161. });
  162. if (
  163. problems.length === 0 ||
  164. problems.some(problem => !problem)
  165. ) {
  166. // If there are no problems or no problems with any group then do not report it.
  167. return;
  168. }
  169. let problemsToReport;
  170. // Gets problems that appear in the same disjunction.
  171. const problemsInSameDisjunction = problems.filter(
  172. problem => problem.messageId !== "disjunctive",
  173. );
  174. if (problemsInSameDisjunction.length) {
  175. // Only report problems that appear in the same disjunction.
  176. problemsToReport = problemsInSameDisjunction;
  177. } else {
  178. // If all groups appear in different disjunctions, report it.
  179. problemsToReport = problems;
  180. }
  181. const [{ messageId, group }, ...other] = problemsToReport;
  182. let otherGroups = "";
  183. if (other.length === 1) {
  184. otherGroups = " and another group";
  185. } else if (other.length > 1) {
  186. otherGroups = ` and other ${other.length} groups`;
  187. }
  188. context.report({
  189. node,
  190. messageId,
  191. data: {
  192. bref: bref.raw,
  193. group: group.raw,
  194. otherGroups,
  195. },
  196. });
  197. },
  198. });
  199. }
  200. return {
  201. "Literal[regex]"(node) {
  202. const { pattern, flags } = node.regex;
  203. checkRegex(node, pattern, flags);
  204. },
  205. Program(node) {
  206. const scope = sourceCode.getScope(node),
  207. tracker = new ReferenceTracker(scope),
  208. traceMap = {
  209. RegExp: {
  210. [CALL]: true,
  211. [CONSTRUCT]: true,
  212. },
  213. };
  214. for (const { node: refNode } of tracker.iterateGlobalReferences(
  215. traceMap,
  216. )) {
  217. const [patternNode, flagsNode] = refNode.arguments,
  218. pattern = getStringIfConstant(patternNode, scope),
  219. flags = getStringIfConstant(flagsNode, scope);
  220. if (typeof pattern === "string") {
  221. checkRegex(refNode, pattern, flags || "");
  222. }
  223. }
  224. },
  225. };
  226. },
  227. };