power-linter 1.0.0 → 1.0.2

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,6 +1,6 @@
1
1
  # power-linter
2
2
 
3
- power-linter — собственный линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.
3
+ power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.
4
4
 
5
5
  - конфиги eslint и prettier из коробки
6
6
  - кастомные правила-линтера (see `eslint-plugin-rules`)
package/package.json CHANGED
@@ -1,50 +1,43 @@
1
1
  {
2
- "name": "power-linter",
3
- "version": "1.0.0",
4
- "description": "power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.",
5
- "main": "src/index.js",
6
- "author": "vollmond148259",
7
- "license": "MIT",
8
- "keywords": [
9
- "eslint",
10
- "prettier",
11
- "linter",
12
- "config",
13
- "eslint-plugin",
14
- "eslint-config",
15
- "eslint-rules",
16
- "custom-rules",
17
- "code-style"
18
- ],
19
- "dependencies": {
20
- "eslint": "^8.56.0",
21
- "prettier": "^3.2.5",
22
- "@babel/eslint-parser": "^7.19.1",
23
- "eslint-config-airbnb": "^19.0.4",
24
- "eslint-config-prettier": "^8.5.0",
25
- "eslint-plugin-rules": "file:./eslint-plugin-rules",
26
- "eslint-plugin-import": "^2.26.0",
27
- "eslint-plugin-jsx-a11y": "^6.6.0",
28
- "eslint-plugin-react": "^7.30.1",
29
- "eslint-plugin-react-hooks": "^4.6.0"
30
- },
31
- "peerDependencies": {
32
- "eslint": "^8.0.0"
33
- },
34
- "files": [
35
- "src/"
36
- ],
37
- "scripts": {
38
- "lint": "eslint . --ext .js,.jsx,.ts,.tsx --config src/eslint/config.js",
39
- "format": "prettier --write .",
40
- "codemod:classToFC": "node scripts/classToFC/run-codemod.js"
41
- },
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/Vollmond148259/power-linter.git"
45
- },
46
- "bugs": {
47
- "url": "https://github.com/Vollmond148259/power-linter/issues"
48
- },
49
- "homepage": "https://github.com/Vollmond148259/power-linter#readme"
2
+ "name": "power-linter",
3
+ "version": "1.0.2",
4
+ "description": "power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.",
5
+ "main": "src/index.js",
6
+ "author": "",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "eslint",
10
+ "prettier",
11
+ "linter",
12
+ "config",
13
+ "eslint-plugin",
14
+ "eslint-config",
15
+ "eslint-rules",
16
+ "custom-rules",
17
+ "code-style"
18
+ ],
19
+ "dependencies": {
20
+ "eslint": "^8.56.0",
21
+ "prettier": "^3.2.5",
22
+ "@babel/eslint-parser": "^7.19.1",
23
+ "eslint-config-airbnb": "^19.0.4",
24
+ "eslint-config-prettier": "^8.5.0",
25
+ "eslint-plugin-rules": "file:./eslint-plugin-rules-1.0.0.tgz",
26
+ "eslint-plugin-import": "^2.26.0",
27
+ "eslint-plugin-jsx-a11y": "^6.6.0",
28
+ "eslint-plugin-react": "^7.30.1",
29
+ "eslint-plugin-react-hooks": "^4.6.0"
30
+ },
31
+ "peerDependencies": {
32
+ "eslint": "^8.0.0"
33
+ },
34
+ "files": [
35
+ "src/",
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
+ }
50
43
  }
@@ -0,0 +1,463 @@
1
+ /**
2
+ * Codemod для преобразования классовых React компонентов в функциональные
3
+ *
4
+ * Условия применения:
5
+ * - Компонент не использует this.state
6
+ * - Компонент имеет от 1 до 4 методов класса
7
+ * - Компонент не имеет сложных lifecycle методов
8
+ *
9
+ * Использование:
10
+ * npm run codemod:classToFC -- <file-path>
11
+ */
12
+
13
+ /**
14
+ * Проверяет, является ли класс React компонентом
15
+ */
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
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Проверяет, содержит ли узел использование this.state или this.setState
43
+ */
44
+ 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;
66
+ }
67
+
68
+ /**
69
+ * Получает имя компонента
70
+ */
71
+ 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;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Проверяет, является ли метод методом жизненного цикла
95
+ */
96
+ 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');
110
+ }
111
+
112
+ /**
113
+ * Преобразует методы класса в функции внутри компонента
114
+ */
115
+ 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
+ }
141
+ }
142
+ });
143
+ return { methods, lifecycleMethods, staticProperties };
144
+ }
145
+
146
+ /**
147
+ * Заменяет this.props на props в узле
148
+ */
149
+ 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'));
156
+ }
157
+
158
+ /**
159
+ * Заменяет this.methodName на methodName
160
+ */
161
+ 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
+ }
171
+
172
+ /**
173
+ * Преобразует методы жизненного цикла в хуки React
174
+ */
175
+ 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
+ });
254
+
255
+ return { hooks, needsUseEffect };
256
+ }
257
+
258
+ /**
259
+ * Создает функциональный компонент из классового
260
+ */
261
+ 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;
280
+ }
281
+ let renderBody;
282
+ if (renderMethod.type === 'MethodDefinition') {
283
+ renderBody = renderMethod.value.body;
284
+ } else {
285
+ renderBody = renderMethod.value.body;
286
+ }
287
+ if (!renderBody) {
288
+ return null;
289
+ }
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
+ }
330
+ });
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 };
367
+ }
368
+
369
+ 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
+ }
384
+ }
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
+ }
404
+ }
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
+ }
423
+ }
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
+ }
448
+ }
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')
458
+ );
459
+ root.find(j.Program).get('body', 0).insertBefore(useEffectImport);
460
+ }
461
+ }
462
+ return hasTransformations ? root.toSource({ quote: 'single', trailingComma: true }) : null;
463
+ };
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Обертка для запуска codemod скрипта
5
+ *
6
+ * Использование:
7
+ * npm run codemod:classToFC -- <file-path>
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ const filePath = process.argv[2];
15
+ if (!filePath) {
16
+ console.error('Ошибка: Укажите путь к файлу для преобразования');
17
+ console.error('Использование: npm run codemod:classToFC -- <file-path>');
18
+ process.exit(1);
19
+ }
20
+ // Определяем корень проекта: если запускаемся из power-linter, используем родительскую директорию
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);
25
+ if (!fs.existsSync(absolutePath)) {
26
+ console.error(`Ошибка: Файл не найден: ${absolutePath}`);
27
+ process.exit(1);
28
+ }
29
+ const ext = path.extname(absolutePath);
30
+ if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
31
+ console.error(`Ошибка: Файл должен иметь расширение .js, .jsx, .ts или .tsx`);
32
+ process.exit(1);
33
+ }
34
+ const codemodPath = path.join(__dirname, 'class-to-functional.js');
35
+ // eslint-disable-next-line no-console
36
+ console.log(`Преобразование файла: ${filePath}`);
37
+ // eslint-disable-next-line no-console
38
+ console.log(`Использование codemod: ${codemodPath}`);
39
+ 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('Проверьте файл и при необходимости внесите ручные исправления.');
51
+ } catch (error) {
52
+ console.error('\n✗ Ошибка при выполнении преобразования:', error.message);
53
+ process.exit(1);
54
+ }