routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+357
@@ -0,0 +1,357 @@
|
||||
import {hasSideEffect} from '@eslint-community/eslint-utils';
|
||||
import isSameReference from './utils/is-same-reference.js';
|
||||
import getIndentString from './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, context) => {
|
||||
const {sourceCode} = context;
|
||||
return sourceCode.getRange(inner)[0] >= sourceCode.getRange(outer)[0] && sourceCode.getRange(inner)[1] <= sourceCode.getRange(outer)[1];
|
||||
};
|
||||
|
||||
function hasBreakInside(breakStatements, node, context) {
|
||||
for (const breakStatement of breakStatements) {
|
||||
if (!isNodeInsideNode(breakStatement, node, context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const breakTarget = getBreakTarget(breakStatement);
|
||||
|
||||
if (!breakTarget) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNodeInsideNode(node, breakTarget, context)) {
|
||||
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}, context, options) {
|
||||
const {sourceCode} = context;
|
||||
const discriminantText = sourceCode.getText(discriminant);
|
||||
|
||||
return function * (fixer) {
|
||||
const firstStatement = ifStatements[0].statement;
|
||||
const indent = getIndentString(firstStatement, context);
|
||||
yield fixer.insertTextBefore(firstStatement, `switch (${discriminantText}) {`);
|
||||
|
||||
const lastStatement = ifStatements.at(-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} = statement;
|
||||
|
||||
if (alternate) {
|
||||
const [, start] = sourceCode.getRange(consequent);
|
||||
const [end] = sourceCode.getRange(alternate);
|
||||
yield fixer.removeRange([start, end]);
|
||||
}
|
||||
|
||||
const headRange = [
|
||||
sourceCode.getRange(statement)[0],
|
||||
sourceCode.getRange(consequent)[0],
|
||||
];
|
||||
yield fixer.removeRange(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 = {
|
||||
...context.options[0],
|
||||
insertBreakInDefaultCase: false,
|
||||
};
|
||||
const {sourceCode} = context;
|
||||
const ifStatements = new Set();
|
||||
const breakStatements = [];
|
||||
const checked = new Set();
|
||||
|
||||
context.on('IfStatement', node => {
|
||||
ifStatements.add(node);
|
||||
});
|
||||
|
||||
context.on('BreakStatement', node => {
|
||||
if (!node.label) {
|
||||
breakStatements.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
context.on('Program:exit', function * () {
|
||||
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: sourceCode.getLoc(node).start,
|
||||
end: sourceCode.getLoc(node.consequent).start,
|
||||
},
|
||||
messageId: MESSAGE_ID,
|
||||
};
|
||||
|
||||
if (
|
||||
!hasSideEffect(discriminant, sourceCode)
|
||||
&& !ifStatements.some(({statement}) => hasBreakInside(breakStatements, statement, context))
|
||||
) {
|
||||
problem.fix = fix({discriminant, ifStatements}, context, options);
|
||||
}
|
||||
|
||||
yield problem;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const schema = [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
minimumCases: {
|
||||
type: 'integer',
|
||||
minimum: 2,
|
||||
description: 'The minimum number of `if`/`else if` cases before suggesting a `switch` statement.',
|
||||
},
|
||||
emptyDefaultCase: {
|
||||
enum: [
|
||||
'no-default-comment',
|
||||
'do-nothing-comment',
|
||||
'no-default-case',
|
||||
],
|
||||
description: 'How to handle an empty `default` case.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Prefer `switch` over multiple `else-if`.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema,
|
||||
defaultOptions: [
|
||||
{
|
||||
minimumCases: 3,
|
||||
emptyDefaultCase: 'no-default-comment',
|
||||
},
|
||||
],
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user