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,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: formatting-blank-lines
|
|
3
|
+
*
|
|
4
|
+
* Проверяет расстановку пустых строк согласно соглашению:
|
|
5
|
+
*
|
|
6
|
+
* Добавляем пустую строку:
|
|
7
|
+
* - После последнего импорта
|
|
8
|
+
* - Между стилизованными styled-компонентами
|
|
9
|
+
* - Перед объявлениями функций внутри react-компонента
|
|
10
|
+
* - Перед return обозначающим начало блока рендера в react-компоненте
|
|
11
|
+
* - Перед экспортом по дефолту
|
|
12
|
+
*
|
|
13
|
+
* Не добавляем пустую строку:
|
|
14
|
+
* - Внутри тел функций или методов react-компонента
|
|
15
|
+
* - Внутри стилизованного styled-компонента
|
|
16
|
+
* - Внутри блока рендера react-компонента
|
|
17
|
+
* - Между объявлениями переменных, констант, вызовами функций
|
|
18
|
+
* (только если их много или необходимо логическое разделение)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Проверяет, является ли узел styled-компонентом
|
|
23
|
+
*/
|
|
24
|
+
function isStyledComponent(node) {
|
|
25
|
+
if (node.type !== 'VariableDeclarator' || !node.init) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const { init } = node;
|
|
29
|
+
if (init.type === 'CallExpression') {
|
|
30
|
+
const { callee } = init;
|
|
31
|
+
if (callee.type === 'MemberExpression' && callee.object.name === 'styled') {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (callee.type === 'CallExpression' && callee.callee && callee.callee.name === 'styled') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (init.type === 'CallExpression' && init.callee.name === 'styled') {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Проверяет, является ли узел верхнего уровня объявлением функции
|
|
46
|
+
*/
|
|
47
|
+
function isTopLevelFunction(node) {
|
|
48
|
+
if (node.type === 'FunctionDeclaration') {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (node.type === 'VariableDeclaration') {
|
|
52
|
+
return node.declarations.some(
|
|
53
|
+
(decl) =>
|
|
54
|
+
decl.init && (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression')
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Проверяет, является ли узел верхнего уровня styled-компонентом
|
|
62
|
+
*/
|
|
63
|
+
function isTopLevelStyledComponent(node) {
|
|
64
|
+
if (node.type === 'VariableDeclaration') {
|
|
65
|
+
return node.declarations.some((decl) => isStyledComponent(decl));
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Проверяет, является ли statement объявлением функции (внутри React-компонента)
|
|
72
|
+
*/
|
|
73
|
+
function isFunctionStatementInsideReactComponent(stmt) {
|
|
74
|
+
if (stmt.type === 'FunctionDeclaration') {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
78
|
+
return stmt.declarations.some((decl) => {
|
|
79
|
+
if (!decl.init) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression') {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (decl.init.type === 'CallExpression') {
|
|
86
|
+
const { callee } = decl.init;
|
|
87
|
+
const hookName = callee.name || (callee.type === 'MemberExpression' && callee.property.name);
|
|
88
|
+
if (hookName === 'useCallback' || hookName === 'useMemo') {
|
|
89
|
+
const firstArg = decl.init.arguments && decl.init.arguments[0];
|
|
90
|
+
if (
|
|
91
|
+
firstArg &&
|
|
92
|
+
(firstArg.type === 'ArrowFunctionExpression' || firstArg.type === 'FunctionExpression')
|
|
93
|
+
) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'CallExpression') {
|
|
102
|
+
const { callee } = stmt.expression;
|
|
103
|
+
const hookName = callee.name || (callee.type === 'MemberExpression' && callee.property.name);
|
|
104
|
+
if (hookName === 'useEffect' || hookName === 'useLayoutEffect') {
|
|
105
|
+
const firstArg = stmt.expression.arguments && stmt.expression.arguments[0];
|
|
106
|
+
if (firstArg && (firstArg.type === 'ArrowFunctionExpression' || firstArg.type === 'FunctionExpression')) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Получает количество пустых строк между двумя токенами
|
|
116
|
+
*/
|
|
117
|
+
function getBlankLinesBetween(sourceCode, token1, token2) {
|
|
118
|
+
if (!token1 || !token2) {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
const token1Line = token1.loc.end.line;
|
|
122
|
+
const token2Line = token2.loc.start.line;
|
|
123
|
+
if (token2Line <= token1Line) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
let blankLines = 0;
|
|
127
|
+
const lines = sourceCode.text.split(/\r?\n/);
|
|
128
|
+
for (let line = token1Line + 1; line < token2Line; line++) {
|
|
129
|
+
const lineIndex = line - 1;
|
|
130
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
131
|
+
const lineText = lines[lineIndex];
|
|
132
|
+
if (lineText.trim() === '') {
|
|
133
|
+
blankLines++;
|
|
134
|
+
} else {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return blankLines;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Получает количество пустых строк перед узлом
|
|
144
|
+
*/
|
|
145
|
+
function getBlankLinesBefore(sourceCode, node) {
|
|
146
|
+
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: false });
|
|
147
|
+
if (!tokenBefore) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const firstToken = sourceCode.getFirstToken(node);
|
|
151
|
+
return getBlankLinesBetween(sourceCode, tokenBefore, firstToken);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
meta: {
|
|
156
|
+
type: 'layout',
|
|
157
|
+
docs: {
|
|
158
|
+
description: 'Проверяет расстановку пустых строк согласно соглашению форматирования',
|
|
159
|
+
category: 'Stylistic Issues',
|
|
160
|
+
recommended: false,
|
|
161
|
+
},
|
|
162
|
+
fixable: 'whitespace',
|
|
163
|
+
schema: [],
|
|
164
|
+
messages: {
|
|
165
|
+
requireBlankLine: 'Требуется пустая строка перед {{nodeType}}',
|
|
166
|
+
disallowBlankLine: 'Не должно быть пустой строки перед {{nodeType}}',
|
|
167
|
+
requireBlankLineAfterImports: 'Требуется пустая строка после последнего импорта',
|
|
168
|
+
requireBlankLineBetweenStyled: 'Требуется пустая строка между styled-компонентами',
|
|
169
|
+
requireBlankLineAfterLastStyled: 'Требуется пустая строка после последнего styled-компонента',
|
|
170
|
+
disallowBlankLineInFunction: 'Не должно быть пустой строки внутри тела функции',
|
|
171
|
+
requireBlankLineBeforeFunction: 'Требуется пустая строка перед объявлением функции внутри React-компонента',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
create(context) {
|
|
176
|
+
const sourceCode = context.getSourceCode();
|
|
177
|
+
const functionStack = [];
|
|
178
|
+
return {
|
|
179
|
+
Program(node) {
|
|
180
|
+
const { body } = node;
|
|
181
|
+
let lastImportNode = null;
|
|
182
|
+
let lastStyledNode = null;
|
|
183
|
+
let lastStyledComponentInFile = null;
|
|
184
|
+
for (let i = 0; i < body.length; i++) {
|
|
185
|
+
const current = body[i];
|
|
186
|
+
if (lastImportNode && current.type !== 'ImportDeclaration') {
|
|
187
|
+
const blankLines = getBlankLinesBefore(sourceCode, current);
|
|
188
|
+
if (blankLines === 0) {
|
|
189
|
+
context.report({
|
|
190
|
+
node: current,
|
|
191
|
+
messageId: 'requireBlankLineAfterImports',
|
|
192
|
+
fix(fixer) {
|
|
193
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(current), '\n');
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
lastImportNode = null;
|
|
198
|
+
}
|
|
199
|
+
if (isTopLevelStyledComponent(current)) {
|
|
200
|
+
if (lastStyledNode) {
|
|
201
|
+
const blankLines = getBlankLinesBefore(sourceCode, current);
|
|
202
|
+
if (blankLines === 0) {
|
|
203
|
+
context.report({
|
|
204
|
+
node: current,
|
|
205
|
+
messageId: 'requireBlankLineBetweenStyled',
|
|
206
|
+
fix(fixer) {
|
|
207
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(current), '\n');
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
lastStyledNode = current;
|
|
213
|
+
lastStyledComponentInFile = current;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (isTopLevelFunction(current)) {
|
|
217
|
+
if (i > 0) {
|
|
218
|
+
const prevNode = body[i - 1];
|
|
219
|
+
const shouldHaveBlankLine =
|
|
220
|
+
prevNode.type !== 'FunctionDeclaration' &&
|
|
221
|
+
!isTopLevelFunction(prevNode) &&
|
|
222
|
+
!isTopLevelStyledComponent(prevNode);
|
|
223
|
+
if (shouldHaveBlankLine) {
|
|
224
|
+
const blankLines = getBlankLinesBefore(sourceCode, current);
|
|
225
|
+
if (blankLines === 0) {
|
|
226
|
+
context.report({
|
|
227
|
+
node: current,
|
|
228
|
+
messageId: 'requireBlankLine',
|
|
229
|
+
data: { nodeType: 'объявлением функции' },
|
|
230
|
+
fix(fixer) {
|
|
231
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(current), '\n');
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
lastStyledNode = null;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (current.type === 'ExportDefaultDeclaration') {
|
|
241
|
+
const blankLines = getBlankLinesBefore(sourceCode, current);
|
|
242
|
+
if (blankLines === 0) {
|
|
243
|
+
context.report({
|
|
244
|
+
node: current,
|
|
245
|
+
messageId: 'requireBlankLine',
|
|
246
|
+
data: { nodeType: 'экспортом по дефолту' },
|
|
247
|
+
fix(fixer) {
|
|
248
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(current), '\n');
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
lastStyledNode = null;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
lastStyledNode = null;
|
|
256
|
+
}
|
|
257
|
+
if (lastStyledComponentInFile) {
|
|
258
|
+
const lastStyledIndex = body.indexOf(lastStyledComponentInFile);
|
|
259
|
+
if (lastStyledIndex >= 0 && lastStyledIndex < body.length - 1) {
|
|
260
|
+
const nextNode = body[lastStyledIndex + 1];
|
|
261
|
+
const blankLines = getBlankLinesBefore(sourceCode, nextNode);
|
|
262
|
+
if (blankLines === 0) {
|
|
263
|
+
context.report({
|
|
264
|
+
node: nextNode,
|
|
265
|
+
messageId: 'requireBlankLineAfterLastStyled',
|
|
266
|
+
fix(fixer) {
|
|
267
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(nextNode), '\n');
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
FunctionDeclaration(node) {
|
|
276
|
+
if (node.id && /^[A-Z]/.test(node.id.name)) {
|
|
277
|
+
functionStack.push({ type: 'declaration', node, name: node.id.name });
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
'FunctionDeclaration:exit': function (node) {
|
|
281
|
+
if (functionStack.length > 0) {
|
|
282
|
+
const top = functionStack[functionStack.length - 1];
|
|
283
|
+
if (top.type === 'declaration' && top.node === node) {
|
|
284
|
+
functionStack.pop();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
ArrowFunctionExpression(node) {
|
|
290
|
+
const { parent } = node;
|
|
291
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id) {
|
|
292
|
+
if (/^[A-Z]/.test(parent.id.name)) {
|
|
293
|
+
functionStack.push({ type: 'arrow', node, name: parent.id.name });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
'ArrowFunctionExpression:exit': function (node) {
|
|
299
|
+
if (functionStack.length > 0) {
|
|
300
|
+
const top = functionStack[functionStack.length - 1];
|
|
301
|
+
if (top.type === 'arrow' && top.node === node) {
|
|
302
|
+
functionStack.pop();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
FunctionExpression(node) {
|
|
308
|
+
const { parent } = node;
|
|
309
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id) {
|
|
310
|
+
if (/^[A-Z]/.test(parent.id.name)) {
|
|
311
|
+
functionStack.push({ type: 'expression', node, name: parent.id.name });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
'FunctionExpression:exit': function (node) {
|
|
317
|
+
if (functionStack.length > 0) {
|
|
318
|
+
const top = functionStack[functionStack.length - 1];
|
|
319
|
+
if (top.type === 'expression' && top.node === node) {
|
|
320
|
+
functionStack.pop();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
ReturnStatement(node) {
|
|
326
|
+
let isInComponent = false;
|
|
327
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
328
|
+
const func = functionStack[i];
|
|
329
|
+
if (func.type === 'declaration' || func.type === 'arrow' || func.type === 'expression') {
|
|
330
|
+
isInComponent = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (isInComponent) {
|
|
335
|
+
const { parent } = node;
|
|
336
|
+
if (parent && parent.body && Array.isArray(parent.body)) {
|
|
337
|
+
const returnIndex = parent.body.indexOf(node);
|
|
338
|
+
if (returnIndex > 0) {
|
|
339
|
+
const blankLines = getBlankLinesBefore(sourceCode, node);
|
|
340
|
+
if (blankLines === 0) {
|
|
341
|
+
context.report({
|
|
342
|
+
node,
|
|
343
|
+
messageId: 'requireBlankLine',
|
|
344
|
+
data: { nodeType: 'return' },
|
|
345
|
+
fix(fixer) {
|
|
346
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(node), '\n');
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
BlockStatement(node) {
|
|
356
|
+
const { parent } = node;
|
|
357
|
+
if (
|
|
358
|
+
parent.type !== 'FunctionDeclaration' &&
|
|
359
|
+
parent.type !== 'ArrowFunctionExpression' &&
|
|
360
|
+
parent.type !== 'FunctionExpression'
|
|
361
|
+
) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
let isComponent = false;
|
|
365
|
+
if (parent.type === 'FunctionDeclaration') {
|
|
366
|
+
isComponent = parent.id && /^[A-Z]/.test(parent.id.name);
|
|
367
|
+
} else if (parent.parent && parent.parent.type === 'VariableDeclarator') {
|
|
368
|
+
const varDecl = parent.parent;
|
|
369
|
+
isComponent = varDecl.id && /^[A-Z]/.test(varDecl.id.name);
|
|
370
|
+
}
|
|
371
|
+
if (!isComponent) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const statements = node.body;
|
|
375
|
+
for (let i = 1; i < statements.length; i++) {
|
|
376
|
+
const prevStmt = statements[i - 1];
|
|
377
|
+
const currentStmt = statements[i];
|
|
378
|
+
if (currentStmt.type === 'ReturnStatement') {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (isFunctionStatementInsideReactComponent(currentStmt)) {
|
|
382
|
+
const blankLines = getBlankLinesBetween(
|
|
383
|
+
sourceCode,
|
|
384
|
+
sourceCode.getLastToken(prevStmt),
|
|
385
|
+
sourceCode.getFirstToken(currentStmt)
|
|
386
|
+
);
|
|
387
|
+
if (blankLines === 0) {
|
|
388
|
+
context.report({
|
|
389
|
+
node: currentStmt,
|
|
390
|
+
messageId: 'requireBlankLineBeforeFunction',
|
|
391
|
+
fix(fixer) {
|
|
392
|
+
return fixer.insertTextBefore(sourceCode.getFirstToken(currentStmt), '\n');
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const blankLines = getBlankLinesBetween(
|
|
399
|
+
sourceCode,
|
|
400
|
+
sourceCode.getLastToken(prevStmt),
|
|
401
|
+
sourceCode.getFirstToken(currentStmt)
|
|
402
|
+
);
|
|
403
|
+
if (blankLines > 0) {
|
|
404
|
+
context.report({
|
|
405
|
+
node: currentStmt,
|
|
406
|
+
messageId: 'disallowBlankLineInFunction',
|
|
407
|
+
fix(fixer) {
|
|
408
|
+
const prevToken = sourceCode.getLastToken(prevStmt);
|
|
409
|
+
const currentToken = sourceCode.getFirstToken(currentStmt);
|
|
410
|
+
const text = sourceCode.getText().slice(prevToken.range[1], currentToken.range[0]);
|
|
411
|
+
const fixedText = text.replace(/\n\s*\n/g, '\n');
|
|
412
|
+
return fixer.replaceTextRange([prevToken.range[1], currentToken.range[0]], fixedText);
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
},
|
|
420
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: id-naming-convention
|
|
3
|
+
*
|
|
4
|
+
* Проверяет именование идентификаторов согласно соглашению:
|
|
5
|
+
*
|
|
6
|
+
* Если переменная обозначает идентификатор чего-либо, то id пишется всегда капсом - assetID, taskID и т.п.
|
|
7
|
+
*
|
|
8
|
+
* Пример неправильного использования:
|
|
9
|
+
* const assetId = 123;
|
|
10
|
+
* const taskId = 456;
|
|
11
|
+
* function getUser(userId) {}
|
|
12
|
+
*
|
|
13
|
+
* Пример правильного использования:
|
|
14
|
+
* const assetID = 123;
|
|
15
|
+
* const taskID = 456;
|
|
16
|
+
* function getUser(userID) {}
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Проверяет, содержит ли имя переменной "id" (в любом регистре)
|
|
21
|
+
*/
|
|
22
|
+
function containsId(name) {
|
|
23
|
+
if (!name || typeof name !== 'string') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return /id/i.test(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Проверяет, правильно ли написано "ID" в имени переменной
|
|
31
|
+
*/
|
|
32
|
+
function hasCorrectIdFormat(name) {
|
|
33
|
+
if (!name || typeof name !== 'string') {
|
|
34
|
+
return true; // Если имени нет, пропускаем
|
|
35
|
+
}
|
|
36
|
+
const lowerName = name.toLowerCase();
|
|
37
|
+
const excludedWords = [
|
|
38
|
+
'identifier',
|
|
39
|
+
'valid',
|
|
40
|
+
'invalid',
|
|
41
|
+
'provide',
|
|
42
|
+
'divide',
|
|
43
|
+
'decide',
|
|
44
|
+
'hide',
|
|
45
|
+
'slide',
|
|
46
|
+
'guide',
|
|
47
|
+
'wide',
|
|
48
|
+
'side',
|
|
49
|
+
'messageid',
|
|
50
|
+
];
|
|
51
|
+
if (excludedWords.some((word) => lowerName.includes(word)) || lowerName==="id") {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const idMatches = name.matchAll(/([a-zA-Z]*?)([Ii]d|ID)([a-zA-Z]*)/g);
|
|
55
|
+
const matchesArray = Array.from(idMatches);
|
|
56
|
+
for (const match of matchesArray) {
|
|
57
|
+
const idPart = match[2];
|
|
58
|
+
const afterID = match[3];
|
|
59
|
+
if (afterID && afterID.length > 0) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (idPart !== 'ID') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Получает имя из узла объявления переменной
|
|
71
|
+
*/
|
|
72
|
+
function getVariableName(node) {
|
|
73
|
+
if (!node) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (node.type === 'Identifier') {
|
|
77
|
+
return node.name;
|
|
78
|
+
}
|
|
79
|
+
if (node.type === 'VariableDeclarator' && node.id) {
|
|
80
|
+
if (node.id.type === 'Identifier') {
|
|
81
|
+
return node.id.name;
|
|
82
|
+
}
|
|
83
|
+
if (node.id.type === 'ObjectPattern') {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Генерирует правильное имя с ID в капсе
|
|
92
|
+
*/
|
|
93
|
+
function fixIdName(name) {
|
|
94
|
+
if (!name || typeof name !== 'string') {
|
|
95
|
+
return name;
|
|
96
|
+
}
|
|
97
|
+
return name.replace(/([a-zA-Z]*?)([Ii]d)([^a-zA-Z]*|$)/g, (match, before, idPart, after) => {
|
|
98
|
+
if (after && /[a-zA-Z]/.test(after)) {
|
|
99
|
+
return match;
|
|
100
|
+
}
|
|
101
|
+
return `${before}ID${after}`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Проверяет все идентификаторы в деструктуризации
|
|
107
|
+
*/
|
|
108
|
+
function checkDestructuredProperties(node, context) {
|
|
109
|
+
if (!node || node.type !== 'ObjectPattern') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { properties } = node;
|
|
113
|
+
if (!properties || !Array.isArray(properties)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const prop of properties) {
|
|
117
|
+
if (prop.type === 'Property') {
|
|
118
|
+
const { key } = prop;
|
|
119
|
+
if (key && key.type === 'Identifier') {
|
|
120
|
+
const { name } = key;
|
|
121
|
+
if (containsId(name) && !hasCorrectIdFormat(name)) {
|
|
122
|
+
const suggestedName = fixIdName(name);
|
|
123
|
+
context.report({
|
|
124
|
+
node: key,
|
|
125
|
+
messageId: 'idMustBeUppercase',
|
|
126
|
+
data: {
|
|
127
|
+
variableName: name,
|
|
128
|
+
suggestedName,
|
|
129
|
+
},
|
|
130
|
+
fix(fixer) {
|
|
131
|
+
return fixer.replaceText(key, suggestedName);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
meta: {
|
|
142
|
+
type: 'suggestion',
|
|
143
|
+
docs: {
|
|
144
|
+
description: 'Проверяет, что идентификаторы используют ID в капсе',
|
|
145
|
+
category: 'Stylistic Issues',
|
|
146
|
+
recommended: false,
|
|
147
|
+
},
|
|
148
|
+
fixable: 'code',
|
|
149
|
+
schema: [],
|
|
150
|
+
messages: {
|
|
151
|
+
idMustBeUppercase:
|
|
152
|
+
'Идентификаторы должны использовать "ID" в капсе. ' +
|
|
153
|
+
'Вместо "{{variableName}}" используйте "{{suggestedName}}"',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
create(context) {
|
|
158
|
+
return {
|
|
159
|
+
VariableDeclarator(node) {
|
|
160
|
+
const variableName = getVariableName(node);
|
|
161
|
+
if (variableName && containsId(variableName) && !hasCorrectIdFormat(variableName)) {
|
|
162
|
+
const suggestedName = fixIdName(variableName);
|
|
163
|
+
context.report({
|
|
164
|
+
node: node.id,
|
|
165
|
+
messageId: 'idMustBeUppercase',
|
|
166
|
+
data: {
|
|
167
|
+
variableName,
|
|
168
|
+
suggestedName,
|
|
169
|
+
},
|
|
170
|
+
fix(fixer) {
|
|
171
|
+
return fixer.replaceText(node.id, suggestedName);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (node.id && node.id.type === 'ObjectPattern') {
|
|
176
|
+
checkDestructuredProperties(node.id, context);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
FunctionDeclaration(node) {
|
|
181
|
+
if (!node.params || !Array.isArray(node.params)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const param of node.params) {
|
|
185
|
+
if (param.type === 'Identifier') {
|
|
186
|
+
const { name } = param;
|
|
187
|
+
if (containsId(name) && !hasCorrectIdFormat(name)) {
|
|
188
|
+
const suggestedName = fixIdName(name);
|
|
189
|
+
context.report({
|
|
190
|
+
node: param,
|
|
191
|
+
messageId: 'idMustBeUppercase',
|
|
192
|
+
data: {
|
|
193
|
+
variableName: name,
|
|
194
|
+
suggestedName,
|
|
195
|
+
},
|
|
196
|
+
fix(fixer) {
|
|
197
|
+
return fixer.replaceText(param, suggestedName);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} else if (param.type === 'ObjectPattern') {
|
|
202
|
+
checkDestructuredProperties(param, context);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
ArrowFunctionExpression(node) {
|
|
208
|
+
if (!node.params || !Array.isArray(node.params)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const param of node.params) {
|
|
212
|
+
if (param.type === 'Identifier') {
|
|
213
|
+
const { name } = param;
|
|
214
|
+
if (containsId(name) && !hasCorrectIdFormat(name)) {
|
|
215
|
+
const suggestedName = fixIdName(name);
|
|
216
|
+
context.report({
|
|
217
|
+
node: param,
|
|
218
|
+
messageId: 'idMustBeUppercase',
|
|
219
|
+
data: {
|
|
220
|
+
variableName: name,
|
|
221
|
+
suggestedName,
|
|
222
|
+
},
|
|
223
|
+
fix(fixer) {
|
|
224
|
+
return fixer.replaceText(param, suggestedName);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} else if (param.type === 'ObjectPattern') {
|
|
229
|
+
checkDestructuredProperties(param, context);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
Property(node) {
|
|
235
|
+
if (node.key && node.key.type === 'Identifier') {
|
|
236
|
+
const { name } = node.key;
|
|
237
|
+
if (containsId(name) && !hasCorrectIdFormat(name)) {
|
|
238
|
+
const suggestedName = fixIdName(name);
|
|
239
|
+
context.report({
|
|
240
|
+
node: node.key,
|
|
241
|
+
messageId: 'idMustBeUppercase',
|
|
242
|
+
data: {
|
|
243
|
+
variableName: name,
|
|
244
|
+
suggestedName,
|
|
245
|
+
},
|
|
246
|
+
fix(fixer) {
|
|
247
|
+
return fixer.replaceText(node.key, suggestedName);
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
};
|