@nkzw/eslint-plugin-fbtee 0.1.3 → 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/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@nkzw/eslint-plugin-fbtee",
4
- version: "0.1.3",
4
+ version: "0.1.4",
5
5
  description: "",
6
6
  keywords: [],
7
7
  repository: {
@@ -29,7 +29,7 @@ var package_default = {
29
29
  // src/utils.tsx
30
30
  import { ESLintUtils } from "@typescript-eslint/utils";
31
31
  var createRule = ESLintUtils.RuleCreator(
32
- (name) => `https://github.com/nkzw-tech/fbtee/tree/main/packages/eslint-plugin-fbt/docs/rules/${name}`
32
+ (name) => `https://github.com/nkzw-tech/fbtee/blob/main/packages/eslint-plugin-fbtee/docs/rules/${name}.md`
33
33
  );
34
34
  function resolveMemberExpressions(object, property) {
35
35
  if (object.type === "JSXMemberExpression") {
@@ -95,13 +95,47 @@ function resolveNodeValue(node) {
95
95
  }
96
96
  return null;
97
97
  }
98
+ function getPropName(prop) {
99
+ if (prop.name.type === "JSXNamespacedName") {
100
+ return `${prop.name.namespace.name}:${prop.name.name.name}`;
101
+ }
102
+ return prop.name.name;
103
+ }
104
+ function hasFbtParent(node) {
105
+ let current = node.parent;
106
+ while (current) {
107
+ if (isFbtNode(current)) {
108
+ return true;
109
+ }
110
+ current = current.parent;
111
+ }
112
+ return false;
113
+ }
114
+ function isFbtNode(node) {
115
+ if (!node) {
116
+ return null;
117
+ }
118
+ if (node.type === "JSXElement") {
119
+ const name = elementType(node);
120
+ return name === "fbt" || name === "fbs" || name?.startsWith("fbt:");
121
+ }
122
+ if (node.type === "CallExpression" && node.callee.type === "Identifier") {
123
+ const name = node.callee.name;
124
+ return name === "fbt" || name === "fbs";
125
+ }
126
+ if (node.type === "MemberExpression" && node.object.type === "CallExpression" && node.object.callee.type === "Identifier") {
127
+ const name = node.object.callee.name;
128
+ return name === "fbt" || name === "fbs";
129
+ }
130
+ return false;
131
+ }
98
132
 
99
133
  // src/rules/no-empty-strings.tsx
100
134
  var no_empty_strings_default = createRule({
101
135
  create(context) {
102
136
  return {
103
137
  CallExpression(node) {
104
- if (node.callee.type !== "Identifier" || !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
138
+ if (!isFbtNode(node)) {
105
139
  return;
106
140
  }
107
141
  const [text] = node.arguments;
@@ -113,18 +147,18 @@ var no_empty_strings_default = createRule({
113
147
  }
114
148
  },
115
149
  JSXElement(node) {
116
- if (elementType(node) !== "fbt") {
150
+ if (!isFbtNode(node)) {
117
151
  return;
118
152
  }
119
- const nodesToReport = /* @__PURE__ */ new Set();
120
- const hasTextContent = validateChildren(node, nodesToReport);
153
+ if (node.openingElement.name.type === "JSXNamespacedName") {
154
+ return;
155
+ }
156
+ const hasTextContent = validateChildren(node);
121
157
  if (!hasTextContent) {
122
- for (const node2 of nodesToReport) {
123
- context.report({
124
- messageId: "jsxEmptyString",
125
- node: node2
126
- });
127
- }
158
+ context.report({
159
+ messageId: "jsxEmptyString",
160
+ node
161
+ });
128
162
  }
129
163
  }
130
164
  };
@@ -135,30 +169,30 @@ var no_empty_strings_default = createRule({
135
169
  description: "Disallow empty strings in fbt elements or function calls."
136
170
  },
137
171
  messages: {
138
- emptyString: "Empty strings are not allowed in fbt() or fbs() function arguments.",
139
- jsxEmptyString: "Empty strings are not allowed as children of <fbt> tags."
172
+ emptyString: "Empty strings are not allowed in fbt() and fbs() function arguments.",
173
+ jsxEmptyString: "Empty strings are not allowed as children of `<fbt>` and `<fbs>` tags."
140
174
  },
141
175
  schema: [],
142
176
  type: "problem"
143
177
  },
144
178
  name: "no-empty-strings"
145
179
  });
146
- function validateChildren(node, nodesToReport) {
180
+ function validateChildren(node) {
147
181
  let hasTextContent = false;
148
182
  for (const child of node.children) {
149
- if (child.type === "JSXText" && node.children.length === 1) {
150
- if (child.value.trim() === "") {
151
- nodesToReport.add(child);
152
- } else {
183
+ if (child.type === "JSXText" && child.value.trim() !== "") {
184
+ hasTextContent = true;
185
+ }
186
+ if (child.type === "JSXExpressionContainer") {
187
+ if (child.expression.type === "JSXEmptyExpression") {
188
+ continue;
189
+ }
190
+ if (child.expression.type === "Identifier" && child.expression.name !== "undefined") {
153
191
  hasTextContent = true;
192
+ continue;
154
193
  }
155
- }
156
- if (child.type === "JSXExpressionContainer" && child.expression.type !== "JSXEmptyExpression") {
157
- if (isEmptyString(child.expression) && (node.children.length === 1 || node.children.every(
158
- (otherChild) => child === otherChild || otherChild.type === "JSXText" && otherChild.value.trim() === ""
159
- ))) {
160
- nodesToReport.add(child.expression);
161
- } else {
194
+ const value = resolveNodeValue(child.expression)?.trim();
195
+ if (value) {
162
196
  hasTextContent = true;
163
197
  }
164
198
  }
@@ -168,15 +202,12 @@ function validateChildren(node, nodesToReport) {
168
202
  hasTextContent = true;
169
203
  continue;
170
204
  }
171
- hasTextContent ||= validateChildren(child, nodesToReport);
205
+ hasTextContent ||= validateChildren(child);
172
206
  }
173
207
  if (hasTextContent) {
174
208
  break;
175
209
  }
176
210
  }
177
- if (!hasTextContent && nodesToReport.size === 0) {
178
- nodesToReport.add(node);
179
- }
180
211
  return hasTextContent;
181
212
  }
182
213
  function isEmptyString(node) {
@@ -188,7 +219,7 @@ var no_unhelpful_desc_default = createRule({
188
219
  create(context) {
189
220
  return {
190
221
  CallExpression(node) {
191
- if (node.callee.type !== "Identifier" || !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
222
+ if (!isFbtNode(node)) {
192
223
  return;
193
224
  }
194
225
  const [textArg, descArg] = node.arguments;
@@ -216,7 +247,7 @@ var no_unhelpful_desc_default = createRule({
216
247
  }
217
248
  },
218
249
  JSXAttribute(node) {
219
- if (elementType(node.parent.parent) !== "fbt") {
250
+ if (!isFbtNode(node.parent.parent)) {
220
251
  return;
221
252
  }
222
253
  if (node.name.name !== "desc" || !node.value) {
@@ -267,6 +298,112 @@ var no_unhelpful_desc_default = createRule({
267
298
  name: "no-unhelpful-desc"
268
299
  });
269
300
 
301
+ // src/rules/no-untranslated-strings.tsx
302
+ var attributes = /* @__PURE__ */ new Set([
303
+ "title",
304
+ "placeholder",
305
+ "alt",
306
+ "label",
307
+ "aria-label",
308
+ "aria-errormessage"
309
+ ]);
310
+ var no_untranslated_strings_default = createRule({
311
+ create(context, options) {
312
+ const ignoredWords = new Set(
313
+ options[0].ignoredWords.map((s) => s.trim().toLowerCase())
314
+ );
315
+ function isIgnoredWord(value) {
316
+ return ignoredWords.has(value.trim().toLowerCase());
317
+ }
318
+ return {
319
+ JSXAttribute(node) {
320
+ const propName = getPropName(node);
321
+ if (!attributes.has(propName)) {
322
+ return;
323
+ }
324
+ const value = node.value ? resolveNodeValue(node.value) : null;
325
+ if (!value) {
326
+ return;
327
+ }
328
+ if (isIgnoredWord(value)) {
329
+ return;
330
+ }
331
+ if (node.value?.type === "JSXExpressionContainer" && isFbtNode(node.value.expression)) {
332
+ return;
333
+ }
334
+ context.report({
335
+ messageId: "unwrappedString",
336
+ node
337
+ });
338
+ },
339
+ JSXExpressionContainer(node) {
340
+ if (node.parent.type === "JSXAttribute") {
341
+ return;
342
+ }
343
+ const value = resolveNodeValue(node.expression);
344
+ if (!value) {
345
+ return;
346
+ }
347
+ if (isIgnoredWord(value)) {
348
+ return;
349
+ }
350
+ if (!hasFbtParent(node)) {
351
+ context.report({
352
+ messageId: "unwrappedString",
353
+ node
354
+ });
355
+ }
356
+ },
357
+ JSXText(node) {
358
+ const value = node.value.trim();
359
+ if (!value) {
360
+ return;
361
+ }
362
+ if (isIgnoredWord(value)) {
363
+ return;
364
+ }
365
+ if (!hasFbtParent(node)) {
366
+ const startOffset = node.range[0] + node.value.indexOf(value);
367
+ const endOffset = startOffset + value.length;
368
+ context.report({
369
+ loc: {
370
+ end: context.sourceCode.getLocFromIndex(endOffset),
371
+ start: context.sourceCode.getLocFromIndex(startOffset)
372
+ },
373
+ messageId: "unwrappedString"
374
+ });
375
+ }
376
+ }
377
+ };
378
+ },
379
+ defaultOptions: [{ ignoredWords: [] }],
380
+ meta: {
381
+ docs: {
382
+ description: "Enforce text content to be wrapped with `<fbt>`, `fbt()` or `fbs()`"
383
+ },
384
+ messages: {
385
+ unwrappedString: "Text content must be wrapped in `<fbt>`, `fbt()` or `fbs()`"
386
+ },
387
+ schema: [
388
+ {
389
+ additionalProperties: false,
390
+ properties: {
391
+ ignoredWords: {
392
+ items: {
393
+ type: "string"
394
+ },
395
+ type: "array",
396
+ uniqueItems: true
397
+ }
398
+ },
399
+ type: "object"
400
+ }
401
+ ],
402
+ type: "problem"
403
+ },
404
+ name: "no-untranslated-strings"
405
+ });
406
+
270
407
  // src/index.tsx
271
408
  var meta = {
272
409
  name: package_default.name,
@@ -274,14 +411,22 @@ var meta = {
274
411
  };
275
412
  var rules = {
276
413
  "no-empty-strings": no_empty_strings_default,
277
- "no-unhelpful-desc": no_unhelpful_desc_default
414
+ "no-unhelpful-desc": no_unhelpful_desc_default,
415
+ "no-untranslated-strings": no_untranslated_strings_default
278
416
  };
279
417
  var configs = {
280
- strict: {
418
+ recommended: {
281
419
  rules: {
282
420
  "@nkzw/fbtee/no-empty-strings": 2,
283
421
  "@nkzw/fbtee/no-unhelpful-desc": 2
284
422
  }
423
+ },
424
+ strict: {
425
+ rules: {
426
+ "@nkzw/fbtee/no-empty-strings": 2,
427
+ "@nkzw/fbtee/no-unhelpful-desc": 2,
428
+ "@nkzw/fbtee/no-untranslated-strings": 2
429
+ }
285
430
  }
286
431
  };
287
432
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/eslint-plugin-fbtee",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -66,7 +66,70 @@ ruleTester.run('no-empty-strings', rule, {
66
66
  `,
67
67
  errors: [
68
68
  {
69
- line: 3,
69
+ line: 2,
70
+ messageId: 'jsxEmptyString',
71
+ },
72
+ ],
73
+ },
74
+ {
75
+ code: `
76
+ <fbt desc="Greeting">{}</fbt>;
77
+ `,
78
+ errors: [
79
+ {
80
+ line: 2,
81
+ messageId: 'jsxEmptyString',
82
+ },
83
+ ],
84
+ },
85
+ {
86
+ code: `
87
+ <fbt desc="Greeting">
88
+ {}
89
+ </fbt>;
90
+ `,
91
+ errors: [
92
+ {
93
+ line: 2,
94
+ messageId: 'jsxEmptyString',
95
+ },
96
+ ],
97
+ },
98
+ {
99
+ code: `
100
+ <fbt desc="Greeting">
101
+ {null}
102
+ </fbt>;
103
+ `,
104
+ errors: [
105
+ {
106
+ line: 2,
107
+ messageId: 'jsxEmptyString',
108
+ },
109
+ ],
110
+ },
111
+ {
112
+ code: `
113
+ <fbt desc="Greeting">
114
+ {undefined}
115
+ </fbt>;
116
+ `,
117
+ errors: [
118
+ {
119
+ line: 2,
120
+ messageId: 'jsxEmptyString',
121
+ },
122
+ ],
123
+ },
124
+ {
125
+ code: `
126
+ <fbt desc="Greeting">
127
+ {/* This is a comment */}
128
+ </fbt>;
129
+ `,
130
+ errors: [
131
+ {
132
+ line: 2,
70
133
  messageId: 'jsxEmptyString',
71
134
  },
72
135
  ],
@@ -79,7 +142,7 @@ ruleTester.run('no-empty-strings', rule, {
79
142
  `,
80
143
  errors: [
81
144
  {
82
- line: 3,
145
+ line: 2,
83
146
  messageId: 'jsxEmptyString',
84
147
  },
85
148
  ],
@@ -92,7 +155,20 @@ ruleTester.run('no-empty-strings', rule, {
92
155
  `,
93
156
  errors: [
94
157
  {
95
- line: 3,
158
+ line: 2,
159
+ messageId: 'jsxEmptyString',
160
+ },
161
+ ],
162
+ },
163
+
164
+ // <fbs>
165
+ {
166
+ code: `
167
+ <fbs desc="Greeting"></fbs>;
168
+ `,
169
+ errors: [
170
+ {
171
+ line: 2,
96
172
  messageId: 'jsxEmptyString',
97
173
  },
98
174
  ],
@@ -164,6 +240,13 @@ ruleTester.run('no-empty-strings', rule, {
164
240
  <fbt desc="Greeting">Hello world</fbt>;
165
241
  `,
166
242
  },
243
+ {
244
+ code: `
245
+ <fbt desc="Greeting">
246
+ Hello <span>world</span>
247
+ </fbt>;
248
+ `,
249
+ },
167
250
  {
168
251
  code: `
169
252
  <fbt desc="Greeting">{dynamicValue}</fbt>;
@@ -240,6 +323,13 @@ ruleTester.run('no-empty-strings', rule, {
240
323
  `,
241
324
  },
242
325
 
326
+ // <fbs>
327
+ {
328
+ code: `
329
+ <fbs desc="Greeting">Hello world</fbs>;
330
+ `,
331
+ },
332
+
243
333
  // fbt()
244
334
  {
245
335
  code: `
@@ -114,6 +114,19 @@ ruleTester.run('unhelpful-desc', rule, {
114
114
  ],
115
115
  },
116
116
 
117
+ // <fbs>
118
+ {
119
+ code: `
120
+ <fbt desc="">Hello world</fbt>;
121
+ `,
122
+ errors: [
123
+ {
124
+ line: 2,
125
+ messageId: 'jsxEmptyDesc',
126
+ },
127
+ ],
128
+ },
129
+
117
130
  // fbt()
118
131
  {
119
132
  code: `
@@ -274,6 +287,13 @@ ruleTester.run('unhelpful-desc', rule, {
274
287
  `,
275
288
  },
276
289
 
290
+ // <fbs>
291
+ {
292
+ code: `
293
+ <fbs desc="Greeting">Hello world</fbs>;
294
+ `,
295
+ },
296
+
277
297
  // fbt()
278
298
  {
279
299
  code: `
@@ -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
+ });