@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/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.2",
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,43 +169,45 @@ 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
  }
165
199
  if (child.type === "JSXElement" || child.type === "JSXFragment") {
166
- hasTextContent ||= validateChildren(child, nodesToReport);
200
+ const childName = elementType(child);
201
+ if (/^[A-Z]/.test(childName) || childName.startsWith("fbt:")) {
202
+ hasTextContent = true;
203
+ continue;
204
+ }
205
+ hasTextContent ||= validateChildren(child);
167
206
  }
168
207
  if (hasTextContent) {
169
208
  break;
170
209
  }
171
210
  }
172
- if (!hasTextContent && nodesToReport.size === 0) {
173
- nodesToReport.add(node);
174
- }
175
211
  return hasTextContent;
176
212
  }
177
213
  function isEmptyString(node) {
@@ -183,7 +219,7 @@ var no_unhelpful_desc_default = createRule({
183
219
  create(context) {
184
220
  return {
185
221
  CallExpression(node) {
186
- if (node.callee.type !== "Identifier" || !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
222
+ if (!isFbtNode(node)) {
187
223
  return;
188
224
  }
189
225
  const [textArg, descArg] = node.arguments;
@@ -211,7 +247,7 @@ var no_unhelpful_desc_default = createRule({
211
247
  }
212
248
  },
213
249
  JSXAttribute(node) {
214
- if (elementType(node.parent.parent) !== "fbt") {
250
+ if (!isFbtNode(node.parent.parent)) {
215
251
  return;
216
252
  }
217
253
  if (node.name.name !== "desc" || !node.value) {
@@ -262,6 +298,112 @@ var no_unhelpful_desc_default = createRule({
262
298
  name: "no-unhelpful-desc"
263
299
  });
264
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
+
265
407
  // src/index.tsx
266
408
  var meta = {
267
409
  name: package_default.name,
@@ -269,14 +411,22 @@ var meta = {
269
411
  };
270
412
  var rules = {
271
413
  "no-empty-strings": no_empty_strings_default,
272
- "no-unhelpful-desc": no_unhelpful_desc_default
414
+ "no-unhelpful-desc": no_unhelpful_desc_default,
415
+ "no-untranslated-strings": no_untranslated_strings_default
273
416
  };
274
417
  var configs = {
275
- strict: {
418
+ recommended: {
276
419
  rules: {
277
420
  "@nkzw/fbtee/no-empty-strings": 2,
278
421
  "@nkzw/fbtee/no-unhelpful-desc": 2
279
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
+ }
280
430
  }
281
431
  };
282
432
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/eslint-plugin-fbtee",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -13,6 +13,7 @@ const ruleTester = new RuleTester({
13
13
 
14
14
  ruleTester.run('no-empty-strings', rule, {
15
15
  invalid: [
16
+ // <fbt>
16
17
  {
17
18
  code: `
18
19
  <fbt desc="Greeting"></fbt>;
@@ -65,7 +66,70 @@ ruleTester.run('no-empty-strings', rule, {
65
66
  `,
66
67
  errors: [
67
68
  {
68
- 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,
69
133
  messageId: 'jsxEmptyString',
70
134
  },
71
135
  ],
@@ -78,7 +142,7 @@ ruleTester.run('no-empty-strings', rule, {
78
142
  `,
79
143
  errors: [
80
144
  {
81
- line: 3,
145
+ line: 2,
82
146
  messageId: 'jsxEmptyString',
83
147
  },
84
148
  ],
@@ -91,11 +155,26 @@ ruleTester.run('no-empty-strings', rule, {
91
155
  `,
92
156
  errors: [
93
157
  {
94
- 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,
95
172
  messageId: 'jsxEmptyString',
96
173
  },
97
174
  ],
98
175
  },
176
+
177
+ // fbt()
99
178
  {
100
179
  code: `
101
180
  fbt('', 'Greeting');
@@ -140,6 +219,8 @@ ruleTester.run('no-empty-strings', rule, {
140
219
  },
141
220
  ],
142
221
  },
222
+
223
+ // fbs()
143
224
  {
144
225
  code: `
145
226
  fbs('', 'Greeting');
@@ -153,6 +234,7 @@ ruleTester.run('no-empty-strings', rule, {
153
234
  },
154
235
  ],
155
236
  valid: [
237
+ // <fbt>
156
238
  {
157
239
  code: `
158
240
  <fbt desc="Greeting">Hello world</fbt>;
@@ -160,28 +242,24 @@ ruleTester.run('no-empty-strings', rule, {
160
242
  },
161
243
  {
162
244
  code: `
163
- <fbt desc="Greeting">{dynamicValue}</fbt>;
164
- `,
165
- },
166
- {
167
- code: `
168
- <fbt desc="Greeting">Hello {name}</fbt>;
245
+ <fbt desc="Greeting">
246
+ Hello <span>world</span>
247
+ </fbt>;
169
248
  `,
170
249
  },
171
250
  {
172
251
  code: `
173
- <fbt desc="Greeting">{'Hello'}</fbt>;
252
+ <fbt desc="Greeting">{dynamicValue}</fbt>;
174
253
  `,
175
254
  },
176
255
  {
177
256
  code: `
178
- fbt('Hello world', 'Greeting');
257
+ <fbt desc="Greeting">Hello {name}</fbt>;
179
258
  `,
180
259
  },
181
260
  {
182
261
  code: `
183
- const text = 'Hello';
184
- fbt(text, 'Greeting');
262
+ <fbt desc="Greeting">{'Hello'}</fbt>;
185
263
  `,
186
264
  },
187
265
  {
@@ -213,6 +291,59 @@ ruleTester.run('no-empty-strings', rule, {
213
291
  </fbt>;
214
292
  `,
215
293
  },
294
+ {
295
+ code: `
296
+ <fbt desc="buy prompt">
297
+ Buy a new
298
+ <fbt:enum
299
+ enum-range={{
300
+ CAR: 'car',
301
+ HOUSE: 'house',
302
+ BOAT: 'boat',
303
+ HOUSEBOAT: 'houseboat',
304
+ }}
305
+ value={enumVal}
306
+ />!
307
+ </fbt>;
308
+ `,
309
+ },
310
+ {
311
+ code: `
312
+ <fbt desc="welcome message">
313
+ Hello
314
+ <fbt:param name="name" value="Steve" />
315
+ </fbt>;
316
+ `,
317
+ },
318
+ {
319
+ code: `
320
+ <fbt desc="greeting">
321
+ <Component />
322
+ </fbt>;
323
+ `,
324
+ },
325
+
326
+ // <fbs>
327
+ {
328
+ code: `
329
+ <fbs desc="Greeting">Hello world</fbs>;
330
+ `,
331
+ },
332
+
333
+ // fbt()
334
+ {
335
+ code: `
336
+ fbt('Hello world', 'Greeting');
337
+ `,
338
+ },
339
+ {
340
+ code: `
341
+ const text = 'Hello';
342
+ fbt(text, 'Greeting');
343
+ `,
344
+ },
345
+
346
+ // other
216
347
  {
217
348
  code: `
218
349
  name.replaceAll(' ', '-');
@@ -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: `