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