@nkzw/eslint-plugin-fbtee 0.1.3 → 0.2.0

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.2.0",
5
5
  description: "",
6
6
  keywords: [],
7
7
  repository: {
@@ -19,17 +19,17 @@ var package_default = {
19
19
  build: "tsup src/index.tsx -d lib --target=node22 --format=esm,cjs --clean --no-splitting --dts"
20
20
  },
21
21
  dependencies: {
22
- "@typescript-eslint/utils": "^8.18.0"
22
+ "@typescript-eslint/utils": "^8.18.2"
23
23
  },
24
24
  devDependencies: {
25
- "@typescript-eslint/rule-tester": "^8.18.0"
25
+ "@typescript-eslint/rule-tester": "^8.18.2"
26
26
  }
27
27
  };
28
28
 
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") {
@@ -93,15 +93,59 @@ function resolveNodeValue(node) {
93
93
  if (node.type === "JSXExpressionContainer") {
94
94
  return resolveNodeValue(node.expression);
95
95
  }
96
+ if (node.type === "ConditionalExpression") {
97
+ const consequent = resolveNodeValue(node.consequent);
98
+ const alternate = resolveNodeValue(node.alternate);
99
+ return consequent || alternate || null;
100
+ }
101
+ if (node.type === "LogicalExpression") {
102
+ const left = resolveNodeValue(node.left);
103
+ const right = resolveNodeValue(node.right);
104
+ return left || right || null;
105
+ }
96
106
  return null;
97
107
  }
108
+ function getPropName(prop) {
109
+ if (prop.name.type === "JSXNamespacedName") {
110
+ return `${prop.name.namespace.name}:${prop.name.name.name}`;
111
+ }
112
+ return prop.name.name;
113
+ }
114
+ function hasFbtParent(node) {
115
+ let current = node.parent;
116
+ while (current) {
117
+ if (isFbtNode(current)) {
118
+ return true;
119
+ }
120
+ current = current.parent;
121
+ }
122
+ return false;
123
+ }
124
+ function isFbtNode(node) {
125
+ if (!node) {
126
+ return null;
127
+ }
128
+ if (node.type === "JSXElement") {
129
+ const name = elementType(node);
130
+ return name === "fbt" || name === "fbs" || name?.startsWith("fbt:");
131
+ }
132
+ if (node.type === "CallExpression" && node.callee.type === "Identifier") {
133
+ const name = node.callee.name;
134
+ return name === "fbt" || name === "fbs";
135
+ }
136
+ if (node.type === "MemberExpression" && node.object.type === "CallExpression" && node.object.callee.type === "Identifier") {
137
+ const name = node.object.callee.name;
138
+ return name === "fbt" || name === "fbs";
139
+ }
140
+ return false;
141
+ }
98
142
 
99
143
  // src/rules/no-empty-strings.tsx
100
144
  var no_empty_strings_default = createRule({
101
145
  create(context) {
102
146
  return {
103
147
  CallExpression(node) {
104
- if (node.callee.type !== "Identifier" || !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
148
+ if (!isFbtNode(node)) {
105
149
  return;
106
150
  }
107
151
  const [text] = node.arguments;
@@ -113,18 +157,18 @@ var no_empty_strings_default = createRule({
113
157
  }
114
158
  },
115
159
  JSXElement(node) {
116
- if (elementType(node) !== "fbt") {
160
+ if (!isFbtNode(node)) {
161
+ return;
162
+ }
163
+ if (node.openingElement.name.type === "JSXNamespacedName") {
117
164
  return;
118
165
  }
119
- const nodesToReport = /* @__PURE__ */ new Set();
120
- const hasTextContent = validateChildren(node, nodesToReport);
166
+ const hasTextContent = validateChildren(node);
121
167
  if (!hasTextContent) {
122
- for (const node2 of nodesToReport) {
123
- context.report({
124
- messageId: "jsxEmptyString",
125
- node: node2
126
- });
127
- }
168
+ context.report({
169
+ messageId: "jsxEmptyString",
170
+ node
171
+ });
128
172
  }
129
173
  }
130
174
  };
@@ -135,30 +179,30 @@ var no_empty_strings_default = createRule({
135
179
  description: "Disallow empty strings in fbt elements or function calls."
136
180
  },
137
181
  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."
182
+ emptyString: "Empty strings are not allowed in fbt() and fbs() function arguments.",
183
+ jsxEmptyString: "Empty strings are not allowed as children of `<fbt>` and `<fbs>` tags."
140
184
  },
141
185
  schema: [],
142
186
  type: "problem"
143
187
  },
144
188
  name: "no-empty-strings"
145
189
  });
146
- function validateChildren(node, nodesToReport) {
190
+ function validateChildren(node) {
147
191
  let hasTextContent = false;
148
192
  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 {
193
+ if (child.type === "JSXText" && child.value.trim() !== "") {
194
+ hasTextContent = true;
195
+ }
196
+ if (child.type === "JSXExpressionContainer") {
197
+ if (child.expression.type === "JSXEmptyExpression") {
198
+ continue;
199
+ }
200
+ if (child.expression.type === "Identifier" && child.expression.name !== "undefined") {
153
201
  hasTextContent = true;
202
+ continue;
154
203
  }
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 {
204
+ const value = resolveNodeValue(child.expression)?.trim();
205
+ if (value) {
162
206
  hasTextContent = true;
163
207
  }
164
208
  }
@@ -168,15 +212,12 @@ function validateChildren(node, nodesToReport) {
168
212
  hasTextContent = true;
169
213
  continue;
170
214
  }
171
- hasTextContent ||= validateChildren(child, nodesToReport);
215
+ hasTextContent ||= validateChildren(child);
172
216
  }
173
217
  if (hasTextContent) {
174
218
  break;
175
219
  }
176
220
  }
177
- if (!hasTextContent && nodesToReport.size === 0) {
178
- nodesToReport.add(node);
179
- }
180
221
  return hasTextContent;
181
222
  }
182
223
  function isEmptyString(node) {
@@ -188,7 +229,7 @@ var no_unhelpful_desc_default = createRule({
188
229
  create(context) {
189
230
  return {
190
231
  CallExpression(node) {
191
- if (node.callee.type !== "Identifier" || !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
232
+ if (!isFbtNode(node)) {
192
233
  return;
193
234
  }
194
235
  const [textArg, descArg] = node.arguments;
@@ -216,7 +257,7 @@ var no_unhelpful_desc_default = createRule({
216
257
  }
217
258
  },
218
259
  JSXAttribute(node) {
219
- if (elementType(node.parent.parent) !== "fbt") {
260
+ if (!isFbtNode(node.parent.parent)) {
220
261
  return;
221
262
  }
222
263
  if (node.name.name !== "desc" || !node.value) {
@@ -267,6 +308,112 @@ var no_unhelpful_desc_default = createRule({
267
308
  name: "no-unhelpful-desc"
268
309
  });
269
310
 
311
+ // src/rules/no-untranslated-strings.tsx
312
+ var attributes = /* @__PURE__ */ new Set([
313
+ "title",
314
+ "placeholder",
315
+ "alt",
316
+ "label",
317
+ "aria-label",
318
+ "aria-errormessage"
319
+ ]);
320
+ var no_untranslated_strings_default = createRule({
321
+ create(context, options) {
322
+ const ignoredWords = new Set(
323
+ options[0].ignoredWords.map((s) => s.trim().toLowerCase())
324
+ );
325
+ function isIgnoredWord(value) {
326
+ return ignoredWords.has(value.trim().toLowerCase());
327
+ }
328
+ return {
329
+ JSXAttribute(node) {
330
+ const propName = getPropName(node);
331
+ if (!attributes.has(propName)) {
332
+ return;
333
+ }
334
+ const value = node.value ? resolveNodeValue(node.value) : null;
335
+ if (!value) {
336
+ return;
337
+ }
338
+ if (isIgnoredWord(value)) {
339
+ return;
340
+ }
341
+ if (node.value?.type === "JSXExpressionContainer" && isFbtNode(node.value.expression)) {
342
+ return;
343
+ }
344
+ context.report({
345
+ messageId: "unwrappedString",
346
+ node
347
+ });
348
+ },
349
+ JSXExpressionContainer(node) {
350
+ if (node.parent.type === "JSXAttribute") {
351
+ return;
352
+ }
353
+ const value = resolveNodeValue(node.expression);
354
+ if (!value) {
355
+ return;
356
+ }
357
+ if (isIgnoredWord(value)) {
358
+ return;
359
+ }
360
+ if (!hasFbtParent(node)) {
361
+ context.report({
362
+ messageId: "unwrappedString",
363
+ node
364
+ });
365
+ }
366
+ },
367
+ JSXText(node) {
368
+ const value = node.value.trim();
369
+ if (!value) {
370
+ return;
371
+ }
372
+ if (isIgnoredWord(value)) {
373
+ return;
374
+ }
375
+ if (!hasFbtParent(node)) {
376
+ const startOffset = node.range[0] + node.value.indexOf(value);
377
+ const endOffset = startOffset + value.length;
378
+ context.report({
379
+ loc: {
380
+ end: context.sourceCode.getLocFromIndex(endOffset),
381
+ start: context.sourceCode.getLocFromIndex(startOffset)
382
+ },
383
+ messageId: "unwrappedString"
384
+ });
385
+ }
386
+ }
387
+ };
388
+ },
389
+ defaultOptions: [{ ignoredWords: [] }],
390
+ meta: {
391
+ docs: {
392
+ description: "Enforce text content to be wrapped with `<fbt>`, `fbt()` or `fbs()`"
393
+ },
394
+ messages: {
395
+ unwrappedString: "Text content must be wrapped in `<fbt>`, `fbt()` or `fbs()`"
396
+ },
397
+ schema: [
398
+ {
399
+ additionalProperties: false,
400
+ properties: {
401
+ ignoredWords: {
402
+ items: {
403
+ type: "string"
404
+ },
405
+ type: "array",
406
+ uniqueItems: true
407
+ }
408
+ },
409
+ type: "object"
410
+ }
411
+ ],
412
+ type: "problem"
413
+ },
414
+ name: "no-untranslated-strings"
415
+ });
416
+
270
417
  // src/index.tsx
271
418
  var meta = {
272
419
  name: package_default.name,
@@ -274,14 +421,22 @@ var meta = {
274
421
  };
275
422
  var rules = {
276
423
  "no-empty-strings": no_empty_strings_default,
277
- "no-unhelpful-desc": no_unhelpful_desc_default
424
+ "no-unhelpful-desc": no_unhelpful_desc_default,
425
+ "no-untranslated-strings": no_untranslated_strings_default
278
426
  };
279
427
  var configs = {
280
- strict: {
428
+ recommended: {
281
429
  rules: {
282
430
  "@nkzw/fbtee/no-empty-strings": 2,
283
431
  "@nkzw/fbtee/no-unhelpful-desc": 2
284
432
  }
433
+ },
434
+ strict: {
435
+ rules: {
436
+ "@nkzw/fbtee/no-empty-strings": 2,
437
+ "@nkzw/fbtee/no-unhelpful-desc": 2,
438
+ "@nkzw/fbtee/no-untranslated-strings": 2
439
+ }
285
440
  }
286
441
  };
287
442
  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.2.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -15,10 +15,10 @@
15
15
  "require": "./lib/index.cjs"
16
16
  },
17
17
  "dependencies": {
18
- "@typescript-eslint/utils": "^8.18.0"
18
+ "@typescript-eslint/utils": "^8.18.2"
19
19
  },
20
20
  "devDependencies": {
21
- "@typescript-eslint/rule-tester": "^8.18.0"
21
+ "@typescript-eslint/rule-tester": "^8.18.2"
22
22
  },
23
23
  "scripts": {
24
24
  "build": "tsup src/index.tsx -d lib --target=node22 --format=esm,cjs --clean --no-splitting --dts"
@@ -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: `