eslint-plugin-power-esrules 0.1.1
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/INTEGRATION_GUIDE.md +99 -0
- package/index.js +25 -0
- package/lib/rules/class-to-functional.js +236 -0
- package/lib/rules/formatting-blank-lines.js +420 -0
- package/lib/rules/id-naming-convention.js +255 -0
- package/lib/rules/import-sorting.js +251 -0
- package/lib/rules/no-default-props.js +68 -0
- package/lib/rules/no-inline-callbacks-in-jsx.js +87 -0
- package/lib/rules/use-state-naming.js +128 -0
- package/package.json +31 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Руководство по интеграции eslint-plugin-power-esrules
|
|
2
|
+
|
|
3
|
+
### Структура плагина
|
|
4
|
+
Плагин содержит:
|
|
5
|
+
- `package.json` - метаданные плагина (версия 0.1.0)
|
|
6
|
+
- `index.js` - точка входа, экспортирует правила и конфигурацию `recommended`
|
|
7
|
+
- `lib/rules/` - директория с правилами:
|
|
8
|
+
- `formatting-blank-lines.js` - проверка расстановки пустых строк
|
|
9
|
+
- `no-default-props.js` - запрет использования defaultProps
|
|
10
|
+
- `no-inline-callbacks-in-jsx.js` - запрет инлайн-колбэков в JSX
|
|
11
|
+
- `id-naming-convention.js` - проверка именования идентификаторов (ID должно быть капсом)
|
|
12
|
+
- `use-state-naming.js` - проверка именования useState хуков
|
|
13
|
+
- `class-to-functional.js` - определение классовых компонентов для конвертации в функциональные
|
|
14
|
+
- `import-sorting.js` - сортировка импортов
|
|
15
|
+
|
|
16
|
+
### Правила плагина
|
|
17
|
+
|
|
18
|
+
#### 1. `formatting-blank-lines`
|
|
19
|
+
**Назначение:** Проверяет расстановку пустых строк согласно соглашению.
|
|
20
|
+
|
|
21
|
+
**Логика:**
|
|
22
|
+
- Добавляет пустую строку после последнего импорта
|
|
23
|
+
- Добавляет пустую строку между styled-компонентами
|
|
24
|
+
- Добавляет пустую строку перед объявлениями функций внутри React-компонента
|
|
25
|
+
- Добавляет пустую строку перед return, обозначающим начало блока рендера
|
|
26
|
+
- Добавляет пустую строку перед экспортом по умолчанию
|
|
27
|
+
- Не добавляет пустую строку внутри тел функций, внутри styled-компонентов
|
|
28
|
+
|
|
29
|
+
#### 2. `no-default-props`
|
|
30
|
+
**Назначение:** Запрещает использование `defaultProps` в React-компонентах.
|
|
31
|
+
|
|
32
|
+
**Логика:**
|
|
33
|
+
- Выдает ошибку при использовании `Component.defaultProps`
|
|
34
|
+
- Рекомендует использовать дефолтные параметры функций или деструктуризацию с значениями по умолчанию
|
|
35
|
+
|
|
36
|
+
#### 3. `no-inline-callbacks-in-jsx`
|
|
37
|
+
**Назначение:** Запрещает использование инлайн-колбэков в JSX.
|
|
38
|
+
|
|
39
|
+
**Логика:**
|
|
40
|
+
- Выдает ошибку при передаче стрелочных функций или function expressions напрямую в JSX-атрибуты
|
|
41
|
+
- Рекомендует выносить колбэки в отдельные функции или использовать useCallback
|
|
42
|
+
|
|
43
|
+
#### 4. `id-naming-convention`
|
|
44
|
+
**Назначение:** Проверяет именование идентификаторов - `id` должно быть написано капсом (`ID`).
|
|
45
|
+
|
|
46
|
+
**Логика:**
|
|
47
|
+
- Проверяет переменные, параметры функций, свойства объектов
|
|
48
|
+
- Выдает ошибку, если найдено `id` в нижнем регистре (например, `assetId`, `taskId`)
|
|
49
|
+
- Требует использовать `assetID`, `taskID`, `userID` и т.д.
|
|
50
|
+
|
|
51
|
+
#### 5. `use-state-naming`
|
|
52
|
+
**Назначение:** Проверяет именование useState хуков согласно соглашению.
|
|
53
|
+
|
|
54
|
+
**Логика:**
|
|
55
|
+
- Проверяет соответствие именования state переменных и setter функций
|
|
56
|
+
- Выдает ошибку при несоответствии соглашению (например, `const [count, setCount] = useState()`)
|
|
57
|
+
|
|
58
|
+
#### 6. `class-to-functional`
|
|
59
|
+
**Назначение:** Определяет классовые компоненты, которые можно конвертировать в функциональные.
|
|
60
|
+
|
|
61
|
+
**Логика:**
|
|
62
|
+
- Находит классовые компоненты без внутреннего стейта (`this.state`)
|
|
63
|
+
- Проверяет количество методов класса (от 1 до 4, не считая методы жизненного цикла)
|
|
64
|
+
- Выдает предупреждение с рекомендацией запустить codemod скрипт для автоматической конвертации
|
|
65
|
+
|
|
66
|
+
#### 7. `import-sorting`
|
|
67
|
+
**Назначение:** Сортирует импорты согласно правилам установленным в команде разработки.
|
|
68
|
+
|
|
69
|
+
## Что нужно для интеграции
|
|
70
|
+
|
|
71
|
+
### 1. npm install eslint-plugin-power-esrules
|
|
72
|
+
|
|
73
|
+
### 2. Обновить .eslintrc.js
|
|
74
|
+
|
|
75
|
+
**Рекомендуемый вариант:** Использовать готовую конфигурацию `recommended` из плагина:
|
|
76
|
+
```javascript
|
|
77
|
+
module.exports = {
|
|
78
|
+
extends: [
|
|
79
|
+
'airbnb',
|
|
80
|
+
'airbnb/hooks',
|
|
81
|
+
'prettier',
|
|
82
|
+
'plugin:power-esrules/recommended' // Добавить конфигурацию плагина
|
|
83
|
+
],
|
|
84
|
+
parser: '@babel/eslint-parser',
|
|
85
|
+
// ...
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Установить зависимости
|
|
90
|
+
|
|
91
|
+
После добавления в `package.json`:
|
|
92
|
+
```bash
|
|
93
|
+
npm install
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Требования
|
|
97
|
+
|
|
98
|
+
### Peer Dependencies
|
|
99
|
+
Плагин требует ESLint версии `^8.0.0`.
|
package/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rules: {
|
|
3
|
+
"formatting-blank-lines": require("./lib/rules/formatting-blank-lines"),
|
|
4
|
+
"no-default-props": require("./lib/rules/no-default-props"),
|
|
5
|
+
"no-inline-callbacks-in-jsx": require("./lib/rules/no-inline-callbacks-in-jsx"),
|
|
6
|
+
"id-naming-convention": require("./lib/rules/id-naming-convention"),
|
|
7
|
+
"use-state-naming": require("./lib/rules/use-state-naming"),
|
|
8
|
+
"class-to-functional": require("./lib/rules/class-to-functional"),
|
|
9
|
+
"import-sorting": require("./lib/rules/import-sorting"),
|
|
10
|
+
},
|
|
11
|
+
configs: {
|
|
12
|
+
recommended: {
|
|
13
|
+
plugins: ["power-esrules"],
|
|
14
|
+
rules: {
|
|
15
|
+
"power-esrules/formatting-blank-lines": "error",
|
|
16
|
+
"power-esrules/no-default-props": "error",
|
|
17
|
+
"power-esrules/no-inline-callbacks-in-jsx": "error",
|
|
18
|
+
"power-esrules/id-naming-convention": "error",
|
|
19
|
+
"power-esrules/use-state-naming": "error",
|
|
20
|
+
"power-esrules/class-to-functional": "error",
|
|
21
|
+
"power-esrules/import-sorting": "error",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: class-to-functional
|
|
3
|
+
*
|
|
4
|
+
* Автоматически определяет классовые компоненты для конвертации в функциональные
|
|
5
|
+
* и выводит уведомление с рекомендацией запустить codemod скрипт.
|
|
6
|
+
*
|
|
7
|
+
* Условия для определения:
|
|
8
|
+
* 1. Не содержит внутреннего стейта (this.state)
|
|
9
|
+
* 2. Содержит от 1 до 4 методов класса (не считая методы жизненного цикла)
|
|
10
|
+
* Рекомендация: Запустите npm run codemod:classToFC -- <file-path>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Проверяет, является ли класс React компонентом
|
|
17
|
+
*/
|
|
18
|
+
function isReactComponentClass(node) {
|
|
19
|
+
if (!node || (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression')) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (!node.superClass) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const { superClass } = node;
|
|
26
|
+
if (superClass.type === 'Identifier') {
|
|
27
|
+
return superClass.name === 'Component' || superClass.name === 'PureComponent';
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
superClass.type === 'MemberExpression' &&
|
|
31
|
+
superClass.object &&
|
|
32
|
+
superClass.object.type === 'Identifier' &&
|
|
33
|
+
superClass.object.name === 'React' &&
|
|
34
|
+
superClass.property &&
|
|
35
|
+
superClass.property.type === 'Identifier' &&
|
|
36
|
+
(superClass.property.name === 'Component' || superClass.property.name === 'PureComponent')
|
|
37
|
+
) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Проверяет, является ли метод методом жизненного цикла
|
|
45
|
+
*/
|
|
46
|
+
function isLifecycleMethod(methodName) {
|
|
47
|
+
const lifecycleMethods = [
|
|
48
|
+
'componentDidMount',
|
|
49
|
+
'componentDidUpdate',
|
|
50
|
+
'componentWillUnmount',
|
|
51
|
+
'componentWillMount',
|
|
52
|
+
'componentWillReceiveProps',
|
|
53
|
+
'UNSAFE_componentWillMount',
|
|
54
|
+
'UNSAFE_componentWillReceiveProps',
|
|
55
|
+
'UNSAFE_componentWillUpdate',
|
|
56
|
+
'shouldComponentUpdate',
|
|
57
|
+
'componentWillUpdate',
|
|
58
|
+
'getSnapshotBeforeUpdate',
|
|
59
|
+
'componentDidCatch',
|
|
60
|
+
'getDerivedStateFromProps',
|
|
61
|
+
];
|
|
62
|
+
return lifecycleMethods.includes(methodName) || methodName.startsWith('component');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Проверяет, содержит ли узел использование this.state или this.setState
|
|
67
|
+
*/
|
|
68
|
+
function hasStateUsage(node) {
|
|
69
|
+
let hasState = false;
|
|
70
|
+
// Используем обход AST для поиска this.state и this.setState
|
|
71
|
+
function traverse(node) {
|
|
72
|
+
if (!node || hasState) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
node.type === 'MemberExpression' &&
|
|
77
|
+
node.object &&
|
|
78
|
+
node.object.type === 'ThisExpression' &&
|
|
79
|
+
node.property &&
|
|
80
|
+
node.property.type === 'Identifier' &&
|
|
81
|
+
node.property.name === 'state'
|
|
82
|
+
) {
|
|
83
|
+
hasState = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
node.type === 'CallExpression' &&
|
|
88
|
+
node.callee &&
|
|
89
|
+
node.callee.type === 'MemberExpression' &&
|
|
90
|
+
node.callee.object &&
|
|
91
|
+
node.callee.object.type === 'ThisExpression' &&
|
|
92
|
+
node.callee.property &&
|
|
93
|
+
node.callee.property.type === 'Identifier' &&
|
|
94
|
+
node.callee.property.name === 'setState'
|
|
95
|
+
) {
|
|
96
|
+
hasState = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Рекурсивно обходим дочерние узлы
|
|
100
|
+
for (const key in node) {
|
|
101
|
+
if (key !== 'parent' && node[key] && typeof node[key] === 'object') {
|
|
102
|
+
if (Array.isArray(node[key])) {
|
|
103
|
+
node[key].forEach((child) => traverse(child));
|
|
104
|
+
} else {
|
|
105
|
+
traverse(node[key]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
traverse(node);
|
|
111
|
+
return hasState;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Подсчитывает методы класса, исключая lifecycle методы, render и constructor
|
|
116
|
+
*/
|
|
117
|
+
function countNonLifecycleMethods(classBody) {
|
|
118
|
+
let count = 0;
|
|
119
|
+
if (!classBody || !classBody.body || !Array.isArray(classBody.body)) {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
classBody.body.forEach((member) => {
|
|
123
|
+
if (member.type === 'MethodDefinition') {
|
|
124
|
+
const methodName = member.key && member.key.name;
|
|
125
|
+
if (methodName === 'render' || methodName === 'constructor') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!isLifecycleMethod(methodName)) {
|
|
129
|
+
count++;
|
|
130
|
+
}
|
|
131
|
+
} else if (
|
|
132
|
+
member.type === 'Property' ||
|
|
133
|
+
member.type === 'ClassProperty' ||
|
|
134
|
+
member.type === 'PropertyDefinition'
|
|
135
|
+
) {
|
|
136
|
+
const methodName = member.key && member.key.name;
|
|
137
|
+
if (methodName === 'render') {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!isLifecycleMethod(methodName)) {
|
|
141
|
+
if (
|
|
142
|
+
member.value &&
|
|
143
|
+
(member.value.type === 'ArrowFunctionExpression' || member.value.type === 'FunctionExpression')
|
|
144
|
+
) {
|
|
145
|
+
count++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return count;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Получает относительный путь к файлу от корня проекта
|
|
156
|
+
*/
|
|
157
|
+
function getRelativeFilePath(context) {
|
|
158
|
+
const filename = context.getFilename();
|
|
159
|
+
const workspaceRoot = context.getCwd ? context.getCwd() : process.cwd();
|
|
160
|
+
return path.relative(workspaceRoot, filename);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
meta: {
|
|
165
|
+
type: 'suggestion',
|
|
166
|
+
docs: {
|
|
167
|
+
description:
|
|
168
|
+
'Определяет классовые компоненты для конвертации в функциональные и рекомендует запустить codemod',
|
|
169
|
+
category: 'Best Practices',
|
|
170
|
+
recommended: false,
|
|
171
|
+
},
|
|
172
|
+
fixable: null,
|
|
173
|
+
schema: [],
|
|
174
|
+
messages: {
|
|
175
|
+
shouldConvertToFunctional:
|
|
176
|
+
'Классовый компонент "{{componentName}}" подходит для конвертации в функциональный. ' +
|
|
177
|
+
'Запустите: npm run codemod:classToFC -- {{filePath}}',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
create(context) {
|
|
182
|
+
return {
|
|
183
|
+
ClassDeclaration(node) {
|
|
184
|
+
if (!isReactComponentClass(node)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const componentName = node.id ? node.id.name : 'Unknown';
|
|
188
|
+
// Проверяем использование this.state
|
|
189
|
+
if (hasStateUsage(node)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const methodCount = countNonLifecycleMethods(node.body);
|
|
193
|
+
if (methodCount >= 1 && methodCount <= 4) {
|
|
194
|
+
const filePath = getRelativeFilePath(context);
|
|
195
|
+
context.report({
|
|
196
|
+
node: node.id || node,
|
|
197
|
+
messageId: 'shouldConvertToFunctional',
|
|
198
|
+
data: {
|
|
199
|
+
componentName,
|
|
200
|
+
filePath,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
ClassExpression(node) {
|
|
207
|
+
if (!isReactComponentClass(node)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
let componentName = 'Unknown';
|
|
211
|
+
const { parent } = node;
|
|
212
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id) {
|
|
213
|
+
componentName = parent.id.name;
|
|
214
|
+
} else if (parent && parent.type === 'ExportDefaultDeclaration') {
|
|
215
|
+
componentName = 'DefaultExport';
|
|
216
|
+
}
|
|
217
|
+
// Проверяем использование this.state
|
|
218
|
+
if (hasStateUsage(node)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const methodCount = countNonLifecycleMethods(node.body);
|
|
222
|
+
if (methodCount >= 1 && methodCount <= 4) {
|
|
223
|
+
const filePath = getRelativeFilePath(context);
|
|
224
|
+
context.report({
|
|
225
|
+
node: node.parent && node.parent.id ? node.parent.id : node,
|
|
226
|
+
messageId: 'shouldConvertToFunctional',
|
|
227
|
+
data: {
|
|
228
|
+
componentName,
|
|
229
|
+
filePath,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
};
|