power-linter 1.0.10 → 1.0.11

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 CHANGED
@@ -1,16 +1,16 @@
1
1
  # power-linter
2
2
 
3
- power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.
3
+ power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил и скриптов.
4
4
 
5
5
  - конфиги eslint и prettier из коробки
6
- - кастомные правила-линтера (see `eslint-plugin-rules`)
6
+ - кастомные правила-линтера (see `eslint-plugin-power-esrules`)
7
7
 
8
8
  ## Использование
9
9
 
10
10
  1. Установка:
11
11
 
12
12
  ```bash
13
- npm install power-linter
13
+ npm install power-linter --save-dev
14
14
  ```
15
15
 
16
16
  2. Добавьте экспорт конфигурации eslint
@@ -27,7 +27,7 @@ module.exports = require('power-linter/src/eslint/config');
27
27
  module.exports = require('power-linter/src/prettier/config');
28
28
  ```
29
29
 
30
- 4. Для кастомных правил добавляйте файлы в `eslint-plugin-rules`
30
+ 4. Для кастомных правил добавляйте файлы в `eslint-plugin-power-esrules`
31
31
 
32
32
  5. Добавьте в корневой каталог проекта настройки vscode в каталоге .vscode создайте файл settings.json и добавьте в него
33
33
 
@@ -68,11 +68,4 @@ module.exports = require('power-linter/src/prettier/config');
68
68
  ]
69
69
  }`
70
70
 
71
- 6. Для запускаемых скриптов добавьте команды в package.json проекта
72
- `
73
- "format": "cd power-linter && npm run format --prefix . && cd ..",
74
- "lint": "cd power-linter && npm run lint --prefix . && cd ..",
75
- "codemod:classToFC": "cd power-linter && npm run codemod:classToFC --prefix . --",
76
- `
77
-
78
71
  ---
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "power-linter",
3
- "version": "1.0.10",
4
- "description": "power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.",
3
+ "version": "1.0.11",
4
+ "description": "power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил и скриптов.",
5
5
  "main": "src/index.js",
6
- "author": "",
6
+ "author": "vollmond148",
7
7
  "license": "MIT",
8
8
  "keywords": [
9
9
  "eslint",
@@ -34,10 +34,5 @@
34
34
  "files": [
35
35
  "src/",
36
36
  "scripts/"
37
- ],
38
- "scripts": {
39
- "lint": "eslint . --ext .js,.jsx,.ts,.tsx --config src/eslint/config.js",
40
- "format": "prettier --write .",
41
- "codemod:classToFC": "node scripts/classToFC/run-codemod.js"
42
- }
37
+ ]
43
38
  }
@@ -7,457 +7,513 @@
7
7
  * - Компонент не имеет сложных lifecycle методов
8
8
  *
9
9
  * Использование:
10
- * npm run codemod:classToFC -- <file-path>
10
+ * node node_modules/power-linter/scripts/classToFC/run-codemod.js <file-path>
11
11
  */
12
12
 
13
13
  /**
14
14
  * Проверяет, является ли класс React компонентом
15
15
  */
16
16
  function isReactComponentClass(node) {
17
- if (!node || (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression')) {
18
- return false;
19
- }
20
- if (!node.superClass) {
21
- return false;
22
- }
23
- const { superClass } = node;
24
- if (superClass.type === 'Identifier') {
25
- return superClass.name === 'Component' || superClass.name === 'PureComponent';
26
- }
27
- if (
28
- superClass.type === 'MemberExpression' &&
29
- superClass.object &&
30
- superClass.object.type === 'Identifier' &&
31
- superClass.object.name === 'React' &&
32
- superClass.property &&
33
- superClass.property.type === 'Identifier' &&
34
- (superClass.property.name === 'Component' || superClass.property.name === 'PureComponent')
35
- ) {
36
- return true;
37
- }
17
+ if (
18
+ !node ||
19
+ (node.type !== "ClassDeclaration" && node.type !== "ClassExpression")
20
+ ) {
38
21
  return false;
22
+ }
23
+ if (!node.superClass) {
24
+ return false;
25
+ }
26
+ const { superClass } = node;
27
+ if (superClass.type === "Identifier") {
28
+ return (
29
+ superClass.name === "Component" || superClass.name === "PureComponent"
30
+ );
31
+ }
32
+ if (
33
+ superClass.type === "MemberExpression" &&
34
+ superClass.object &&
35
+ superClass.object.type === "Identifier" &&
36
+ superClass.object.name === "React" &&
37
+ superClass.property &&
38
+ superClass.property.type === "Identifier" &&
39
+ (superClass.property.name === "Component" ||
40
+ superClass.property.name === "PureComponent")
41
+ ) {
42
+ return true;
43
+ }
44
+ return false;
39
45
  }
40
46
 
41
47
  /**
42
48
  * Проверяет, содержит ли узел использование this.state или this.setState
43
49
  */
44
50
  function hasStateUsage(node, j) {
45
- let hasState = false;
46
- j(node)
47
- .find(j.MemberExpression, {
48
- object: { type: 'ThisExpression' },
49
- property: { name: 'state' },
50
- })
51
- .forEach(() => {
52
- hasState = true;
53
- });
54
- j(node)
55
- .find(j.CallExpression, {
56
- callee: {
57
- type: 'MemberExpression',
58
- object: { type: 'ThisExpression' },
59
- property: { name: 'setState' },
60
- },
61
- })
62
- .forEach(() => {
63
- hasState = true;
64
- });
65
- return hasState;
51
+ let hasState = false;
52
+ j(node)
53
+ .find(j.MemberExpression, {
54
+ object: { type: "ThisExpression" },
55
+ property: { name: "state" },
56
+ })
57
+ .forEach(() => {
58
+ hasState = true;
59
+ });
60
+ j(node)
61
+ .find(j.CallExpression, {
62
+ callee: {
63
+ type: "MemberExpression",
64
+ object: { type: "ThisExpression" },
65
+ property: { name: "setState" },
66
+ },
67
+ })
68
+ .forEach(() => {
69
+ hasState = true;
70
+ });
71
+ return hasState;
66
72
  }
67
73
 
68
74
  /**
69
75
  * Получает имя компонента
70
76
  */
71
77
  function getComponentName(node, j, path) {
72
- if (node.type === 'ClassDeclaration' && node.id) {
73
- return node.id.name;
74
- }
75
- if (node.type === 'ClassExpression') {
76
- let currentPath = path;
77
- while (currentPath) {
78
- const { parent } = currentPath;
79
- if (parent && parent.value) {
80
- if (parent.value.type === 'VariableDeclarator' && parent.value.id) {
81
- return parent.value.id.name;
82
- }
83
- if (parent.value.type === 'ExportDefaultDeclaration') {
84
- return null;
85
- }
86
- }
87
- currentPath = parent;
78
+ if (node.type === "ClassDeclaration" && node.id) {
79
+ return node.id.name;
80
+ }
81
+ if (node.type === "ClassExpression") {
82
+ let currentPath = path;
83
+ while (currentPath) {
84
+ const { parent } = currentPath;
85
+ if (parent && parent.value) {
86
+ if (parent.value.type === "VariableDeclarator" && parent.value.id) {
87
+ return parent.value.id.name;
88
+ }
89
+ if (parent.value.type === "ExportDefaultDeclaration") {
90
+ return null;
88
91
  }
92
+ }
93
+ currentPath = parent;
89
94
  }
90
- return null;
95
+ }
96
+ return null;
91
97
  }
92
98
 
93
99
  /**
94
100
  * Проверяет, является ли метод методом жизненного цикла
95
101
  */
96
102
  function isLifecycleMethod(methodName) {
97
- const lifecycleMethods = [
98
- 'componentDidMount',
99
- 'componentDidUpdate',
100
- 'componentWillUnmount',
101
- 'componentWillMount',
102
- 'componentWillReceiveProps',
103
- 'UNSAFE_componentWillMount',
104
- 'UNSAFE_componentWillReceiveProps',
105
- 'UNSAFE_componentWillUpdate',
106
- 'shouldComponentUpdate',
107
- 'getSnapshotBeforeUpdate',
108
- ];
109
- return lifecycleMethods.includes(methodName) || methodName.startsWith('component');
103
+ const lifecycleMethods = [
104
+ "componentDidMount",
105
+ "componentDidUpdate",
106
+ "componentWillUnmount",
107
+ "componentWillMount",
108
+ "componentWillReceiveProps",
109
+ "UNSAFE_componentWillMount",
110
+ "UNSAFE_componentWillReceiveProps",
111
+ "UNSAFE_componentWillUpdate",
112
+ "shouldComponentUpdate",
113
+ "getSnapshotBeforeUpdate",
114
+ ];
115
+ return (
116
+ lifecycleMethods.includes(methodName) || methodName.startsWith("component")
117
+ );
110
118
  }
111
119
 
112
120
  /**
113
121
  * Преобразует методы класса в функции внутри компонента
114
122
  */
115
123
  function convertMethodsToFunctions(classBody) {
116
- const methods = [];
117
- const lifecycleMethods = [];
118
- const staticProperties = [];
119
- classBody.body.forEach((member) => {
120
- if (member.type === 'MethodDefinition') {
121
- const methodName = member.key.name;
122
- if (methodName === 'render' || methodName === 'constructor') {
123
- return;
124
- }
125
- if (isLifecycleMethod(methodName)) {
126
- lifecycleMethods.push(member);
127
- } else {
128
- methods.push(member);
129
- }
130
- } else if (member.type === 'Property' || member.type === 'ClassProperty') {
131
- if (member.static) {
132
- staticProperties.push(member);
133
- } else if (member.key && member.key.name !== 'render') {
134
- const methodName = member.key.name;
135
- if (isLifecycleMethod(methodName)) {
136
- lifecycleMethods.push(member);
137
- } else {
138
- methods.push(member);
139
- }
140
- }
124
+ const methods = [];
125
+ const lifecycleMethods = [];
126
+ const staticProperties = [];
127
+ classBody.body.forEach((member) => {
128
+ if (member.type === "MethodDefinition") {
129
+ const methodName = member.key.name;
130
+ if (methodName === "render" || methodName === "constructor") {
131
+ return;
132
+ }
133
+ if (isLifecycleMethod(methodName)) {
134
+ lifecycleMethods.push(member);
135
+ } else {
136
+ methods.push(member);
137
+ }
138
+ } else if (member.type === "Property" || member.type === "ClassProperty") {
139
+ if (member.static) {
140
+ staticProperties.push(member);
141
+ } else if (member.key && member.key.name !== "render") {
142
+ const methodName = member.key.name;
143
+ if (isLifecycleMethod(methodName)) {
144
+ lifecycleMethods.push(member);
145
+ } else {
146
+ methods.push(member);
141
147
  }
142
- });
143
- return { methods, lifecycleMethods, staticProperties };
148
+ }
149
+ }
150
+ });
151
+ return { methods, lifecycleMethods, staticProperties };
144
152
  }
145
153
 
146
154
  /**
147
155
  * Заменяет this.props на props в узле
148
156
  */
149
157
  function replaceThisProps(node, j) {
150
- j(node)
151
- .find(j.MemberExpression, {
152
- object: { type: 'ThisExpression' },
153
- property: { name: 'props' },
154
- })
155
- .replaceWith(() => j.identifier('props'));
158
+ j(node)
159
+ .find(j.MemberExpression, {
160
+ object: { type: "ThisExpression" },
161
+ property: { name: "props" },
162
+ })
163
+ .replaceWith(() => j.identifier("props"));
156
164
  }
157
165
 
158
166
  /**
159
167
  * Заменяет this.methodName на methodName
160
168
  */
161
169
  function replaceThisMethods(node, methodNames, j) {
162
- methodNames.forEach((methodName) => {
163
- j(node)
164
- .find(j.MemberExpression, {
165
- object: { type: 'ThisExpression' },
166
- property: { name: methodName },
167
- })
168
- .replaceWith(() => j.identifier(methodName));
169
- });
170
+ methodNames.forEach((methodName) => {
171
+ j(node)
172
+ .find(j.MemberExpression, {
173
+ object: { type: "ThisExpression" },
174
+ property: { name: methodName },
175
+ })
176
+ .replaceWith(() => j.identifier(methodName));
177
+ });
170
178
  }
171
179
 
172
180
  /**
173
181
  * Преобразует методы жизненного цикла в хуки React
174
182
  */
175
183
  function convertLifecycleMethodsToHooks(lifecycleMethods, j) {
176
- const hooks = [];
177
- let needsUseEffect = false;
178
- lifecycleMethods.forEach((method) => {
179
- const methodName = method.key.name;
180
- let methodBody;
181
- if (method.type === 'MethodDefinition') {
182
- methodBody = method.value.body;
183
- } else if (method.type === 'Property' || method.type === 'ClassProperty') {
184
- methodBody = method.value.body;
185
- }
186
- if (!methodBody) {
187
- return;
188
- }
189
- replaceThisProps(methodBody, j);
190
- if (methodName === 'componentDidMount') {
191
- needsUseEffect = true;
192
- hooks.push(
193
- j.expressionStatement(
194
- j.callExpression(j.identifier('useEffect'), [
195
- j.arrowFunctionExpression([], methodBody),
196
- j.arrayExpression([]),
197
- ])
198
- )
199
- );
200
- } else if (methodName === 'componentWillUnmount') {
201
- needsUseEffect = true;
202
- let cleanupFunction;
203
- if (methodBody.type === 'BlockStatement') {
204
- cleanupFunction = j.arrowFunctionExpression([], methodBody);
205
- } else {
206
- cleanupFunction = j.arrowFunctionExpression([], j.blockStatement([j.returnStatement(methodBody)]));
207
- }
208
- const effectBody = j.blockStatement([j.returnStatement(cleanupFunction)]);
209
- hooks.push(
210
- j.expressionStatement(
211
- j.callExpression(j.identifier('useEffect'), [
212
- j.arrowFunctionExpression([], effectBody),
213
- j.arrayExpression([]),
214
- ])
215
- )
216
- );
217
- } else if (methodName === 'componentDidUpdate') {
218
- needsUseEffect = true;
219
- const params = method.type === 'MethodDefinition' ? method.value.params : method.value.params || [];
220
- if (params.length > 0) {
221
- hooks.push(
222
- j.expressionStatement(
223
- j.callExpression(j.identifier('useEffect'), [
224
- j.arrowFunctionExpression([], methodBody),
225
- j.arrayExpression([j.identifier('props')]),
226
- ])
227
- )
228
- );
229
- } else {
230
- hooks.push(
231
- j.expressionStatement(
232
- j.callExpression(j.identifier('useEffect'), [j.arrowFunctionExpression([], methodBody)])
233
- )
234
- );
235
- }
236
- } else if (methodName === 'UNSAFE_componentWillReceiveProps' || methodName === 'componentWillReceiveProps') {
237
- needsUseEffect = true;
238
- const params = method.type === 'MethodDefinition' ? method.value.params : method.value.params || [];
239
- if (params.length > 0 && params[0].name) {
240
- j(methodBody)
241
- .find(j.Identifier, { name: params[0].name })
242
- .replaceWith(() => j.identifier('props'));
243
- }
244
- hooks.push(
245
- j.expressionStatement(
246
- j.callExpression(j.identifier('useEffect'), [
247
- j.arrowFunctionExpression([], methodBody),
248
- j.arrayExpression([j.identifier('props')]),
249
- ])
250
- )
251
- );
252
- }
253
- });
184
+ const hooks = [];
185
+ let needsUseEffect = false;
186
+ lifecycleMethods.forEach((method) => {
187
+ const methodName = method.key.name;
188
+ let methodBody;
189
+ if (method.type === "MethodDefinition") {
190
+ methodBody = method.value.body;
191
+ } else if (method.type === "Property" || method.type === "ClassProperty") {
192
+ methodBody = method.value.body;
193
+ }
194
+ if (!methodBody) {
195
+ return;
196
+ }
197
+ replaceThisProps(methodBody, j);
198
+ if (methodName === "componentDidMount") {
199
+ needsUseEffect = true;
200
+ hooks.push(
201
+ j.expressionStatement(
202
+ j.callExpression(j.identifier("useEffect"), [
203
+ j.arrowFunctionExpression([], methodBody),
204
+ j.arrayExpression([]),
205
+ ])
206
+ )
207
+ );
208
+ } else if (methodName === "componentWillUnmount") {
209
+ needsUseEffect = true;
210
+ let cleanupFunction;
211
+ if (methodBody.type === "BlockStatement") {
212
+ cleanupFunction = j.arrowFunctionExpression([], methodBody);
213
+ } else {
214
+ cleanupFunction = j.arrowFunctionExpression(
215
+ [],
216
+ j.blockStatement([j.returnStatement(methodBody)])
217
+ );
218
+ }
219
+ const effectBody = j.blockStatement([j.returnStatement(cleanupFunction)]);
220
+ hooks.push(
221
+ j.expressionStatement(
222
+ j.callExpression(j.identifier("useEffect"), [
223
+ j.arrowFunctionExpression([], effectBody),
224
+ j.arrayExpression([]),
225
+ ])
226
+ )
227
+ );
228
+ } else if (methodName === "componentDidUpdate") {
229
+ needsUseEffect = true;
230
+ const params =
231
+ method.type === "MethodDefinition"
232
+ ? method.value.params
233
+ : method.value.params || [];
234
+ if (params.length > 0) {
235
+ hooks.push(
236
+ j.expressionStatement(
237
+ j.callExpression(j.identifier("useEffect"), [
238
+ j.arrowFunctionExpression([], methodBody),
239
+ j.arrayExpression([j.identifier("props")]),
240
+ ])
241
+ )
242
+ );
243
+ } else {
244
+ hooks.push(
245
+ j.expressionStatement(
246
+ j.callExpression(j.identifier("useEffect"), [
247
+ j.arrowFunctionExpression([], methodBody),
248
+ ])
249
+ )
250
+ );
251
+ }
252
+ } else if (
253
+ methodName === "UNSAFE_componentWillReceiveProps" ||
254
+ methodName === "componentWillReceiveProps"
255
+ ) {
256
+ needsUseEffect = true;
257
+ const params =
258
+ method.type === "MethodDefinition"
259
+ ? method.value.params
260
+ : method.value.params || [];
261
+ if (params.length > 0 && params[0].name) {
262
+ j(methodBody)
263
+ .find(j.Identifier, { name: params[0].name })
264
+ .replaceWith(() => j.identifier("props"));
265
+ }
266
+ hooks.push(
267
+ j.expressionStatement(
268
+ j.callExpression(j.identifier("useEffect"), [
269
+ j.arrowFunctionExpression([], methodBody),
270
+ j.arrayExpression([j.identifier("props")]),
271
+ ])
272
+ )
273
+ );
274
+ }
275
+ });
254
276
 
255
- return { hooks, needsUseEffect };
277
+ return { hooks, needsUseEffect };
256
278
  }
257
279
 
258
280
  /**
259
281
  * Создает функциональный компонент из классового
260
282
  */
261
283
  function createFunctionalComponent(classNode, j, path) {
262
- const className = getComponentName(classNode, j, path);
263
- const classBody = classNode.body;
264
- if (hasStateUsage(classNode, j)) {
265
- return null;
266
- }
267
- const { methods, lifecycleMethods, staticProperties } = convertMethodsToFunctions(classBody);
268
- const renderMethod = classBody.body.find((member) => {
269
- if (member.type === 'MethodDefinition' && member.key.name === 'render') {
270
- return true;
271
- }
272
- return (
273
- (member.type === 'Property' || member.type === 'ClassProperty') &&
274
- member.key &&
275
- member.key.name === 'render'
276
- );
277
- });
278
- if (!renderMethod) {
279
- return null;
284
+ const className = getComponentName(classNode, j, path);
285
+ const classBody = classNode.body;
286
+ if (hasStateUsage(classNode, j)) {
287
+ return null;
288
+ }
289
+ const { methods, lifecycleMethods, staticProperties } =
290
+ convertMethodsToFunctions(classBody);
291
+ const renderMethod = classBody.body.find((member) => {
292
+ if (member.type === "MethodDefinition" && member.key.name === "render") {
293
+ return true;
280
294
  }
281
- let renderBody;
282
- if (renderMethod.type === 'MethodDefinition') {
283
- renderBody = renderMethod.value.body;
284
- } else {
285
- renderBody = renderMethod.value.body;
295
+ return (
296
+ (member.type === "Property" || member.type === "ClassProperty") &&
297
+ member.key &&
298
+ member.key.name === "render"
299
+ );
300
+ });
301
+ if (!renderMethod) {
302
+ return null;
303
+ }
304
+ let renderBody;
305
+ if (renderMethod.type === "MethodDefinition") {
306
+ renderBody = renderMethod.value.body;
307
+ } else {
308
+ renderBody = renderMethod.value.body;
309
+ }
310
+ if (!renderBody) {
311
+ return null;
312
+ }
313
+ const { hooks, needsUseEffect } = convertLifecycleMethodsToHooks(
314
+ lifecycleMethods,
315
+ j
316
+ );
317
+ const componentStatements = [];
318
+ const methodNames = [];
319
+ componentStatements.push(...hooks);
320
+ methods.forEach((method) => {
321
+ const methodName = method.key.name;
322
+ methodNames.push(methodName);
323
+ let methodBody;
324
+ let isArrow = false;
325
+ let params = [];
326
+ if (method.type === "MethodDefinition") {
327
+ methodBody = method.value.body;
328
+ params = method.value.params || [];
329
+ } else if (method.type === "Property" || method.type === "ClassProperty") {
330
+ if (method.value.type === "ArrowFunctionExpression") {
331
+ methodBody = method.value.body;
332
+ params = method.value.params || [];
333
+ isArrow = true;
334
+ } else {
335
+ methodBody = method.value.body;
336
+ params = method.value.params || [];
337
+ }
286
338
  }
287
- if (!renderBody) {
288
- return null;
339
+ if (methodBody) {
340
+ replaceThisProps(methodBody, j);
341
+ replaceThisMethods(methodBody, methodNames, j);
342
+ if (
343
+ isArrow ||
344
+ method.type === "Property" ||
345
+ method.type === "ClassProperty"
346
+ ) {
347
+ componentStatements.push(
348
+ j.variableDeclaration("const", [
349
+ j.variableDeclarator(
350
+ j.identifier(methodName),
351
+ j.arrowFunctionExpression(params, methodBody)
352
+ ),
353
+ ])
354
+ );
355
+ } else {
356
+ componentStatements.push(
357
+ j.variableDeclaration("const", [
358
+ j.variableDeclarator(
359
+ j.identifier(methodName),
360
+ j.functionExpression(null, params, methodBody)
361
+ ),
362
+ ])
363
+ );
364
+ }
289
365
  }
290
- const { hooks, needsUseEffect } = convertLifecycleMethodsToHooks(lifecycleMethods, j);
291
- const componentStatements = [];
292
- const methodNames = [];
293
- componentStatements.push(...hooks);
294
- methods.forEach((method) => {
295
- const methodName = method.key.name;
296
- methodNames.push(methodName);
297
- let methodBody;
298
- let isArrow = false;
299
- let params = [];
300
- if (method.type === 'MethodDefinition') {
301
- methodBody = method.value.body;
302
- params = method.value.params || [];
303
- } else if (method.type === 'Property' || method.type === 'ClassProperty') {
304
- if (method.value.type === 'ArrowFunctionExpression') {
305
- methodBody = method.value.body;
306
- params = method.value.params || [];
307
- isArrow = true;
308
- } else {
309
- methodBody = method.value.body;
310
- params = method.value.params || [];
311
- }
312
- }
313
- if (methodBody) {
314
- replaceThisProps(methodBody, j);
315
- replaceThisMethods(methodBody, methodNames, j);
316
- if (isArrow || method.type === 'Property' || method.type === 'ClassProperty') {
317
- componentStatements.push(
318
- j.variableDeclaration('const', [
319
- j.variableDeclarator(j.identifier(methodName), j.arrowFunctionExpression(params, methodBody)),
320
- ])
321
- );
322
- } else {
323
- componentStatements.push(
324
- j.variableDeclaration('const', [
325
- j.variableDeclarator(j.identifier(methodName), j.functionExpression(null, params, methodBody)),
326
- ])
327
- );
328
- }
329
- }
366
+ });
367
+ replaceThisProps(renderBody, j);
368
+ replaceThisMethods(renderBody, methodNames, j);
369
+ let componentBody;
370
+ if (renderBody.type === "BlockStatement") {
371
+ componentBody = j.blockStatement([
372
+ ...componentStatements,
373
+ ...renderBody.body,
374
+ ]);
375
+ } else {
376
+ componentBody = j.blockStatement([
377
+ ...componentStatements,
378
+ j.returnStatement(renderBody),
379
+ ]);
380
+ }
381
+ const componentParams = [j.identifier("props")];
382
+ const arrowFunction = j.arrowFunctionExpression(
383
+ componentParams,
384
+ componentBody
385
+ );
386
+ let componentDeclaration;
387
+ if (className) {
388
+ componentDeclaration = j.variableDeclaration("const", [
389
+ j.variableDeclarator(j.identifier(className), arrowFunction),
390
+ ]);
391
+ } else {
392
+ componentDeclaration = j.variableDeclaration("const", [
393
+ j.variableDeclarator(j.identifier("Component"), arrowFunction),
394
+ ]);
395
+ }
396
+ const result = [componentDeclaration];
397
+ if (staticProperties.length > 0 && className) {
398
+ staticProperties.forEach((staticProp) => {
399
+ const propName = staticProp.key.name;
400
+ result.push(
401
+ j.expressionStatement(
402
+ j.assignmentExpression(
403
+ "=",
404
+ j.memberExpression(j.identifier(className), j.identifier(propName)),
405
+ staticProp.value
406
+ )
407
+ )
408
+ );
330
409
  });
331
- replaceThisProps(renderBody, j);
332
- replaceThisMethods(renderBody, methodNames, j);
333
- let componentBody;
334
- if (renderBody.type === 'BlockStatement') {
335
- componentBody = j.blockStatement([...componentStatements, ...renderBody.body]);
336
- } else {
337
- componentBody = j.blockStatement([...componentStatements, j.returnStatement(renderBody)]);
338
- }
339
- const componentParams = [j.identifier('props')];
340
- const arrowFunction = j.arrowFunctionExpression(componentParams, componentBody);
341
- let componentDeclaration;
342
- if (className) {
343
- componentDeclaration = j.variableDeclaration('const', [
344
- j.variableDeclarator(j.identifier(className), arrowFunction),
345
- ]);
346
- } else {
347
- componentDeclaration = j.variableDeclaration('const', [
348
- j.variableDeclarator(j.identifier('Component'), arrowFunction),
349
- ]);
350
- }
351
- const result = [componentDeclaration];
352
- if (staticProperties.length > 0 && className) {
353
- staticProperties.forEach((staticProp) => {
354
- const propName = staticProp.key.name;
355
- result.push(
356
- j.expressionStatement(
357
- j.assignmentExpression(
358
- '=',
359
- j.memberExpression(j.identifier(className), j.identifier(propName)),
360
- staticProp.value
361
- )
362
- )
363
- );
364
- });
365
- }
366
- return { result, needsUseEffect };
410
+ }
411
+ return { result, needsUseEffect };
367
412
  }
368
413
 
369
414
  module.exports = function transformer(file, api) {
370
- const j = api.jscodeshift;
371
- const root = j(file.source);
372
- let hasTransformations = false;
373
- let needsUseEffect = false;
374
- root.find(j.ClassDeclaration).forEach((path) => {
375
- if (isReactComponentClass(path.node)) {
376
- const transformed = createFunctionalComponent(path.node, j, path);
377
- if (transformed) {
378
- j(path).replaceWith(...transformed.result);
379
- if (transformed.needsUseEffect) {
380
- needsUseEffect = true;
381
- }
382
- hasTransformations = true;
383
- }
415
+ const j = api.jscodeshift;
416
+ const root = j(file.source);
417
+ let hasTransformations = false;
418
+ let needsUseEffect = false;
419
+ root.find(j.ClassDeclaration).forEach((path) => {
420
+ if (isReactComponentClass(path.node)) {
421
+ const transformed = createFunctionalComponent(path.node, j, path);
422
+ if (transformed) {
423
+ j(path).replaceWith(...transformed.result);
424
+ if (transformed.needsUseEffect) {
425
+ needsUseEffect = true;
384
426
  }
385
- });
386
- root.find(j.ClassExpression).forEach((path) => {
387
- if (isReactComponentClass(path.node)) {
388
- const transformed = createFunctionalComponent(path.node, j, path);
389
- if (transformed) {
390
- const parent = path.parent.value;
391
- if (parent && parent.type === 'ReturnStatement') {
392
- const arrowFunction = transformed.result[0].declarations[0].init;
393
- j(path.parent).replaceWith(j.returnStatement(arrowFunction));
394
- } else if (parent && parent.type === 'VariableDeclarator') {
395
- j(path.parent.parent).replaceWith(...transformed.result);
396
- } else {
397
- j(path).replaceWith(...transformed.result);
398
- }
399
- if (transformed.needsUseEffect) {
400
- needsUseEffect = true;
401
- }
402
- hasTransformations = true;
403
- }
427
+ hasTransformations = true;
428
+ }
429
+ }
430
+ });
431
+ root.find(j.ClassExpression).forEach((path) => {
432
+ if (isReactComponentClass(path.node)) {
433
+ const transformed = createFunctionalComponent(path.node, j, path);
434
+ if (transformed) {
435
+ const parent = path.parent.value;
436
+ if (parent && parent.type === "ReturnStatement") {
437
+ const arrowFunction = transformed.result[0].declarations[0].init;
438
+ j(path.parent).replaceWith(j.returnStatement(arrowFunction));
439
+ } else if (parent && parent.type === "VariableDeclarator") {
440
+ j(path.parent.parent).replaceWith(...transformed.result);
441
+ } else {
442
+ j(path).replaceWith(...transformed.result);
404
443
  }
405
- });
406
- root.find(j.ImportDeclaration, {
407
- source: { value: 'react' },
408
- }).forEach((path) => {
409
- const specifiers = path.value.specifiers || [];
410
- const componentSpecifier = specifiers.find(
411
- (s) => s.type === 'ImportSpecifier' && s.imported.name === 'Component'
412
- );
413
- const useEffectSpecifier = specifiers.find(
414
- (s) => s.type === 'ImportSpecifier' && s.imported.name === 'useEffect'
415
- );
416
- if (needsUseEffect && !useEffectSpecifier) {
417
- const newSpecifier = j.importSpecifier(j.identifier('useEffect'));
418
- if (specifiers.length === 0) {
419
- path.value.specifiers = [newSpecifier];
420
- } else {
421
- path.value.specifiers.push(newSpecifier);
422
- }
444
+ if (transformed.needsUseEffect) {
445
+ needsUseEffect = true;
423
446
  }
424
- if (componentSpecifier) {
425
- const componentName = componentSpecifier.local.name;
426
- const hasOtherUsage = root.find(j.Identifier, { name: componentName }).some((p) => {
427
- const parent = p.parent.value;
428
- return (
429
- parent.type !== 'ImportSpecifier' ||
430
- (parent.type === 'ImportSpecifier' && parent.imported.name !== 'Component')
431
- );
432
- });
433
- if (!hasOtherUsage) {
434
- const newSpecifiers = specifiers.filter((s) => s !== componentSpecifier);
435
- if (newSpecifiers.length === 0) {
436
- const hasDefault = specifiers.some(
437
- (s) => s.type === 'ImportDefaultSpecifier' || s.type === 'ImportNamespaceSpecifier'
438
- );
439
- if (!hasDefault) {
440
- j(path).remove();
441
- } else {
442
- path.value.specifiers = newSpecifiers;
443
- }
444
- } else {
445
- path.value.specifiers = newSpecifiers;
446
- }
447
- }
447
+ hasTransformations = true;
448
+ }
449
+ }
450
+ });
451
+ root
452
+ .find(j.ImportDeclaration, {
453
+ source: { value: "react" },
454
+ })
455
+ .forEach((path) => {
456
+ const specifiers = path.value.specifiers || [];
457
+ const componentSpecifier = specifiers.find(
458
+ (s) => s.type === "ImportSpecifier" && s.imported.name === "Component"
459
+ );
460
+ const useEffectSpecifier = specifiers.find(
461
+ (s) => s.type === "ImportSpecifier" && s.imported.name === "useEffect"
462
+ );
463
+ if (needsUseEffect && !useEffectSpecifier) {
464
+ const newSpecifier = j.importSpecifier(j.identifier("useEffect"));
465
+ if (specifiers.length === 0) {
466
+ path.value.specifiers = [newSpecifier];
467
+ } else {
468
+ path.value.specifiers.push(newSpecifier);
448
469
  }
449
- });
450
- if (needsUseEffect) {
451
- const reactImports = root.find(j.ImportDeclaration, {
452
- source: { value: 'react' },
453
- });
454
- if (reactImports.length === 0) {
455
- const useEffectImport = j.importDeclaration(
456
- [j.importSpecifier(j.identifier('useEffect'))],
457
- j.literal('react')
470
+ }
471
+ if (componentSpecifier) {
472
+ const componentName = componentSpecifier.local.name;
473
+ const hasOtherUsage = root
474
+ .find(j.Identifier, { name: componentName })
475
+ .some((p) => {
476
+ const parent = p.parent.value;
477
+ return (
478
+ parent.type !== "ImportSpecifier" ||
479
+ (parent.type === "ImportSpecifier" &&
480
+ parent.imported.name !== "Component")
481
+ );
482
+ });
483
+ if (!hasOtherUsage) {
484
+ const newSpecifiers = specifiers.filter(
485
+ (s) => s !== componentSpecifier
486
+ );
487
+ if (newSpecifiers.length === 0) {
488
+ const hasDefault = specifiers.some(
489
+ (s) =>
490
+ s.type === "ImportDefaultSpecifier" ||
491
+ s.type === "ImportNamespaceSpecifier"
458
492
  );
459
- root.find(j.Program).get('body', 0).insertBefore(useEffectImport);
493
+ if (!hasDefault) {
494
+ j(path).remove();
495
+ } else {
496
+ path.value.specifiers = newSpecifiers;
497
+ }
498
+ } else {
499
+ path.value.specifiers = newSpecifiers;
500
+ }
460
501
  }
502
+ }
503
+ });
504
+ if (needsUseEffect) {
505
+ const reactImports = root.find(j.ImportDeclaration, {
506
+ source: { value: "react" },
507
+ });
508
+ if (reactImports.length === 0) {
509
+ const useEffectImport = j.importDeclaration(
510
+ [j.importSpecifier(j.identifier("useEffect"))],
511
+ j.literal("react")
512
+ );
513
+ root.find(j.Program).get("body", 0).insertBefore(useEffectImport);
461
514
  }
462
- return hasTransformations ? root.toSource({ quote: 'single', trailingComma: true }) : null;
515
+ }
516
+ return hasTransformations
517
+ ? root.toSource({ quote: "single", trailingComma: true })
518
+ : null;
463
519
  };
@@ -4,51 +4,55 @@
4
4
  * Обертка для запуска codemod скрипта
5
5
  *
6
6
  * Использование:
7
- * npm run codemod:classToFC -- <file-path>
7
+ * node node_modules/power-linter/scripts/classToFC/run-codemod.js <file-path>
8
8
  */
9
9
 
10
- const { execSync } = require('child_process');
11
- const path = require('path');
12
- const fs = require('fs');
10
+ const { execSync } = require("child_process");
11
+ const path = require("path");
12
+ const fs = require("fs");
13
13
 
14
14
  const filePath = process.argv[2];
15
15
  if (!filePath) {
16
- console.error('Ошибка: Укажите путь к файлу для преобразования');
17
- console.error('Использование: npm run codemod:classToFC -- <file-path>');
18
- process.exit(1);
16
+ console.error("Ошибка: Укажите путь к файлу для преобразования");
17
+ console.error("Использование: npm run codemod:classToFC -- <file-path>");
18
+ process.exit(1);
19
19
  }
20
20
  // Определяем корень проекта: если запускаемся из power-linter, используем родительскую директорию
21
21
  const currentDir = process.cwd();
22
- const isPowerLinterDir = currentDir.endsWith('power-linter') || path.basename(currentDir) === 'power-linter';
23
- const projectRoot = isPowerLinterDir ? path.join(currentDir, '..') : currentDir;
24
- const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
22
+ const isPowerLinterDir =
23
+ currentDir.endsWith("power-linter") ||
24
+ path.basename(currentDir) === "power-linter";
25
+ const projectRoot = isPowerLinterDir ? path.join(currentDir, "..") : currentDir;
26
+ const absolutePath = path.isAbsolute(filePath)
27
+ ? filePath
28
+ : path.join(projectRoot, filePath);
25
29
  if (!fs.existsSync(absolutePath)) {
26
- console.error(`Ошибка: Файл не найден: ${absolutePath}`);
27
- process.exit(1);
30
+ console.error(`Ошибка: Файл не найден: ${absolutePath}`);
31
+ process.exit(1);
28
32
  }
29
33
  const ext = path.extname(absolutePath);
30
- if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
31
- console.error(`Ошибка: Файл должен иметь расширение .js, .jsx, .ts или .tsx`);
32
- process.exit(1);
34
+ if (![".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
35
+ console.error(`Ошибка: Файл должен иметь расширение .js, .jsx, .ts или .tsx`);
36
+ process.exit(1);
33
37
  }
34
- const codemodPath = path.join(__dirname, 'class-to-functional.js');
38
+ const codemodPath = path.join(__dirname, "class-to-functional.js");
35
39
  // eslint-disable-next-line no-console
36
40
  console.log(`Преобразование файла: ${filePath}`);
37
41
  // eslint-disable-next-line no-console
38
42
  console.log(`Использование codemod: ${codemodPath}`);
39
43
  try {
40
- const command = `npx jscodeshift -t "${codemodPath}" "${absolutePath}"`;
41
- // eslint-disable-next-line no-console
42
- console.log(`Выполнение: ${command}`);
43
- execSync(command, {
44
- stdio: 'inherit',
45
- cwd: projectRoot,
46
- });
47
- // eslint-disable-next-line no-console
48
- console.log('\n✓ Преобразование завершено успешно!');
49
- // eslint-disable-next-line no-console
50
- console.log('Проверьте файл и при необходимости внесите ручные исправления.');
44
+ const command = `npx jscodeshift -t "${codemodPath}" "${absolutePath}"`;
45
+ // eslint-disable-next-line no-console
46
+ console.log(`Выполнение: ${command}`);
47
+ execSync(command, {
48
+ stdio: "inherit",
49
+ cwd: projectRoot,
50
+ });
51
+ // eslint-disable-next-line no-console
52
+ console.log("\n✓ Преобразование завершено успешно!");
53
+ // eslint-disable-next-line no-console
54
+ console.log("Проверьте файл и при необходимости внесите ручные исправления.");
51
55
  } catch (error) {
52
- console.error('\n✗ Ошибка при выполнении преобразования:', error.message);
53
- process.exit(1);
56
+ console.error("\n✗ Ошибка при выполнении преобразования:", error.message);
57
+ process.exit(1);
54
58
  }