123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- /**
- * @fileoverview Rule to enforce the use of `u` flag on RegExp.
- * @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 RegExp",
- 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
- });
- }
- }
- }
- };
- }
- };
|