139 lines
3.8 KiB
JavaScript
139 lines
3.8 KiB
JavaScript
import {isCommentToken} from '@eslint-community/eslint-utils';
|
|
import getIndentString from './utils/get-indent-string.js';
|
|
|
|
const MESSAGE_ID = 'switch-case-break-position';
|
|
const messages = {
|
|
[MESSAGE_ID]: 'Move `{{keyword}}` inside the block statement.',
|
|
};
|
|
|
|
const TERMINATING_STATEMENT_TYPES = new Set([
|
|
'BreakStatement',
|
|
'ReturnStatement',
|
|
'ContinueStatement',
|
|
'ThrowStatement',
|
|
]);
|
|
|
|
function getKeyword(node) {
|
|
switch (node.type) {
|
|
case 'BreakStatement': {
|
|
return 'break';
|
|
}
|
|
|
|
case 'ReturnStatement': {
|
|
return 'return';
|
|
}
|
|
|
|
case 'ContinueStatement': {
|
|
return 'continue';
|
|
}
|
|
|
|
case 'ThrowStatement': {
|
|
return 'throw';
|
|
}
|
|
|
|
// No default
|
|
}
|
|
}
|
|
|
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
const create = context => {
|
|
const {sourceCode} = context;
|
|
|
|
context.on('SwitchCase', node => {
|
|
const {consequent} = node;
|
|
|
|
if (consequent.length < 2) {
|
|
return;
|
|
}
|
|
|
|
const lastStatement = consequent.at(-1);
|
|
|
|
if (!TERMINATING_STATEMENT_TYPES.has(lastStatement.type)) {
|
|
return;
|
|
}
|
|
|
|
// Check if there's a BlockStatement before the terminating statement
|
|
// The block must be the only other statement (ignoring the terminating one)
|
|
const statementsBeforeTerminating = consequent.slice(0, -1);
|
|
|
|
if (
|
|
statementsBeforeTerminating.length !== 1
|
|
|| statementsBeforeTerminating[0].type !== 'BlockStatement'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const blockStatement = statementsBeforeTerminating[0];
|
|
|
|
if (blockStatement.body.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const keyword = getKeyword(lastStatement);
|
|
|
|
return {
|
|
node: lastStatement,
|
|
loc: sourceCode.getLoc(lastStatement),
|
|
messageId: MESSAGE_ID,
|
|
data: {keyword},
|
|
...(lastStatement.type !== 'ReturnStatement' && lastStatement.type !== 'ThrowStatement' && {
|
|
* fix(fixer) {
|
|
// Skip fix if there are comments between the block and the terminating statement
|
|
const closingBrace = sourceCode.getLastToken(blockStatement);
|
|
const hasComments = sourceCode.getTokensBetween(closingBrace, lastStatement, {includeComments: true})
|
|
.some(token => isCommentToken(token));
|
|
if (hasComments) {
|
|
return;
|
|
}
|
|
|
|
// Skip fix for single-line blocks (produces malformed output)
|
|
const blockLoc = sourceCode.getLoc(blockStatement);
|
|
if (blockLoc.start.line === blockLoc.end.line) {
|
|
return;
|
|
}
|
|
|
|
// Skip fix if the terminator has trailing same-line comments
|
|
const trailingComments = sourceCode.getCommentsAfter(lastStatement);
|
|
const terminatorLine = sourceCode.getLoc(lastStatement).end.line;
|
|
if (trailingComments.some(comment => sourceCode.getLoc(comment).start.line === terminatorLine)) {
|
|
return;
|
|
}
|
|
|
|
const lastBodyStatement = blockStatement.body.at(-1);
|
|
const terminatingStatementText = sourceCode.getText(lastStatement);
|
|
const bodyIndent = getIndentString(lastBodyStatement, context);
|
|
|
|
// Insert before the closing brace, after the last token (including any comments)
|
|
// This preserves comment attachment to the original statements
|
|
const lastTokenBeforeBrace = sourceCode.getTokenBefore(closingBrace, {includeComments: true});
|
|
yield fixer.insertTextAfter(
|
|
lastTokenBeforeBrace,
|
|
`\n${bodyIndent}${terminatingStatementText}`,
|
|
);
|
|
|
|
// Remove the terminating statement and whitespace between it and the closing brace
|
|
const closingBraceEnd = sourceCode.getRange(closingBrace)[1];
|
|
const terminatingEnd = sourceCode.getRange(lastStatement)[1];
|
|
yield fixer.removeRange([closingBraceEnd, terminatingEnd]);
|
|
},
|
|
}),
|
|
};
|
|
});
|
|
};
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
const config = {
|
|
create,
|
|
meta: {
|
|
type: 'layout',
|
|
docs: {
|
|
description: 'Enforce consistent `break`/`return`/`continue`/`throw` position in `case` clauses.',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
messages,
|
|
},
|
|
};
|
|
|
|
export default config;
|