securityos/node_modules/eslint-plugin-unicorn/rules/prefer-switch.js

343 lines
7.8 KiB
JavaScript

'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,
},
};