/** * @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('../types').Rule.RuleModule} */ 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, }); } } }, }; }, };