591 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			591 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**
 | 
						|
 * @author Toru Nagashima <https://github.com/mysticatea>
 | 
						|
 */
 | 
						|
"use strict";
 | 
						|
 | 
						|
const {
 | 
						|
	CALL,
 | 
						|
	CONSTRUCT,
 | 
						|
	ReferenceTracker,
 | 
						|
	getStaticValue,
 | 
						|
	getStringIfConstant,
 | 
						|
} = require("@eslint-community/eslint-utils");
 | 
						|
const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
 | 
						|
const {
 | 
						|
	isCombiningCharacter,
 | 
						|
	isEmojiModifier,
 | 
						|
	isRegionalIndicatorSymbol,
 | 
						|
	isSurrogatePair,
 | 
						|
} = require("./utils/unicode");
 | 
						|
const astUtils = require("./utils/ast-utils.js");
 | 
						|
const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
 | 
						|
const {
 | 
						|
	parseStringLiteral,
 | 
						|
	parseTemplateToken,
 | 
						|
} = require("./utils/char-source");
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Helpers
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {import('@eslint-community/regexpp').AST.Character} Character
 | 
						|
 * @typedef {import('@eslint-community/regexpp').AST.CharacterClassElement} CharacterClassElement
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Iterate character sequences of a given nodes.
 | 
						|
 *
 | 
						|
 * CharacterClassRange syntax can steal a part of character sequence,
 | 
						|
 * so this function reverts CharacterClassRange syntax and restore the sequence.
 | 
						|
 * @param {CharacterClassElement[]} nodes The node list to iterate character sequences.
 | 
						|
 * @returns {IterableIterator<Character[]>} The list of character sequences.
 | 
						|
 */
 | 
						|
function* iterateCharacterSequence(nodes) {
 | 
						|
	/** @type {Character[]} */
 | 
						|
	let seq = [];
 | 
						|
 | 
						|
	for (const node of nodes) {
 | 
						|
		switch (node.type) {
 | 
						|
			case "Character":
 | 
						|
				seq.push(node);
 | 
						|
				break;
 | 
						|
 | 
						|
			case "CharacterClassRange":
 | 
						|
				seq.push(node.min);
 | 
						|
				yield seq;
 | 
						|
				seq = [node.max];
 | 
						|
				break;
 | 
						|
 | 
						|
			case "CharacterSet":
 | 
						|
			case "CharacterClass": // [[]] nesting character class
 | 
						|
			case "ClassStringDisjunction": // \q{...}
 | 
						|
			case "ExpressionCharacterClass": // [A--B]
 | 
						|
				if (seq.length > 0) {
 | 
						|
					yield seq;
 | 
						|
					seq = [];
 | 
						|
				}
 | 
						|
				break;
 | 
						|
 | 
						|
			// no default
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if (seq.length > 0) {
 | 
						|
		yield seq;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks whether the given character node is a Unicode code point escape or not.
 | 
						|
 * @param {Character} char the character node to check.
 | 
						|
 * @returns {boolean} `true` if the character node is a Unicode code point escape.
 | 
						|
 */
 | 
						|
function isUnicodeCodePointEscape(char) {
 | 
						|
	return /^\\u\{[\da-f]+\}$/iu.test(char.raw);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Each function returns matched characters if it detects that kind of problem.
 | 
						|
 * @type {Record<string, (chars: Character[]) => IterableIterator<Character[]>>}
 | 
						|
 */
 | 
						|
const findCharacterSequences = {
 | 
						|
	*surrogatePairWithoutUFlag(chars) {
 | 
						|
		for (const [index, char] of chars.entries()) {
 | 
						|
			const previous = chars[index - 1];
 | 
						|
 | 
						|
			if (
 | 
						|
				previous &&
 | 
						|
				char &&
 | 
						|
				isSurrogatePair(previous.value, char.value) &&
 | 
						|
				!isUnicodeCodePointEscape(previous) &&
 | 
						|
				!isUnicodeCodePointEscape(char)
 | 
						|
			) {
 | 
						|
				yield [previous, char];
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
 | 
						|
	*surrogatePair(chars) {
 | 
						|
		for (const [index, char] of chars.entries()) {
 | 
						|
			const previous = chars[index - 1];
 | 
						|
 | 
						|
			if (
 | 
						|
				previous &&
 | 
						|
				char &&
 | 
						|
				isSurrogatePair(previous.value, char.value) &&
 | 
						|
				(isUnicodeCodePointEscape(previous) ||
 | 
						|
					isUnicodeCodePointEscape(char))
 | 
						|
			) {
 | 
						|
				yield [previous, char];
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
 | 
						|
	*combiningClass(chars, unfilteredChars) {
 | 
						|
		/*
 | 
						|
		 * When `allowEscape` is `true`, a combined character should only be allowed if the combining mark appears as an escape sequence.
 | 
						|
		 * This means that the base character should be considered even if it's escaped.
 | 
						|
		 */
 | 
						|
		for (const [index, char] of chars.entries()) {
 | 
						|
			const previous = unfilteredChars[index - 1];
 | 
						|
 | 
						|
			if (
 | 
						|
				previous &&
 | 
						|
				char &&
 | 
						|
				isCombiningCharacter(char.value) &&
 | 
						|
				!isCombiningCharacter(previous.value)
 | 
						|
			) {
 | 
						|
				yield [previous, char];
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
 | 
						|
	*emojiModifier(chars) {
 | 
						|
		for (const [index, char] of chars.entries()) {
 | 
						|
			const previous = chars[index - 1];
 | 
						|
 | 
						|
			if (
 | 
						|
				previous &&
 | 
						|
				char &&
 | 
						|
				isEmojiModifier(char.value) &&
 | 
						|
				!isEmojiModifier(previous.value)
 | 
						|
			) {
 | 
						|
				yield [previous, char];
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
 | 
						|
	*regionalIndicatorSymbol(chars) {
 | 
						|
		for (const [index, char] of chars.entries()) {
 | 
						|
			const previous = chars[index - 1];
 | 
						|
 | 
						|
			if (
 | 
						|
				previous &&
 | 
						|
				char &&
 | 
						|
				isRegionalIndicatorSymbol(char.value) &&
 | 
						|
				isRegionalIndicatorSymbol(previous.value)
 | 
						|
			) {
 | 
						|
				yield [previous, char];
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
 | 
						|
	*zwj(chars) {
 | 
						|
		let sequence = null;
 | 
						|
 | 
						|
		for (const [index, char] of chars.entries()) {
 | 
						|
			const previous = chars[index - 1];
 | 
						|
			const next = chars[index + 1];
 | 
						|
 | 
						|
			if (
 | 
						|
				previous &&
 | 
						|
				char &&
 | 
						|
				next &&
 | 
						|
				char.value === 0x200d &&
 | 
						|
				previous.value !== 0x200d &&
 | 
						|
				next.value !== 0x200d
 | 
						|
			) {
 | 
						|
				if (sequence) {
 | 
						|
					if (sequence.at(-1) === previous) {
 | 
						|
						sequence.push(char, next); // append to the sequence
 | 
						|
					} else {
 | 
						|
						yield sequence;
 | 
						|
						sequence = chars.slice(index - 1, index + 2);
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					sequence = chars.slice(index - 1, index + 2);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if (sequence) {
 | 
						|
			yield sequence;
 | 
						|
		}
 | 
						|
	},
 | 
						|
};
 | 
						|
 | 
						|
const kinds = Object.keys(findCharacterSequences);
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets the value of the given node if it's a static value other than a regular expression object,
 | 
						|
 * or the node's `regex` property.
 | 
						|
 * The purpose of this method is to provide a replacement for `getStaticValue` in environments where certain regular expressions cannot be evaluated.
 | 
						|
 * A known example is Node.js 18 which does not support the `v` flag.
 | 
						|
 * Calling `getStaticValue` on a regular expression node with the `v` flag on Node.js 18 always returns `null`.
 | 
						|
 * A limitation of this method is that it can only detect a regular expression if the specified node is itself a regular expression literal node.
 | 
						|
 * @param {ASTNode | undefined} node The node to be inspected.
 | 
						|
 * @param {Scope} initialScope Scope to start finding variables. This function tries to resolve identifier references which are in the given scope.
 | 
						|
 * @returns {{ value: any } | { regex: { pattern: string, flags: string } } | null} The static value of the node, or `null`.
 | 
						|
 */
 | 
						|
function getStaticValueOrRegex(node, initialScope) {
 | 
						|
	if (!node) {
 | 
						|
		return null;
 | 
						|
	}
 | 
						|
	if (node.type === "Literal" && node.regex) {
 | 
						|
		return { regex: node.regex };
 | 
						|
	}
 | 
						|
 | 
						|
	const staticValue = getStaticValue(node, initialScope);
 | 
						|
 | 
						|
	if (staticValue?.value instanceof RegExp) {
 | 
						|
		return null;
 | 
						|
	}
 | 
						|
	return staticValue;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks whether a specified regexpp character is represented as an acceptable escape sequence.
 | 
						|
 * This function requires the source text of the character to be known.
 | 
						|
 * @param {Character} char Character to check.
 | 
						|
 * @param {string} charSource Source text of the character to check.
 | 
						|
 * @returns {boolean} Whether the specified regexpp character is represented as an acceptable escape sequence.
 | 
						|
 */
 | 
						|
function checkForAcceptableEscape(char, charSource) {
 | 
						|
	if (!charSource.startsWith("\\")) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	const match = /(?<=^\\+).$/su.exec(charSource);
 | 
						|
 | 
						|
	return match?.[0] !== String.fromCodePoint(char.value);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks whether a specified regexpp character is represented as an acceptable escape sequence.
 | 
						|
 * This function works with characters that are produced by a string or template literal.
 | 
						|
 * It requires the source text and the CodeUnit list of the literal to be known.
 | 
						|
 * @param {Character} char Character to check.
 | 
						|
 * @param {string} nodeSource Source text of the string or template literal that produces the character.
 | 
						|
 * @param {CodeUnit[]} codeUnits List of CodeUnit objects of the literal that produces the character.
 | 
						|
 * @returns {boolean} Whether the specified regexpp character is represented as an acceptable escape sequence.
 | 
						|
 */
 | 
						|
function checkForAcceptableEscapeInString(char, nodeSource, codeUnits) {
 | 
						|
	const firstIndex = char.start;
 | 
						|
	const lastIndex = char.end - 1;
 | 
						|
	const start = codeUnits[firstIndex].start;
 | 
						|
	const end = codeUnits[lastIndex].end;
 | 
						|
	const charSource = nodeSource.slice(start, end);
 | 
						|
 | 
						|
	return checkForAcceptableEscape(char, charSource);
 | 
						|
}
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Rule Definition
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
/** @type {import('../types').Rule.RuleModule} */
 | 
						|
module.exports = {
 | 
						|
	meta: {
 | 
						|
		type: "problem",
 | 
						|
 | 
						|
		docs: {
 | 
						|
			description:
 | 
						|
				"Disallow characters which are made with multiple code points in character class syntax",
 | 
						|
			recommended: true,
 | 
						|
			url: "https://eslint.org/docs/latest/rules/no-misleading-character-class",
 | 
						|
		},
 | 
						|
 | 
						|
		hasSuggestions: true,
 | 
						|
 | 
						|
		schema: [
 | 
						|
			{
 | 
						|
				type: "object",
 | 
						|
				properties: {
 | 
						|
					allowEscape: {
 | 
						|
						type: "boolean",
 | 
						|
						default: false,
 | 
						|
					},
 | 
						|
				},
 | 
						|
				additionalProperties: false,
 | 
						|
			},
 | 
						|
		],
 | 
						|
 | 
						|
		messages: {
 | 
						|
			surrogatePairWithoutUFlag:
 | 
						|
				"Unexpected surrogate pair in character class. Use 'u' flag.",
 | 
						|
			surrogatePair: "Unexpected surrogate pair in character class.",
 | 
						|
			combiningClass: "Unexpected combined character in character class.",
 | 
						|
			emojiModifier: "Unexpected modified Emoji in character class.",
 | 
						|
			regionalIndicatorSymbol:
 | 
						|
				"Unexpected national flag in character class.",
 | 
						|
			zwj: "Unexpected joined character sequence in character class.",
 | 
						|
			suggestUnicodeFlag: "Add unicode 'u' flag to regex.",
 | 
						|
		},
 | 
						|
	},
 | 
						|
	create(context) {
 | 
						|
		const allowEscape = context.options[0]?.allowEscape;
 | 
						|
		const sourceCode = context.sourceCode;
 | 
						|
		const parser = new RegExpParser();
 | 
						|
		const checkedPatternNodes = new Set();
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Verify a given regular expression.
 | 
						|
		 * @param {Node} node The node to report.
 | 
						|
		 * @param {string} pattern The regular expression pattern to verify.
 | 
						|
		 * @param {string} flags The flags of the regular expression.
 | 
						|
		 * @param {Function} unicodeFixer Fixer for missing "u" flag.
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		function verify(node, pattern, flags, unicodeFixer) {
 | 
						|
			let patternNode;
 | 
						|
 | 
						|
			try {
 | 
						|
				patternNode = parser.parsePattern(pattern, 0, pattern.length, {
 | 
						|
					unicode: flags.includes("u"),
 | 
						|
					unicodeSets: flags.includes("v"),
 | 
						|
				});
 | 
						|
			} catch {
 | 
						|
				// Ignore regular expressions with syntax errors
 | 
						|
				return;
 | 
						|
			}
 | 
						|
 | 
						|
			let codeUnits = null;
 | 
						|
 | 
						|
			/**
 | 
						|
			 * Checks whether a specified regexpp character is represented as an acceptable escape sequence.
 | 
						|
			 * For the purposes of this rule, an escape sequence is considered acceptable if it consists of one or more backslashes followed by the character being escaped.
 | 
						|
			 * @param {Character} char Character to check.
 | 
						|
			 * @returns {boolean} Whether the specified regexpp character is represented as an acceptable escape sequence.
 | 
						|
			 */
 | 
						|
			function isAcceptableEscapeSequence(char) {
 | 
						|
				if (node.type === "Literal" && node.regex) {
 | 
						|
					return checkForAcceptableEscape(char, char.raw);
 | 
						|
				}
 | 
						|
				if (node.type === "Literal" && typeof node.value === "string") {
 | 
						|
					const nodeSource = node.raw;
 | 
						|
 | 
						|
					codeUnits ??= parseStringLiteral(nodeSource);
 | 
						|
 | 
						|
					return checkForAcceptableEscapeInString(
 | 
						|
						char,
 | 
						|
						nodeSource,
 | 
						|
						codeUnits,
 | 
						|
					);
 | 
						|
				}
 | 
						|
				if (astUtils.isStaticTemplateLiteral(node)) {
 | 
						|
					const nodeSource = sourceCode.getText(node);
 | 
						|
 | 
						|
					codeUnits ??= parseTemplateToken(nodeSource);
 | 
						|
 | 
						|
					return checkForAcceptableEscapeInString(
 | 
						|
						char,
 | 
						|
						nodeSource,
 | 
						|
						codeUnits,
 | 
						|
					);
 | 
						|
				}
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
 | 
						|
			const foundKindMatches = new Map();
 | 
						|
 | 
						|
			visitRegExpAST(patternNode, {
 | 
						|
				onCharacterClassEnter(ccNode) {
 | 
						|
					for (const unfilteredChars of iterateCharacterSequence(
 | 
						|
						ccNode.elements,
 | 
						|
					)) {
 | 
						|
						let chars;
 | 
						|
 | 
						|
						if (allowEscape) {
 | 
						|
							// Replace escape sequences with null to avoid having them flagged.
 | 
						|
							chars = unfilteredChars.map(char =>
 | 
						|
								isAcceptableEscapeSequence(char) ? null : char,
 | 
						|
							);
 | 
						|
						} else {
 | 
						|
							chars = unfilteredChars;
 | 
						|
						}
 | 
						|
						for (const kind of kinds) {
 | 
						|
							const matches = findCharacterSequences[kind](
 | 
						|
								chars,
 | 
						|
								unfilteredChars,
 | 
						|
							);
 | 
						|
 | 
						|
							if (foundKindMatches.has(kind)) {
 | 
						|
								foundKindMatches.get(kind).push(...matches);
 | 
						|
							} else {
 | 
						|
								foundKindMatches.set(kind, [...matches]);
 | 
						|
							}
 | 
						|
						}
 | 
						|
					}
 | 
						|
				},
 | 
						|
			});
 | 
						|
 | 
						|
			/**
 | 
						|
			 * Finds the report loc(s) for a range of matches.
 | 
						|
			 * Only literals and expression-less templates generate granular errors.
 | 
						|
			 * @param {Character[][]} matches Lists of individual characters being reported on.
 | 
						|
			 * @returns {Location[]} locs for context.report.
 | 
						|
			 * @see https://github.com/eslint/eslint/pull/17515
 | 
						|
			 */
 | 
						|
			function getNodeReportLocations(matches) {
 | 
						|
				if (
 | 
						|
					!astUtils.isStaticTemplateLiteral(node) &&
 | 
						|
					node.type !== "Literal"
 | 
						|
				) {
 | 
						|
					return matches.length ? [node.loc] : [];
 | 
						|
				}
 | 
						|
				return matches.map(chars => {
 | 
						|
					const firstIndex = chars[0].start;
 | 
						|
					const lastIndex = chars.at(-1).end - 1;
 | 
						|
					let start;
 | 
						|
					let end;
 | 
						|
 | 
						|
					if (node.type === "TemplateLiteral") {
 | 
						|
						const source = sourceCode.getText(node);
 | 
						|
						const offset = node.range[0];
 | 
						|
 | 
						|
						codeUnits ??= parseTemplateToken(source);
 | 
						|
						start = offset + codeUnits[firstIndex].start;
 | 
						|
						end = offset + codeUnits[lastIndex].end;
 | 
						|
					} else if (typeof node.value === "string") {
 | 
						|
						// String Literal
 | 
						|
						const source = node.raw;
 | 
						|
						const offset = node.range[0];
 | 
						|
 | 
						|
						codeUnits ??= parseStringLiteral(source);
 | 
						|
						start = offset + codeUnits[firstIndex].start;
 | 
						|
						end = offset + codeUnits[lastIndex].end;
 | 
						|
					} else {
 | 
						|
						// RegExp Literal
 | 
						|
						const offset = node.range[0] + 1; // Add 1 to skip the leading slash.
 | 
						|
 | 
						|
						start = offset + firstIndex;
 | 
						|
						end = offset + lastIndex + 1;
 | 
						|
					}
 | 
						|
 | 
						|
					return {
 | 
						|
						start: sourceCode.getLocFromIndex(start),
 | 
						|
						end: sourceCode.getLocFromIndex(end),
 | 
						|
					};
 | 
						|
				});
 | 
						|
			}
 | 
						|
 | 
						|
			for (const [kind, matches] of foundKindMatches) {
 | 
						|
				let suggest;
 | 
						|
 | 
						|
				if (kind === "surrogatePairWithoutUFlag") {
 | 
						|
					suggest = [
 | 
						|
						{
 | 
						|
							messageId: "suggestUnicodeFlag",
 | 
						|
							fix: unicodeFixer,
 | 
						|
						},
 | 
						|
					];
 | 
						|
				}
 | 
						|
 | 
						|
				const locs = getNodeReportLocations(matches);
 | 
						|
 | 
						|
				for (const loc of locs) {
 | 
						|
					context.report({
 | 
						|
						node,
 | 
						|
						loc,
 | 
						|
						messageId: kind,
 | 
						|
						suggest,
 | 
						|
					});
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return {
 | 
						|
			"Literal[regex]"(node) {
 | 
						|
				if (checkedPatternNodes.has(node)) {
 | 
						|
					return;
 | 
						|
				}
 | 
						|
				verify(node, node.regex.pattern, node.regex.flags, fixer => {
 | 
						|
					if (
 | 
						|
						!isValidWithUnicodeFlag(
 | 
						|
							context.languageOptions.ecmaVersion,
 | 
						|
							node.regex.pattern,
 | 
						|
						)
 | 
						|
					) {
 | 
						|
						return null;
 | 
						|
					}
 | 
						|
 | 
						|
					return fixer.insertTextAfter(node, "u");
 | 
						|
				});
 | 
						|
			},
 | 
						|
			Program(node) {
 | 
						|
				const scope = sourceCode.getScope(node);
 | 
						|
				const tracker = new ReferenceTracker(scope);
 | 
						|
 | 
						|
				/*
 | 
						|
				 * Iterate calls of RegExp.
 | 
						|
				 * E.g., `new RegExp()`, `RegExp()`, `new window.RegExp()`,
 | 
						|
				 *       `const {RegExp: a} = window; new a()`, etc...
 | 
						|
				 */
 | 
						|
				for (const { node: refNode } of tracker.iterateGlobalReferences(
 | 
						|
					{
 | 
						|
						RegExp: { [CALL]: true, [CONSTRUCT]: true },
 | 
						|
					},
 | 
						|
				)) {
 | 
						|
					let pattern, flags;
 | 
						|
					const [patternNode, flagsNode] = refNode.arguments;
 | 
						|
					const evaluatedPattern = getStaticValueOrRegex(
 | 
						|
						patternNode,
 | 
						|
						scope,
 | 
						|
					);
 | 
						|
 | 
						|
					if (!evaluatedPattern) {
 | 
						|
						continue;
 | 
						|
					}
 | 
						|
					if (flagsNode) {
 | 
						|
						if (evaluatedPattern.regex) {
 | 
						|
							pattern = evaluatedPattern.regex.pattern;
 | 
						|
							checkedPatternNodes.add(patternNode);
 | 
						|
						} else {
 | 
						|
							pattern = String(evaluatedPattern.value);
 | 
						|
						}
 | 
						|
						flags = getStringIfConstant(flagsNode, scope);
 | 
						|
					} else {
 | 
						|
						if (evaluatedPattern.regex) {
 | 
						|
							continue;
 | 
						|
						}
 | 
						|
						pattern = String(evaluatedPattern.value);
 | 
						|
						flags = "";
 | 
						|
					}
 | 
						|
 | 
						|
					if (typeof flags === "string") {
 | 
						|
						verify(patternNode, pattern, flags, fixer => {
 | 
						|
							if (
 | 
						|
								!isValidWithUnicodeFlag(
 | 
						|
									context.languageOptions.ecmaVersion,
 | 
						|
									pattern,
 | 
						|
								)
 | 
						|
							) {
 | 
						|
								return null;
 | 
						|
							}
 | 
						|
 | 
						|
							if (refNode.arguments.length === 1) {
 | 
						|
								const penultimateToken =
 | 
						|
									sourceCode.getLastToken(refNode, {
 | 
						|
										skip: 1,
 | 
						|
									}); // skip closing parenthesis
 | 
						|
 | 
						|
								return fixer.insertTextAfter(
 | 
						|
									penultimateToken,
 | 
						|
									astUtils.isCommaToken(penultimateToken)
 | 
						|
										? ' "u",'
 | 
						|
										: ', "u"',
 | 
						|
								);
 | 
						|
							}
 | 
						|
 | 
						|
							if (
 | 
						|
								(flagsNode.type === "Literal" &&
 | 
						|
									typeof flagsNode.value === "string") ||
 | 
						|
								flagsNode.type === "TemplateLiteral"
 | 
						|
							) {
 | 
						|
								const range = [
 | 
						|
									flagsNode.range[0],
 | 
						|
									flagsNode.range[1] - 1,
 | 
						|
								];
 | 
						|
 | 
						|
								return fixer.insertTextAfterRange(range, "u");
 | 
						|
							}
 | 
						|
 | 
						|
							return null;
 | 
						|
						});
 | 
						|
					}
 | 
						|
				}
 | 
						|
			},
 | 
						|
		};
 | 
						|
	},
 | 
						|
};
 |