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 (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
111
+ if (
112
+ parent?.type === "VariableDeclarator" &&
113
+ parent.id?.type === "Identifier"
114
+ ) {
112
115
  return parent.id.name;
113
116
  }
114
- if (parent?.type === "AssignmentExpression" && parent.left?.type === "Identifier") {
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 (current.type === "ClassDeclaration" || current.type === "ClassExpression") {
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.type === "RestElement") {
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.type === "RestElement") {
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((a, b) =>
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(patternNode, sortedProperties, sourceCode) {
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 (node.id.type !== "ObjectPattern" || !isPropsInitializer(node.init)) {
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
- // CallExpression больше не нужен, HOC разворачиваются внутри getComponentName
181
- //"CallExpression": collectComponent,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-power-esrules",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "custom ESLint rules",
5
5
  "main": "index.js",
6
6
  "peerDependencies": {
@@ -14,63 +14,63 @@ ruleTester.run("import-sorting", rule, {
14
14
  {
15
15
  name: "correctly sorted imports with blank lines",
16
16
  code: `
17
- import React from 'react';
18
- import _ from 'lodash';
17
+ import React from 'react';
18
+ import _ from 'lodash';
19
19
 
20
- import { fetchData } from 'api';
21
- import { calculate } from 'utils';
20
+ import { fetchData } from 'api';
21
+ import { calculate } from 'utils';
22
22
 
23
- import Button from 'components/Button';
24
- import Header from 'components/Header';
23
+ import Button from 'components/Button';
24
+ import Header from 'components/Header';
25
25
 
26
- import helper from './helper';
26
+ import helper from './helper';
27
27
  `,
28
28
  },
29
29
  {
30
30
  name: "single external import",
31
31
  code: `
32
- import React from 'react';
32
+ import React from 'react';
33
33
  `,
34
34
  },
35
35
  {
36
36
  name: "external and internal imports only",
37
37
  code: `
38
- import React from 'react';
38
+ import React from 'react';
39
39
 
40
- import { fetchData } from 'api';
40
+ import { fetchData } from 'api';
41
41
  `,
42
42
  },
43
43
  {
44
44
  name: "component import after internal",
45
45
  code: `
46
- import React from 'react';
46
+ import React from 'react';
47
47
 
48
- import { fetchData } from 'api';
48
+ import { fetchData } from 'api';
49
49
 
50
- import Button from 'components/Button';
50
+ import Button from 'components/Button';
51
51
  `,
52
52
  },
53
53
  {
54
54
  name: "relative imports last",
55
55
  code: `
56
- import React from 'react';
56
+ import React from 'react';
57
57
 
58
- import { fetchData } from 'api';
58
+ import { fetchData } from 'api';
59
59
 
60
- import Button from 'components/Button';
60
+ import Button from 'components/Button';
61
61
 
62
- import util from '../util';
63
- import helper from './helper';
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
- import { fetchData } from 'api';
70
- import {PageContext} from 'pages/page';
71
-
72
- import ComponentA from 'components/ComponentA';
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
- import { fetchData } from 'api';
83
- import React from 'react';
82
+ import { fetchData } from 'api';
83
+ import React from 'react';
84
84
  `,
85
85
  output: `
86
- import React from 'react';
86
+ import React from 'react';
87
87
 
88
- import { fetchData } from 'api';
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
- import Button from 'components/Button';
96
- import { fetchData } from 'api';
95
+ import Button from 'components/Button';
96
+ import { fetchData } from 'api';
97
97
  `,
98
98
  output: `
99
- import { fetchData } from 'api';
99
+ import { fetchData } from 'api';
100
100
 
101
- import Button from 'components/Button';
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
- import React from 'react';
109
- import _ from 'lodash';
110
- import { fetchData } from 'api';
108
+ import React from 'react';
109
+ import _ from 'lodash';
110
+ import { fetchData } from 'api';
111
111
  `,
112
112
  output: `
113
- import React from 'react';
114
- import _ from 'lodash';
113
+ import React from 'react';
114
+ import _ from 'lodash';
115
115
 
116
- import { fetchData } from 'api';
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
- import helper from './helper';
124
- import { fetchData } from 'api';
123
+ import helper from './helper';
124
+ import { fetchData } from 'api';
125
125
  `,
126
126
  output: `
127
- import { fetchData } from 'api';
127
+ import { fetchData } from 'api';
128
128
 
129
- import helper from './helper';
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
- import Button from 'components/Button';
137
- import React from 'react';
138
- import { fetchData } from 'api';
139
- import helper from './helper';
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
- import React from 'react';
142
+ import React from 'react';
143
143
 
144
- import { fetchData } from 'api';
144
+ import { fetchData } from 'api';
145
145
 
146
- import Button from 'components/Button';
146
+ import Button from 'components/Button';
147
147
 
148
- import helper from './helper';
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: `