|
| 1 | +import { TSESTree, TSESLint } from "@typescript-eslint/utils"; |
| 2 | + |
| 3 | +import { SortRuleMessageIds } from "./sort-rule.message-ids"; |
| 4 | +import { SortRuleOptions } from "./sort-rule.options"; |
| 5 | +import { getDecoratorName } from "../decorator"; |
| 6 | + |
| 7 | +/** |
| 8 | + * The rule listener for sorting the decorators |
| 9 | + * |
| 10 | + * @param context the context the rule is listening |
| 11 | + * @param decorators the decorators to test (and fix) |
| 12 | + * @param options the rule options |
| 13 | + */ |
| 14 | +export function sortRuleListener( |
| 15 | + context: TSESLint.RuleContext<SortRuleMessageIds, [SortRuleOptions]>, |
| 16 | + decorators: TSESTree.Decorator[], |
| 17 | + options: SortRuleOptions |
| 18 | +) { |
| 19 | + const { autoFix, caseSensitive, direction } = options; |
| 20 | + |
| 21 | + // Get the name of a decorator |
| 22 | + const getName = (decorator: TSESTree.Decorator) => { |
| 23 | + const name = getDecoratorName(decorator); |
| 24 | + return caseSensitive ? name : name.toLowerCase(); |
| 25 | + }; |
| 26 | + |
| 27 | + const compare = (a: string, b: string) => { |
| 28 | + const [a1, b1] = direction === "desc" ? [b, a] : [a, b]; |
| 29 | + |
| 30 | + // `localCompare` messes with the upperCase |
| 31 | + if (a1 === b1) { |
| 32 | + return 0; |
| 33 | + } |
| 34 | + |
| 35 | + return a1 < b1 ? -1 : 1; |
| 36 | + }; |
| 37 | + |
| 38 | + const decoratorsWithName = decorators.map(decorator => ({ |
| 39 | + name: getName(decorator), |
| 40 | + node: decorator |
| 41 | + })); |
| 42 | + |
| 43 | + const sortRule = (decorators: typeof decoratorsWithName) => { |
| 44 | + if (decorators.length <= 1) { |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + const [{ name: currentName, node: currentNode }, ...remaining] = decorators; |
| 49 | + |
| 50 | + for (const { name } of remaining) { |
| 51 | + if (compare(currentName, name) > 0) { |
| 52 | + context.report({ |
| 53 | + fix: autoFix |
| 54 | + ? fixer => { |
| 55 | + const sourceCode = context.getSourceCode(); |
| 56 | + const sourceText = sourceCode.getText(); |
| 57 | + |
| 58 | + const sorted = decorators |
| 59 | + .slice() |
| 60 | + .sort(({ name: a }, { name: b }) => compare(a, b)); |
| 61 | + |
| 62 | + const newText = sorted.map(({ node: child }, i) => { |
| 63 | + const textAfter = |
| 64 | + i === sorted.length - 1 |
| 65 | + ? // If it's the last item, there's no text after to append. |
| 66 | + "" |
| 67 | + : // Otherwise, we need to grab the text after the original node. |
| 68 | + sourceText.slice( |
| 69 | + decorators[i].node.range[1], // End index of the current node . |
| 70 | + decorators[i + 1].node.range[0] // Start index of the next node. |
| 71 | + ); |
| 72 | + |
| 73 | + return sourceCode.getText(child) + textAfter; |
| 74 | + }); |
| 75 | + |
| 76 | + return fixer.replaceTextRange( |
| 77 | + [ |
| 78 | + decorators[0].node.range[0], |
| 79 | + decorators[decorators.length - 1].node.range[1] |
| 80 | + ], |
| 81 | + newText.join("") |
| 82 | + ); |
| 83 | + } |
| 84 | + : undefined, |
| 85 | + messageId: "incorrect-order", |
| 86 | + node: currentNode |
| 87 | + }); |
| 88 | + |
| 89 | + return; |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + if (autoFix) { |
| 94 | + sortRule(decorators.slice(1)); |
| 95 | + } |
| 96 | + }; |
| 97 | + |
| 98 | + sortRule(decoratorsWithName); |
| 99 | +} |
0 commit comments