@blumintinc/eslint-plugin-blumint 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/docs/rules/class-methods-read-top-to-bottom.md +46 -0
- package/docs/rules/dynamic-https-errors.md +1 -1
- package/docs/rules/require-memo.md +5 -1
- package/lib/index.js +4 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +72 -0
- package/lib/utils/ASTHelpers.js +160 -1
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphBuilder.js +80 -0
- package/lib/utils/graph/ClassGraphSorter.js +10 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +94 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -56,21 +56,24 @@ Or use the recommended config:
|
|
|
56
56
|
✅ Set in the `recommended` configuration.\
|
|
57
57
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
|
|
58
58
|
|
|
59
|
-
| Name
|
|
60
|
-
|
|
|
61
|
-
| [array-methods-this-context](docs/rules/array-methods-this-context.md)
|
|
62
|
-
| [
|
|
63
|
-
| [
|
|
64
|
-
| [
|
|
65
|
-
| [
|
|
66
|
-
| [
|
|
67
|
-
| [no-
|
|
68
|
-
| [no-
|
|
69
|
-
| [no-
|
|
70
|
-
| [no-
|
|
71
|
-
| [no-
|
|
72
|
-
| [
|
|
73
|
-
| [
|
|
59
|
+
| Name | Description | 💼 | ⚠️ | 🔧 |
|
|
60
|
+
| :--------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
|
|
61
|
+
| [array-methods-this-context](docs/rules/array-methods-this-context.md) | Prevent misuse of Array methods in OOP | | ✅ | |
|
|
62
|
+
| [class-methods-read-top-to-bottom](docs/rules/class-methods-read-top-to-bottom.md) | Ensures classes read linearly from top to bottom. | | ✅ | 🔧 |
|
|
63
|
+
| [dynamic-https-errors](docs/rules/dynamic-https-errors.md) | Dynamic error details should only be in the third argument of the HttpsError constructor. The second argument is hashed to produce a unique id. | | ✅ | |
|
|
64
|
+
| [export-if-in-doubt](docs/rules/export-if-in-doubt.md) | All top-level const definitions, type definitions, and functions should be exported | | | |
|
|
65
|
+
| [extract-global-constants](docs/rules/extract-global-constants.md) | Extract constants/functions to the global scope when possible | | | |
|
|
66
|
+
| [generic-starts-with-t](docs/rules/generic-starts-with-t.md) | Enforce TypeScript generic types to start with T | | ✅ | |
|
|
67
|
+
| [no-async-array-filter](docs/rules/no-async-array-filter.md) | Disallow async callbacks for Array.filter | ✅ | | |
|
|
68
|
+
| [no-async-foreach](docs/rules/no-async-foreach.md) | Disallow Array.forEach with an async callback function | ✅ | | |
|
|
69
|
+
| [no-conditional-literals-in-jsx](docs/rules/no-conditional-literals-in-jsx.md) | Disallow use of conditional literals in JSX code | ✅ | | |
|
|
70
|
+
| [no-filter-without-return](docs/rules/no-filter-without-return.md) | Disallow Array.filter callbacks without an explicit return (if part of a block statement) | ✅ | | |
|
|
71
|
+
| [no-misused-switch-case](docs/rules/no-misused-switch-case.md) | Prevent misuse of logical OR in switch case statements | ✅ | | |
|
|
72
|
+
| [no-unpinned-dependencies](docs/rules/no-unpinned-dependencies.md) | Enforces pinned dependencies | ✅ | | 🔧 |
|
|
73
|
+
| [no-useless-fragment](docs/rules/no-useless-fragment.md) | Prevent unnecessary use of React fragments | | ✅ | 🔧 |
|
|
74
|
+
| [prefer-fragment-shorthand](docs/rules/prefer-fragment-shorthand.md) | Prefer <> shorthand for <React.Fragment> | | ✅ | 🔧 |
|
|
75
|
+
| [prefer-type-over-interface](docs/rules/prefer-type-over-interface.md) | Prefer using type alias over interface | | ✅ | 🔧 |
|
|
76
|
+
| [require-memo](docs/rules/require-memo.md) | React components must be memoized | ✅ | | 🔧 |
|
|
74
77
|
|
|
75
78
|
<!-- end auto-generated rules list -->
|
|
76
79
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Ensures classes read linearly from top to bottom (`@blumintinc/blumint/class-methods-read-top-to-bottom`)
|
|
2
|
+
|
|
3
|
+
⚠️ This rule _warns_ in the ✅ `recommended` config.
|
|
4
|
+
|
|
5
|
+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
6
|
+
|
|
7
|
+
<!-- end auto-generated rule header -->
|
|
8
|
+
|
|
9
|
+
This rule enforces an ordering of class methods according to BluMint's code style.
|
|
10
|
+
|
|
11
|
+
## Rule Details
|
|
12
|
+
|
|
13
|
+
This rule warns for classes that don't follow the 'top to bottom' ordering - such that properties appear at the top of the class, the constructor (if present) follows, and other methods follow in a top-to-bottom order.
|
|
14
|
+
|
|
15
|
+
### Examples of incorrect code for this rule:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
class IncorrectlyOrdered {
|
|
19
|
+
field1: string;
|
|
20
|
+
field2: number;
|
|
21
|
+
methodA() {
|
|
22
|
+
this.methodB();
|
|
23
|
+
}
|
|
24
|
+
constructor() {
|
|
25
|
+
this.methodA();
|
|
26
|
+
this.methodC();
|
|
27
|
+
}
|
|
28
|
+
methodB() {}
|
|
29
|
+
methodC() {}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Examples of correct code for this rule:
|
|
34
|
+
```typescript
|
|
35
|
+
class CorrectlyOrdered {
|
|
36
|
+
field1: string;
|
|
37
|
+
field2: number;
|
|
38
|
+
constructor() {
|
|
39
|
+
this.methodA();
|
|
40
|
+
}
|
|
41
|
+
methodA() {
|
|
42
|
+
this.methodB();
|
|
43
|
+
}
|
|
44
|
+
methodB() {}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Dynamic
|
|
1
|
+
# Dynamic error details should only be in the third argument of the HttpsError constructor. The second argument is hashed to produce a unique id (`@blumintinc/blumint/dynamic-https-errors`)
|
|
2
2
|
|
|
3
3
|
⚠️ This rule _warns_ in the ✅ `recommended` config.
|
|
4
4
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# React components must be memoized (`@blumintinc/blumint/require-memo`)
|
|
2
|
+
|
|
3
|
+
💼 This rule is enabled in the ✅ `recommended` config.
|
|
4
|
+
|
|
5
|
+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
2
6
|
|
|
3
7
|
<!-- end auto-generated rule header -->
|
|
4
8
|
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const array_methods_this_context_1 = require("./rules/array-methods-this-context");
|
|
4
|
+
const class_methods_read_top_to_bottom_1 = require("./rules/class-methods-read-top-to-bottom");
|
|
4
5
|
const dynamic_https_errors_1 = require("./rules/dynamic-https-errors");
|
|
5
6
|
const export_if_in_doubt_1 = require("./rules/export-if-in-doubt");
|
|
6
7
|
const extract_global_constants_1 = require("./rules/extract-global-constants");
|
|
@@ -18,7 +19,7 @@ const require_memo_1 = require("./rules/require-memo");
|
|
|
18
19
|
module.exports = {
|
|
19
20
|
meta: {
|
|
20
21
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
21
|
-
version: '0.1.
|
|
22
|
+
version: '0.1.23',
|
|
22
23
|
},
|
|
23
24
|
parseOptions: {
|
|
24
25
|
ecmaVersion: 2020,
|
|
@@ -28,6 +29,7 @@ module.exports = {
|
|
|
28
29
|
plugins: ['@blumintinc/blumint'],
|
|
29
30
|
rules: {
|
|
30
31
|
'@blumintinc/blumint/array-methods-this-context': 'warn',
|
|
32
|
+
'@blumintinc/blumint/class-methods-read-top-to-bottom': 'warn',
|
|
31
33
|
'@blumintinc/blumint/dynamic-https-errors': 'warn',
|
|
32
34
|
// '@blumintinc/blumint/export-if-in-doubt': 'warn',
|
|
33
35
|
// '@blumintinc/blumint/extract-global-constants': 'warn',
|
|
@@ -47,6 +49,7 @@ module.exports = {
|
|
|
47
49
|
},
|
|
48
50
|
rules: {
|
|
49
51
|
'array-methods-this-context': array_methods_this_context_1.arrayMethodsThisContext,
|
|
52
|
+
'class-methods-read-top-to-bottom': class_methods_read_top_to_bottom_1.classMethodsReadTopToBottom,
|
|
50
53
|
'dynamic-https-errors': dynamic_https_errors_1.dynamicHttpsErrors,
|
|
51
54
|
'export-if-in-doubt': export_if_in_doubt_1.exportIfInDoubt,
|
|
52
55
|
'extract-global-constants': extract_global_constants_1.extractGlobalConstants,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classMethodsReadTopToBottom = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const ClassGraphBuilder_1 = require("../utils/graph/ClassGraphBuilder");
|
|
6
|
+
function getMemberName(member) {
|
|
7
|
+
if (member.type === 'MethodDefinition' ||
|
|
8
|
+
member.type === 'PropertyDefinition') {
|
|
9
|
+
return member.key.type === 'Identifier' ? member.key.name : null;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
14
|
+
name: 'class-methods-read-top-to-bottom',
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
docs: {
|
|
18
|
+
description: 'Ensures classes read linearly from top to bottom.',
|
|
19
|
+
recommended: 'warn',
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
classMethodsReadTopToBottom: 'Methods should be ordered for top-down readability.',
|
|
24
|
+
},
|
|
25
|
+
fixable: 'code', // To allow ESLint to autofix issues.
|
|
26
|
+
},
|
|
27
|
+
defaultOptions: [],
|
|
28
|
+
create(context) {
|
|
29
|
+
let className;
|
|
30
|
+
return {
|
|
31
|
+
ClassDeclaration(node) {
|
|
32
|
+
className = node.id?.name || '';
|
|
33
|
+
},
|
|
34
|
+
'ClassBody:exit'(node) {
|
|
35
|
+
const graphBuilder = new ClassGraphBuilder_1.ClassGraphBuilder(className, node);
|
|
36
|
+
const sortedOrder = graphBuilder.memberNamesSorted;
|
|
37
|
+
const actualOrder = node.body
|
|
38
|
+
.map((member) => member.type === 'MethodDefinition' ||
|
|
39
|
+
member.type === 'PropertyDefinition'
|
|
40
|
+
? member.key.name
|
|
41
|
+
: null)
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
for (let i = 0; i < actualOrder.length; i++) {
|
|
44
|
+
if (actualOrder[i] !== sortedOrder[i]) {
|
|
45
|
+
const sourceCode = context.getSourceCode();
|
|
46
|
+
const newClassBody = sortedOrder
|
|
47
|
+
.map((n) => {
|
|
48
|
+
// Fetch the actual AST node corresponding to the name
|
|
49
|
+
const memberNode = node.body.find((member) => getMemberName(member) === n);
|
|
50
|
+
const comments = sourceCode.getCommentsBefore(memberNode);
|
|
51
|
+
memberNode.range = [
|
|
52
|
+
Math.min(memberNode.range[0], Math.min(...comments.map((comment) => comment.range[0]))),
|
|
53
|
+
Math.max(memberNode.range[1], Math.max(...comments.map((comment) => comment.range[1]))),
|
|
54
|
+
];
|
|
55
|
+
return sourceCode.getText(memberNode);
|
|
56
|
+
})
|
|
57
|
+
.join('\n');
|
|
58
|
+
return context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: 'classMethodsReadTopToBottom',
|
|
61
|
+
fix(fixer) {
|
|
62
|
+
return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], // Exclude the curly braces
|
|
63
|
+
newClassBody);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=class-methods-read-top-to-bottom.js.map
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -90,6 +90,164 @@ class ASTHelpers {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
static classMethodDependenciesOf(node, graph, className) {
|
|
94
|
+
const dependencies = [];
|
|
95
|
+
if (!node) {
|
|
96
|
+
return dependencies;
|
|
97
|
+
}
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case 'MethodDefinition':
|
|
100
|
+
const functionBody = node.value.body;
|
|
101
|
+
return (functionBody?.body || [])
|
|
102
|
+
.map((statement) => ASTHelpers.classMethodDependenciesOf(statement, graph, className))
|
|
103
|
+
.flat();
|
|
104
|
+
case 'Identifier':
|
|
105
|
+
dependencies.push(node.name);
|
|
106
|
+
break;
|
|
107
|
+
case 'ExpressionStatement':
|
|
108
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
109
|
+
case 'MemberExpression':
|
|
110
|
+
if ((node.object.type === 'ThisExpression' &&
|
|
111
|
+
node.property.type === 'Identifier') ||
|
|
112
|
+
(node.object.type === 'Identifier' &&
|
|
113
|
+
node.object.name === className &&
|
|
114
|
+
node.property.type === 'Identifier')) {
|
|
115
|
+
dependencies.push(node.property.name);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
return [
|
|
119
|
+
...ASTHelpers.classMethodDependenciesOf(node.object, graph, className),
|
|
120
|
+
...ASTHelpers.classMethodDependenciesOf(node.property, graph, className),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case 'TSNonNullExpression':
|
|
125
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
126
|
+
case 'ArrayPattern':
|
|
127
|
+
return node.elements
|
|
128
|
+
.map((element) => ASTHelpers.classMethodDependenciesOf(element, graph, className))
|
|
129
|
+
.flat();
|
|
130
|
+
case 'ObjectPattern':
|
|
131
|
+
return node.properties
|
|
132
|
+
.map((property) => ASTHelpers.classMethodDependenciesOf(property.value || null, graph, className))
|
|
133
|
+
.flat();
|
|
134
|
+
case 'AssignmentPattern':
|
|
135
|
+
return ASTHelpers.classMethodDependenciesOf(node.left, graph, className);
|
|
136
|
+
case 'RestElement':
|
|
137
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
138
|
+
case 'AwaitExpression':
|
|
139
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
140
|
+
case 'AssignmentExpression':
|
|
141
|
+
return [
|
|
142
|
+
...ASTHelpers.classMethodDependenciesOf(node.left, graph, className),
|
|
143
|
+
...ASTHelpers.classMethodDependenciesOf(node.right, graph, className),
|
|
144
|
+
];
|
|
145
|
+
case 'BlockStatement':
|
|
146
|
+
return node.body
|
|
147
|
+
.map((statement) => ASTHelpers.classMethodDependenciesOf(statement, graph, className))
|
|
148
|
+
.flat()
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
case 'IfStatement':
|
|
151
|
+
return [
|
|
152
|
+
...ASTHelpers.classMethodDependenciesOf(node.test, graph, className),
|
|
153
|
+
...ASTHelpers.classMethodDependenciesOf(node.consequent, graph, className),
|
|
154
|
+
...ASTHelpers.classMethodDependenciesOf(node.alternate, graph, className),
|
|
155
|
+
];
|
|
156
|
+
case 'TSTypeAssertion':
|
|
157
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
158
|
+
case 'Identifier':
|
|
159
|
+
return dependencies;
|
|
160
|
+
case 'SpreadElement':
|
|
161
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
162
|
+
case 'ChainExpression':
|
|
163
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
164
|
+
case 'ArrayExpression':
|
|
165
|
+
return node.elements
|
|
166
|
+
.map((element) => element &&
|
|
167
|
+
(element.type === 'SpreadElement'
|
|
168
|
+
? ASTHelpers.classMethodDependenciesOf(element.argument, graph, className)
|
|
169
|
+
: ASTHelpers.classMethodDependenciesOf(element, graph, className)))
|
|
170
|
+
.flat()
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
case 'ObjectExpression':
|
|
173
|
+
return node.properties
|
|
174
|
+
.map((property) => {
|
|
175
|
+
if (property.type === 'Property') {
|
|
176
|
+
return ASTHelpers.classMethodDependenciesOf(property.value, graph, className);
|
|
177
|
+
}
|
|
178
|
+
else if (property.type === 'SpreadElement') {
|
|
179
|
+
return ASTHelpers.classMethodDependenciesOf(property.argument, graph, className);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
})
|
|
183
|
+
.flat()
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
case 'Property':
|
|
186
|
+
return ASTHelpers.classMethodDependenciesOf(node.value, graph, className);
|
|
187
|
+
case 'BinaryExpression':
|
|
188
|
+
case 'LogicalExpression':
|
|
189
|
+
return [
|
|
190
|
+
...ASTHelpers.classMethodDependenciesOf(node.left, graph, className),
|
|
191
|
+
...ASTHelpers.classMethodDependenciesOf(node.right, graph, className),
|
|
192
|
+
];
|
|
193
|
+
case 'UnaryExpression':
|
|
194
|
+
case 'UpdateExpression':
|
|
195
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
196
|
+
case 'CallExpression':
|
|
197
|
+
case 'NewExpression':
|
|
198
|
+
// For function and constructor calls, we care about both the callee and the arguments.
|
|
199
|
+
return [
|
|
200
|
+
...ASTHelpers.classMethodDependenciesOf(node.callee, graph, className),
|
|
201
|
+
...node.arguments
|
|
202
|
+
.map((arg) => ASTHelpers.classMethodDependenciesOf(arg, graph, className))
|
|
203
|
+
.flat(),
|
|
204
|
+
];
|
|
205
|
+
case 'ConditionalExpression':
|
|
206
|
+
return [
|
|
207
|
+
...ASTHelpers.classMethodDependenciesOf(node.test, graph, className),
|
|
208
|
+
...ASTHelpers.classMethodDependenciesOf(node.consequent, graph, className),
|
|
209
|
+
...ASTHelpers.classMethodDependenciesOf(node.alternate, graph, className),
|
|
210
|
+
];
|
|
211
|
+
case 'TSAsExpression':
|
|
212
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
213
|
+
case 'VariableDeclaration':
|
|
214
|
+
return node.declarations
|
|
215
|
+
.map((declaration) => ASTHelpers.classMethodDependenciesOf(declaration, graph, className))
|
|
216
|
+
.flat()
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
case 'VariableDeclarator':
|
|
219
|
+
return ASTHelpers.classMethodDependenciesOf(node.init, graph, className);
|
|
220
|
+
case 'ForOfStatement':
|
|
221
|
+
return [
|
|
222
|
+
...ASTHelpers.classMethodDependenciesOf(node.left, graph, className),
|
|
223
|
+
...ASTHelpers.classMethodDependenciesOf(node.body, graph, className),
|
|
224
|
+
...ASTHelpers.classMethodDependenciesOf(node.right, graph, className),
|
|
225
|
+
];
|
|
226
|
+
case 'ForStatement':
|
|
227
|
+
return [node.body, node.init, node.test, node.update]
|
|
228
|
+
.map((node) => ASTHelpers.classMethodDependenciesOf(node, graph, className))
|
|
229
|
+
.flat();
|
|
230
|
+
case 'ThrowStatement':
|
|
231
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
232
|
+
case 'TemplateLiteral':
|
|
233
|
+
return node.expressions
|
|
234
|
+
.map((expression) => ASTHelpers.classMethodDependenciesOf(expression, graph, className))
|
|
235
|
+
.flat();
|
|
236
|
+
case 'ReturnStatement':
|
|
237
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
238
|
+
case 'ArrowFunctionExpression':
|
|
239
|
+
return [
|
|
240
|
+
...node.params.flatMap((param) => ASTHelpers.classMethodDependenciesOf(param, graph, className)),
|
|
241
|
+
...ASTHelpers.classMethodDependenciesOf(node.body, graph, className),
|
|
242
|
+
];
|
|
243
|
+
default:
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
// Removing duplicates
|
|
247
|
+
return [
|
|
248
|
+
...new Set(dependencies.filter((dep) => graph?.[dep]?.type !== 'property')),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
93
251
|
static isNode(value) {
|
|
94
252
|
return typeof value === 'object' && value !== null && 'type' in value;
|
|
95
253
|
}
|
|
@@ -165,7 +323,8 @@ class ASTHelpers {
|
|
|
165
323
|
}
|
|
166
324
|
}
|
|
167
325
|
if (node.type === 'ConditionalExpression') {
|
|
168
|
-
return (
|
|
326
|
+
return (ASTHelpers.returnsJSX(node.consequent) ||
|
|
327
|
+
ASTHelpers.returnsJSX(node.alternate));
|
|
169
328
|
}
|
|
170
329
|
return false;
|
|
171
330
|
}
|
package/lib/utils/createRule.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createRule = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
-
exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/BluMintInc/custom-
|
|
5
|
+
exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/BluMintInc/eslint-custom-rules/plugin/docs/rules/${name}.md`);
|
|
6
6
|
//# sourceMappingURL=createRule.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClassGraphBuilder = void 0;
|
|
4
|
+
/* eslint-disable security/detect-object-injection */
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
const ClassGraphSorterReadability_1 = require("./ClassGraphSorterReadability");
|
|
7
|
+
const ASTHelpers_1 = require("../ASTHelpers");
|
|
8
|
+
/**
|
|
9
|
+
* Builds a graph of class methods and properties with their dependencies from a class declaration.
|
|
10
|
+
* A dependency in this case is the name of another class method.
|
|
11
|
+
*/
|
|
12
|
+
class ClassGraphBuilder {
|
|
13
|
+
constructor(className, classBody) {
|
|
14
|
+
this.className = className;
|
|
15
|
+
this.classBody = classBody;
|
|
16
|
+
this.graph = {};
|
|
17
|
+
this.buildGraph();
|
|
18
|
+
// Note: extension requires injection of other sorters
|
|
19
|
+
this.sorter = new ClassGraphSorterReadability_1.ClassGraphSorterReadability(this.graph);
|
|
20
|
+
}
|
|
21
|
+
buildGraph() {
|
|
22
|
+
// NOTE: these need to be run sequentially for each member,
|
|
23
|
+
// since we need to know the class members before we can search
|
|
24
|
+
// methods for dependencies
|
|
25
|
+
this.classBody.body.forEach((member) => {
|
|
26
|
+
if (ClassGraphBuilder.isClassMember(member)) {
|
|
27
|
+
this.addMemberToGraph(member);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
this.classBody.body.forEach((member) => {
|
|
31
|
+
if (ClassGraphBuilder.isNamedClassMethod(member)) {
|
|
32
|
+
const { key } = member;
|
|
33
|
+
if (utils_1.ASTUtils.isIdentifier(key)) {
|
|
34
|
+
this.addDependencies(member, key.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
static isClassMember(node) {
|
|
40
|
+
return (node.type === 'MethodDefinition' || node.type === 'PropertyDefinition');
|
|
41
|
+
}
|
|
42
|
+
addMemberToGraph(member) {
|
|
43
|
+
const name = member.key.name;
|
|
44
|
+
const type = ClassGraphBuilder.nodeTypeOf(member);
|
|
45
|
+
const node = ClassGraphBuilder.createGraphNode(name, type, member.accessibility, member.static);
|
|
46
|
+
this.graph[name] = node;
|
|
47
|
+
}
|
|
48
|
+
static nodeTypeOf(member) {
|
|
49
|
+
if (member.type === 'MethodDefinition') {
|
|
50
|
+
return member.kind === 'constructor' ? 'constructor' : 'method';
|
|
51
|
+
}
|
|
52
|
+
return 'property';
|
|
53
|
+
}
|
|
54
|
+
static createGraphNode(name, type, accessibility, isStatic = false) {
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
type,
|
|
58
|
+
accessibility,
|
|
59
|
+
isStatic,
|
|
60
|
+
dependencies: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
static isNamedClassMethod(node) {
|
|
64
|
+
return node.type === 'MethodDefinition';
|
|
65
|
+
}
|
|
66
|
+
addDependencies(node, methodName) {
|
|
67
|
+
const newDependencies = ASTHelpers_1.ASTHelpers.classMethodDependenciesOf(node, this.graph, this.className).filter((name) => !!this.graph[name] && name !== methodName);
|
|
68
|
+
if (this.graph[methodName]) {
|
|
69
|
+
this.graph[methodName].dependencies.push(...newDependencies);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
get graphSorted() {
|
|
73
|
+
return this.sorter.nodesSorted;
|
|
74
|
+
}
|
|
75
|
+
get memberNamesSorted() {
|
|
76
|
+
return this.sorter.nodeNamesSorted;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.ClassGraphBuilder = ClassGraphBuilder;
|
|
80
|
+
//# sourceMappingURL=ClassGraphBuilder.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClassGraphSorter = void 0;
|
|
4
|
+
class ClassGraphSorter {
|
|
5
|
+
constructor(graph) {
|
|
6
|
+
this.graph = graph;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
exports.ClassGraphSorter = ClassGraphSorter;
|
|
10
|
+
//# sourceMappingURL=ClassGraphSorter.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClassGraphSorterReadability = void 0;
|
|
4
|
+
const ClassGraphSorter_1 = require("./ClassGraphSorter");
|
|
5
|
+
class ClassGraphSorterReadability extends ClassGraphSorter_1.ClassGraphSorter {
|
|
6
|
+
constructor(graph) {
|
|
7
|
+
super(graph);
|
|
8
|
+
this.graph = graph;
|
|
9
|
+
}
|
|
10
|
+
get nodeNamesSorted() {
|
|
11
|
+
return this.nodesSorted.map((node) => node.name);
|
|
12
|
+
}
|
|
13
|
+
get nodesSorted() {
|
|
14
|
+
return this.sortNodes();
|
|
15
|
+
}
|
|
16
|
+
sortNodes() {
|
|
17
|
+
const { methods, properties, classConstructor } = this.groupNodesByType();
|
|
18
|
+
const [propertiesSorted, methodsSorted] = [properties, methods].map((nodeGroup) => nodeGroup.sort(ClassGraphSorterReadability.sortMembersForReadability));
|
|
19
|
+
const searchNodeCandidates = [classConstructor, ...methodsSorted].filter((node) => !!node);
|
|
20
|
+
const searchNodeCandidatesValidated = searchNodeCandidates.filter((method, _index, arr) => ClassGraphSorterReadability.SEARCH_NODE_PRIORITY_FUNCTIONS.some((fn) => !!fn(method, arr)));
|
|
21
|
+
const searchNodesSorted = searchNodeCandidatesValidated.sort((a, b) => {
|
|
22
|
+
const getPriority = (method) => {
|
|
23
|
+
const priority = ClassGraphSorterReadability.SEARCH_NODE_PRIORITY_FUNCTIONS.findIndex((fn) => !!fn(method, methodsSorted));
|
|
24
|
+
return priority === -1
|
|
25
|
+
? ClassGraphSorterReadability.SEARCH_NODE_PRIORITY_FUNCTIONS.length
|
|
26
|
+
: priority;
|
|
27
|
+
};
|
|
28
|
+
return getPriority(a) - getPriority(b);
|
|
29
|
+
});
|
|
30
|
+
const dfsSortedNodes = this.dependencyDfs(searchNodesSorted);
|
|
31
|
+
return [...propertiesSorted, ...dfsSortedNodes];
|
|
32
|
+
}
|
|
33
|
+
groupNodesByType() {
|
|
34
|
+
const nodes = Object.values(this.graph);
|
|
35
|
+
return nodes.reduce((acc, node) => {
|
|
36
|
+
switch (node.type) {
|
|
37
|
+
case 'property':
|
|
38
|
+
acc.properties.push(node);
|
|
39
|
+
break;
|
|
40
|
+
case 'constructor':
|
|
41
|
+
acc.classConstructor = node;
|
|
42
|
+
break;
|
|
43
|
+
case 'method':
|
|
44
|
+
acc.methods.push(node);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return acc;
|
|
48
|
+
}, {
|
|
49
|
+
properties: [],
|
|
50
|
+
methods: [],
|
|
51
|
+
classConstructor: null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
static sortMembersForReadability(a, b) {
|
|
55
|
+
// NOTE: the ordering from Object.entries is safe here since it is readonly
|
|
56
|
+
for (const [key, priorities] of Object.entries(ClassGraphSorterReadability.MODIFIER_PRIORITY_MAP)) {
|
|
57
|
+
const indexA = priorities.indexOf(a[key]);
|
|
58
|
+
const indexB = priorities.indexOf(b[key]);
|
|
59
|
+
if (indexA !== indexB) {
|
|
60
|
+
return indexA - indexB;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
dependencyDfs(searchNodes) {
|
|
66
|
+
const visited = new Set();
|
|
67
|
+
const dfsSortedNodes = [];
|
|
68
|
+
const dfs = (node) => {
|
|
69
|
+
if (visited.has(node.name) || !node) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
visited.add(node.name);
|
|
73
|
+
dfsSortedNodes.push(node);
|
|
74
|
+
for (const dep of node.dependencies) {
|
|
75
|
+
dfs(this.graph[String(dep)]);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
searchNodes.forEach((node) => dfs(node));
|
|
79
|
+
return dfsSortedNodes;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.ClassGraphSorterReadability = ClassGraphSorterReadability;
|
|
83
|
+
ClassGraphSorterReadability.MODIFIER_PRIORITY_MAP = {
|
|
84
|
+
isStatic: [true, false],
|
|
85
|
+
accessibility: ['public', undefined, 'private'],
|
|
86
|
+
};
|
|
87
|
+
ClassGraphSorterReadability.SEARCH_NODE_PRIORITY_FUNCTIONS = [
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
89
|
+
(method, _methods) => method.type === 'constructor',
|
|
90
|
+
(method, _methods) => !_methods.some((node) => node.dependencies.includes(method.name)),
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
92
|
+
(method, _methods) => method.dependencies.length === 0,
|
|
93
|
+
];
|
|
94
|
+
//# sourceMappingURL=ClassGraphSorterReadability.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blumintinc/eslint-plugin-blumint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "Custom eslint rules for use at BluMint",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"lint": "npm-run-all \"lint:*\"",
|
|
15
15
|
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
|
|
16
|
-
"lint:js": "eslint
|
|
16
|
+
"lint:js": "eslint ./src",
|
|
17
17
|
"test": "jest",
|
|
18
18
|
"docs": "./scripts/make-docs.sh && npm run update:eslint-docs",
|
|
19
19
|
"update:eslint-docs": "eslint-doc-generator"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"ts-jest": "29.0.5"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": "^
|
|
39
|
+
"node": "^20.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"eslint": ">=7"
|