You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
8.8 KiB
210 lines
8.8 KiB
/** |
|
* @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 |
|
}); |
|
} |
|
} |
|
} |
|
}; |
|
} |
|
};
|
|
|