eslint-plugin-power-esrules 0.1.12 → 0.1.13
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.
|
@@ -91,7 +91,8 @@ function getImportType(node) {
|
|
|
91
91
|
spec.type === "ImportSpecifier" &&
|
|
92
92
|
spec.imported &&
|
|
93
93
|
/^[A-Z][a-zA-Z]*$/.test(spec.imported.name) && // Имя начинается с большой буквы и не все заглавные
|
|
94
|
-
spec.imported.name !== spec.imported.name.toUpperCase()
|
|
94
|
+
spec.imported.name !== spec.imported.name.toUpperCase() && // Не константа (не все заглавные)
|
|
95
|
+
!spec.imported.name.endsWith("Context"), // не содержит ключевого слова Context
|
|
95
96
|
);
|
|
96
97
|
if (isDefaultImport || hasComponentName) {
|
|
97
98
|
return "component";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// /**
|
|
2
|
+
// * Rule: require-data-testid
|
|
3
|
+
// *
|
|
4
|
+
// * Проверяет наличие data-testid атрибута у корневого контейнера JSX файла
|
|
5
|
+
// * и рекомендует запустить codemod скрипт для его добавления, если атрибут отсутствует.
|
|
6
|
+
// */
|
|
7
|
+
|
|
8
|
+
// const path = require("path");
|
|
9
|
+
|
|
10
|
+
// const packetName = "@hubex/power-linter";
|
|
11
|
+
|
|
12
|
+
// /**
|
|
13
|
+
// * Получает имя JSX элемента
|
|
14
|
+
// */
|
|
15
|
+
// function getJSXName(jsxName) {
|
|
16
|
+
// if (!jsxName) return null;
|
|
17
|
+
// switch (jsxName.type) {
|
|
18
|
+
// case "JSXIdentifier":
|
|
19
|
+
// return jsxName.name || null;
|
|
20
|
+
// case "JSXMemberExpression":
|
|
21
|
+
// // Берем правую часть: UI.Button -> Button
|
|
22
|
+
// return getJSXName(jsxName.property);
|
|
23
|
+
// case "JSXNamespacedName":
|
|
24
|
+
// return `${jsxName.namespace?.name || "ns"}:${
|
|
25
|
+
// jsxName.name?.name || "Name"
|
|
26
|
+
// }`;
|
|
27
|
+
// default:
|
|
28
|
+
// return null;
|
|
29
|
+
// }
|
|
30
|
+
// }
|
|
31
|
+
|
|
32
|
+
// /**
|
|
33
|
+
// * Проверяет, является ли узел верхнеуровневым JSX элементом
|
|
34
|
+
// * (не вложенным в другой JSX элемент или Fragment)
|
|
35
|
+
// */
|
|
36
|
+
// function isTopLevelJSX(node) {
|
|
37
|
+
// let current = node.parent;
|
|
38
|
+
// while (current) {
|
|
39
|
+
// const parentType = current.type;
|
|
40
|
+
// // Если родитель - JSX элемент или Fragment, значит это не верхнеуровневый элемент
|
|
41
|
+
// if (parentType === "JSXElement" || parentType === "JSXFragment") {
|
|
42
|
+
// return false;
|
|
43
|
+
// }
|
|
44
|
+
// // Если родитель - ReturnStatement, это возвращаемое значение компонента (верхнеуровневый)
|
|
45
|
+
// if (parentType === "ReturnStatement") {
|
|
46
|
+
// return true;
|
|
47
|
+
// }
|
|
48
|
+
// // Если родитель - ExportDefaultDeclaration или ExportNamedDeclaration,
|
|
49
|
+
// // это может быть верхнеуровневый JSX
|
|
50
|
+
// if (
|
|
51
|
+
// parentType === "ExportDefaultDeclaration" ||
|
|
52
|
+
// parentType === "ExportNamedDeclaration"
|
|
53
|
+
// ) {
|
|
54
|
+
// return true;
|
|
55
|
+
// }
|
|
56
|
+
// current = current.parent;
|
|
57
|
+
// }
|
|
58
|
+
// return false;
|
|
59
|
+
// }
|
|
60
|
+
|
|
61
|
+
// /**
|
|
62
|
+
// * Спускается от Fragment к первому рендеримому JSXElement
|
|
63
|
+
// */
|
|
64
|
+
// function descendToFirstRenderable(node) {
|
|
65
|
+
// if (!node) return null;
|
|
66
|
+
|
|
67
|
+
// // 1) Если это JSXFragment — обходим детей
|
|
68
|
+
// if (node.type === "JSXFragment") {
|
|
69
|
+
// for (const child of node.children || []) {
|
|
70
|
+
// if (child.type === "JSXElement") {
|
|
71
|
+
// const resolved = descendToFirstRenderable(child);
|
|
72
|
+
// if (resolved) return resolved;
|
|
73
|
+
// }
|
|
74
|
+
// }
|
|
75
|
+
// return null;
|
|
76
|
+
// }
|
|
77
|
+
|
|
78
|
+
// // 2) Если это JSXElement с именем Fragment
|
|
79
|
+
// if (node.type === "JSXElement") {
|
|
80
|
+
// const name = getJSXName(node.openingElement?.name);
|
|
81
|
+
// if (name === "Fragment") {
|
|
82
|
+
// for (const child of node.children || []) {
|
|
83
|
+
// if (child.type === "JSXElement") {
|
|
84
|
+
// const resolved = descendToFirstRenderable(child);
|
|
85
|
+
// if (resolved) return resolved;
|
|
86
|
+
// }
|
|
87
|
+
// }
|
|
88
|
+
// return null;
|
|
89
|
+
// }
|
|
90
|
+
// // Иначе это реальный рендеримый элемент
|
|
91
|
+
// return node;
|
|
92
|
+
// }
|
|
93
|
+
|
|
94
|
+
// return null;
|
|
95
|
+
// }
|
|
96
|
+
|
|
97
|
+
// /**
|
|
98
|
+
// * Проверяет наличие data-testid или dataTestID атрибута
|
|
99
|
+
// */
|
|
100
|
+
// function hasDataTestId(openingElement) {
|
|
101
|
+
// if (!openingElement || !openingElement.attributes) {
|
|
102
|
+
// return false;
|
|
103
|
+
// }
|
|
104
|
+
// return openingElement.attributes.some((attr) => {
|
|
105
|
+
// if (!attr || attr.type !== "JSXAttribute" || !attr.name) {
|
|
106
|
+
// return false;
|
|
107
|
+
// }
|
|
108
|
+
// const attrName = attr.name.name;
|
|
109
|
+
// return attrName === "data-testid" || attrName === "dataTestID";
|
|
110
|
+
// });
|
|
111
|
+
// }
|
|
112
|
+
|
|
113
|
+
// /**
|
|
114
|
+
// * Получает относительный путь к файлу от корня проекта
|
|
115
|
+
// */
|
|
116
|
+
// function getRelativeFilePath(context) {
|
|
117
|
+
// const filename = context.getFilename();
|
|
118
|
+
// const workspaceRoot = context.getCwd ? context.getCwd() : process.cwd();
|
|
119
|
+
// return path.relative(workspaceRoot, filename);
|
|
120
|
+
// }
|
|
121
|
+
|
|
122
|
+
// module.exports = {
|
|
123
|
+
// meta: {
|
|
124
|
+
// type: "suggestion",
|
|
125
|
+
// docs: {
|
|
126
|
+
// description:
|
|
127
|
+
// "Проверяет наличие data-testid атрибута у корневого контейнера JSX файла и рекомендует запустить codemod",
|
|
128
|
+
// category: "Best Practices",
|
|
129
|
+
// recommended: false,
|
|
130
|
+
// },
|
|
131
|
+
// fixable: null,
|
|
132
|
+
// schema: [],
|
|
133
|
+
// messages: {
|
|
134
|
+
// missingDataTestId:
|
|
135
|
+
// "Корневой контейнер JSX не содержит атрибут data-testid. " +
|
|
136
|
+
// `Запустите: node node_modules/${packetName}/scripts/addDataTestId/run-codemod.js {{filePath}}` +
|
|
137
|
+
// ` или node node_modules/${packetName}/scripts/addDataTestId/run-codemod.js src для всего проекта`,
|
|
138
|
+
// },
|
|
139
|
+
// },
|
|
140
|
+
|
|
141
|
+
// create(context) {
|
|
142
|
+
// let hasReported = false;
|
|
143
|
+
// let rootJSXNode = null;
|
|
144
|
+
|
|
145
|
+
// return {
|
|
146
|
+
// ReturnStatement(node) {
|
|
147
|
+
// // Проверяем только один раз на файл
|
|
148
|
+
// if (hasReported || rootJSXNode) {
|
|
149
|
+
// return;
|
|
150
|
+
// }
|
|
151
|
+
|
|
152
|
+
// const returnArgument = node.argument;
|
|
153
|
+
// if (!returnArgument) {
|
|
154
|
+
// return;
|
|
155
|
+
// }
|
|
156
|
+
|
|
157
|
+
// // Если возвращается JSX элемент или Fragment
|
|
158
|
+
// if (
|
|
159
|
+
// returnArgument.type === "JSXElement" ||
|
|
160
|
+
// returnArgument.type === "JSXFragment"
|
|
161
|
+
// ) {
|
|
162
|
+
// // Проверяем, что это не вложенный JSX (не имеет JSX родителя)
|
|
163
|
+
// if (isTopLevelJSX(returnArgument)) {
|
|
164
|
+
// rootJSXNode = returnArgument;
|
|
165
|
+
// }
|
|
166
|
+
// }
|
|
167
|
+
// },
|
|
168
|
+
|
|
169
|
+
// JSXElement(node) {
|
|
170
|
+
// // Проверяем только один раз на файл
|
|
171
|
+
// if (hasReported || rootJSXNode) {
|
|
172
|
+
// return;
|
|
173
|
+
// }
|
|
174
|
+
|
|
175
|
+
// // Проверяем верхнеуровневые JSX элементы вне ReturnStatement
|
|
176
|
+
// // (например, экспортируемые напрямую)
|
|
177
|
+
// if (isTopLevelJSX(node)) {
|
|
178
|
+
// rootJSXNode = node;
|
|
179
|
+
// }
|
|
180
|
+
// },
|
|
181
|
+
|
|
182
|
+
// JSXFragment(node) {
|
|
183
|
+
// // Проверяем только один раз на файл
|
|
184
|
+
// if (hasReported || rootJSXNode) {
|
|
185
|
+
// return;
|
|
186
|
+
// }
|
|
187
|
+
|
|
188
|
+
// // Проверяем верхнеуровневые Fragment вне ReturnStatement
|
|
189
|
+
// if (isTopLevelJSX(node)) {
|
|
190
|
+
// rootJSXNode = node;
|
|
191
|
+
// }
|
|
192
|
+
// },
|
|
193
|
+
|
|
194
|
+
// "Program:exit"() {
|
|
195
|
+
// // После обхода всего файла проверяем корневой JSX элемент
|
|
196
|
+
// if (hasReported || !rootJSXNode) {
|
|
197
|
+
// return;
|
|
198
|
+
// }
|
|
199
|
+
|
|
200
|
+
// // Если это Fragment, спускаемся к первому рендеримому элементу
|
|
201
|
+
// const rootElement = descendToFirstRenderable(rootJSXNode);
|
|
202
|
+
// const elementToCheck = rootElement || rootJSXNode;
|
|
203
|
+
|
|
204
|
+
// if (!elementToCheck.openingElement) {
|
|
205
|
+
// return;
|
|
206
|
+
// }
|
|
207
|
+
|
|
208
|
+
// // Проверяем наличие data-testid
|
|
209
|
+
// if (!hasDataTestId(elementToCheck.openingElement)) {
|
|
210
|
+
// hasReported = true;
|
|
211
|
+
// const filePath = getRelativeFilePath(context);
|
|
212
|
+
// context.report({
|
|
213
|
+
// node: elementToCheck.openingElement,
|
|
214
|
+
// messageId: "missingDataTestId",
|
|
215
|
+
// data: {
|
|
216
|
+
// filePath,
|
|
217
|
+
// },
|
|
218
|
+
// });
|
|
219
|
+
// }
|
|
220
|
+
// },
|
|
221
|
+
// };
|
|
222
|
+
// },
|
|
223
|
+
// };
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rule: require-data-testid
|
|
3
3
|
*
|
|
4
|
-
* Проверяет наличие data-testid атрибута у корневого
|
|
4
|
+
* Проверяет наличие data-testid атрибута у корневого JSX элемента React-компонента
|
|
5
5
|
* и рекомендует запустить codemod скрипт для его добавления, если атрибут отсутствует.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const path = require("path");
|
|
9
|
-
|
|
10
9
|
const packetName = "@hubex/power-linter";
|
|
11
10
|
|
|
12
|
-
/**
|
|
13
|
-
* Получает имя JSX элемента
|
|
14
|
-
*/
|
|
11
|
+
/** Получает имя JSX элемента */
|
|
15
12
|
function getJSXName(jsxName) {
|
|
16
13
|
if (!jsxName) return null;
|
|
17
14
|
switch (jsxName.type) {
|
|
18
15
|
case "JSXIdentifier":
|
|
19
16
|
return jsxName.name || null;
|
|
20
17
|
case "JSXMemberExpression":
|
|
21
|
-
// Берем правую часть: UI.Button -> Button
|
|
22
18
|
return getJSXName(jsxName.property);
|
|
23
19
|
case "JSXNamespacedName":
|
|
24
20
|
return `${jsxName.namespace?.name || "ns"}:${
|
|
@@ -29,42 +25,81 @@ function getJSXName(jsxName) {
|
|
|
29
25
|
}
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
/** Проверка PascalCase */
|
|
29
|
+
function isPascalCase(name) {
|
|
30
|
+
return /^[A-Z]/.test(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Находит имя компонента, учитывая HOC (memo, forwardRef, observer) */
|
|
34
|
+
function getComponentName(node) {
|
|
35
|
+
if (!node) return null;
|
|
36
|
+
|
|
37
|
+
// Функция объявлена напрямую
|
|
38
|
+
if (node.type === "FunctionDeclaration") {
|
|
39
|
+
return node.id?.name ?? null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Переменная с функцией
|
|
43
|
+
if (
|
|
44
|
+
(node.type === "ArrowFunctionExpression" ||
|
|
45
|
+
node.type === "FunctionExpression") &&
|
|
46
|
+
node.parent?.type === "VariableDeclarator"
|
|
47
|
+
) {
|
|
48
|
+
return node.parent.id?.name ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// HOC обёртка: memo(() => {}) или forwardRef(() => {}) или observer(() => {})
|
|
52
|
+
if (node.type === "CallExpression" && node.arguments.length > 0) {
|
|
53
|
+
return getComponentName(node.arguments[0]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Проверяет, является ли функция React-компонентом */
|
|
60
|
+
function isReactComponent(node) {
|
|
61
|
+
const name = getComponentName(node);
|
|
62
|
+
return Boolean(name && isPascalCase(name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Находит корневой JSX элемент компонента, поддержка HOC */
|
|
66
|
+
function getRootJSXFromComponent(fnNode) {
|
|
67
|
+
if (!fnNode) return null;
|
|
68
|
+
|
|
69
|
+
// Разворачиваем HOC
|
|
70
|
+
if (fnNode.type === "CallExpression" && fnNode.arguments.length > 0) {
|
|
71
|
+
return getRootJSXFromComponent(fnNode.arguments[0]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Arrow function без блока: const X = () => <Page />
|
|
75
|
+
if (
|
|
76
|
+
fnNode.body?.type === "JSXElement" ||
|
|
77
|
+
fnNode.body?.type === "JSXFragment"
|
|
78
|
+
) {
|
|
79
|
+
return fnNode.body;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Function с блоком
|
|
83
|
+
if (fnNode.body?.type !== "BlockStatement") return null;
|
|
84
|
+
|
|
85
|
+
for (const stmt of fnNode.body.body) {
|
|
50
86
|
if (
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
stmt.type === "ReturnStatement" &&
|
|
88
|
+
stmt.argument &&
|
|
89
|
+
(stmt.argument.type === "JSXElement" ||
|
|
90
|
+
stmt.argument.type === "JSXFragment")
|
|
53
91
|
) {
|
|
54
|
-
return
|
|
92
|
+
return stmt.argument;
|
|
55
93
|
}
|
|
56
|
-
current = current.parent;
|
|
57
94
|
}
|
|
58
|
-
|
|
95
|
+
|
|
96
|
+
return null;
|
|
59
97
|
}
|
|
60
98
|
|
|
61
|
-
/**
|
|
62
|
-
* Спускается от Fragment к первому рендеримому JSXElement
|
|
63
|
-
*/
|
|
99
|
+
/** Рекурсивно спускается через Fragment к первому рендеримому JSXElement */
|
|
64
100
|
function descendToFirstRenderable(node) {
|
|
65
101
|
if (!node) return null;
|
|
66
102
|
|
|
67
|
-
// 1) Если это JSXFragment — обходим детей
|
|
68
103
|
if (node.type === "JSXFragment") {
|
|
69
104
|
for (const child of node.children || []) {
|
|
70
105
|
if (child.type === "JSXElement") {
|
|
@@ -75,7 +110,6 @@ function descendToFirstRenderable(node) {
|
|
|
75
110
|
return null;
|
|
76
111
|
}
|
|
77
112
|
|
|
78
|
-
// 2) Если это JSXElement с именем Fragment
|
|
79
113
|
if (node.type === "JSXElement") {
|
|
80
114
|
const name = getJSXName(node.openingElement?.name);
|
|
81
115
|
if (name === "Fragment") {
|
|
@@ -87,32 +121,24 @@ function descendToFirstRenderable(node) {
|
|
|
87
121
|
}
|
|
88
122
|
return null;
|
|
89
123
|
}
|
|
90
|
-
// Иначе это реальный рендеримый элемент
|
|
91
124
|
return node;
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
return null;
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
/**
|
|
98
|
-
* Проверяет наличие data-testid или dataTestID атрибута
|
|
99
|
-
*/
|
|
130
|
+
/** Проверяет наличие data-testid или dataTestID */
|
|
100
131
|
function hasDataTestId(openingElement) {
|
|
101
|
-
if (!openingElement || !openingElement.attributes)
|
|
102
|
-
|
|
103
|
-
}
|
|
132
|
+
if (!openingElement || !openingElement.attributes) return false;
|
|
133
|
+
|
|
104
134
|
return openingElement.attributes.some((attr) => {
|
|
105
|
-
if (!attr || attr.type !== "JSXAttribute" || !attr.name)
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
135
|
+
if (!attr || attr.type !== "JSXAttribute" || !attr.name) return false;
|
|
108
136
|
const attrName = attr.name.name;
|
|
109
137
|
return attrName === "data-testid" || attrName === "dataTestID";
|
|
110
138
|
});
|
|
111
139
|
}
|
|
112
140
|
|
|
113
|
-
/**
|
|
114
|
-
* Получает относительный путь к файлу от корня проекта
|
|
115
|
-
*/
|
|
141
|
+
/** Получает относительный путь к файлу */
|
|
116
142
|
function getRelativeFilePath(context) {
|
|
117
143
|
const filename = context.getFilename();
|
|
118
144
|
const workspaceRoot = context.getCwd ? context.getCwd() : process.cwd();
|
|
@@ -124,7 +150,7 @@ module.exports = {
|
|
|
124
150
|
type: "suggestion",
|
|
125
151
|
docs: {
|
|
126
152
|
description:
|
|
127
|
-
"Проверяет наличие data-testid атрибута у корневого
|
|
153
|
+
"Проверяет наличие data-testid атрибута у корневого JSX элемента React-компонента",
|
|
128
154
|
category: "Best Practices",
|
|
129
155
|
recommended: false,
|
|
130
156
|
},
|
|
@@ -139,83 +165,37 @@ module.exports = {
|
|
|
139
165
|
},
|
|
140
166
|
|
|
141
167
|
create(context) {
|
|
142
|
-
|
|
143
|
-
let rootJSXNode = null;
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
ReturnStatement(node) {
|
|
147
|
-
// Проверяем только один раз на файл
|
|
148
|
-
if (hasReported || rootJSXNode) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const returnArgument = node.argument;
|
|
153
|
-
if (!returnArgument) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Если возвращается JSX элемент или Fragment
|
|
158
|
-
if (
|
|
159
|
-
returnArgument.type === "JSXElement" ||
|
|
160
|
-
returnArgument.type === "JSXFragment"
|
|
161
|
-
) {
|
|
162
|
-
// Проверяем, что это не вложенный JSX (не имеет JSX родителя)
|
|
163
|
-
if (isTopLevelJSX(returnArgument)) {
|
|
164
|
-
rootJSXNode = returnArgument;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
JSXElement(node) {
|
|
170
|
-
// Проверяем только один раз на файл
|
|
171
|
-
if (hasReported || rootJSXNode) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Проверяем верхнеуровневые JSX элементы вне ReturnStatement
|
|
176
|
-
// (например, экспортируемые напрямую)
|
|
177
|
-
if (isTopLevelJSX(node)) {
|
|
178
|
-
rootJSXNode = node;
|
|
179
|
-
}
|
|
180
|
-
},
|
|
168
|
+
const components = new Set();
|
|
181
169
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
function collectComponent(node) {
|
|
171
|
+
if (isReactComponent(node)) {
|
|
172
|
+
components.add(node);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
187
175
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
176
|
+
return {
|
|
177
|
+
FunctionDeclaration: collectComponent,
|
|
178
|
+
FunctionExpression: collectComponent,
|
|
179
|
+
ArrowFunctionExpression: collectComponent,
|
|
180
|
+
// CallExpression больше не нужен, HOC разворачиваются внутри getComponentName
|
|
181
|
+
//"CallExpression": collectComponent,
|
|
193
182
|
|
|
194
183
|
"Program:exit"() {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
hasReported = true;
|
|
211
|
-
const filePath = getRelativeFilePath(context);
|
|
212
|
-
context.report({
|
|
213
|
-
node: elementToCheck.openingElement,
|
|
214
|
-
messageId: "missingDataTestId",
|
|
215
|
-
data: {
|
|
216
|
-
filePath,
|
|
217
|
-
},
|
|
218
|
-
});
|
|
184
|
+
for (const component of components) {
|
|
185
|
+
const rootJSX = getRootJSXFromComponent(component);
|
|
186
|
+
if (!rootJSX) continue;
|
|
187
|
+
|
|
188
|
+
const elementToCheck = descendToFirstRenderable(rootJSX) || rootJSX;
|
|
189
|
+
if (!elementToCheck?.openingElement) continue;
|
|
190
|
+
|
|
191
|
+
if (!hasDataTestId(elementToCheck.openingElement)) {
|
|
192
|
+
const filePath = getRelativeFilePath(context);
|
|
193
|
+
context.report({
|
|
194
|
+
node: elementToCheck.openingElement,
|
|
195
|
+
messageId: "missingDataTestId",
|
|
196
|
+
data: { filePath },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
219
199
|
}
|
|
220
200
|
},
|
|
221
201
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-power-esrules",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "custom ESLint rules",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"peerDependencies": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"lib": "lib"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "jest"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"homepage": "https://github.com/Vollmond148259/eslint-plugin-power-esrules#readme",
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"eslint": "^8.57.1"
|
|
32
|
+
"eslint": "^8.57.1",
|
|
33
|
+
"jest": "^29.3.1"
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -11,42 +11,42 @@ const ruleTester = new RuleTester({
|
|
|
11
11
|
|
|
12
12
|
ruleTester.run("props-destructuring-sort", rule, {
|
|
13
13
|
valid: [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
{
|
|
15
|
+
name: "sorted destructuring from props in functional component",
|
|
16
|
+
code: `
|
|
17
17
|
function MyComponent(props) {
|
|
18
18
|
const { alpha, beta } = props;
|
|
19
19
|
return <div>{alpha}{beta}</div>;
|
|
20
20
|
}
|
|
21
21
|
`,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "single property from props",
|
|
25
|
+
code: `
|
|
26
26
|
function MyComponent(props) {
|
|
27
27
|
const { only } = props;
|
|
28
28
|
return <div>{only}</div>;
|
|
29
29
|
}
|
|
30
30
|
`,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "unsorted props outside React component",
|
|
34
|
+
code: `
|
|
35
35
|
function util(props) {
|
|
36
36
|
const { z, a } = props;
|
|
37
37
|
return z + a;
|
|
38
38
|
}
|
|
39
39
|
`,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "sorted params in arrow component",
|
|
43
|
+
code: `
|
|
44
44
|
const MyComponent = ({ alpha, beta }) => <div>{alpha}{beta}</div>;
|
|
45
45
|
`,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "sorted this.props in class component",
|
|
49
|
+
code: `
|
|
50
50
|
class MyComponent extends React.Component {
|
|
51
51
|
render() {
|
|
52
52
|
const { alpha, beta } = this.props;
|
|
@@ -54,62 +54,62 @@ ruleTester.run("props-destructuring-sort", rule, {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
`,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "rest element last when already sorted",
|
|
60
|
+
code: `
|
|
61
61
|
function MyComponent(props) {
|
|
62
62
|
const { alpha, beta, ...rest } = props;
|
|
63
63
|
return <div>{alpha}</div>;
|
|
64
64
|
}
|
|
65
65
|
`,
|
|
66
|
-
|
|
66
|
+
},
|
|
67
67
|
],
|
|
68
68
|
|
|
69
69
|
invalid: [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
{
|
|
71
|
+
name: "const from props in functional component",
|
|
72
|
+
code: `
|
|
73
73
|
function MyComponent(props) {
|
|
74
74
|
const { zebra, alpha } = props;
|
|
75
75
|
return <div>{zebra}{alpha}</div>;
|
|
76
76
|
}
|
|
77
77
|
`,
|
|
78
|
-
|
|
78
|
+
output: `
|
|
79
79
|
function MyComponent(props) {
|
|
80
80
|
const { alpha, zebra } = props;
|
|
81
81
|
return <div>{zebra}{alpha}</div>;
|
|
82
82
|
}
|
|
83
83
|
`,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "destructured params in function declaration",
|
|
88
|
+
code: `
|
|
89
89
|
function MyComponent({ zebra, alpha }) {
|
|
90
90
|
return <div>{zebra}{alpha}</div>;
|
|
91
91
|
}
|
|
92
92
|
`,
|
|
93
|
-
|
|
93
|
+
output: `
|
|
94
94
|
function MyComponent({ alpha, zebra }) {
|
|
95
95
|
return <div>{zebra}{alpha}</div>;
|
|
96
96
|
}
|
|
97
97
|
`,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "destructured params in arrow component with JSX",
|
|
102
|
+
code: `
|
|
103
103
|
const MyComponent = ({ zebra, alpha }) => <div>{zebra}{alpha}</div>;
|
|
104
104
|
`,
|
|
105
|
-
|
|
105
|
+
output: `
|
|
106
106
|
const MyComponent = ({ alpha, zebra }) => <div>{zebra}{alpha}</div>;
|
|
107
107
|
`,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "this.props in class component",
|
|
112
|
+
code: `
|
|
113
113
|
class MyComponent extends Component {
|
|
114
114
|
render() {
|
|
115
115
|
const { zebra, alpha } = this.props;
|
|
@@ -117,7 +117,7 @@ ruleTester.run("props-destructuring-sort", rule, {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
`,
|
|
120
|
-
|
|
120
|
+
output: `
|
|
121
121
|
class MyComponent extends Component {
|
|
122
122
|
render() {
|
|
123
123
|
const { alpha, zebra } = this.props;
|
|
@@ -125,33 +125,35 @@ ruleTester.run("props-destructuring-sort", rule, {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
`,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "rest stays last after sort",
|
|
132
|
+
code: `
|
|
133
133
|
function MyComponent(props) {
|
|
134
134
|
const { zebra, alpha, ...rest } = props;
|
|
135
135
|
return <div>{zebra}</div>;
|
|
136
136
|
}
|
|
137
137
|
`,
|
|
138
|
-
|
|
138
|
+
output: `
|
|
139
139
|
function MyComponent(props) {
|
|
140
140
|
const { alpha, zebra, ...rest } = props;
|
|
141
141
|
return <div>{zebra}</div>;
|
|
142
142
|
}
|
|
143
143
|
`,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "memo-wrapped component params",
|
|
148
|
+
code: `
|
|
149
149
|
const MyComponent = memo(({ zebra, alpha }) => <div>{zebra}</div>);
|
|
150
150
|
`,
|
|
151
|
-
|
|
151
|
+
output: `
|
|
152
152
|
const MyComponent = memo(({ alpha, zebra }) => <div>{zebra}</div>);
|
|
153
153
|
`,
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
errors: [{ messageId: "incorrectOrder" }],
|
|
155
|
+
},
|
|
156
156
|
],
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
console.log("OK");
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const { RuleTester } = require("eslint");
|
|
2
|
+
const rule = require("../lib/rules/require-data-testid");
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 2022,
|
|
7
|
+
sourceType: "module",
|
|
8
|
+
ecmaFeatures: { jsx: true },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
ruleTester.run("require-data-testid", rule, {
|
|
13
|
+
valid: [
|
|
14
|
+
{
|
|
15
|
+
name: "function declaration with data-testid",
|
|
16
|
+
code: `
|
|
17
|
+
function MyComponent() {
|
|
18
|
+
return <div data-testid="my-component">Hello</div>;
|
|
19
|
+
}
|
|
20
|
+
`,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "arrow component with dataTestID",
|
|
24
|
+
code: `
|
|
25
|
+
const MyComponent = () => <section dataTestID="section">Hi</section>;
|
|
26
|
+
`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "memo-wrapped component with data-testid",
|
|
30
|
+
code: `
|
|
31
|
+
const MyComponent = memo(() => <div data-testid="memo">Memo</div>);
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "forwardRef component with data-testid",
|
|
36
|
+
code: `
|
|
37
|
+
const MyComponent = forwardRef((props, ref) => <div ref={ref} data-testid="ref">Ref</div>);
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "nested JSX inside Fragment",
|
|
42
|
+
code: `
|
|
43
|
+
const MyComponent = () => (
|
|
44
|
+
<>
|
|
45
|
+
<div data-testid="inside">Hello</div>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "non-PascalCase function is ignored",
|
|
52
|
+
code: `
|
|
53
|
+
function utilFunction() {
|
|
54
|
+
return <div>Hello</div>;
|
|
55
|
+
}
|
|
56
|
+
`,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
invalid: [
|
|
61
|
+
{
|
|
62
|
+
name: "function declaration without data-testid",
|
|
63
|
+
code: `
|
|
64
|
+
function MyComponent() {
|
|
65
|
+
return <div>Hello</div>;
|
|
66
|
+
}
|
|
67
|
+
`,
|
|
68
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "arrow component missing data-testid",
|
|
72
|
+
code: `
|
|
73
|
+
const MyComponent = () => <section>Hi</section>;
|
|
74
|
+
`,
|
|
75
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "memo-wrapped component missing data-testid",
|
|
79
|
+
code: `
|
|
80
|
+
const MyComponent = memo(() => <div>Memo</div>);
|
|
81
|
+
`,
|
|
82
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "forwardRef component missing data-testid",
|
|
86
|
+
code: `
|
|
87
|
+
const MyComponent = forwardRef((props, ref) => <div ref={ref}>Ref</div>);
|
|
88
|
+
`,
|
|
89
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "nested JSX in Fragment missing data-testid",
|
|
93
|
+
code: `
|
|
94
|
+
const MyComponent = () => (
|
|
95
|
+
<>
|
|
96
|
+
<div>Hello</div>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
`,
|
|
100
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "HOC wrapped component without data-testid",
|
|
104
|
+
code: `
|
|
105
|
+
const MyComponent = observer(() => <div>Hello</div>);
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: "missingDataTestId" }],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log("OK");
|