123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- 'use strict';
- const {hasSideEffect} = require('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,
- },
- };
|