/** * @fileoverview Rule to enforce the use of `u` or `v` flag on regular expressions. * @author Toru Nagashima */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils"); const astUtils = require("./utils/ast-utils.js"); const { isValidWithUnicodeFlag } = require("./utils/regular-expressions"); /** * Checks whether the flag configuration should be treated as a missing flag. * @param {"u"|"v"|undefined} requireFlag A particular flag to require * @param {string} flags The regex flags * @returns {boolean} Whether the flag configuration results in a missing flag. */ function checkFlags(requireFlag, flags) { let missingFlag; if (requireFlag === "v") { missingFlag = !flags.includes("v"); } else if (requireFlag === "u") { missingFlag = !flags.includes("u"); } else { missingFlag = !flags.includes("u") && !flags.includes("v"); } return missingFlag; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", docs: { description: "Enforce the use of `u` or `v` flag on regular expressions", recommended: false, url: "https://eslint.org/docs/latest/rules/require-unicode-regexp" }, hasSuggestions: true, messages: { addUFlag: "Add the 'u' flag.", addVFlag: "Add the 'v' flag.", requireUFlag: "Use the 'u' flag.", requireVFlag: "Use the 'v' flag." }, schema: [ { type: "object", properties: { requireFlag: { enum: ["u", "v"] } }, additionalProperties: false } ] }, create(context) { const sourceCode = context.sourceCode; const { requireFlag } = context.options[0] ?? {}; return { "Literal[regex]"(node) { const flags = node.regex.flags || ""; const missingFlag = checkFlags(requireFlag, flags); if (missingFlag) { context.report({ messageId: requireFlag === "v" ? "requireVFlag" : "requireUFlag", node, suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern, requireFlag) ? [ { fix(fixer) { const replaceFlag = requireFlag ?? "u"; const regex = sourceCode.getText(node); const slashPos = regex.lastIndexOf("/"); if (requireFlag) { const flag = requireFlag === "u" ? "v" : "u"; if (regex.includes(flag, slashPos)) { return fixer.replaceText( node, regex.slice(0, slashPos) + regex.slice(slashPos).replace(flag, requireFlag) ); } } return fixer.insertTextAfter(node, replaceFlag); }, messageId: requireFlag === "v" ? "addVFlag" : "addUFlag" } ] : null }); } }, Program(node) { const scope = sourceCode.getScope(node); const tracker = new ReferenceTracker(scope); const trackMap = { RegExp: { [CALL]: true, [CONSTRUCT]: true } }; for (const { node: refNode } of tracker.iterateGlobalReferences(trackMap)) { const [patternNode, flagsNode] = refNode.arguments; if (patternNode && patternNode.type === "SpreadElement") { continue; } const pattern = getStringIfConstant(patternNode, scope); const flags = getStringIfConstant(flagsNode, scope); let missingFlag = !flagsNode; if (typeof flags === "string") { missingFlag = checkFlags(requireFlag, flags); } if (missingFlag) { context.report({ messageId: requireFlag === "v" ? "requireVFlag" : "requireUFlag", node: refNode, suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern, requireFlag) ? [ { fix(fixer) { const replaceFlag = requireFlag ?? "u"; if (flagsNode) { if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") { const flagsNodeText = sourceCode.getText(flagsNode); const flag = requireFlag === "u" ? "v" : "u"; if (flags.includes(flag)) { // Avoid replacing "u" in escapes like `\uXXXX` if (flagsNode.type === "Literal" && flagsNode.raw.includes("\\")) { return null; } // Avoid replacing "u" in expressions like "`${regularFlags}g`" if (flagsNode.type === "TemplateLiteral" && ( flagsNode.expressions.length || flagsNode.quasis.some(({ value: { raw } }) => raw.includes("\\")) )) { return null; } return fixer.replaceText(flagsNode, flagsNodeText.replace(flag, replaceFlag)); } return fixer.replaceText(flagsNode, [ flagsNodeText.slice(0, flagsNodeText.length - 1), flagsNodeText.slice(flagsNodeText.length - 1) ].join(replaceFlag)); } // We intentionally don't suggest concatenating + "u" to non-literals return null; } const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis return fixer.insertTextAfter( penultimateToken, astUtils.isCommaToken(penultimateToken) ? ` "${replaceFlag}",` : `, "${replaceFlag}"` ); }, messageId: requireFlag === "v" ? "addVFlag" : "addUFlag" } ] : null }); } } } }; } };