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 +4 -11
- package/package.json +4 -9
- package/scripts/classToFC/class-to-functional.js +436 -380
- package/scripts/classToFC/run-codemod.js +33 -29
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-
|
|
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-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/**
|
|
94
100
|
* Проверяет, является ли метод методом жизненного цикла
|
|
95
101
|
*/
|
|
96
102
|
function isLifecycleMethod(methodName) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
277
|
+
return { hooks, needsUseEffect };
|
|
256
278
|
}
|
|
257
279
|
|
|
258
280
|
/**
|
|
259
281
|
* Создает функциональный компонент из классового
|
|
260
282
|
*/
|
|
261
283
|
function createFunctionalComponent(classNode, j, path) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 (
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
+
* node node_modules/power-linter/scripts/classToFC/run-codemod.js <file-path>
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { execSync } = require(
|
|
11
|
-
const path = require(
|
|
12
|
-
const fs = require(
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
30
|
+
console.error(`Ошибка: Файл не найден: ${absolutePath}`);
|
|
31
|
+
process.exit(1);
|
|
28
32
|
}
|
|
29
33
|
const ext = path.extname(absolutePath);
|
|
30
|
-
if (![
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
56
|
+
console.error("\n✗ Ошибка при выполнении преобразования:", error.message);
|
|
57
|
+
process.exit(1);
|
|
54
58
|
}
|