| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 | 'use strict';const {hasSideEffect} = require('@eslint-community/eslint-utils');const isSameReference = require('./utils/is-same-reference.js');const getIndentString = require('./utils/get-indent-string.js');const MESSAGE_ID = 'prefer-switch';const messages = {	[MESSAGE_ID]: 'Use `switch` instead of multiple `else-if`.',};const isSame = (nodeA, nodeB) => nodeA === nodeB || isSameReference(nodeA, nodeB);function getEqualityComparisons(node) {	const nodes = [node];	const compareExpressions = [];	while (nodes.length > 0) {		node = nodes.pop();		if (node.type === 'LogicalExpression' && node.operator === '||') {			nodes.push(node.right, node.left);			continue;		}		if (node.type !== 'BinaryExpression' || node.operator !== '===') {			return [];		}		compareExpressions.push(node);	}	return compareExpressions;}function getCommonReferences(expressions, candidates) {	for (const {left, right} of expressions) {		candidates = candidates.filter(node => isSame(node, left) || isSame(node, right));		if (candidates.length === 0) {			break;		}	}	return candidates;}function getStatements(statement) {	let discriminantCandidates;	const ifStatements = [];	for (; statement && statement.type === 'IfStatement'; statement = statement.alternate) {		const {test} = statement;		const compareExpressions = getEqualityComparisons(test);		if (compareExpressions.length === 0) {			break;		}		if (!discriminantCandidates) {			const [{left, right}] = compareExpressions;			discriminantCandidates = [left, right];		}		const candidates = getCommonReferences(			compareExpressions,			discriminantCandidates,		);		if (candidates.length === 0) {			break;		}		discriminantCandidates = candidates;		ifStatements.push({			statement,			compareExpressions,		});	}	return {		ifStatements,		discriminant: discriminantCandidates && discriminantCandidates[0],	};}const breakAbleNodeTypes = new Set([	'WhileStatement',	'DoWhileStatement',	'ForStatement',	'ForOfStatement',	'ForInStatement',	'SwitchStatement',]);const getBreakTarget = node => {	for (;node.parent; node = node.parent) {		if (breakAbleNodeTypes.has(node.type)) {			return node;		}	}};const isNodeInsideNode = (inner, outer) =>	inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];function hasBreakInside(breakStatements, node) {	for (const breakStatement of breakStatements) {		if (!isNodeInsideNode(breakStatement, node)) {			continue;		}		const breakTarget = getBreakTarget(breakStatement);		if (!breakTarget) {			return true;		}		if (isNodeInsideNode(node, breakTarget)) {			return true;		}	}	return false;}function * insertBracesIfNotBlockStatement(node, fixer, indent) {	if (!node || node.type === 'BlockStatement') {		return;	}	yield fixer.insertTextBefore(node, `{\n${indent}`);	yield fixer.insertTextAfter(node, `\n${indent}}`);}function * insertBreakStatement(node, fixer, sourceCode, indent) {	if (node.type === 'BlockStatement') {		const lastToken = sourceCode.getLastToken(node);		yield fixer.insertTextBefore(lastToken, `\n${indent}break;\n${indent}`);	} else {		yield fixer.insertTextAfter(node, `\n${indent}break;`);	}}function getBlockStatementLastNode(blockStatement) {	const {body} = blockStatement;	for (let index = body.length - 1; index >= 0; index--) {		const node = body[index];		if (node.type === 'FunctionDeclaration' || node.type === 'EmptyStatement') {			continue;		}		if (node.type === 'BlockStatement') {			const last = getBlockStatementLastNode(node);			if (last) {				return last;			}			continue;		}		return node;	}}function shouldInsertBreakStatement(node) {	switch (node.type) {		case 'ReturnStatement':		case 'ThrowStatement': {			return false;		}		case 'IfStatement': {			return !node.alternate				|| shouldInsertBreakStatement(node.consequent)				|| shouldInsertBreakStatement(node.alternate);		}		case 'BlockStatement': {			const lastNode = getBlockStatementLastNode(node);			return !lastNode || shouldInsertBreakStatement(lastNode);		}		default: {			return true;		}	}}function fix({discriminant, ifStatements}, sourceCode, options) {	const discriminantText = sourceCode.getText(discriminant);	return function * (fixer) {		const firstStatement = ifStatements[0].statement;		const indent = getIndentString(firstStatement, sourceCode);		yield fixer.insertTextBefore(firstStatement, `switch (${discriminantText}) {`);		const lastStatement = ifStatements[ifStatements.length - 1].statement;		if (lastStatement.alternate) {			const {alternate} = lastStatement;			yield fixer.insertTextBefore(alternate, `\n${indent}default: `);			/*			Technically, we should insert braces for the following case,			but who writes like this? And using `let`/`const` is invalid.			```js			if (foo === 1) {}			else if (foo === 2) {}			else if (foo === 3) {}			else var a = 1;			```			*/		} else {			switch (options.emptyDefaultCase) {				case 'no-default-comment': {					yield fixer.insertTextAfter(firstStatement, `\n${indent}// No default`);					break;				}				case 'do-nothing-comment': {					yield fixer.insertTextAfter(firstStatement, `\n${indent}default:\n${indent}// Do nothing`);					break;				}				// No default			}		}		yield fixer.insertTextAfter(firstStatement, `\n${indent}}`);		for (const {statement, compareExpressions} of ifStatements) {			const {consequent, alternate, range} = statement;			const headRange = [range[0], consequent.range[0]];			if (alternate) {				const [, start] = consequent.range;				const [end] = alternate.range;				yield fixer.replaceTextRange([start, end], '');			}			yield fixer.replaceTextRange(headRange, '');			for (const {left, right} of compareExpressions) {				const node = isSame(left, discriminant) ? right : left;				const text = sourceCode.getText(node);				yield fixer.insertTextBefore(consequent, `\n${indent}case ${text}: `);			}			if (shouldInsertBreakStatement(consequent)) {				yield * insertBreakStatement(consequent, fixer, sourceCode, indent);				yield * insertBracesIfNotBlockStatement(consequent, fixer, indent);			}		}	};}/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const options = {		minimumCases: 3,		emptyDefaultCase: 'no-default-comment',		insertBreakInDefaultCase: false,		...context.options[0],	};	const sourceCode = context.getSourceCode();	const ifStatements = new Set();	const breakStatements = [];	const checked = new Set();	return {		'IfStatement'(node) {			ifStatements.add(node);		},		'BreakStatement:not([label])'(node) {			breakStatements.push(node);		},		* 'Program:exit'() {			for (const node of ifStatements) {				if (checked.has(node)) {					continue;				}				const {discriminant, ifStatements} = getStatements(node);				if (!discriminant || ifStatements.length < options.minimumCases) {					continue;				}				for (const {statement} of ifStatements) {					checked.add(statement);				}				const problem = {					loc: {						start: node.loc.start,						end: node.consequent.loc.start,					},					messageId: MESSAGE_ID,				};				if (					!hasSideEffect(discriminant, sourceCode)					&& !ifStatements.some(({statement}) => hasBreakInside(breakStatements, statement))				) {					problem.fix = fix({discriminant, ifStatements}, sourceCode, options);				}				yield problem;			}		},	};};const schema = [	{		type: 'object',		additionalProperties: false,		properties: {			minimumCases: {				type: 'integer',				minimum: 2,				default: 3,			},			emptyDefaultCase: {				enum: [					'no-default-comment',					'do-nothing-comment',					'no-default-case',				],				default: 'no-default-comment',			},		},	},];/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer `switch` over multiple `else-if`.',		},		fixable: 'code',		schema,		messages,	},};
 |