Skip to content

Commit 427c4e3

Browse files
author
Mendes Hugo
committed
feat: add a basic implementation
1 parent 4651d8f commit 427c4e3

17 files changed

+531
-48
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@
295295
"@cspell/spellchecker": [
296296
"warn",
297297
{
298-
"autoFix": true
298+
"autoFix": true,
299+
"customWordListFile": "./tools/cspell/words.txt"
299300
}
300301
],
301302
"@shopify/no-useless-computed-properties": "error",
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { parse } from "@typescript-eslint/parser";
2+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
3+
4+
import { getDecoratorName } from "./get-decorator-name";
5+
6+
describe("getDecoratorName", () => {
7+
const getADecoratorNode = (decorator: string) => {
8+
const [classA] = parse(`@${decorator} class A {}`).body;
9+
10+
if (classA.type === AST_NODE_TYPES.ClassDeclaration) {
11+
return classA.decorators![0];
12+
}
13+
14+
throw new Error("Decorator is wrongly set");
15+
};
16+
17+
const names = ["A", "B", "c", "d", "myDecorator", "YOUR_decorator", "GET", "Post"];
18+
19+
it("should return the name of simple decorators", () => {
20+
for (const name of names) {
21+
const decorator = getADecoratorNode(name);
22+
expect(getDecoratorName(decorator)).toBe(name);
23+
}
24+
});
25+
26+
describe("Decorator factories", () => {
27+
it("should return the name of an empty factory", () => {
28+
for (const name of names) {
29+
const decorator = getADecoratorNode(`${name}()`);
30+
expect(getDecoratorName(decorator)).toBe(name);
31+
}
32+
});
33+
34+
it("should return the name of a factory with parameters", () => {
35+
for (const name of names) {
36+
const decorator = getADecoratorNode(`${name}("parameter", 123, {})`);
37+
expect(getDecoratorName(decorator)).toBe(name);
38+
}
39+
});
40+
});
41+
42+
describe("factory of factory of ... of decorator factories", () => {
43+
it("should return the name of an empty factory of ... factory", () => {
44+
for (const name of names) {
45+
const decorator = getADecoratorNode(`${name}()()`);
46+
expect(getDecoratorName(decorator)).toBe(name);
47+
}
48+
});
49+
50+
it("should return the name of a factory of ... factory with parameters", () => {
51+
for (const name of names) {
52+
const decorator = getADecoratorNode(`${name}("parameter")(123)({})`);
53+
expect(getDecoratorName(decorator)).toBe(name);
54+
}
55+
});
56+
});
57+
58+
it("should fail when it is not a decorator", () => {
59+
expect(() =>
60+
getDecoratorName({
61+
expression: {
62+
elements: [],
63+
loc: { end: { column: 0, line: 0 }, start: { column: 0, line: 0 } },
64+
range: [0, 0],
65+
type: AST_NODE_TYPES.ArrayPattern
66+
},
67+
loc: { end: { column: 0, line: 0 }, start: { column: 0, line: 0 } },
68+
range: [0, 0],
69+
type: AST_NODE_TYPES.Decorator
70+
})
71+
).toThrow();
72+
});
73+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { LeftHandSideExpression } from "@typescript-eslint/types/dist/generated/ast-spec";
2+
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
3+
4+
/**
5+
* @param decorator the decorator node to get the name from
6+
* @returns the name of the given decorator
7+
*/
8+
export function getDecoratorName(decorator: TSESTree.Decorator): string {
9+
const getName = (expression: LeftHandSideExpression): string => {
10+
switch (expression.type) {
11+
case AST_NODE_TYPES.CallExpression:
12+
return getName(expression.callee);
13+
14+
case AST_NODE_TYPES.Identifier:
15+
return expression.name;
16+
}
17+
18+
throw new Error("Not a valid decorator.");
19+
};
20+
21+
return getName(decorator.expression);
22+
}

src/lib/decorator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./get-decorator-name";

src/lib/sort-rule/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./sort-rule";
2+
export * from "./sort-rule.listener";
23
export * from "./sort-rule.message-ids";
34
export * from "./sort-rule.options";
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
}

src/rules/sort-on-accessors.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,58 @@
1-
import { createSortRule } from "../lib/sort-rule";
1+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
2+
import { TSESTree } from "@typescript-eslint/utils";
3+
4+
import { createSortRule, sortRuleListener } from "../lib/sort-rule";
25

36
export const SORT_ON_ACCESSORS_NAME = "sort-on-accessors";
47

58
export const sortOnAccessors = createSortRule({
6-
create: () => {
7-
throw new Error("Not implemented yet");
9+
create: (context, [optionsWithDefault]) => {
10+
const { autoFix } = optionsWithDefault;
11+
12+
const getDecorated = (node: TSESTree.Decorator) => {
13+
const { parent } = node;
14+
15+
// Only get the decorated node, if it is an accessor
16+
if (parent?.type === AST_NODE_TYPES.MethodDefinition && parent.kind === "get") {
17+
return parent;
18+
}
19+
20+
return false;
21+
};
22+
23+
return autoFix
24+
? {
25+
// TODO: a selector for the accessors only
26+
Decorator(node) {
27+
const parent = getDecorated(node);
28+
if (!parent) {
29+
return;
30+
}
31+
32+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the current node comes from there
33+
const decorators = parent.decorators!;
34+
35+
// Run the listener only when on the first node
36+
if (decorators[0] === node) {
37+
sortRuleListener(context, decorators, optionsWithDefault);
38+
}
39+
}
40+
}
41+
: {
42+
Decorator(node) {
43+
const parent = getDecorated(node);
44+
if (!parent) {
45+
return;
46+
}
47+
48+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the current node comes from there
49+
const decorators = parent.decorators!;
50+
51+
// Get only the decorators after the current one
52+
const nodeIndex = decorators.findIndex(decorator => decorator === node);
53+
sortRuleListener(context, decorators.slice(nodeIndex), optionsWithDefault);
54+
}
55+
};
856
},
957
meta: {
1058
docs: {

src/rules/sort-on-classes.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
1-
import { createSortRule } from "../lib/sort-rule";
1+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
2+
3+
import { createSortRule, sortRuleListener } from "../lib/sort-rule";
24

35
export const SORT_ON_CLASSES_NAME = "sort-on-classes";
46

57
export const sortOnClasses = createSortRule({
6-
create: () => {
7-
throw new Error("Not implemented yet");
8+
create: (context, [optionsWithDefault]) => {
9+
const { autoFix } = optionsWithDefault;
10+
11+
return autoFix
12+
? {
13+
ClassDeclaration({ decorators }) {
14+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- always defined on class declarations
15+
sortRuleListener(context, decorators!, optionsWithDefault);
16+
}
17+
}
18+
: {
19+
Decorator(node) {
20+
const { parent } = node;
21+
if (parent?.type !== AST_NODE_TYPES.ClassDeclaration) {
22+
// Only for classes decorators
23+
return;
24+
}
25+
26+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the current node comes from there
27+
const decorators = parent.decorators!;
28+
29+
// Get only the decorators after the current one
30+
const nodeIndex = decorators.findIndex(decorator => decorator === node);
31+
sortRuleListener(context, decorators.slice(nodeIndex), optionsWithDefault);
32+
}
33+
};
834
},
935
meta: {
1036
docs: {

src/rules/sort-on-methods.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
1-
import { createSortRule } from "../lib/sort-rule";
1+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
2+
3+
import { createSortRule, sortRuleListener } from "../lib/sort-rule";
24

35
export const SORT_ON_METHODS_NAME = "sort-on-methods";
46

57
export const sortOnMethods = createSortRule({
6-
create: () => {
7-
throw new Error("Not implemented yet");
8+
create: (context, [optionsWithDefault]) => {
9+
const { autoFix } = optionsWithDefault;
10+
11+
return autoFix
12+
? {
13+
MethodDefinition({ decorators }) {
14+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- always defined on method definitions
15+
sortRuleListener(context, decorators!, optionsWithDefault);
16+
}
17+
}
18+
: {
19+
Decorator(node) {
20+
const { parent } = node;
21+
if (parent?.type !== AST_NODE_TYPES.MethodDefinition) {
22+
// Only for methods decorators
23+
return;
24+
}
25+
26+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the current node comes from there
27+
const decorators = parent.decorators!;
28+
29+
// Get only the decorators after the current one
30+
const nodeIndex = decorators.findIndex(decorator => decorator === node);
31+
sortRuleListener(context, decorators.slice(nodeIndex), optionsWithDefault);
32+
}
33+
};
834
},
935
meta: {
1036
docs: {

0 commit comments

Comments
 (0)