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