@nkzw/eslint-plugin-fbtee 0.0.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/LICENSE +21 -0
- package/README.md +223 -0
- package/docs/rules/no-empty-strings.md +29 -0
- package/docs/rules/no-unhelpful-desc.md +41 -0
- package/lib/index.cjs +313 -0
- package/lib/index.d.cts +20 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +284 -0
- package/package.json +26 -0
- package/src/__tests__/no-empty-strings-test.tsx +210 -0
- package/src/__tests__/no-unhelpful-desc-test.tsx +310 -0
- package/src/index.tsx +22 -0
- package/src/rules/no-empty-strings.tsx +106 -0
- package/src/rules/no-unhelpful-desc.tsx +122 -0
- package/src/utils.tsx +109 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import rule from '../rules/no-unhelpful-desc.tsx';
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
ruleTester.run('unhelpful-desc', rule, {
|
|
15
|
+
invalid: [
|
|
16
|
+
{
|
|
17
|
+
// <fbt>
|
|
18
|
+
code: `
|
|
19
|
+
<fbt desc="">Hello</fbt>;
|
|
20
|
+
`,
|
|
21
|
+
errors: [
|
|
22
|
+
{
|
|
23
|
+
line: 2,
|
|
24
|
+
messageId: 'jsxEmptyDesc',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: `
|
|
30
|
+
<fbt desc={''}>Hello</fbt>;
|
|
31
|
+
`,
|
|
32
|
+
errors: [
|
|
33
|
+
{
|
|
34
|
+
line: 2,
|
|
35
|
+
messageId: 'jsxEmptyDesc',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
code: `
|
|
41
|
+
<fbt desc="Greeting">Greeting</fbt>;
|
|
42
|
+
`,
|
|
43
|
+
errors: [
|
|
44
|
+
{
|
|
45
|
+
line: 2,
|
|
46
|
+
messageId: 'duplicateDesc',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: `
|
|
52
|
+
<fbt desc={'Greeting'}>Greeting</fbt>;
|
|
53
|
+
`,
|
|
54
|
+
errors: [
|
|
55
|
+
{
|
|
56
|
+
line: 2,
|
|
57
|
+
messageId: 'duplicateDesc',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
code: `
|
|
63
|
+
<fbt desc={'Greeting' + ''}>Greeting</fbt>;
|
|
64
|
+
`,
|
|
65
|
+
errors: [
|
|
66
|
+
{
|
|
67
|
+
line: 2,
|
|
68
|
+
messageId: 'duplicateDesc',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
code: `
|
|
74
|
+
<fbt desc="greeting">Greeting</fbt>;
|
|
75
|
+
`,
|
|
76
|
+
errors: [
|
|
77
|
+
{
|
|
78
|
+
line: 2,
|
|
79
|
+
messageId: 'duplicateDesc',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: `
|
|
85
|
+
<fbt desc="Hey">Hello world</fbt>;
|
|
86
|
+
`,
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
line: 2,
|
|
90
|
+
messageId: 'shortDesc',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
code: `
|
|
96
|
+
<fbt desc={'Hey'}>Hello world</fbt>;
|
|
97
|
+
`,
|
|
98
|
+
errors: [
|
|
99
|
+
{
|
|
100
|
+
line: 2,
|
|
101
|
+
messageId: 'shortDesc',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
code: `
|
|
107
|
+
<fbt desc={'Hey' + ''}>Hello world</fbt>;
|
|
108
|
+
`,
|
|
109
|
+
errors: [
|
|
110
|
+
{
|
|
111
|
+
line: 2,
|
|
112
|
+
messageId: 'shortDesc',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// fbt()
|
|
118
|
+
{
|
|
119
|
+
code: `
|
|
120
|
+
fbt('Hello world', '');
|
|
121
|
+
`,
|
|
122
|
+
errors: [
|
|
123
|
+
{
|
|
124
|
+
line: 2,
|
|
125
|
+
messageId: 'emptyDesc',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
code: `
|
|
131
|
+
fbt('Hello world', \`\`);
|
|
132
|
+
`,
|
|
133
|
+
errors: [
|
|
134
|
+
{
|
|
135
|
+
line: 2,
|
|
136
|
+
messageId: 'emptyDesc',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
code: `
|
|
142
|
+
fbt('Hello world', '' + '');
|
|
143
|
+
`,
|
|
144
|
+
errors: [
|
|
145
|
+
{
|
|
146
|
+
line: 2,
|
|
147
|
+
messageId: 'emptyDesc',
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
code: `
|
|
153
|
+
fbt('Hello world', 'Hey');
|
|
154
|
+
`,
|
|
155
|
+
errors: [
|
|
156
|
+
{
|
|
157
|
+
line: 2,
|
|
158
|
+
messageId: 'shortDesc',
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
code: `
|
|
164
|
+
fbt('Greeting', 'Greeting');
|
|
165
|
+
`,
|
|
166
|
+
errors: [
|
|
167
|
+
{
|
|
168
|
+
line: 2,
|
|
169
|
+
messageId: 'duplicateDesc',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
code: `
|
|
175
|
+
fbt('Greeting', 'greeting');
|
|
176
|
+
`,
|
|
177
|
+
errors: [
|
|
178
|
+
{
|
|
179
|
+
line: 2,
|
|
180
|
+
messageId: 'duplicateDesc',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// fbs()
|
|
186
|
+
{
|
|
187
|
+
code: `
|
|
188
|
+
fbs('Hello world', '');
|
|
189
|
+
`,
|
|
190
|
+
errors: [
|
|
191
|
+
{
|
|
192
|
+
line: 2,
|
|
193
|
+
messageId: 'emptyDesc',
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
code: `
|
|
199
|
+
fbs('Hello world', 'a');
|
|
200
|
+
`,
|
|
201
|
+
errors: [
|
|
202
|
+
{
|
|
203
|
+
line: 2,
|
|
204
|
+
messageId: 'shortDesc',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
code: `
|
|
210
|
+
fbs('Hello world', 'Hello world');
|
|
211
|
+
`,
|
|
212
|
+
errors: [
|
|
213
|
+
{
|
|
214
|
+
line: 2,
|
|
215
|
+
messageId: 'duplicateDesc',
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
code: `
|
|
221
|
+
<input
|
|
222
|
+
placeholder={fbs('Hello world', '')}
|
|
223
|
+
/>;
|
|
224
|
+
`,
|
|
225
|
+
errors: [
|
|
226
|
+
{
|
|
227
|
+
line: 3,
|
|
228
|
+
messageId: 'emptyDesc',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
valid: [
|
|
234
|
+
// <fbt>
|
|
235
|
+
{
|
|
236
|
+
code: `
|
|
237
|
+
<fbt desc="Greeting">Hello world</fbt>;
|
|
238
|
+
`,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
code: `
|
|
242
|
+
<fbt desc={'Greeting'}>Hello</fbt>;
|
|
243
|
+
`,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
code: `
|
|
247
|
+
<fbt desc={\`Greeting\`}>Hello</fbt>;
|
|
248
|
+
`,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
code: `
|
|
252
|
+
<fbt
|
|
253
|
+
desc={
|
|
254
|
+
'A very long multiline description that ' +
|
|
255
|
+
'is spanning multiple lines.'
|
|
256
|
+
}
|
|
257
|
+
>
|
|
258
|
+
Hello
|
|
259
|
+
</fbt>;
|
|
260
|
+
`,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
code: `
|
|
264
|
+
const description = 'Greeting';
|
|
265
|
+
<fbt desc={description}>Hello</fbt>;
|
|
266
|
+
`,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
code: `
|
|
270
|
+
function getDescription() {
|
|
271
|
+
return 'Greeting';
|
|
272
|
+
}
|
|
273
|
+
<fbt desc={getDescription()}>Hello</fbt>;
|
|
274
|
+
`,
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// fbt()
|
|
278
|
+
{
|
|
279
|
+
code: `
|
|
280
|
+
fbt('Hello world', 'Greeting');
|
|
281
|
+
`,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
code: `
|
|
285
|
+
fbt('Hello world', \`Greeting\`);
|
|
286
|
+
`,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
code: `
|
|
290
|
+
fbt(
|
|
291
|
+
'Hello world',
|
|
292
|
+
'A very long multiline description that ' +
|
|
293
|
+
'is spanning multiple lines.'
|
|
294
|
+
);
|
|
295
|
+
`,
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// fbs()
|
|
299
|
+
{
|
|
300
|
+
code: `
|
|
301
|
+
fbs('Hello world', 'Greeting');
|
|
302
|
+
`,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
code: `
|
|
306
|
+
<input placeholder={fbs('Hello world', 'Greeting')} />;
|
|
307
|
+
`,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
});
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
2
|
+
import noEmptyStringsRule from './rules/no-empty-strings.tsx';
|
|
3
|
+
import noUnhelpfulDesc from './rules/no-unhelpful-desc.tsx';
|
|
4
|
+
|
|
5
|
+
export const meta = {
|
|
6
|
+
name: packageJson.name,
|
|
7
|
+
version: packageJson.version,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const rules = {
|
|
11
|
+
'no-empty-strings': noEmptyStringsRule,
|
|
12
|
+
'no-unhelpful-desc': noUnhelpfulDesc,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const configs = {
|
|
16
|
+
strict: {
|
|
17
|
+
rules: {
|
|
18
|
+
'@nkzw/fbtee/no-empty-strings': 2,
|
|
19
|
+
'@nkzw/fbtee/no-unhelpful-desc': 2,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { createRule, elementType, resolveNodeValue } from '../utils.tsx';
|
|
3
|
+
|
|
4
|
+
export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
5
|
+
create(context) {
|
|
6
|
+
return {
|
|
7
|
+
CallExpression(node) {
|
|
8
|
+
if (
|
|
9
|
+
node.callee.type === 'Identifier' &&
|
|
10
|
+
!(node.callee.name === 'fbt' || node.callee.name === 'fbs')
|
|
11
|
+
) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [text] = node.arguments;
|
|
16
|
+
|
|
17
|
+
if (text && text.type !== 'SpreadElement' && isEmptyString(text)) {
|
|
18
|
+
context.report({
|
|
19
|
+
messageId: 'emptyString',
|
|
20
|
+
node: text,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
JSXElement(node) {
|
|
26
|
+
if (elementType(node) !== 'fbt') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Collect nodes to report and validate all children recursively.
|
|
31
|
+
const nodesToReport = new Set<TSESTree.Node>();
|
|
32
|
+
const hasTextContent = validateChildren(node, nodesToReport);
|
|
33
|
+
|
|
34
|
+
if (!hasTextContent) {
|
|
35
|
+
for (const node of nodesToReport) {
|
|
36
|
+
context.report({
|
|
37
|
+
messageId: 'jsxEmptyString',
|
|
38
|
+
node,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: [],
|
|
46
|
+
meta: {
|
|
47
|
+
docs: {
|
|
48
|
+
description: 'Disallow empty strings in fbt elements or function calls.',
|
|
49
|
+
},
|
|
50
|
+
messages: {
|
|
51
|
+
emptyString:
|
|
52
|
+
'Empty strings are not allowed in fbt() or fbs() function arguments.',
|
|
53
|
+
jsxEmptyString:
|
|
54
|
+
'Empty strings are not allowed as children of <fbt> tags.',
|
|
55
|
+
},
|
|
56
|
+
schema: [],
|
|
57
|
+
type: 'problem',
|
|
58
|
+
},
|
|
59
|
+
name: 'no-empty-strings',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function validateChildren(
|
|
63
|
+
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
64
|
+
nodesToReport: Set<TSESTree.Node>,
|
|
65
|
+
): boolean {
|
|
66
|
+
let hasTextContent = false;
|
|
67
|
+
|
|
68
|
+
for (const child of node.children) {
|
|
69
|
+
if (child.type === 'JSXText' && node.children.length === 1) {
|
|
70
|
+
if (child.value.trim() === '') {
|
|
71
|
+
nodesToReport.add(child);
|
|
72
|
+
} else {
|
|
73
|
+
hasTextContent = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
child.type === 'JSXExpressionContainer' &&
|
|
79
|
+
child.expression.type !== 'JSXEmptyExpression'
|
|
80
|
+
) {
|
|
81
|
+
if (isEmptyString(child.expression)) {
|
|
82
|
+
nodesToReport.add(child.expression);
|
|
83
|
+
} else {
|
|
84
|
+
hasTextContent = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (child.type === 'JSXElement' || child.type === 'JSXFragment') {
|
|
89
|
+
hasTextContent ||= validateChildren(child, nodesToReport);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (hasTextContent) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!hasTextContent && nodesToReport.size === 0) {
|
|
98
|
+
nodesToReport.add(node);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return hasTextContent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isEmptyString(node: TSESTree.Expression | TSESTree.Literal): boolean {
|
|
105
|
+
return resolveNodeValue(node)?.trim() === '';
|
|
106
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRule,
|
|
3
|
+
elementType,
|
|
4
|
+
resolveJsxElementTextContent,
|
|
5
|
+
resolveNodeValue,
|
|
6
|
+
} from '../utils.tsx';
|
|
7
|
+
|
|
8
|
+
export default createRule<
|
|
9
|
+
[],
|
|
10
|
+
'emptyDesc' | 'jsxEmptyDesc' | 'duplicateDesc' | 'shortDesc'
|
|
11
|
+
>({
|
|
12
|
+
create(context) {
|
|
13
|
+
return {
|
|
14
|
+
CallExpression(node) {
|
|
15
|
+
if (
|
|
16
|
+
node.callee.type !== 'Identifier' ||
|
|
17
|
+
!(node.callee.name === 'fbt' || node.callee.name === 'fbs')
|
|
18
|
+
) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [textArg, descArg] = node.arguments;
|
|
23
|
+
|
|
24
|
+
const desc =
|
|
25
|
+
descArg && descArg.type !== 'SpreadElement'
|
|
26
|
+
? resolveNodeValue(descArg)?.trim()
|
|
27
|
+
: null;
|
|
28
|
+
|
|
29
|
+
if (!desc) {
|
|
30
|
+
context.report({
|
|
31
|
+
messageId: 'emptyDesc',
|
|
32
|
+
node: descArg || node,
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (desc.length <= 4) {
|
|
38
|
+
context.report({
|
|
39
|
+
messageId: 'shortDesc',
|
|
40
|
+
node: descArg || node,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const text =
|
|
46
|
+
textArg && textArg.type !== 'SpreadElement'
|
|
47
|
+
? resolveNodeValue(textArg)?.trim()
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
if (desc.toLowerCase() === text?.toLowerCase()) {
|
|
51
|
+
context.report({
|
|
52
|
+
messageId: 'duplicateDesc',
|
|
53
|
+
node: descArg || node,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
JSXAttribute(node) {
|
|
59
|
+
if (elementType(node.parent.parent) !== 'fbt') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (node.name.name !== 'desc' || !node.value) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
node.value.type === 'JSXExpressionContainer' &&
|
|
69
|
+
(node.value.expression.type === 'Identifier' ||
|
|
70
|
+
node.value.expression.type === 'CallExpression')
|
|
71
|
+
) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const desc = resolveNodeValue(node.value)?.trim();
|
|
76
|
+
|
|
77
|
+
if (!desc) {
|
|
78
|
+
context.report({
|
|
79
|
+
messageId: 'jsxEmptyDesc',
|
|
80
|
+
node: node.value,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (desc.length <= 4) {
|
|
86
|
+
context.report({
|
|
87
|
+
messageId: 'shortDesc',
|
|
88
|
+
node: node.value,
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const textContent = resolveJsxElementTextContent(node.parent.parent);
|
|
94
|
+
|
|
95
|
+
if (desc.toLowerCase() === textContent.toLowerCase()) {
|
|
96
|
+
context.report({
|
|
97
|
+
messageId: 'duplicateDesc',
|
|
98
|
+
node: node.value,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
defaultOptions: [],
|
|
105
|
+
meta: {
|
|
106
|
+
docs: {
|
|
107
|
+
description:
|
|
108
|
+
'Enforce meaningful and non-empty descriptions in <fbt> elements and fbt()/fbs() function calls.',
|
|
109
|
+
},
|
|
110
|
+
messages: {
|
|
111
|
+
duplicateDesc:
|
|
112
|
+
'The "desc" attribute or argument should not be a duplicate of the text content.',
|
|
113
|
+
emptyDesc: 'The "desc" argument in fbt() and fbs() must not be empty.',
|
|
114
|
+
jsxEmptyDesc: 'The "desc" attribute in <fbt> must not be empty.',
|
|
115
|
+
shortDesc:
|
|
116
|
+
'The "desc" attribute or argument is too short. Use a more descriptive and meaningful explanation.',
|
|
117
|
+
},
|
|
118
|
+
schema: [],
|
|
119
|
+
type: 'problem',
|
|
120
|
+
},
|
|
121
|
+
name: 'no-unhelpful-desc',
|
|
122
|
+
});
|
package/src/utils.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
export const createRule = ESLintUtils.RuleCreator(
|
|
5
|
+
(name) =>
|
|
6
|
+
`https://github.com/nkzw-tech/fbtee/tree/main/packages/eslint-plugin-fbt/docs/rules/${name}`,
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
function resolveMemberExpressions(
|
|
10
|
+
object: TSESTree.JSXTagNameExpression,
|
|
11
|
+
property: TSESTree.JSXIdentifier,
|
|
12
|
+
): string {
|
|
13
|
+
if (object.type === 'JSXMemberExpression') {
|
|
14
|
+
return `${resolveMemberExpressions(object.object, object.property)}.${property.name}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return `${object.name}.${property.name}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function elementType(
|
|
21
|
+
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
22
|
+
): string {
|
|
23
|
+
if (node.type === 'JSXFragment') {
|
|
24
|
+
return '<>';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { name } = node.openingElement;
|
|
28
|
+
|
|
29
|
+
switch (name.type) {
|
|
30
|
+
case 'JSXIdentifier': {
|
|
31
|
+
return name.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
case 'JSXMemberExpression': {
|
|
35
|
+
const { object, property } = name;
|
|
36
|
+
return resolveMemberExpressions(object, property);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'JSXNamespacedName': {
|
|
40
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
default: {
|
|
44
|
+
throw new Error('Unsupported JSX node type.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveJsxElementTextContent(
|
|
50
|
+
node: TSESTree.JSXElement,
|
|
51
|
+
): string {
|
|
52
|
+
return node.children
|
|
53
|
+
.map((child) => {
|
|
54
|
+
if (child.type === 'JSXText') {
|
|
55
|
+
return child.value.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
59
|
+
return resolveNodeValue(child.expression) || '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return '';
|
|
63
|
+
})
|
|
64
|
+
.join('')
|
|
65
|
+
.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveBinaryExpression(
|
|
69
|
+
node: TSESTree.BinaryExpression,
|
|
70
|
+
): string | null {
|
|
71
|
+
if (node.operator !== '+') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const leftValue = resolveNodeValue(node.left);
|
|
76
|
+
const rightValue = resolveNodeValue(node.right);
|
|
77
|
+
|
|
78
|
+
if (leftValue !== null && rightValue !== null) {
|
|
79
|
+
return leftValue + rightValue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resolveNodeValue(
|
|
86
|
+
node:
|
|
87
|
+
| TSESTree.Expression
|
|
88
|
+
| TSESTree.JSXExpression
|
|
89
|
+
| TSESTree.JSXEmptyExpression
|
|
90
|
+
| TSESTree.PrivateIdentifier,
|
|
91
|
+
): string | null {
|
|
92
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
93
|
+
return node.value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
97
|
+
return node.quasis.map((quasi) => quasi.value.raw).join('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (node.type === 'BinaryExpression') {
|
|
101
|
+
return resolveBinaryExpression(node);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (node.type === 'JSXExpressionContainer') {
|
|
105
|
+
return resolveNodeValue(node.expression);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|