sortier
Version:
An opinionated code sorter
186 lines (185 loc) • 8.5 kB
JavaScript
import { compare, getContextGroups, reorderValues } from "../../utilities/sort-utils.js";
var HasImmediateExitOption;
(function (HasImmediateExitOption) {
HasImmediateExitOption[HasImmediateExitOption["Indeterminate"] = 0] = "Indeterminate";
HasImmediateExitOption[HasImmediateExitOption["True"] = 1] = "True";
HasImmediateExitOption[HasImmediateExitOption["False"] = 2] = "False";
})(HasImmediateExitOption || (HasImmediateExitOption = {}));
export function sortSwitchCases(cases, comments, fileContents,
// Left in for consistency with other sort functions
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options) {
if (cases.length <= 1) {
return fileContents;
}
// Never sort the default portion, it always comes last
if (cases[cases.length - 1].test == null) {
cases = cases.slice(0, cases.length - 1);
}
if (cases.findIndex((value) => {
return value.test == null;
}) !== -1) {
// If there is a default statement in the middle of the case statements, that is something we do not handle currently
return fileContents;
}
let doesBreakOut = HasImmediateExitOption.Indeterminate;
let newFileContents = fileContents.slice();
const contextGroups = getContextGroups(cases, comments, fileContents);
for (let x = 0; x < contextGroups.length; x++) {
const cases = contextGroups[x].nodes;
const comments = contextGroups[x].comments;
if (cases.length <= 1) {
// No need to sort if there's only one case
continue;
}
doesBreakOut = doesCaseBreakOutOfSwitch(cases[cases.length - 1]);
if (doesBreakOut !== HasImmediateExitOption.True) {
cases.pop();
}
// Determine where the "break" statements are so we dont' sort through them
// which would change the logic of the code
const switchGroupsWithBreaks = [];
let switchCaseStart = 0;
let switchCaseEnd = 0;
for (switchCaseEnd = 0; switchCaseEnd < cases.length; switchCaseEnd++) {
// Do not rearrange items that are in a non-break statement
doesBreakOut = doesCaseBreakOutOfSwitch(cases[switchCaseEnd]);
if (doesBreakOut === HasImmediateExitOption.Indeterminate) {
return fileContents;
}
if (doesBreakOut === HasImmediateExitOption.False) {
continue;
}
switchGroupsWithBreaks.push(cases.slice(switchCaseStart, switchCaseEnd + 1));
switchCaseStart = switchCaseEnd + 1;
}
if (switchCaseStart < switchCaseEnd) {
switchGroupsWithBreaks.push(cases.slice(switchCaseStart, switchCaseEnd + 1));
}
// Within each case group with a break, if there are any case statements that share the same
// execution block, they need to be sorted
for (let x = 0; x < switchGroupsWithBreaks.length; x++) {
const cases = switchGroupsWithBreaks[x];
switchCaseStart = 0;
switchCaseEnd = 0;
for (let casesIndex = 0; casesIndex < cases.length; casesIndex++) {
const caseStatement = cases[casesIndex];
if (caseStatement.consequent == null || caseStatement.consequent.length === 0) {
switchCaseEnd++;
}
else if (switchCaseStart < switchCaseEnd) {
switchCaseEnd++;
const unsorted = cases
.slice(switchCaseStart, switchCaseEnd)
.map((value) => {
return value.test;
})
.filter((value) => value != null);
const sorted = unsorted.slice().sort((a, b) => {
const aText = getSortableText(a, fileContents);
const bText = getSortableText(b, fileContents);
return compare(aText, bText);
});
newFileContents = reorderValues(newFileContents, comments, unsorted, sorted);
switchCaseStart = switchCaseEnd;
}
}
}
// If the last switch group is a fall through, don't include it in the swap
doesBreakOut = doesCaseBreakOutOfSwitch(cases[cases.length - 1]);
if (doesBreakOut !== HasImmediateExitOption.True) {
switchGroupsWithBreaks.pop();
}
// Now sort the actual switch groups
const switchGroupsWithBreaksSorted = switchGroupsWithBreaks.slice();
const switchGroupToLowestCase = new Map();
for (const switchGroupsWithBreak of switchGroupsWithBreaksSorted) {
let lowestText = null;
for (const caseStatement of switchGroupsWithBreak) {
const testRange = caseStatement.test?.range;
if (testRange == null) {
continue;
}
const text = fileContents.substring(testRange[0], testRange[1]);
if (lowestText == null || compare(lowestText, text) > 0) {
lowestText = text;
}
}
if (lowestText != null) {
switchGroupToLowestCase.set(switchGroupsWithBreak, lowestText);
}
}
switchGroupsWithBreaksSorted.sort((a, b) => {
const aValue = switchGroupToLowestCase.get(a);
const bValue = switchGroupToLowestCase.get(b);
if (aValue == null || bValue == null) {
throw new Error("Null value for switch case statement");
}
return compare(aValue, bValue);
});
newFileContents = reorderValues(newFileContents, comments, caseGroupsToMinimumTypeinformations(switchGroupsWithBreaks), caseGroupsToMinimumTypeinformations(switchGroupsWithBreaksSorted));
}
return newFileContents;
}
function doesCaseBreakOutOfSwitch(caseStatement) {
return doesHaveImmediateExit(caseStatement.consequent);
}
function doesHaveImmediateExit(values) {
let isIndeterminate = false;
for (const value of values) {
switch (value.type) {
case "BlockStatement": {
return doesHaveImmediateExit(value.body);
}
case "BreakStatement":
case "ReturnStatement":
case "ThrowStatement": {
return HasImmediateExitOption.True;
}
case "SwitchStatement": {
// If the last option in the switch statement has an exit, then either
// all previous consequents have an exit (e.g. not getting to the last one)
// or they all fall through to the last one which means we exit
if (doesHaveImmediateExit(value.cases[value.cases.length - 1].consequent) === HasImmediateExitOption.True) {
return HasImmediateExitOption.True;
}
// falls through
}
default:
// There are several types which are a bit more complicated which
// leaves us in an undeterminate state if we will exit or not
if (
// Value is some sort of loop
("body" in value && value.body != null) ||
// Value is some sort of conditional
("consequent" in value && value.consequent != null) ||
// Value is a try catch
("block" in value && value.block != null)) {
isIndeterminate = true;
}
}
}
return isIndeterminate ? HasImmediateExitOption.Indeterminate : HasImmediateExitOption.False;
}
function getSortableText(a, fileContents) {
if (a != null) {
if ("range" in a && a.range != null) {
return fileContents.substring(a.range[0], a.range[1]);
}
}
throw new Error("Switch case test has no text?");
}
function caseGroupsToMinimumTypeinformations(switchGroupsWithBreaks) {
return switchGroupsWithBreaks.map((value) => {
const firstNode = value[0];
const firstRange = firstNode.range;
const lastRange = value[value.length - 1].range;
if (firstRange == null || lastRange == null) {
throw new Error("Range is null");
}
const result = {
range: [firstRange[0], lastRange[1]],
};
return result;
});
}