eslint-plugin-power-esrules 0.1.14 → 0.1.16
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.
|
@@ -108,10 +108,16 @@ function getReactComponentName(funcNode) {
|
|
|
108
108
|
return funcNode.id.name;
|
|
109
109
|
}
|
|
110
110
|
const { parent } = funcNode;
|
|
111
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
parent?.type === "VariableDeclarator" &&
|
|
113
|
+
parent.id?.type === "Identifier"
|
|
114
|
+
) {
|
|
112
115
|
return parent.id.name;
|
|
113
116
|
}
|
|
114
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
parent?.type === "AssignmentExpression" &&
|
|
119
|
+
parent.left?.type === "Identifier"
|
|
120
|
+
) {
|
|
115
121
|
return parent.left.name;
|
|
116
122
|
}
|
|
117
123
|
return null;
|
|
@@ -195,7 +201,10 @@ function getEnclosingFunction(node) {
|
|
|
195
201
|
function isInsideReactClass(node) {
|
|
196
202
|
let current = node.parent;
|
|
197
203
|
while (current) {
|
|
198
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
current.type === "ClassDeclaration" ||
|
|
206
|
+
current.type === "ClassExpression"
|
|
207
|
+
) {
|
|
199
208
|
return isReactComponentClass(current);
|
|
200
209
|
}
|
|
201
210
|
current = current.parent;
|
|
@@ -211,8 +220,14 @@ function isInsideReactComponentFunction(node) {
|
|
|
211
220
|
return isReactComponentFunction(enclosingFunction);
|
|
212
221
|
}
|
|
213
222
|
|
|
223
|
+
function isRestProperty(prop) {
|
|
224
|
+
return (
|
|
225
|
+
prop.type === "RestElement" || prop.type === "ExperimentalRestProperty"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
214
229
|
function getPropertySortKey(prop) {
|
|
215
|
-
if (prop
|
|
230
|
+
if (isRestProperty(prop)) {
|
|
216
231
|
return null;
|
|
217
232
|
}
|
|
218
233
|
if (prop.type === "Property") {
|
|
@@ -230,7 +245,7 @@ function partitionProperties(properties) {
|
|
|
230
245
|
const regular = [];
|
|
231
246
|
const rest = [];
|
|
232
247
|
for (const prop of properties) {
|
|
233
|
-
if (prop
|
|
248
|
+
if (isRestProperty(prop)) {
|
|
234
249
|
rest.push(prop);
|
|
235
250
|
} else {
|
|
236
251
|
regular.push(prop);
|
|
@@ -239,6 +254,21 @@ function partitionProperties(properties) {
|
|
|
239
254
|
return { regular, rest };
|
|
240
255
|
}
|
|
241
256
|
|
|
257
|
+
function comparePatternProperties(a, b) {
|
|
258
|
+
const aRest = isRestProperty(a);
|
|
259
|
+
const bRest = isRestProperty(b);
|
|
260
|
+
if (aRest && bRest) {
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
if (aRest) {
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
if (bRest) {
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
return getPropertySortKey(a).localeCompare(getPropertySortKey(b));
|
|
270
|
+
}
|
|
271
|
+
|
|
242
272
|
function isAlphabeticallySorted(regular) {
|
|
243
273
|
for (let i = 1; i < regular.length; i += 1) {
|
|
244
274
|
const prevKey = getPropertySortKey(regular[i - 1]);
|
|
@@ -252,9 +282,7 @@ function isAlphabeticallySorted(regular) {
|
|
|
252
282
|
|
|
253
283
|
function sortObjectPatternProperties(properties) {
|
|
254
284
|
const { regular, rest } = partitionProperties(properties);
|
|
255
|
-
const sortedRegular = [...regular].sort(
|
|
256
|
-
getPropertySortKey(a).localeCompare(getPropertySortKey(b)),
|
|
257
|
-
);
|
|
285
|
+
const sortedRegular = [...regular].sort(comparePatternProperties);
|
|
258
286
|
return [...sortedRegular, ...rest];
|
|
259
287
|
}
|
|
260
288
|
|
|
@@ -271,7 +299,11 @@ function getPropertiesSeparator(sourceCode, properties) {
|
|
|
271
299
|
return sourceCode.text.slice(comma.range[0], properties[1].range[0]);
|
|
272
300
|
}
|
|
273
301
|
|
|
274
|
-
function buildSortedObjectPatternText(
|
|
302
|
+
function buildSortedObjectPatternText(
|
|
303
|
+
patternNode,
|
|
304
|
+
sortedProperties,
|
|
305
|
+
sourceCode,
|
|
306
|
+
) {
|
|
275
307
|
const openBrace = sourceCode.getFirstToken(patternNode);
|
|
276
308
|
const closeBrace = sourceCode.getLastToken(patternNode);
|
|
277
309
|
const originalProperties = patternNode.properties;
|
|
@@ -359,7 +391,10 @@ module.exports = {
|
|
|
359
391
|
create(context) {
|
|
360
392
|
return {
|
|
361
393
|
VariableDeclarator(node) {
|
|
362
|
-
if (
|
|
394
|
+
if (
|
|
395
|
+
node.id.type !== "ObjectPattern" ||
|
|
396
|
+
!isPropsInitializer(node.init)
|
|
397
|
+
) {
|
|
363
398
|
return;
|
|
364
399
|
}
|
|
365
400
|
const isThisProps =
|
|
@@ -34,6 +34,17 @@ function isPascalCase(name) {
|
|
|
34
34
|
function getComponentName(node) {
|
|
35
35
|
if (!node) return null;
|
|
36
36
|
|
|
37
|
+
if (node.type === "ClassDeclaration") {
|
|
38
|
+
return node.id?.name ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
node.type === "ClassExpression" &&
|
|
43
|
+
node.parent?.type === "VariableDeclarator"
|
|
44
|
+
) {
|
|
45
|
+
return node.parent.id?.name ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
// Функция объявлена напрямую
|
|
38
49
|
if (node.type === "FunctionDeclaration") {
|
|
39
50
|
return node.id?.name ?? null;
|
|
@@ -48,6 +59,20 @@ function getComponentName(node) {
|
|
|
48
59
|
return node.parent.id?.name ?? null;
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
if (
|
|
63
|
+
node.parent?.type === "CallExpression" &&
|
|
64
|
+
node.parent.parent?.type === "VariableDeclarator"
|
|
65
|
+
) {
|
|
66
|
+
node.parent.parent.id?.name ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
node.type === "CallExpression" &&
|
|
71
|
+
node.parent?.type === "VariableDeclarator"
|
|
72
|
+
) {
|
|
73
|
+
return node.parent.id?.name ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
// HOC обёртка: memo(() => {}) или forwardRef(() => {}) или observer(() => {})
|
|
52
77
|
if (node.type === "CallExpression" && node.arguments.length > 0) {
|
|
53
78
|
return getComponentName(node.arguments[0]);
|
|
@@ -66,6 +91,28 @@ function isReactComponent(node) {
|
|
|
66
91
|
function getRootJSXFromComponent(fnNode) {
|
|
67
92
|
if (!fnNode) return null;
|
|
68
93
|
|
|
94
|
+
if (fnNode.type === "ClassDeclaration" || fnNode.type === "ClassExpression") {
|
|
95
|
+
const renderMethod = fnNode.body.body.find(
|
|
96
|
+
(member) =>
|
|
97
|
+
member.type === "MethodDefinition" && member.key?.name === "render",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!renderMethod) return null;
|
|
101
|
+
|
|
102
|
+
const body = renderMethod.value.body.body;
|
|
103
|
+
|
|
104
|
+
for (const stmt of body) {
|
|
105
|
+
if (
|
|
106
|
+
stmt.type === "ReturnStatement" &&
|
|
107
|
+
stmt.argument &&
|
|
108
|
+
(stmt.argument.type === "JSXElement" ||
|
|
109
|
+
stmt.argument.type === "JSXFragment")
|
|
110
|
+
) {
|
|
111
|
+
return stmt.argument;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
// Разворачиваем HOC
|
|
70
117
|
if (fnNode.type === "CallExpression" && fnNode.arguments.length > 0) {
|
|
71
118
|
return getRootJSXFromComponent(fnNode.arguments[0]);
|
|
@@ -177,8 +224,9 @@ module.exports = {
|
|
|
177
224
|
FunctionDeclaration: collectComponent,
|
|
178
225
|
FunctionExpression: collectComponent,
|
|
179
226
|
ArrowFunctionExpression: collectComponent,
|
|
180
|
-
|
|
181
|
-
|
|
227
|
+
ClassDeclaration: collectComponent,
|
|
228
|
+
ClassExpression: collectComponent,
|
|
229
|
+
CallExpression: collectComponent,
|
|
182
230
|
|
|
183
231
|
"Program:exit"() {
|
|
184
232
|
for (const component of components) {
|
package/package.json
CHANGED
|
@@ -14,63 +14,63 @@ ruleTester.run("import-sorting", rule, {
|
|
|
14
14
|
{
|
|
15
15
|
name: "correctly sorted imports with blank lines",
|
|
16
16
|
code: `
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import _ from 'lodash';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
import { fetchData } from 'api';
|
|
21
|
+
import { calculate } from 'utils';
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
import Button from 'components/Button';
|
|
24
|
+
import Header from 'components/Header';
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
import helper from './helper';
|
|
27
27
|
`,
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: "single external import",
|
|
31
31
|
code: `
|
|
32
|
-
|
|
32
|
+
import React from 'react';
|
|
33
33
|
`,
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: "external and internal imports only",
|
|
37
37
|
code: `
|
|
38
|
-
|
|
38
|
+
import React from 'react';
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
import { fetchData } from 'api';
|
|
41
41
|
`,
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: "component import after internal",
|
|
45
45
|
code: `
|
|
46
|
-
|
|
46
|
+
import React from 'react';
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
import { fetchData } from 'api';
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
import Button from 'components/Button';
|
|
51
51
|
`,
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
name: "relative imports last",
|
|
55
55
|
code: `
|
|
56
|
-
|
|
56
|
+
import React from 'react';
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
import { fetchData } from 'api';
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
import Button from 'components/Button';
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
import util from '../util';
|
|
63
|
+
import helper from './helper';
|
|
64
64
|
`,
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
name: "React context import is inner import",
|
|
68
68
|
code: `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
import { fetchData } from 'api';
|
|
70
|
+
import {PageContext} from 'pages/page';
|
|
71
|
+
|
|
72
|
+
import ComponentA from 'components/ComponentA';
|
|
73
|
+
|
|
74
74
|
`,
|
|
75
75
|
},
|
|
76
76
|
],
|
|
@@ -79,78 +79,75 @@ ruleTester.run("import-sorting", rule, {
|
|
|
79
79
|
{
|
|
80
80
|
name: "external import after internal",
|
|
81
81
|
code: `
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
import { fetchData } from 'api';
|
|
83
|
+
import React from 'react';
|
|
84
84
|
`,
|
|
85
85
|
output: `
|
|
86
|
-
|
|
86
|
+
import React from 'react';
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
import { fetchData } from 'api';
|
|
89
89
|
`,
|
|
90
90
|
errors: [{ messageId: "incorrectOrder" }],
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
name: "component import before internal",
|
|
94
94
|
code: `
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
import Button from 'components/Button';
|
|
96
|
+
import { fetchData } from 'api';
|
|
97
97
|
`,
|
|
98
98
|
output: `
|
|
99
|
-
|
|
99
|
+
import { fetchData } from 'api';
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
import Button from 'components/Button';
|
|
102
102
|
`,
|
|
103
103
|
errors: [{ messageId: "incorrectOrder" }],
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
name: "missing blank line between groups",
|
|
107
107
|
code: `
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
import React from 'react';
|
|
109
|
+
import _ from 'lodash';
|
|
110
|
+
import { fetchData } from 'api';
|
|
111
111
|
`,
|
|
112
112
|
output: `
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
import React from 'react';
|
|
114
|
+
import _ from 'lodash';
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
import { fetchData } from 'api';
|
|
117
117
|
`,
|
|
118
118
|
errors: [{ messageId: "missingBlankLine" }],
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
name: "relative import not last",
|
|
122
122
|
code: `
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
import helper from './helper';
|
|
124
|
+
import { fetchData } from 'api';
|
|
125
125
|
`,
|
|
126
126
|
output: `
|
|
127
|
-
|
|
127
|
+
import { fetchData } from 'api';
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
import helper from './helper';
|
|
130
130
|
`,
|
|
131
131
|
errors: [{ messageId: "incorrectOrder" }],
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
name: "multiple issues at once",
|
|
135
135
|
code: `
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
import Button from 'components/Button';
|
|
137
|
+
import React from 'react';
|
|
138
|
+
import { fetchData } from 'api';
|
|
139
|
+
import helper from './helper';
|
|
140
140
|
`,
|
|
141
141
|
output: `
|
|
142
|
-
|
|
142
|
+
import React from 'react';
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
import { fetchData } from 'api';
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
import Button from 'components/Button';
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
import helper from './helper';
|
|
149
149
|
`,
|
|
150
|
-
errors: [
|
|
151
|
-
{ messageId: "incorrectOrder" },
|
|
152
|
-
{ messageId: "missingBlankLine" },
|
|
153
|
-
],
|
|
150
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
154
151
|
},
|
|
155
152
|
],
|
|
156
153
|
});
|
|
@@ -1,14 +1,57 @@
|
|
|
1
|
+
const espree = require("espree");
|
|
1
2
|
const { RuleTester } = require("eslint");
|
|
2
3
|
const rule = require("../lib/rules/props-destructuring-sort");
|
|
3
4
|
|
|
5
|
+
const defaultParserOptions = {
|
|
6
|
+
ecmaVersion: 2022,
|
|
7
|
+
sourceType: "module",
|
|
8
|
+
ecmaFeatures: { jsx: true },
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
const ruleTester = new RuleTester({
|
|
5
|
-
parserOptions:
|
|
6
|
-
ecmaVersion: 2022,
|
|
7
|
-
sourceType: "module",
|
|
8
|
-
ecmaFeatures: { jsx: true },
|
|
9
|
-
},
|
|
12
|
+
parserOptions: defaultParserOptions,
|
|
10
13
|
});
|
|
11
14
|
|
|
15
|
+
function rewriteRestElementsToExperimentalRestProperty(node) {
|
|
16
|
+
if (!node || typeof node !== "object") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (node.type === "RestElement") {
|
|
20
|
+
node.type = "ExperimentalRestProperty";
|
|
21
|
+
}
|
|
22
|
+
for (const key of Object.keys(node)) {
|
|
23
|
+
if (key === "parent") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const value = node[key];
|
|
27
|
+
if (!value) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (const item of value) {
|
|
32
|
+
rewriteRestElementsToExperimentalRestProperty(item);
|
|
33
|
+
}
|
|
34
|
+
} else if (typeof value === "object" && value.type) {
|
|
35
|
+
rewriteRestElementsToExperimentalRestProperty(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const babelLegacyRestParser = {
|
|
41
|
+
parse(code, options) {
|
|
42
|
+
const ast = espree.parse(code, {
|
|
43
|
+
ecmaVersion: options.ecmaVersion,
|
|
44
|
+
sourceType: options.sourceType,
|
|
45
|
+
loc: true,
|
|
46
|
+
range: true,
|
|
47
|
+
tokens: true,
|
|
48
|
+
ecmaFeatures: options.ecmaFeatures,
|
|
49
|
+
});
|
|
50
|
+
rewriteRestElementsToExperimentalRestProperty(ast);
|
|
51
|
+
return ast;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
12
55
|
ruleTester.run("props-destructuring-sort", rule, {
|
|
13
56
|
valid: [
|
|
14
57
|
{
|
|
@@ -143,6 +186,53 @@ ruleTester.run("props-destructuring-sort", rule, {
|
|
|
143
186
|
`,
|
|
144
187
|
errors: [{ messageId: "incorrectOrder" }],
|
|
145
188
|
},
|
|
189
|
+
{
|
|
190
|
+
name: "rest stays last when sorting three props",
|
|
191
|
+
code: `
|
|
192
|
+
function MyComponent(props) {
|
|
193
|
+
const { c, b, a, ...d } = props;
|
|
194
|
+
return <div>{a}</div>;
|
|
195
|
+
}
|
|
196
|
+
`,
|
|
197
|
+
output: `
|
|
198
|
+
function MyComponent(props) {
|
|
199
|
+
const { a, b, c, ...d } = props;
|
|
200
|
+
return <div>{a}</div>;
|
|
201
|
+
}
|
|
202
|
+
`,
|
|
203
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "rest stays last with babel legacy ExperimentalRestProperty AST",
|
|
207
|
+
parser: babelLegacyRestParser,
|
|
208
|
+
code: `
|
|
209
|
+
function MyComponent(props) {
|
|
210
|
+
const { c, b, a, ...d } = props;
|
|
211
|
+
return <div>{a}</div>;
|
|
212
|
+
}
|
|
213
|
+
`,
|
|
214
|
+
output: `
|
|
215
|
+
function MyComponent(props) {
|
|
216
|
+
const { a, b, c, ...d } = props;
|
|
217
|
+
return <div>{a}</div>;
|
|
218
|
+
}
|
|
219
|
+
`,
|
|
220
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "rest stays last in destructured params",
|
|
224
|
+
code: `
|
|
225
|
+
function MyComponent({ c, b, a, ...d }) {
|
|
226
|
+
return <div>{a}</div>;
|
|
227
|
+
}
|
|
228
|
+
`,
|
|
229
|
+
output: `
|
|
230
|
+
function MyComponent({ a, b, c, ...d }) {
|
|
231
|
+
return <div>{a}</div>;
|
|
232
|
+
}
|
|
233
|
+
`,
|
|
234
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
235
|
+
},
|
|
146
236
|
{
|
|
147
237
|
name: "memo-wrapped component params",
|
|
148
238
|
code: `
|
|
@@ -19,6 +19,16 @@ ruleTester.run("require-data-testid", rule, {
|
|
|
19
19
|
}
|
|
20
20
|
`,
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
name: "class component with data-testid",
|
|
24
|
+
code: `
|
|
25
|
+
class MyComponent extends PureComponent {
|
|
26
|
+
render() {
|
|
27
|
+
return <div data-testid="my-component">Hello</div>;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`,
|
|
31
|
+
},
|
|
22
32
|
{
|
|
23
33
|
name: "arrow component with dataTestID",
|
|
24
34
|
code: `
|
|
@@ -67,6 +77,17 @@ ruleTester.run("require-data-testid", rule, {
|
|
|
67
77
|
`,
|
|
68
78
|
errors: [{ messageId: "missingDataTestId" }],
|
|
69
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: "class component without data-testid",
|
|
82
|
+
code: `
|
|
83
|
+
class MyComponent extends PureComponent {
|
|
84
|
+
render() {
|
|
85
|
+
return <div>Hello</div>;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`,
|
|
89
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
90
|
+
},
|
|
70
91
|
{
|
|
71
92
|
name: "arrow component missing data-testid",
|
|
72
93
|
code: `
|