| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 | 'use strict';const {isParenthesized} = require('@eslint-community/eslint-utils');const avoidCapture = require('./utils/avoid-capture.js');const needsSemicolon = require('./utils/needs-semicolon.js');const isSameReference = require('./utils/is-same-reference.js');const getIndentString = require('./utils/get-indent-string.js');const {getParenthesizedText} = require('./utils/parentheses.js');const shouldAddParenthesesToConditionalExpressionChild = require('./utils/should-add-parentheses-to-conditional-expression-child.js');const {extendFixRange} = require('./fix/index.js');const getScopes = require('./utils/get-scopes.js');const messageId = 'prefer-ternary';const selector = [	'IfStatement',	':not(IfStatement > .alternate)',	'[test.type!="ConditionalExpression"]',	'[consequent]',	'[alternate]',].join('');const isTernary = node => node?.type === 'ConditionalExpression';function getNodeBody(node) {	/* c8 ignore next 3 */	if (!node) {		return;	}	if (node.type === 'ExpressionStatement') {		return getNodeBody(node.expression);	}	if (node.type === 'BlockStatement') {		const body = node.body.filter(({type}) => type !== 'EmptyStatement');		if (body.length === 1) {			return getNodeBody(body[0]);		}	}	return node;}const isSingleLineNode = node => node.loc.start.line === node.loc.end.line;/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const onlySingleLine = context.options[0] === 'only-single-line';	const sourceCode = context.getSourceCode();	const scopeToNamesGeneratedByFixer = new WeakMap();	const isSafeName = (name, scopes) => scopes.every(scope => {		const generatedNames = scopeToNamesGeneratedByFixer.get(scope);		return !generatedNames || !generatedNames.has(name);	});	const getText = node => {		let text = getParenthesizedText(node, sourceCode);		if (			!isParenthesized(node, sourceCode)			&& shouldAddParenthesesToConditionalExpressionChild(node)		) {			text = `(${text})`;		}		return text;	};	function merge(options, mergeOptions) {		const {			before = '',			after = ';',			consequent,			alternate,			node,		} = options;		const {			checkThrowStatement,			returnFalseIfNotMergeable,		} = {			checkThrowStatement: false,			returnFalseIfNotMergeable: false,			...mergeOptions,		};		if (!consequent || !alternate || consequent.type !== alternate.type) {			return returnFalseIfNotMergeable ? false : options;		}		const {type, argument, delegate, left, right, operator} = consequent;		if (			type === 'ReturnStatement'			&& !isTernary(argument)			&& !isTernary(alternate.argument)		) {			return merge({				before: `${before}return `,				after,				consequent: argument === null ? 'undefined' : argument,				alternate: alternate.argument === null ? 'undefined' : alternate.argument,				node,			});		}		if (			type === 'YieldExpression'			&& delegate === alternate.delegate			&& !isTernary(argument)			&& !isTernary(alternate.argument)		) {			return merge({				before: `${before}yield${delegate ? '*' : ''} (`,				after: `)${after}`,				consequent: argument === null ? 'undefined' : argument,				alternate: alternate.argument === null ? 'undefined' : alternate.argument,				node,			});		}		if (			type === 'AwaitExpression'			&& !isTernary(argument)			&& !isTernary(alternate.argument)		) {			return merge({				before: `${before}await (`,				after: `)${after}`,				consequent: argument,				alternate: alternate.argument,				node,			});		}		if (			checkThrowStatement			&& type === 'ThrowStatement'			&& !isTernary(argument)			&& !isTernary(alternate.argument)		) {			// `ThrowStatement` don't check nested			// If `IfStatement` is not a `BlockStatement`, need add `{}`			const {parent} = node;			const needBraces = parent && parent.type !== 'BlockStatement';			return {				type,				before: `${before}${needBraces ? '{\n{{INDENT_STRING}}' : ''}const {{ERROR_NAME}} = `,				after: `;\n{{INDENT_STRING}}throw {{ERROR_NAME}};${needBraces ? '\n}' : ''}`,				consequent: argument,				alternate: alternate.argument,			};		}		if (			type === 'AssignmentExpression'			&& operator === alternate.operator			&& !isTernary(left)			&& !isTernary(alternate.left)			&& !isTernary(right)			&& !isTernary(alternate.right)			&& isSameReference(left, alternate.left)		) {			return merge({				before: `${before}${sourceCode.getText(left)} ${operator} `,				after,				consequent: right,				alternate: alternate.right,				node,			});		}		return returnFalseIfNotMergeable ? false : options;	}	return {		[selector](node) {			const consequent = getNodeBody(node.consequent);			const alternate = getNodeBody(node.alternate);			if (				onlySingleLine				&& [consequent, alternate, node.test].some(node => !isSingleLineNode(node))			) {				return;			}			const result = merge({node, consequent, alternate}, {				checkThrowStatement: true,				returnFalseIfNotMergeable: true,			});			if (!result) {				return;			}			const problem = {node, messageId};			// Don't fix if there are comments			if (sourceCode.getCommentsInside(node).length > 0) {				return problem;			}			const scope = context.getScope();			problem.fix = function * (fixer) {				const testText = getText(node.test);				const consequentText = typeof result.consequent === 'string'					? result.consequent					: getText(result.consequent);				const alternateText = typeof result.alternate === 'string'					? result.alternate					: getText(result.alternate);				let {type, before, after} = result;				let generateNewVariables = false;				if (type === 'ThrowStatement') {					const scopes = getScopes(scope);					const errorName = avoidCapture('error', scopes, isSafeName);					for (const scope of scopes) {						if (!scopeToNamesGeneratedByFixer.has(scope)) {							scopeToNamesGeneratedByFixer.set(scope, new Set());						}						const generatedNames = scopeToNamesGeneratedByFixer.get(scope);						generatedNames.add(errorName);					}					const indentString = getIndentString(node, sourceCode);					after = after						.replace('{{INDENT_STRING}}', indentString)						.replace('{{ERROR_NAME}}', errorName);					before = before						.replace('{{INDENT_STRING}}', indentString)						.replace('{{ERROR_NAME}}', errorName);					generateNewVariables = true;				}				let fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`;				const tokenBefore = sourceCode.getTokenBefore(node);				const shouldAddSemicolonBefore = needsSemicolon(tokenBefore, sourceCode, fixed);				if (shouldAddSemicolonBefore) {					fixed = `;${fixed}`;				}				yield fixer.replaceText(node, fixed);				if (generateNewVariables) {					yield * extendFixRange(fixer, sourceCode.ast.range);				}			};			return problem;		},	};};const schema = [	{		enum: ['always', 'only-single-line'],		default: 'always',	},];/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer ternary expressions over simple `if-else` statements.',		},		fixable: 'code',		schema,		messages: {			[messageId]: 'This `if` statement can be replaced by a ternary expression.',		},	},};
 |