@nkzw/eslint-plugin-fbtee 0.1.2 → 0.1.4
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 +13 -3
- package/docs/rules/no-unhelpful-desc.md +1 -1
- package/docs/rules/no-unwrapped-strings.md +55 -0
- package/lib/index.cjs +184 -34
- package/lib/index.d.cts +10 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +184 -34
- package/package.json +1 -1
- package/src/__tests__/no-empty-strings-test.tsx +144 -13
- package/src/__tests__/no-unhelpful-desc-test.tsx +20 -0
- package/src/__tests__/no-untranslated-strings-test.tsx +223 -0
- package/src/index.tsx +9 -0
- package/src/rules/no-empty-strings.tsx +44 -41
- package/src/rules/no-unhelpful-desc.tsx +3 -6
- package/src/rules/no-untranslated-strings.tsx +141 -0
- package/src/utils.tsx +50 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import rule from '../rules/no-untranslated-strings.tsx';
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
ruleTester.run('no-untranslated-strings', rule, {
|
|
15
|
+
invalid: [
|
|
16
|
+
{
|
|
17
|
+
code: `
|
|
18
|
+
<span>Hello world</span>;
|
|
19
|
+
`,
|
|
20
|
+
errors: [
|
|
21
|
+
{
|
|
22
|
+
messageId: 'unwrappedString',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: `
|
|
28
|
+
<span>{'Hello world'}</span>;
|
|
29
|
+
`,
|
|
30
|
+
errors: [
|
|
31
|
+
{
|
|
32
|
+
messageId: 'unwrappedString',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: `
|
|
38
|
+
<span>{'Hello world' + ''}</span>;
|
|
39
|
+
`,
|
|
40
|
+
errors: [
|
|
41
|
+
{
|
|
42
|
+
messageId: 'unwrappedString',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: `
|
|
48
|
+
<div>
|
|
49
|
+
<span>Hello world</span>
|
|
50
|
+
</div>;
|
|
51
|
+
`,
|
|
52
|
+
errors: [
|
|
53
|
+
{
|
|
54
|
+
line: 3,
|
|
55
|
+
messageId: 'unwrappedString',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: `
|
|
61
|
+
<div>
|
|
62
|
+
<div></div>
|
|
63
|
+
Hello world
|
|
64
|
+
</div>;
|
|
65
|
+
`,
|
|
66
|
+
errors: [
|
|
67
|
+
{
|
|
68
|
+
line: 4,
|
|
69
|
+
messageId: 'unwrappedString',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: `
|
|
75
|
+
<>Hello world</>
|
|
76
|
+
`,
|
|
77
|
+
errors: [
|
|
78
|
+
{
|
|
79
|
+
messageId: 'unwrappedString',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: `
|
|
85
|
+
<input placeholder="Enter name..." />;
|
|
86
|
+
`,
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
messageId: 'unwrappedString',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
code: `
|
|
95
|
+
<div aria-label={'Label'}></div>
|
|
96
|
+
`,
|
|
97
|
+
errors: [
|
|
98
|
+
{
|
|
99
|
+
messageId: 'unwrappedString',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
code: `
|
|
105
|
+
<div aria-label={\`Label\`}></div>
|
|
106
|
+
`,
|
|
107
|
+
errors: [
|
|
108
|
+
{
|
|
109
|
+
messageId: 'unwrappedString',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
|
|
115
|
+
valid: [
|
|
116
|
+
{
|
|
117
|
+
code: `
|
|
118
|
+
<fbt desc="Greeting">Hello world</fbt>
|
|
119
|
+
`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
code: `
|
|
123
|
+
<fbt desc="Greeting">
|
|
124
|
+
Hello world
|
|
125
|
+
</fbt>
|
|
126
|
+
`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
code: `
|
|
130
|
+
<fbt desc="Greeting">
|
|
131
|
+
<span>Hello world</span>
|
|
132
|
+
</fbt>
|
|
133
|
+
`,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
code: `
|
|
137
|
+
<span>
|
|
138
|
+
{fbt('Hello', 'Greeting')}
|
|
139
|
+
</span>
|
|
140
|
+
`,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
code: `
|
|
144
|
+
<fbt desc="Counter">
|
|
145
|
+
Count:
|
|
146
|
+
<fbt:plural count={1} showCount="yes" many="items">
|
|
147
|
+
item
|
|
148
|
+
</fbt:plural>
|
|
149
|
+
</fbt>
|
|
150
|
+
`,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
code: `
|
|
154
|
+
<fbt desc="Greeting">
|
|
155
|
+
<>Hello</>
|
|
156
|
+
</fbt>
|
|
157
|
+
`,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
code: `
|
|
161
|
+
<fbt desc="Greeting">
|
|
162
|
+
Hello <span>world</span>
|
|
163
|
+
</fbt>;
|
|
164
|
+
`,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
code: `
|
|
168
|
+
<fbt desc="Welcome message">
|
|
169
|
+
Welcome <fbt:param name="name">Steve</fbt:param>
|
|
170
|
+
</fbt>
|
|
171
|
+
`,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
code: `
|
|
175
|
+
<div>
|
|
176
|
+
<p>GitHub</p>
|
|
177
|
+
<p>{'GitHub'}</p>
|
|
178
|
+
<p>{\`GitHub\`}</p>
|
|
179
|
+
</div>
|
|
180
|
+
`,
|
|
181
|
+
options: [
|
|
182
|
+
{
|
|
183
|
+
ignoredWords: ['GitHub'],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
code: `
|
|
189
|
+
<textarea rows={2}></textarea>
|
|
190
|
+
`,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
code: `
|
|
194
|
+
<a aria-current></a>
|
|
195
|
+
`,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
code: `
|
|
199
|
+
<img src="" alt={fbs('A photo of a dog', 'Image description')} />
|
|
200
|
+
`,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
code: `
|
|
204
|
+
<div aria-label={fbs('Hello', 'Greeting')}></div>
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
code: `
|
|
209
|
+
<div aria-label={fbt('Hello', 'Greeting').toString()}></div>
|
|
210
|
+
`,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
code: `
|
|
214
|
+
foo('Bar')
|
|
215
|
+
`,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
code: `
|
|
219
|
+
foo.bar('Baz')
|
|
220
|
+
`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import packageJson from '../package.json' with { type: 'json' };
|
|
2
2
|
import noEmptyStringsRule from './rules/no-empty-strings.tsx';
|
|
3
3
|
import noUnhelpfulDesc from './rules/no-unhelpful-desc.tsx';
|
|
4
|
+
import noUntranslatedStrings from './rules/no-untranslated-strings.tsx';
|
|
4
5
|
|
|
5
6
|
export const meta = {
|
|
6
7
|
name: packageJson.name,
|
|
@@ -10,13 +11,21 @@ export const meta = {
|
|
|
10
11
|
export const rules = {
|
|
11
12
|
'no-empty-strings': noEmptyStringsRule,
|
|
12
13
|
'no-unhelpful-desc': noUnhelpfulDesc,
|
|
14
|
+
'no-untranslated-strings': noUntranslatedStrings,
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export const configs = {
|
|
18
|
+
recommended: {
|
|
19
|
+
rules: {
|
|
20
|
+
'@nkzw/fbtee/no-empty-strings': 2,
|
|
21
|
+
'@nkzw/fbtee/no-unhelpful-desc': 2,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
16
24
|
strict: {
|
|
17
25
|
rules: {
|
|
18
26
|
'@nkzw/fbtee/no-empty-strings': 2,
|
|
19
27
|
'@nkzw/fbtee/no-unhelpful-desc': 2,
|
|
28
|
+
'@nkzw/fbtee/no-untranslated-strings': 2,
|
|
20
29
|
},
|
|
21
30
|
},
|
|
22
31
|
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createRule,
|
|
4
|
+
elementType,
|
|
5
|
+
isFbtNode,
|
|
6
|
+
resolveNodeValue,
|
|
7
|
+
} from '../utils.tsx';
|
|
3
8
|
|
|
4
9
|
export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
5
10
|
create(context) {
|
|
6
11
|
return {
|
|
7
12
|
CallExpression(node) {
|
|
8
|
-
if (
|
|
9
|
-
node.callee.type !== 'Identifier' ||
|
|
10
|
-
!(node.callee.name === 'fbt' || node.callee.name === 'fbs')
|
|
11
|
-
) {
|
|
13
|
+
if (!isFbtNode(node)) {
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -23,21 +25,22 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
23
25
|
},
|
|
24
26
|
|
|
25
27
|
JSXElement(node) {
|
|
26
|
-
if (
|
|
28
|
+
if (!isFbtNode(node)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node.openingElement.name.type === 'JSXNamespacedName') {
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
// Collect nodes to report and validate all children recursively.
|
|
31
|
-
const
|
|
32
|
-
const hasTextContent = validateChildren(node, nodesToReport);
|
|
37
|
+
const hasTextContent = validateChildren(node);
|
|
33
38
|
|
|
34
39
|
if (!hasTextContent) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
}
|
|
40
|
+
context.report({
|
|
41
|
+
messageId: 'jsxEmptyString',
|
|
42
|
+
node,
|
|
43
|
+
});
|
|
41
44
|
}
|
|
42
45
|
},
|
|
43
46
|
};
|
|
@@ -49,9 +52,9 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
49
52
|
},
|
|
50
53
|
messages: {
|
|
51
54
|
emptyString:
|
|
52
|
-
'Empty strings are not allowed in fbt()
|
|
55
|
+
'Empty strings are not allowed in fbt() and fbs() function arguments.',
|
|
53
56
|
jsxEmptyString:
|
|
54
|
-
'Empty strings are not allowed as children of
|
|
57
|
+
'Empty strings are not allowed as children of `<fbt>` and `<fbs>` tags.',
|
|
55
58
|
},
|
|
56
59
|
schema: [],
|
|
57
60
|
type: 'problem',
|
|
@@ -61,40 +64,44 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
61
64
|
|
|
62
65
|
function validateChildren(
|
|
63
66
|
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
64
|
-
nodesToReport: Set<TSESTree.Node>,
|
|
65
67
|
): boolean {
|
|
66
68
|
let hasTextContent = false;
|
|
67
69
|
|
|
68
70
|
for (const child of node.children) {
|
|
69
|
-
if (child.type === 'JSXText' &&
|
|
70
|
-
|
|
71
|
-
nodesToReport.add(child);
|
|
72
|
-
} else {
|
|
73
|
-
hasTextContent = true;
|
|
74
|
-
}
|
|
71
|
+
if (child.type === 'JSXText' && child.value.trim() !== '') {
|
|
72
|
+
hasTextContent = true;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
if (
|
|
78
|
-
child.type === '
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
76
|
+
if (child.expression.type === 'JSXEmptyExpression') {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Ignore when a variable is used <fbt desc="Greeting">{dynamicValue}</fbt>
|
|
81
81
|
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
node.children.every(
|
|
85
|
-
(otherChild) =>
|
|
86
|
-
child === otherChild ||
|
|
87
|
-
(otherChild.type === 'JSXText' && otherChild.value.trim() === ''),
|
|
88
|
-
))
|
|
82
|
+
child.expression.type === 'Identifier' &&
|
|
83
|
+
child.expression.name !== 'undefined'
|
|
89
84
|
) {
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
hasTextContent = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const value = resolveNodeValue(child.expression)?.trim();
|
|
90
|
+
|
|
91
|
+
if (value) {
|
|
92
92
|
hasTextContent = true;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (child.type === 'JSXElement' || child.type === 'JSXFragment') {
|
|
97
|
-
|
|
97
|
+
const childName = elementType(child);
|
|
98
|
+
|
|
99
|
+
if (/^[A-Z]/.test(childName) || childName.startsWith('fbt:')) {
|
|
100
|
+
hasTextContent = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
hasTextContent ||= validateChildren(child);
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
if (hasTextContent) {
|
|
@@ -102,10 +109,6 @@ function validateChildren(
|
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
if (!hasTextContent && nodesToReport.size === 0) {
|
|
106
|
-
nodesToReport.add(node);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
112
|
return hasTextContent;
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRule,
|
|
3
|
-
|
|
3
|
+
isFbtNode,
|
|
4
4
|
resolveJsxElementTextContent,
|
|
5
5
|
resolveNodeValue,
|
|
6
6
|
} from '../utils.tsx';
|
|
@@ -12,10 +12,7 @@ export default createRule<
|
|
|
12
12
|
create(context) {
|
|
13
13
|
return {
|
|
14
14
|
CallExpression(node) {
|
|
15
|
-
if (
|
|
16
|
-
node.callee.type !== 'Identifier' ||
|
|
17
|
-
!(node.callee.name === 'fbt' || node.callee.name === 'fbs')
|
|
18
|
-
) {
|
|
15
|
+
if (!isFbtNode(node)) {
|
|
19
16
|
return;
|
|
20
17
|
}
|
|
21
18
|
|
|
@@ -56,7 +53,7 @@ export default createRule<
|
|
|
56
53
|
},
|
|
57
54
|
|
|
58
55
|
JSXAttribute(node) {
|
|
59
|
-
if (
|
|
56
|
+
if (!isFbtNode(node.parent.parent)) {
|
|
60
57
|
return;
|
|
61
58
|
}
|
|
62
59
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRule,
|
|
3
|
+
getPropName,
|
|
4
|
+
hasFbtParent,
|
|
5
|
+
isFbtNode,
|
|
6
|
+
resolveNodeValue,
|
|
7
|
+
} from '../utils.tsx';
|
|
8
|
+
|
|
9
|
+
type Options = [{ ignoredWords: Array<string> }];
|
|
10
|
+
|
|
11
|
+
const attributes = new Set([
|
|
12
|
+
'title',
|
|
13
|
+
'placeholder',
|
|
14
|
+
'alt',
|
|
15
|
+
'label',
|
|
16
|
+
'aria-label',
|
|
17
|
+
'aria-errormessage',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export default createRule<Options, 'unwrappedString'>({
|
|
21
|
+
create(context, options) {
|
|
22
|
+
const ignoredWords = new Set(
|
|
23
|
+
options[0].ignoredWords.map((s) => s.trim().toLowerCase()),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function isIgnoredWord(value: string) {
|
|
27
|
+
return ignoredWords.has(value.trim().toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
JSXAttribute(node) {
|
|
32
|
+
const propName = getPropName(node);
|
|
33
|
+
|
|
34
|
+
if (!attributes.has(propName)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const value = node.value ? resolveNodeValue(node.value) : null;
|
|
39
|
+
|
|
40
|
+
if (!value) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isIgnoredWord(value)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
node.value?.type === 'JSXExpressionContainer' &&
|
|
50
|
+
isFbtNode(node.value.expression)
|
|
51
|
+
) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
context.report({
|
|
56
|
+
messageId: 'unwrappedString',
|
|
57
|
+
node,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
JSXExpressionContainer(node) {
|
|
62
|
+
if (node.parent.type === 'JSXAttribute') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const value = resolveNodeValue(node.expression);
|
|
67
|
+
|
|
68
|
+
if (!value) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isIgnoredWord(value)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!hasFbtParent(node)) {
|
|
77
|
+
context.report({
|
|
78
|
+
messageId: 'unwrappedString',
|
|
79
|
+
node,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
JSXText(node) {
|
|
85
|
+
const value = node.value.trim();
|
|
86
|
+
|
|
87
|
+
if (!value) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isIgnoredWord(value)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasFbtParent(node)) {
|
|
96
|
+
const startOffset = node.range[0] + node.value.indexOf(value);
|
|
97
|
+
const endOffset = startOffset + value.length;
|
|
98
|
+
|
|
99
|
+
context.report({
|
|
100
|
+
loc: {
|
|
101
|
+
end: context.sourceCode.getLocFromIndex(endOffset),
|
|
102
|
+
start: context.sourceCode.getLocFromIndex(startOffset),
|
|
103
|
+
},
|
|
104
|
+
messageId: 'unwrappedString',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
defaultOptions: [{ ignoredWords: [] }],
|
|
112
|
+
|
|
113
|
+
meta: {
|
|
114
|
+
docs: {
|
|
115
|
+
description:
|
|
116
|
+
'Enforce text content to be wrapped with `<fbt>`, `fbt()` or `fbs()`',
|
|
117
|
+
},
|
|
118
|
+
messages: {
|
|
119
|
+
unwrappedString:
|
|
120
|
+
'Text content must be wrapped in `<fbt>`, `fbt()` or `fbs()`',
|
|
121
|
+
},
|
|
122
|
+
schema: [
|
|
123
|
+
{
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
properties: {
|
|
126
|
+
ignoredWords: {
|
|
127
|
+
items: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
},
|
|
130
|
+
type: 'array',
|
|
131
|
+
uniqueItems: true,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
type: 'object',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
type: 'problem',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
name: 'no-untranslated-strings',
|
|
141
|
+
});
|
package/src/utils.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import type { TSESTree } from '@typescript-eslint/utils';
|
|
|
3
3
|
|
|
4
4
|
export const createRule = ESLintUtils.RuleCreator(
|
|
5
5
|
(name) =>
|
|
6
|
-
`https://github.com/nkzw-tech/fbtee/
|
|
6
|
+
`https://github.com/nkzw-tech/fbtee/blob/main/packages/eslint-plugin-fbtee/docs/rules/${name}.md`,
|
|
7
7
|
);
|
|
8
8
|
|
|
9
9
|
function resolveMemberExpressions(
|
|
@@ -47,7 +47,7 @@ export function elementType(
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function resolveJsxElementTextContent(
|
|
50
|
-
node: TSESTree.JSXElement,
|
|
50
|
+
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
51
51
|
): string {
|
|
52
52
|
return node.children
|
|
53
53
|
.map((child) => {
|
|
@@ -107,3 +107,51 @@ export function resolveNodeValue(
|
|
|
107
107
|
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
export function getPropName(prop: TSESTree.JSXAttribute) {
|
|
112
|
+
if (prop.name.type === 'JSXNamespacedName') {
|
|
113
|
+
return `${prop.name.namespace.name}:${prop.name.name.name}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return prop.name.name;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function hasFbtParent(node: TSESTree.Node) {
|
|
120
|
+
let current = node.parent;
|
|
121
|
+
|
|
122
|
+
while (current) {
|
|
123
|
+
if (isFbtNode(current)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
current = current.parent;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isFbtNode(node?: TSESTree.Node) {
|
|
133
|
+
if (!node) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node.type === 'JSXElement') {
|
|
138
|
+
const name = elementType(node);
|
|
139
|
+
return name === 'fbt' || name === 'fbs' || name?.startsWith('fbt:');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
|
|
143
|
+
const name = node.callee.name;
|
|
144
|
+
return name === 'fbt' || name === 'fbs';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
node.type === 'MemberExpression' &&
|
|
149
|
+
node.object.type === 'CallExpression' &&
|
|
150
|
+
node.object.callee.type === 'Identifier'
|
|
151
|
+
) {
|
|
152
|
+
const name = node.object.callee.name;
|
|
153
|
+
return name === 'fbt' || name === 'fbs';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
}
|