[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to enforce requiring named capture groups in regular expression.
|
---|
| 3 | * @author Pig Fang <https://github.com/g-plane>
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | //------------------------------------------------------------------------------
|
---|
| 9 | // Requirements
|
---|
| 10 | //------------------------------------------------------------------------------
|
---|
| 11 |
|
---|
| 12 | const {
|
---|
| 13 | CALL,
|
---|
| 14 | CONSTRUCT,
|
---|
| 15 | ReferenceTracker,
|
---|
| 16 | getStringIfConstant
|
---|
| 17 | } = require("@eslint-community/eslint-utils");
|
---|
| 18 | const regexpp = require("@eslint-community/regexpp");
|
---|
| 19 |
|
---|
| 20 | //------------------------------------------------------------------------------
|
---|
| 21 | // Helpers
|
---|
| 22 | //------------------------------------------------------------------------------
|
---|
| 23 |
|
---|
| 24 | const parser = new regexpp.RegExpParser();
|
---|
| 25 |
|
---|
| 26 | /**
|
---|
| 27 | * Creates fixer suggestions for the regex, if statically determinable.
|
---|
| 28 | * @param {number} groupStart Starting index of the regex group.
|
---|
| 29 | * @param {string} pattern The regular expression pattern to be checked.
|
---|
| 30 | * @param {string} rawText Source text of the regexNode.
|
---|
| 31 | * @param {ASTNode} regexNode AST node which contains the regular expression.
|
---|
| 32 | * @returns {Array<SuggestionResult>} Fixer suggestions for the regex, if statically determinable.
|
---|
| 33 | */
|
---|
| 34 | function suggestIfPossible(groupStart, pattern, rawText, regexNode) {
|
---|
| 35 | switch (regexNode.type) {
|
---|
| 36 | case "Literal":
|
---|
| 37 | if (typeof regexNode.value === "string" && rawText.includes("\\")) {
|
---|
| 38 | return null;
|
---|
| 39 | }
|
---|
| 40 | break;
|
---|
| 41 | case "TemplateLiteral":
|
---|
| 42 | if (regexNode.expressions.length || rawText.slice(1, -1) !== pattern) {
|
---|
| 43 | return null;
|
---|
| 44 | }
|
---|
| 45 | break;
|
---|
| 46 | default:
|
---|
| 47 | return null;
|
---|
| 48 | }
|
---|
| 49 |
|
---|
| 50 | const start = regexNode.range[0] + groupStart + 2;
|
---|
| 51 |
|
---|
| 52 | return [
|
---|
| 53 | {
|
---|
| 54 | fix(fixer) {
|
---|
| 55 | const existingTemps = pattern.match(/temp\d+/gu) || [];
|
---|
| 56 | const highestTempCount = existingTemps.reduce(
|
---|
| 57 | (previous, next) =>
|
---|
| 58 | Math.max(previous, Number(next.slice("temp".length))),
|
---|
| 59 | 0
|
---|
| 60 | );
|
---|
| 61 |
|
---|
| 62 | return fixer.insertTextBeforeRange(
|
---|
| 63 | [start, start],
|
---|
| 64 | `?<temp${highestTempCount + 1}>`
|
---|
| 65 | );
|
---|
| 66 | },
|
---|
| 67 | messageId: "addGroupName"
|
---|
| 68 | },
|
---|
| 69 | {
|
---|
| 70 | fix(fixer) {
|
---|
| 71 | return fixer.insertTextBeforeRange(
|
---|
| 72 | [start, start],
|
---|
| 73 | "?:"
|
---|
| 74 | );
|
---|
| 75 | },
|
---|
| 76 | messageId: "addNonCapture"
|
---|
| 77 | }
|
---|
| 78 | ];
|
---|
| 79 | }
|
---|
| 80 |
|
---|
| 81 | //------------------------------------------------------------------------------
|
---|
| 82 | // Rule Definition
|
---|
| 83 | //------------------------------------------------------------------------------
|
---|
| 84 |
|
---|
| 85 | /** @type {import('../shared/types').Rule} */
|
---|
| 86 | module.exports = {
|
---|
| 87 | meta: {
|
---|
| 88 | type: "suggestion",
|
---|
| 89 |
|
---|
| 90 | docs: {
|
---|
| 91 | description: "Enforce using named capture group in regular expression",
|
---|
| 92 | recommended: false,
|
---|
| 93 | url: "https://eslint.org/docs/latest/rules/prefer-named-capture-group"
|
---|
| 94 | },
|
---|
| 95 |
|
---|
| 96 | hasSuggestions: true,
|
---|
| 97 |
|
---|
| 98 | schema: [],
|
---|
| 99 |
|
---|
| 100 | messages: {
|
---|
| 101 | addGroupName: "Add name to capture group.",
|
---|
| 102 | addNonCapture: "Convert group to non-capturing.",
|
---|
| 103 | required: "Capture group '{{group}}' should be converted to a named or non-capturing group."
|
---|
| 104 | }
|
---|
| 105 | },
|
---|
| 106 |
|
---|
| 107 | create(context) {
|
---|
| 108 | const sourceCode = context.sourceCode;
|
---|
| 109 |
|
---|
| 110 | /**
|
---|
| 111 | * Function to check regular expression.
|
---|
| 112 | * @param {string} pattern The regular expression pattern to be checked.
|
---|
| 113 | * @param {ASTNode} node AST node which contains the regular expression or a call/new expression.
|
---|
| 114 | * @param {ASTNode} regexNode AST node which contains the regular expression.
|
---|
| 115 | * @param {string|null} flags The regular expression flags to be checked.
|
---|
| 116 | * @returns {void}
|
---|
| 117 | */
|
---|
| 118 | function checkRegex(pattern, node, regexNode, flags) {
|
---|
| 119 | let ast;
|
---|
| 120 |
|
---|
| 121 | try {
|
---|
| 122 | ast = parser.parsePattern(pattern, 0, pattern.length, {
|
---|
| 123 | unicode: Boolean(flags && flags.includes("u")),
|
---|
| 124 | unicodeSets: Boolean(flags && flags.includes("v"))
|
---|
| 125 | });
|
---|
| 126 | } catch {
|
---|
| 127 |
|
---|
| 128 | // ignore regex syntax errors
|
---|
| 129 | return;
|
---|
| 130 | }
|
---|
| 131 |
|
---|
| 132 | regexpp.visitRegExpAST(ast, {
|
---|
| 133 | onCapturingGroupEnter(group) {
|
---|
| 134 | if (!group.name) {
|
---|
| 135 | const rawText = sourceCode.getText(regexNode);
|
---|
| 136 | const suggest = suggestIfPossible(group.start, pattern, rawText, regexNode);
|
---|
| 137 |
|
---|
| 138 | context.report({
|
---|
| 139 | node,
|
---|
| 140 | messageId: "required",
|
---|
| 141 | data: {
|
---|
| 142 | group: group.raw
|
---|
| 143 | },
|
---|
| 144 | suggest
|
---|
| 145 | });
|
---|
| 146 | }
|
---|
| 147 | }
|
---|
| 148 | });
|
---|
| 149 | }
|
---|
| 150 |
|
---|
| 151 | return {
|
---|
| 152 | Literal(node) {
|
---|
| 153 | if (node.regex) {
|
---|
| 154 | checkRegex(node.regex.pattern, node, node, node.regex.flags);
|
---|
| 155 | }
|
---|
| 156 | },
|
---|
| 157 | Program(node) {
|
---|
| 158 | const scope = sourceCode.getScope(node);
|
---|
| 159 | const tracker = new ReferenceTracker(scope);
|
---|
| 160 | const traceMap = {
|
---|
| 161 | RegExp: {
|
---|
| 162 | [CALL]: true,
|
---|
| 163 | [CONSTRUCT]: true
|
---|
| 164 | }
|
---|
| 165 | };
|
---|
| 166 |
|
---|
| 167 | for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
|
---|
| 168 | const regex = getStringIfConstant(refNode.arguments[0]);
|
---|
| 169 | const flags = getStringIfConstant(refNode.arguments[1]);
|
---|
| 170 |
|
---|
| 171 | if (regex) {
|
---|
| 172 | checkRegex(regex, refNode, refNode.arguments[0], flags);
|
---|
| 173 | }
|
---|
| 174 | }
|
---|
| 175 | }
|
---|
| 176 | };
|
---|
| 177 | }
|
---|
| 178 | };
|
---|