@lichens-innovation/eslint-plugin-coding-guide 1.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/ast-utils.d.ts +2 -0
  4. package/dist/configs/recommended.d.ts +6 -0
  5. package/dist/create-rule.d.ts +4 -0
  6. package/dist/eslint.config.d.ts +2 -0
  7. package/dist/index.d.ts +114 -0
  8. package/dist/index.js +1294 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/rule-tester.d.ts +2 -0
  11. package/dist/rules/filename-convention-by-export-shape.d.ts +4 -0
  12. package/dist/rules/hoist-static-component-constants.d.ts +5 -0
  13. package/dist/rules/index.d.ts +101 -0
  14. package/dist/rules/max-params-project.d.ts +8 -0
  15. package/dist/rules/no-explicit-undefined-optional.d.ts +4 -0
  16. package/dist/rules/no-exported-mutable-state.d.ts +4 -0
  17. package/dist/rules/no-hook-returning-jsx.d.ts +4 -0
  18. package/dist/rules/no-inline-array-chain-in-jsx.d.ts +4 -0
  19. package/dist/rules/no-inline-curried-handler.d.ts +4 -0
  20. package/dist/rules/no-inline-guard-chain-handler.d.ts +4 -0
  21. package/dist/rules/no-inline-object-param-type.d.ts +4 -0
  22. package/dist/rules/no-inline-render-function.d.ts +5 -0
  23. package/dist/rules/no-jsx-in-variable.d.ts +4 -0
  24. package/dist/rules/no-nested-try.d.ts +4 -0
  25. package/dist/rules/no-non-hook-use-prefix.d.ts +4 -0
  26. package/dist/rules/no-render-fn-in-usecallback.d.ts +4 -0
  27. package/dist/rules/no-tests-in-dunder-folder.d.ts +4 -0
  28. package/dist/rules/no-trivial-usememo.d.ts +4 -0
  29. package/dist/rules/no-unguarded-json-parse.d.ts +4 -0
  30. package/dist/rules/prefer-element-ref-type.d.ts +4 -0
  31. package/dist/rules/prefer-includes-over-or-chain.d.ts +4 -0
  32. package/dist/rules/prefer-jsx-short-circuit.d.ts +4 -0
  33. package/dist/rules/prefer-nullish-helpers.d.ts +4 -0
  34. package/dist/rules/prefer-positive-condition.d.ts +4 -0
  35. package/dist/rules/prefer-props-with-children.d.ts +4 -0
  36. package/dist/rules/prefer-reactnode-over-jsxelement-union.d.ts +4 -0
  37. package/dist/rules/prefer-role-query-over-testid.d.ts +4 -0
  38. package/dist/rules/prefer-some-over-find-check.d.ts +4 -0
  39. package/dist/rules/prefer-state-updater-form.d.ts +4 -0
  40. package/dist/rules/require-effect-cleanup.d.ts +4 -0
  41. package/dist/rules/require-fallback-on-deep-chain.d.ts +7 -0
  42. package/dist/rules/require-numeric-enum-initializer.d.ts +4 -0
  43. package/dist/rules/require-usestate-useref-generic.d.ts +4 -0
  44. package/dist/rules/todo-ticket-ref.d.ts +12 -0
  45. package/dist/vite.config.d.mts +2 -0
  46. package/dist/vitest.config.d.mts +2 -0
  47. package/package.json +92 -0
package/dist/index.js ADDED
@@ -0,0 +1,1294 @@
1
+ import { ESLintUtils as e } from "@typescript-eslint/utils";
2
+ //#region src/configs/recommended.ts
3
+ var t = (e) => ({
4
+ name: "coding-guide/recommended",
5
+ files: ["**/*.{ts,tsx}"],
6
+ plugins: { "coding-guide": e },
7
+ rules: Object.fromEntries(Object.keys(e.rules).map((e) => [`coding-guide/${e}`, "error"]))
8
+ }), n = e.RuleCreator((e) => `https://github.com/Lichens-Innovation/eslint-plugin-coding-guide/blob/main/docs/rules/${e}.md`), r = /* @__PURE__ */ new Set([
9
+ "utils",
10
+ "types",
11
+ "helpers",
12
+ "constants",
13
+ "config",
14
+ "client",
15
+ "index"
16
+ ]), i = [
17
+ {
18
+ nameSuffix: "Page",
19
+ kebabSuffix: "-page"
20
+ },
21
+ {
22
+ nameSuffix: "Dialog",
23
+ kebabSuffix: "-dialog"
24
+ },
25
+ {
26
+ nameSuffix: "Provider",
27
+ kebabSuffix: "-provider"
28
+ }
29
+ ], a = (e) => (e.replaceAll("\\", "/").split("/").pop() ?? e).replace(/\.(tsx|ts|jsx|js)$/, ""), o = (e) => {
30
+ let t = [];
31
+ for (let n of e.declarations) {
32
+ if (n.id.type !== "Identifier") continue;
33
+ let e = n.init?.type === "ArrowFunctionExpression" || n.init?.type === "FunctionExpression";
34
+ t.push({
35
+ name: n.id.name,
36
+ isFunctionLike: e
37
+ });
38
+ }
39
+ return t;
40
+ }, s = (e) => {
41
+ let t = [];
42
+ for (let n of e) {
43
+ if (n.type !== "ExportNamedDeclaration" || !n.declaration) continue;
44
+ let e = n.declaration;
45
+ if (e.type === "FunctionDeclaration" && e.id) {
46
+ t.push({
47
+ name: e.id.name,
48
+ isFunctionLike: !0
49
+ });
50
+ continue;
51
+ }
52
+ if (e.type === "ClassDeclaration" && e.id) {
53
+ t.push({
54
+ name: e.id.name,
55
+ isFunctionLike: !1
56
+ });
57
+ continue;
58
+ }
59
+ e.type === "VariableDeclaration" && t.push(...o(e));
60
+ }
61
+ return t;
62
+ }, c = n({
63
+ name: "filename-convention-by-export-shape",
64
+ meta: {
65
+ type: "suggestion",
66
+ docs: { description: "Enforce filename conventions against a file's exported shape" },
67
+ schema: [],
68
+ messages: {
69
+ genericBasename: "'{{basename}}' is a generic root-level filename — prefix it with its domain (e.g. '{{example}}').",
70
+ hookFilename: "This file's sole export '{{name}}' is a hook — rename the file to start with 'use-'.",
71
+ suffixFilename: "This file's sole export '{{name}}' ends in '{{nameSuffix}}' — rename the file to end with '{{kebabSuffix}}'."
72
+ }
73
+ },
74
+ defaultOptions: [],
75
+ create(e) {
76
+ return { "Program:exit"(t) {
77
+ let n = a(e.filename);
78
+ r.has(n.toLowerCase()) && e.report({
79
+ node: t,
80
+ messageId: "genericBasename",
81
+ data: {
82
+ basename: n,
83
+ example: `<domain>.${n}.ts`
84
+ }
85
+ });
86
+ let o = s(t.body);
87
+ if (o.length !== 1) return;
88
+ let [c] = o;
89
+ if (!c.isFunctionLike) return;
90
+ let { name: l } = c;
91
+ if (/^use[A-Z]/.test(l)) {
92
+ n.startsWith("use-") || e.report({
93
+ node: t,
94
+ messageId: "hookFilename",
95
+ data: { name: l }
96
+ });
97
+ return;
98
+ }
99
+ for (let { nameSuffix: r, kebabSuffix: a } of i) if (l.endsWith(r)) {
100
+ n.endsWith(a) || e.report({
101
+ node: t,
102
+ messageId: "suffixFilename",
103
+ data: {
104
+ name: l,
105
+ nameSuffix: r,
106
+ kebabSuffix: a
107
+ }
108
+ });
109
+ return;
110
+ }
111
+ } };
112
+ }
113
+ }), l = (e) => typeof e == "object" && !!e && typeof e.type == "string", u = (e) => Object.entries(e).filter(([e]) => e !== "parent").flatMap(([, e]) => Array.isArray(e) ? e : [e]).filter(l), ee = (e) => e?.type === "ArrayExpression" || e?.type === "ObjectExpression", te = ({ scope: e, ancestorScope: t }) => {
114
+ let n = e;
115
+ for (; n;) {
116
+ if (n === t) return !0;
117
+ n = n.upper;
118
+ }
119
+ return !1;
120
+ }, d = ({ node: e, functionScope: t, sourceCode: n }) => {
121
+ let r = !1, i = (e) => {
122
+ if (!r) {
123
+ if (e.type === "Identifier") {
124
+ let i = n.getScope(e).references.find((t) => t.identifier === e)?.resolved;
125
+ i && te({
126
+ scope: i.scope,
127
+ ancestorScope: t
128
+ }) && (r = !0);
129
+ return;
130
+ }
131
+ for (let t of u(e)) i(t);
132
+ }
133
+ };
134
+ return i(e), r;
135
+ }, f = (e) => e.type === "ArrayExpression" && e.elements.length > 0 || e.type === "ObjectExpression" && e.properties.length > 0, p = (e) => {
136
+ let t = e.block;
137
+ if (t.type === "FunctionDeclaration" && t.id) return t.id.name;
138
+ if (t.parent?.type === "VariableDeclarator" && t.parent.id.type === "Identifier") return t.parent.id.name;
139
+ }, ne = (e) => !!e && (/^[A-Z]/.test(e) || /^use[A-Z]/.test(e)), re = n({
140
+ name: "hoist-static-component-constants",
141
+ meta: {
142
+ type: "suggestion",
143
+ docs: { description: "Disallow a static array/object literal declared inside a component body" },
144
+ schema: [],
145
+ messages: { hoist: "'{{name}}' has no dependency on this component's scope — hoist it to module scope." }
146
+ },
147
+ defaultOptions: [],
148
+ create(e) {
149
+ let t = e.sourceCode;
150
+ return { VariableDeclarator(n) {
151
+ if (n.id.type !== "Identifier" || !ee(n.init) || !f(n.init)) return;
152
+ let r = t.getScope(n);
153
+ r.type === "function" && ne(p(r)) && (d({
154
+ node: n.init,
155
+ functionScope: r,
156
+ sourceCode: t
157
+ }) || e.report({
158
+ node: n,
159
+ messageId: "hoist",
160
+ data: { name: n.id.name }
161
+ }));
162
+ } };
163
+ }
164
+ }), ie = /* @__PURE__ */ new Set([
165
+ "Property",
166
+ "ObjectExpression",
167
+ "ArrayExpression",
168
+ "SpreadElement",
169
+ "ChainExpression",
170
+ "ConditionalExpression",
171
+ "LogicalExpression",
172
+ "TSAsExpression",
173
+ "TSSatisfiesExpression",
174
+ "TSNonNullExpression",
175
+ "TSTypeAssertion"
176
+ ]), ae = "\n{{name}} has {{count}} parameters. Maximum allowed is {{max}}.\nThe idea is to group related parameters together into a single object making callsites more readable because the caller needs to explicitly name each parameter.\nThe house style is a named Args interface: capitalize the function name, add an Args suffix, and take a single destructured object of that type. Do not leave the shape inline and anonymous.\nExample:\n interface BuildSiteEquipmentKeyArgs {\n siteSlug?: string;\n equipmentSlug?: string;\n }\n\n const buildSiteEquipmentSlugKey = ({ siteSlug, equipmentSlug }: BuildSiteEquipmentKeyArgs) => {\n // ...\n }\n", oe = (e) => {
177
+ let t = e.parent;
178
+ if (e.type === "ArrowFunctionExpression") return "Arrow function";
179
+ if (t?.type === "MethodDefinition" || t?.type === "Property" && t.method) {
180
+ let e = t.key;
181
+ return e.type === "Identifier" ? `Method '${e.name}'` : "Method";
182
+ }
183
+ return e.type === "FunctionDeclaration" && e.id?.name ? `Function '${e.id.name}'` : t?.type === "VariableDeclarator" && t.id.type === "Identifier" ? `Function '${t.id.name}'` : "Function";
184
+ }, se = (e) => {
185
+ let t = e.parent;
186
+ return t?.type === "JSXExpressionContainer" && t.parent?.type === "JSXAttribute";
187
+ }, m = (e) => {
188
+ let t = e, n = /* @__PURE__ */ new Set();
189
+ for (; t.parent && !n.has(t);) {
190
+ n.add(t);
191
+ let e = t.parent;
192
+ if (e.type === "CallExpression" || e.type === "NewExpression") return e.arguments.includes(t);
193
+ if (ie.has(e.type)) {
194
+ t = e;
195
+ continue;
196
+ }
197
+ break;
198
+ }
199
+ return !1;
200
+ }, h = (e) => {
201
+ let t = e.replaceAll("\\", "/");
202
+ return t.includes("/node_modules/") || t.includes("/typescript/lib/");
203
+ }, ce = (e) => {
204
+ let t = /* @__PURE__ */ new Set(), n = (e) => {
205
+ if (t.has(e)) return [];
206
+ if (t.add(e), e.isUnion() || e.isIntersection()) return e.types.flatMap(n);
207
+ let r = [];
208
+ for (let t of e.getCallSignatures()) {
209
+ let e = t.getDeclaration();
210
+ e && r.push(e.getSourceFile().fileName);
211
+ }
212
+ let i = e.aliasSymbol ?? e.getSymbol();
213
+ for (let e of i?.getDeclarations() ?? []) r.push(e.getSourceFile().fileName);
214
+ return r;
215
+ };
216
+ return n(e).some(h);
217
+ }, le = ({ base: e, methodName: t, checker: n }) => (n.getPropertyOfType(e, t)?.getDeclarations() ?? []).some((e) => h(e.getSourceFile().fileName)), ue = ({ functionNode: e, services: t, checker: n }) => {
218
+ let r = e.parent;
219
+ if (r?.type !== "MethodDefinition") return !1;
220
+ let i = r.parent.parent;
221
+ if (i.type !== "ClassDeclaration" && i.type !== "ClassExpression") return !1;
222
+ let a = r.key.type === "Identifier" ? r.key.name : null;
223
+ if (!a) return !1;
224
+ let o = t.esTreeNodeToTSNodeMap.get(i);
225
+ return (n.getTypeAtLocation(o).getBaseTypes() ?? []).some((e) => le({
226
+ base: e,
227
+ methodName: a,
228
+ checker: n
229
+ }));
230
+ }, de = ({ node: e, tsNode: t, checker: n }) => {
231
+ if (e.type === "FunctionDeclaration") return !1;
232
+ let r = n.getContextualType(t);
233
+ return !!r && ce(r);
234
+ }, fe = n({
235
+ name: "max-params-project",
236
+ meta: {
237
+ type: "suggestion",
238
+ docs: { description: "Enforce a maximum number of parameters on project-owned functions" },
239
+ schema: [{ oneOf: [{
240
+ type: "integer",
241
+ minimum: 0
242
+ }, {
243
+ type: "object",
244
+ properties: { max: {
245
+ type: "integer",
246
+ minimum: 0
247
+ } },
248
+ additionalProperties: !1
249
+ }] }],
250
+ messages: { exceed: ae }
251
+ },
252
+ defaultOptions: [1],
253
+ create(t, [n]) {
254
+ let r = typeof n == "number" ? n : n.max ?? 1, i = (e) => {
255
+ let n = t.sourceCode.getScope(e);
256
+ for (; n;) {
257
+ let t = n.variables.find((t) => t.name === e.name);
258
+ if (t) return t;
259
+ n = n.upper;
260
+ }
261
+ return null;
262
+ }, a = (e) => {
263
+ let t = e.parent;
264
+ if (t?.type !== "VariableDeclarator" || t.init !== e || t.id.type !== "Identifier") return !1;
265
+ let n = i(t.id);
266
+ return n ? n.references.some((e) => e.identifier !== t.id && m(e.identifier)) : !1;
267
+ }, o = (e) => se(e) || m(e) ? !0 : a(e), s = (n) => {
268
+ try {
269
+ let r = e.getParserServices(t, !0);
270
+ if (!r.program) return !1;
271
+ let i = r.program.getTypeChecker(), a = r.esTreeNodeToTSNodeMap.get(n);
272
+ return a ? de({
273
+ node: n,
274
+ tsNode: a,
275
+ checker: i
276
+ }) ? !0 : ue({
277
+ functionNode: n,
278
+ services: r,
279
+ checker: i
280
+ }) : !1;
281
+ } catch {
282
+ return !1;
283
+ }
284
+ }, c = (e) => {
285
+ e.params.length <= r || o(e) || s(e) || t.report({
286
+ node: e,
287
+ messageId: "exceed",
288
+ data: {
289
+ name: oe(e),
290
+ count: e.params.length,
291
+ max: r
292
+ }
293
+ });
294
+ };
295
+ return {
296
+ FunctionDeclaration: c,
297
+ FunctionExpression: c,
298
+ ArrowFunctionExpression: c
299
+ };
300
+ }
301
+ }), pe = (e) => e.type === "TSUndefinedKeyword", me = (e) => {
302
+ let t = e.parent;
303
+ if (!t || t.type === "AssignmentPattern") return null;
304
+ let n = t.type === "TSParameterProperty" ? t : e, r = n.parent;
305
+ return r && "params" in r && Array.isArray(r.params) && r.params.includes(n) && e.type === "Identifier" ? e : null;
306
+ }, g = (e) => {
307
+ let t = e.parent;
308
+ if (!t || t.type !== "TSTypeAnnotation") return null;
309
+ let n = t.parent;
310
+ return n ? n.type === "TSPropertySignature" || n.type === "PropertyDefinition" ? n : n.type === "Identifier" || n.type === "ObjectPattern" || n.type === "ArrayPattern" ? me(n) : null : null;
311
+ }, _ = n({
312
+ name: "no-explicit-undefined-optional",
313
+ meta: {
314
+ type: "suggestion",
315
+ docs: { description: "Use `?` instead of an explicit `| undefined` on params/properties that support it" },
316
+ fixable: "code",
317
+ schema: [],
318
+ messages: {
319
+ useOptionalModifier: "Do not use explicit `| undefined` to mark {{what}} optional — use `?` instead.",
320
+ redundantUndefined: "{{what}} is already optional (`?`) — remove the redundant explicit `undefined`."
321
+ }
322
+ },
323
+ defaultOptions: [],
324
+ create(e) {
325
+ let t = e.sourceCode;
326
+ return { TSUnionType(n) {
327
+ let r = n.types.filter((e) => !pe(e));
328
+ if (r.length === n.types.length || r.length === 0) return;
329
+ let i = g(n);
330
+ if (!i) return;
331
+ let a = i.optional === !0, o = ["TSPropertySignature", "PropertyDefinition"].includes(i.type) ? "this property" : "this parameter";
332
+ e.report({
333
+ node: n,
334
+ messageId: a ? "redundantUndefined" : "useOptionalModifier",
335
+ data: { what: o },
336
+ fix: r.length === 1 ? (e) => {
337
+ let i = [e.replaceText(n, t.getText(r[0]))];
338
+ return !a && n.parent && i.push(e.insertTextBefore(n.parent, "?")), i;
339
+ } : null
340
+ });
341
+ } };
342
+ }
343
+ }), v = n({
344
+ name: "no-exported-mutable-state",
345
+ meta: {
346
+ type: "suggestion",
347
+ docs: { description: "Disallow exporting a mutable (let/var) module-scoped binding" },
348
+ schema: [],
349
+ messages: { noMutableExport: "Do not export a mutable ({{kind}}) binding — export a const, or a getter/setter pair." }
350
+ },
351
+ defaultOptions: [],
352
+ create(e) {
353
+ return { ExportNamedDeclaration(t) {
354
+ let n = t.declaration;
355
+ !n || n.type !== "VariableDeclaration" || n.kind !== "const" && e.report({
356
+ node: t,
357
+ messageId: "noMutableExport",
358
+ data: { kind: n.kind }
359
+ });
360
+ } };
361
+ }
362
+ }), y = /^use[A-Z]/, b = (e) => e.argument?.type === "JSXElement" || e.argument?.type === "JSXFragment", x = (e) => {
363
+ let t = !1, n = (r) => {
364
+ if (!t && !(r !== e && [
365
+ "FunctionDeclaration",
366
+ "FunctionExpression",
367
+ "ArrowFunctionExpression"
368
+ ].includes(r.type))) {
369
+ if (r.type === "ReturnStatement" && b(r)) {
370
+ t = !0;
371
+ return;
372
+ }
373
+ for (let e of u(r)) n(e);
374
+ }
375
+ };
376
+ return ["JSXElement", "JSXFragment"].includes(e.body.type) ? !0 : (n(e.body), t);
377
+ }, S = n({
378
+ name: "no-hook-returning-jsx",
379
+ meta: {
380
+ type: "suggestion",
381
+ docs: { description: "Disallow a use* hook returning JSX" },
382
+ schema: [],
383
+ messages: { hookReturnsJsx: "'{{name}}' is named like a hook but returns JSX — hooks should return data, not markup." }
384
+ },
385
+ defaultOptions: [],
386
+ create(e) {
387
+ let t = ({ node: t, name: n }) => {
388
+ !n || !y.test(n) || x(t) && e.report({
389
+ node: t,
390
+ messageId: "hookReturnsJsx",
391
+ data: { name: n }
392
+ });
393
+ };
394
+ return {
395
+ FunctionDeclaration(e) {
396
+ t({
397
+ node: e,
398
+ name: e.id?.name
399
+ });
400
+ },
401
+ "VariableDeclarator > ArrowFunctionExpression"(e) {
402
+ let n = e.parent;
403
+ t({
404
+ node: e,
405
+ name: n.id.type === "Identifier" ? n.id.name : void 0
406
+ });
407
+ }
408
+ };
409
+ }
410
+ }), C = /* @__PURE__ */ new Set([
411
+ "filter",
412
+ "sort",
413
+ "map",
414
+ "reduce"
415
+ ]), w = (e) => {
416
+ let t = 0, n = e;
417
+ for (; n.type === "CallExpression";) {
418
+ let e = n.callee;
419
+ if (e.type !== "MemberExpression" || e.computed || e.property.type !== "Identifier" || !C.has(e.property.name) || (t += 1, e.object.type === "Super")) break;
420
+ n = e.object;
421
+ }
422
+ return t;
423
+ }, T = n({
424
+ name: "no-inline-array-chain-in-jsx",
425
+ meta: {
426
+ type: "suggestion",
427
+ docs: { description: "Disallow chained array methods directly inside a JSX expression" },
428
+ schema: [],
429
+ messages: { precompute: "Chained {{count}} array methods inline in JSX — precompute the list with an imported helper." }
430
+ },
431
+ defaultOptions: [],
432
+ create(e) {
433
+ return { JSXExpressionContainer(t) {
434
+ let n = t.expression;
435
+ if (n.type !== "CallExpression") return;
436
+ let r = w(n);
437
+ r < 2 || e.report({
438
+ node: n,
439
+ messageId: "precompute",
440
+ data: { count: r }
441
+ });
442
+ } };
443
+ }
444
+ }), E = (e) => e.type === "ArrowFunctionExpression" ? e.body.type === "ArrowFunctionExpression" || e.body.type === "BlockStatement" && e.body.body.some((e) => e.type === "ReturnStatement" && e.argument?.type === "ArrowFunctionExpression") : !1, D = n({
445
+ name: "no-inline-curried-handler",
446
+ meta: {
447
+ type: "suggestion",
448
+ docs: { description: "Disallow a curried handler factory declared as a local component variable" },
449
+ schema: [],
450
+ messages: { extractToUtils: "'{{name}}' is a curried handler factory declared inside a component — move it to a *.utils.ts file." }
451
+ },
452
+ defaultOptions: [],
453
+ create(e) {
454
+ return { VariableDeclarator(t) {
455
+ !t.init || !E(t.init) || t.id.type === "Identifier" && e.sourceCode.getScope(t).type === "function" && e.report({
456
+ node: t,
457
+ messageId: "extractToUtils",
458
+ data: { name: t.id.name }
459
+ });
460
+ } };
461
+ }
462
+ }), O = (e) => e.type === "LogicalExpression" && e.operator === "&&" ? O(e.left) + O(e.right) : 1, k = n({
463
+ name: "no-inline-guard-chain-handler",
464
+ meta: {
465
+ type: "suggestion",
466
+ docs: { description: "Disallow a JSX prop arrow whose body is a long && guard chain" },
467
+ schema: [],
468
+ messages: { extractHandler: "JSX prop arrow guards with a {{count}}-term `&&` chain — extract a named handler with early returns." }
469
+ },
470
+ defaultOptions: [],
471
+ create(e) {
472
+ return { JSXAttribute(t) {
473
+ let n = t.value;
474
+ if (n?.type !== "JSXExpressionContainer") return;
475
+ let r = n.expression;
476
+ if (r.type !== "ArrowFunctionExpression" || r.body.type !== "LogicalExpression" || r.body.operator !== "&&") return;
477
+ let i = O(r.body);
478
+ i < 3 || e.report({
479
+ node: r,
480
+ messageId: "extractHandler",
481
+ data: { count: i }
482
+ });
483
+ } };
484
+ }
485
+ }), A = n({
486
+ name: "no-inline-object-param-type",
487
+ meta: {
488
+ type: "suggestion",
489
+ docs: { description: "Disallow inline object type literals on function parameters" },
490
+ schema: [],
491
+ messages: { extractInterface: "Inline object type on this parameter — extract a named `interface` above the function." }
492
+ },
493
+ defaultOptions: [],
494
+ create(e) {
495
+ let t = (t) => {
496
+ let n = "typeAnnotation" in t ? t.typeAnnotation?.typeAnnotation : void 0;
497
+ n?.type === "TSTypeLiteral" && e.report({
498
+ node: n,
499
+ messageId: "extractInterface"
500
+ });
501
+ }, n = (e) => {
502
+ e.params.forEach(t);
503
+ };
504
+ return {
505
+ FunctionDeclaration: n,
506
+ FunctionExpression: n,
507
+ ArrowFunctionExpression: n
508
+ };
509
+ }
510
+ }), j = /^render[A-Z]/, M = (e) => {
511
+ if (e?.scope.type !== "function") return !1;
512
+ let t = e.defs[0];
513
+ return t?.type === "FunctionName" || t?.type === "Variable";
514
+ }, N = (e) => {
515
+ let t = e.parent;
516
+ for (; t;) {
517
+ if (t.type === "JSXExpressionContainer") return !0;
518
+ t = t.parent;
519
+ }
520
+ return !1;
521
+ }, P = n({
522
+ name: "no-inline-render-function",
523
+ meta: {
524
+ type: "suggestion",
525
+ docs: { description: "Disallow calling a locally-declared render* helper function from within JSX" },
526
+ schema: [],
527
+ messages: { extractSubcomponent: "'{{name}}' is a local render helper called from JSX — extract a `<{{suggested}} />` subcomponent instead." }
528
+ },
529
+ defaultOptions: [],
530
+ create(e) {
531
+ return { CallExpression(t) {
532
+ if (t.callee.type !== "Identifier" || !j.test(t.callee.name) || !N(t) || !M(e.sourceCode.getScope(t).references.find((e) => e.identifier === t.callee)?.resolved ?? void 0)) return;
533
+ let n = t.callee.name, r = n.slice(6) || "Section";
534
+ e.report({
535
+ node: t,
536
+ messageId: "extractSubcomponent",
537
+ data: {
538
+ name: n,
539
+ suggested: r
540
+ }
541
+ });
542
+ } };
543
+ }
544
+ }), F = n({
545
+ name: "no-jsx-in-variable",
546
+ meta: {
547
+ type: "suggestion",
548
+ docs: { description: "Disallow storing a JSX element/fragment in a variable" },
549
+ schema: [],
550
+ messages: { declareComponent: "JSX stored in a variable — declare a small named component with explicit props instead." }
551
+ },
552
+ defaultOptions: [],
553
+ create(e) {
554
+ return { VariableDeclarator(t) {
555
+ t.init && (t.init.type === "JSXElement" || t.init.type === "JSXFragment") && e.report({
556
+ node: t,
557
+ messageId: "declareComponent"
558
+ });
559
+ } };
560
+ }
561
+ }), I = n({
562
+ name: "no-nested-try",
563
+ meta: {
564
+ type: "suggestion",
565
+ docs: { description: "Disallow nesting a try statement inside another try block or catch handler" },
566
+ schema: [],
567
+ messages: { nestedTry: "Nested try/catch — flatten into a single try/catch that handles both error paths." }
568
+ },
569
+ defaultOptions: [],
570
+ create(e) {
571
+ return { TryStatement(t) {
572
+ let n = t, r = t.parent;
573
+ for (; r;) {
574
+ if ([
575
+ "FunctionDeclaration",
576
+ "FunctionExpression",
577
+ "ArrowFunctionExpression"
578
+ ].includes(r.type)) return;
579
+ if (r.type === "TryStatement" && (n === r.block || n === r.handler)) {
580
+ e.report({
581
+ node: t,
582
+ messageId: "nestedTry"
583
+ });
584
+ return;
585
+ }
586
+ n = r, r = r.parent;
587
+ }
588
+ } };
589
+ }
590
+ }), L = /^use[A-Z]/, R = (e) => e.type === "CallExpression" && e.callee.type === "Identifier" && L.test(e.callee.name) ? !0 : u(e).some((e) => R(e)), z = n({
591
+ name: "no-non-hook-use-prefix",
592
+ meta: {
593
+ type: "suggestion",
594
+ docs: { description: "Disallow a use* named function whose body calls no hook" },
595
+ schema: [],
596
+ messages: { misnamed: "'{{name}}' is named like a hook but calls no hook internally — rename it to a plain action verb." }
597
+ },
598
+ defaultOptions: [],
599
+ create(e) {
600
+ let t = ({ node: t, name: n }) => {
601
+ !n || !L.test(n) || R(t.body) || e.report({
602
+ node: t,
603
+ messageId: "misnamed",
604
+ data: { name: n }
605
+ });
606
+ };
607
+ return {
608
+ FunctionDeclaration(e) {
609
+ t({
610
+ node: e,
611
+ name: e.id?.name
612
+ });
613
+ },
614
+ "VariableDeclarator > ArrowFunctionExpression"(e) {
615
+ let n = e.parent;
616
+ t({
617
+ node: e,
618
+ name: n.id.type === "Identifier" ? n.id.name : void 0
619
+ });
620
+ }
621
+ };
622
+ }
623
+ }), he = /^render/i, ge = (e) => e ? ["JSXElement", "JSXFragment"].includes(e.body.type) ? !0 : e.body.type === "BlockStatement" && e.body.body.some((e) => e.type === "ReturnStatement" && e.argument && ["JSXElement", "JSXFragment"].includes(e.argument.type)) : !1, _e = n({
624
+ name: "no-render-fn-in-usecallback",
625
+ meta: {
626
+ type: "suggestion",
627
+ docs: { description: "Disallow useCallback wrapping a JSX-returning or render*-named function" },
628
+ schema: [],
629
+ messages: { extractSubcomponent: "useCallback wraps a render function — extract a `<Component />` instead; reserve useCallback for event handlers." }
630
+ },
631
+ defaultOptions: [],
632
+ create(e) {
633
+ return { CallExpression(t) {
634
+ if (t.callee.type !== "Identifier" || t.callee.name !== "useCallback") return;
635
+ let [n] = t.arguments;
636
+ if (!n || n.type !== "ArrowFunctionExpression" && n.type !== "FunctionExpression") return;
637
+ let r = t.parent.type === "VariableDeclarator" ? t.parent : null, i = r?.id.type === "Identifier" ? r.id.name : "";
638
+ !ge(n) && !he.test(i) || e.report({
639
+ node: t,
640
+ messageId: "extractSubcomponent"
641
+ });
642
+ } };
643
+ }
644
+ }), ve = n({
645
+ name: "no-tests-in-dunder-folder",
646
+ meta: {
647
+ type: "suggestion",
648
+ docs: { description: "Disallow test files inside a __tests__ folder" },
649
+ schema: [],
650
+ messages: { colocate: "This test lives in a `__tests__` folder — colocate it as `*.test.ts(x)` next to its source file." }
651
+ },
652
+ defaultOptions: [],
653
+ create(e) {
654
+ return { Program(t) {
655
+ e.filename.replaceAll("\\", "/").includes("/__tests__/") && e.report({
656
+ node: t,
657
+ messageId: "colocate"
658
+ });
659
+ } };
660
+ }
661
+ }), B = (e) => ["CallExpression", "NewExpression"].includes(e.type) ? !0 : u(e).some((e) => B(e)), ye = n({
662
+ name: "no-trivial-usememo",
663
+ meta: {
664
+ type: "suggestion",
665
+ docs: { description: "Disallow useMemo whose body has no function call (likely unnecessary memoization)" },
666
+ schema: [],
667
+ messages: { unnecessaryMemo: "useMemo body has no function call inside — likely too trivial to be worth memoizing." }
668
+ },
669
+ defaultOptions: [],
670
+ create(e) {
671
+ return { CallExpression(t) {
672
+ if (t.callee.type !== "Identifier" || t.callee.name !== "useMemo") return;
673
+ let [n] = t.arguments;
674
+ if (!n || n.type !== "ArrowFunctionExpression") return;
675
+ let r = n.body.type === "BlockStatement" ? n.body.body.find((e) => e.type === "ReturnStatement")?.argument : n.body;
676
+ r && (B(r) || e.report({
677
+ node: t,
678
+ messageId: "unnecessaryMemo"
679
+ }));
680
+ } };
681
+ }
682
+ }), be = (e) => e.callee.type === "MemberExpression" && e.callee.object.type === "Identifier" && e.callee.object.name === "JSON" && e.callee.property.type === "Identifier" && e.callee.property.name === "parse", xe = (e) => {
683
+ let t = e, n;
684
+ for (; t;) {
685
+ if (t.type === "TryStatement" && n === t.block) return !0;
686
+ if ([
687
+ "FunctionDeclaration",
688
+ "FunctionExpression",
689
+ "ArrowFunctionExpression"
690
+ ].includes(t.type)) return !1;
691
+ n = t, t = t.parent;
692
+ }
693
+ return !1;
694
+ }, Se = n({
695
+ name: "no-unguarded-json-parse",
696
+ meta: {
697
+ type: "suggestion",
698
+ docs: { description: "Require JSON.parse(...) to be wrapped in a try/catch" },
699
+ schema: [],
700
+ messages: { unguarded: "JSON.parse(...) can throw on malformed input — wrap it in a try/catch." }
701
+ },
702
+ defaultOptions: [],
703
+ create(e) {
704
+ return { CallExpression(t) {
705
+ be(t) && (xe(t) || e.report({
706
+ node: t,
707
+ messageId: "unguarded"
708
+ }));
709
+ } };
710
+ }
711
+ }), Ce = {
712
+ HTMLDivElement: "div",
713
+ HTMLSpanElement: "span",
714
+ HTMLButtonElement: "button",
715
+ HTMLInputElement: "input",
716
+ HTMLTextAreaElement: "textarea",
717
+ HTMLSelectElement: "select",
718
+ HTMLFormElement: "form",
719
+ HTMLAnchorElement: "a",
720
+ HTMLUListElement: "ul",
721
+ HTMLLIElement: "li",
722
+ HTMLTableElement: "table",
723
+ HTMLCanvasElement: "canvas",
724
+ HTMLVideoElement: "video",
725
+ HTMLAudioElement: "audio",
726
+ HTMLImageElement: "img",
727
+ HTMLParagraphElement: "p",
728
+ HTMLHeadingElement: "h1",
729
+ HTMLLabelElement: "label"
730
+ }, we = (e) => e.body.some((e) => e.type === "ImportDeclaration" && e.source.value === "react" && e.specifiers.some((e) => e.type === "ImportSpecifier" && e.imported.type === "Identifier" && e.imported.name === "ElementRef")), Te = n({
731
+ name: "prefer-element-ref-type",
732
+ meta: {
733
+ type: "suggestion",
734
+ docs: { description: "Prefer useRef<ElementRef<\"tag\">>(null) over a raw HTMLXxxElement type argument" },
735
+ fixable: "code",
736
+ schema: [],
737
+ messages: { preferElementRef: "Use `ElementRef<\"{{tag}}\">` instead of `{{typeName}}` for this ref." }
738
+ },
739
+ defaultOptions: [],
740
+ create(e) {
741
+ return { CallExpression(t) {
742
+ if (t.callee.type !== "Identifier" || t.callee.name !== "useRef") return;
743
+ let n = t.typeArguments?.params[0];
744
+ if (!n || n.type !== "TSTypeReference" || n.typeName.type !== "Identifier") return;
745
+ let r = n.typeName.name;
746
+ if (!/^HTML\w*Element$/.test(r)) return;
747
+ let i = Ce[r];
748
+ if (!i) {
749
+ e.report({
750
+ node: n,
751
+ messageId: "preferElementRef",
752
+ data: {
753
+ typeName: r,
754
+ tag: "?"
755
+ }
756
+ });
757
+ return;
758
+ }
759
+ let a = we(e.sourceCode.ast);
760
+ e.report({
761
+ node: n,
762
+ messageId: "preferElementRef",
763
+ data: {
764
+ typeName: r,
765
+ tag: i
766
+ },
767
+ fix: a ? (e) => e.replaceText(n, `ElementRef<"${i}">`) : void 0
768
+ });
769
+ } };
770
+ }
771
+ }), V = (e) => e.type === "LogicalExpression" && e.operator === "||" ? [...V(e.left), ...V(e.right)] : [e], Ee = (e) => e.type === "BinaryExpression" && ["===", "=="].includes(e.operator) && e.right.type === "Literal", De = n({
772
+ name: "prefer-includes-over-or-chain",
773
+ meta: {
774
+ type: "suggestion",
775
+ docs: { description: "Prefer Array#includes over a chain of === comparisons against the same value" },
776
+ fixable: "code",
777
+ schema: [],
778
+ messages: { preferIncludes: "Repeated equality checks against the same value — use `[...].includes(...)` instead." }
779
+ },
780
+ defaultOptions: [],
781
+ create(e) {
782
+ let t = e.sourceCode;
783
+ return { LogicalExpression(n) {
784
+ if (n.operator !== "||" || n.parent.type === "LogicalExpression" && n.parent.operator === "||") return;
785
+ let r = V(n);
786
+ if (r.length < 2 || !r.every(Ee)) return;
787
+ let i = r, a = i.map((e) => t.getText(e.left)), [o] = a;
788
+ a.every((e) => e === o) && e.report({
789
+ node: n,
790
+ messageId: "preferIncludes",
791
+ fix: (e) => {
792
+ let r = i.map((e) => t.getText(e.right)).join(", ");
793
+ return e.replaceText(n, `[${r}].includes(${o})`);
794
+ }
795
+ });
796
+ } };
797
+ }
798
+ }), Oe = 270620, H = (e) => {
799
+ let t = e;
800
+ for (; t.type === "TSAsExpression" || t.type === "TSSatisfiesExpression" || t.type === "TSTypeAssertion" || t.type === "TSNonNullExpression" || t.type === "ChainExpression";) t = t.expression;
801
+ return t;
802
+ }, U = (e) => !!e && ["JSXElement", "JSXFragment"].includes(e.type), W = (e) => {
803
+ let t = e.parent;
804
+ if (!t || t.type !== "JSXExpressionContainer") return !1;
805
+ let n = t.parent;
806
+ return !!n && ["JSXElement", "JSXFragment"].includes(n.type);
807
+ }, ke = (e) => {
808
+ let t = H(e);
809
+ return t.type === "Literal" && (t.value === null || t.value === !1) || t.type === "Identifier" && t.name === "undefined";
810
+ }, G = (e) => {
811
+ let t = H(e);
812
+ return t.type === "MemberExpression" && !t.computed && t.property.type === "Identifier" && t.property.name === "length";
813
+ }, K = (e) => {
814
+ let t = H(e);
815
+ return t.type === "UnaryExpression" && t.operator === "!" || t.type === "BinaryExpression" || t.type === "CallExpression" || t.type === "Literal" && typeof t.value == "boolean" || t.type === "LogicalExpression" && t.operator === "&&" && K(t.left) && K(t.right);
816
+ }, q = (e) => e ? e.flags & Oe ? !0 : e.isUnion() ? e.types.every(q) : !1 : !1, J = (e) => U(e) ? [] : e.type === "LogicalExpression" && e.operator === "&&" ? [...J(e.left), ...J(e.right)] : [e], Ae = n({
817
+ name: "prefer-jsx-short-circuit",
818
+ meta: {
819
+ type: "suggestion",
820
+ docs: { description: "Prefer && short-circuit for optional JSX, with a boolean left side" },
821
+ fixable: "code",
822
+ schema: [],
823
+ messages: {
824
+ preferShortCircuit: "Use `&&` short-circuit instead of `cond ? jsx : null` for optional rendering.",
825
+ requireBooleanGuard: "Left side of `&&` in JSX may leak a non-boolean value — use `!!value` or a boolean comparison."
826
+ }
827
+ },
828
+ defaultOptions: [],
829
+ create(e) {
830
+ let t = e.sourceCode, n = (e) => {
831
+ let n = t.getTokenBefore(e), r = t.getTokenAfter(e);
832
+ return n?.value === "(" && r?.value === ")" ? t.text.slice(n.range[0], r.range[1]) : t.getText(e);
833
+ }, r = (e) => {
834
+ try {
835
+ let n = t.parserServices;
836
+ if (!n?.program || !n.esTreeNodeToTSNodeMap) return;
837
+ let r = n.esTreeNodeToTSNodeMap.get(e);
838
+ return r ? n.program.getTypeChecker().getTypeAtLocation(r) : void 0;
839
+ } catch {
840
+ return;
841
+ }
842
+ }, i = (e) => {
843
+ if (K(e)) return !1;
844
+ if (G(e)) return !0;
845
+ let t = r(e);
846
+ if (t) return !q(t);
847
+ let n = H(e);
848
+ return n.type === "Identifier" || n.type === "Literal" && typeof n.value == "boolean" ? !1 : n.type !== "JSXElement" && n.type !== "JSXFragment";
849
+ }, a = (e) => {
850
+ let n = t.getText(e);
851
+ if (G(e)) return `${n} > 0`;
852
+ let r = H(e);
853
+ return [
854
+ "Identifier",
855
+ "MemberExpression",
856
+ "ChainExpression"
857
+ ].includes(r.type) ? `!!${n}` : `!!(${n})`;
858
+ }, o = (e) => e.type === "LogicalExpression" && e.operator === "&&" ? `${o(e.left)} && ${o(e.right)}` : i(e) ? a(e) : t.getText(e);
859
+ return {
860
+ "JSXExpressionContainer > ConditionalExpression"(t) {
861
+ t.type === "ConditionalExpression" && W(t) && (!U(t.consequent) || !ke(t.alternate) || e.report({
862
+ node: t,
863
+ messageId: "preferShortCircuit",
864
+ fix: (e) => {
865
+ let r = o(t.test), i = n(t.consequent);
866
+ return e.replaceText(t, `${r} && ${i}`);
867
+ }
868
+ }));
869
+ },
870
+ "JSXExpressionContainer > LogicalExpression[operator='&&']"(t) {
871
+ if (t.type === "LogicalExpression" && W(t)) for (let n of J(t)) i(n) && e.report({
872
+ node: n,
873
+ messageId: "requireBooleanGuard",
874
+ fix: (e) => e.replaceText(n, a(n))
875
+ });
876
+ }
877
+ };
878
+ }
879
+ }), je = (e) => e.type === "Literal" && e.value === null || e.type === "Identifier" && e.name === "undefined", Me = n({
880
+ name: "prefer-nullish-helpers",
881
+ meta: {
882
+ type: "suggestion",
883
+ docs: { description: "Prefer isNullish/!isNullish over a manual null-and-undefined comparison pair" },
884
+ schema: [],
885
+ messages: {
886
+ preferNotNullish: "Manual `!== null && !== undefined` pair — use `!isNullish({{expr}})` instead.",
887
+ preferNullish: "Manual `=== null || === undefined` pair — use `isNullish({{expr}})` instead."
888
+ }
889
+ },
890
+ defaultOptions: [],
891
+ create(e) {
892
+ let t = e.sourceCode, n = ({ node: n, operator: r, logicalOperator: i, messageId: a }) => {
893
+ if (n.operator !== i) return;
894
+ let o = (e) => e.type === "BinaryExpression" && e.operator === r && je(e.right);
895
+ if (!o(n.left) || !o(n.right)) return;
896
+ let s = t.getText(n.left.left);
897
+ s === t.getText(n.right.left) && e.report({
898
+ node: n,
899
+ messageId: a,
900
+ data: { expr: s }
901
+ });
902
+ };
903
+ return { LogicalExpression(e) {
904
+ n({
905
+ node: e,
906
+ operator: "!==",
907
+ logicalOperator: "&&",
908
+ messageId: "preferNotNullish"
909
+ }), n({
910
+ node: e,
911
+ operator: "===",
912
+ logicalOperator: "||",
913
+ messageId: "preferNullish"
914
+ });
915
+ } };
916
+ }
917
+ }), Ne = n({
918
+ name: "prefer-positive-condition",
919
+ meta: {
920
+ type: "suggestion",
921
+ docs: { description: "Prefer a positive condition in a ternary over a negated one with swapped branches" },
922
+ fixable: "code",
923
+ schema: [],
924
+ messages: { preferPositive: "Negated ternary condition — swap the branches and drop the `!` instead." }
925
+ },
926
+ defaultOptions: [],
927
+ create(e) {
928
+ let t = e.sourceCode;
929
+ return { ConditionalExpression(n) {
930
+ let r = n.test;
931
+ r.type !== "UnaryExpression" || r.operator !== "!" || !r.prefix || e.report({
932
+ node: n,
933
+ messageId: "preferPositive",
934
+ fix: (e) => {
935
+ let i = t.getText(r.argument), a = t.getText(n.consequent), o = t.getText(n.alternate);
936
+ return e.replaceText(n, `${i} ? ${o} : ${a}`);
937
+ }
938
+ });
939
+ } };
940
+ }
941
+ }), Pe = n({
942
+ name: "prefer-props-with-children",
943
+ meta: {
944
+ type: "suggestion",
945
+ docs: { description: "Prefer PropsWithChildren<Props> over a hand-declared children property" },
946
+ schema: [],
947
+ messages: { preferPropsWithChildren: "'{{name}}' hand-declares a `children` property — wrap the props type in `PropsWithChildren<...>` instead." }
948
+ },
949
+ defaultOptions: [],
950
+ create(e) {
951
+ return { TSInterfaceDeclaration(t) {
952
+ let n = t.body.body.find((e) => e.type === "TSPropertySignature" && e.key.type === "Identifier" && e.key.name === "children");
953
+ n && e.report({
954
+ node: n,
955
+ messageId: "preferPropsWithChildren",
956
+ data: { name: t.id.name }
957
+ });
958
+ } };
959
+ }
960
+ }), Fe = (e) => {
961
+ if (e.type !== "TSTypeReference") return !1;
962
+ let t = e.typeName;
963
+ return t.type === "TSQualifiedName" ? t.left.type === "Identifier" && t.left.name === "JSX" && t.right.name === "Element" : t.type === "Identifier" && ["ReactElement", "JSX.Element"].includes(t.name);
964
+ }, Ie = (e) => ["TSNullKeyword", "TSUndefinedKeyword"].includes(e.type), Le = (e) => e.body.some((e) => e.type === "ImportDeclaration" && e.source.value === "react" && e.specifiers.some((e) => e.type === "ImportSpecifier" && e.imported.type === "Identifier" && e.imported.name === "ReactNode")), Re = n({
965
+ name: "prefer-reactnode-over-jsxelement-union",
966
+ meta: {
967
+ type: "suggestion",
968
+ docs: { description: "Prefer ReactNode over a JSX.Element | null | undefined union" },
969
+ fixable: "code",
970
+ schema: [],
971
+ messages: { preferReactNode: "Use `ReactNode` instead of a `JSX.Element`/`ReactElement` union with null/undefined." }
972
+ },
973
+ defaultOptions: [],
974
+ create(e) {
975
+ return { TSUnionType(t) {
976
+ let n = t.types.some(Fe), r = t.types.some(Ie);
977
+ if (!n || !r) return;
978
+ let i = Le(e.sourceCode.ast);
979
+ e.report({
980
+ node: t,
981
+ messageId: "preferReactNode",
982
+ fix: i ? (e) => e.replaceText(t, "ReactNode") : void 0
983
+ });
984
+ } };
985
+ }
986
+ }), ze = /^(get|query|find)(All)?ByTestId$/, Be = n({
987
+ name: "prefer-role-query-over-testid",
988
+ meta: {
989
+ type: "suggestion",
990
+ docs: { description: "Prefer Testing Library's *ByRole queries over *ByTestId" },
991
+ schema: [],
992
+ messages: { preferRole: "'{{name}}' — prefer the equivalent `*ByRole` query for a semantic, accessible selector." }
993
+ },
994
+ defaultOptions: [],
995
+ create(e) {
996
+ let t = ({ node: t, name: n }) => {
997
+ ze.test(n) && e.report({
998
+ node: t,
999
+ messageId: "preferRole",
1000
+ data: { name: n }
1001
+ });
1002
+ };
1003
+ return { CallExpression(e) {
1004
+ e.callee.type === "Identifier" ? t({
1005
+ node: e,
1006
+ name: e.callee.name
1007
+ }) : e.callee.type === "MemberExpression" && e.callee.property.type === "Identifier" && t({
1008
+ node: e.callee.property,
1009
+ name: e.callee.property.name
1010
+ });
1011
+ } };
1012
+ }
1013
+ }), Y = (e) => e.type === "CallExpression" && e.callee.type === "MemberExpression" && !e.callee.computed && e.callee.property.type === "Identifier" && e.callee.property.name === "find", Ve = n({
1014
+ name: "prefer-some-over-find-check",
1015
+ meta: {
1016
+ type: "suggestion",
1017
+ docs: { description: "Prefer Array#some over comparing Array#find's result to undefined" },
1018
+ fixable: "code",
1019
+ schema: [],
1020
+ messages: { preferSome: "Use `.some(...)` instead of comparing `.find(...)` to undefined for an existence check." }
1021
+ },
1022
+ defaultOptions: [],
1023
+ create(e) {
1024
+ let t = e.sourceCode, n = (e) => {
1025
+ let n = e.callee;
1026
+ return `${t.getText(n.object)}.some(${e.arguments.map((e) => t.getText(e)).join(", ")})`;
1027
+ };
1028
+ return {
1029
+ BinaryExpression(t) {
1030
+ if (t.operator !== "!==" && t.operator !== "===") return;
1031
+ let [r, i] = t.left.type === "CallExpression" ? [t.left, t.right] : [t.right, t.left];
1032
+ if (!Y(r) || i.type !== "Identifier" || i.name !== "undefined") return;
1033
+ let a = t.operator === "===";
1034
+ e.report({
1035
+ node: t,
1036
+ messageId: "preferSome",
1037
+ fix: (e) => {
1038
+ let i = n(r);
1039
+ return e.replaceText(t, a ? `!${i}` : i);
1040
+ }
1041
+ });
1042
+ },
1043
+ UnaryExpression(t) {
1044
+ if (t.operator !== "!" || !t.prefix) return;
1045
+ let r = t.argument;
1046
+ Y(r) && e.report({
1047
+ node: t,
1048
+ messageId: "preferSome",
1049
+ fix: (e) => e.replaceText(t, `!${n(r)}`)
1050
+ });
1051
+ }
1052
+ };
1053
+ }
1054
+ }), He = (e) => e.charAt(0).toLowerCase() + e.slice(1), Ue = ({ node: e, key: t }) => (e.type === "MemberExpression" || e.type === "MethodDefinition") && t === "property" && !e.computed || e.type === "Property" && t === "key" && !e.computed, We = (e) => !!e && typeof e == "object" && typeof e.type == "string", Ge = (e) => {
1055
+ let t = (n) => {
1056
+ if (!We(n)) return !1;
1057
+ if (n.type === "Identifier" && n.name === e) return !0;
1058
+ for (let e of Object.keys(n)) {
1059
+ if (e === "parent" || Ue({
1060
+ node: n,
1061
+ key: e
1062
+ })) continue;
1063
+ let r = n[e];
1064
+ if (Array.isArray(r) ? r.some(t) : t(r)) return !0;
1065
+ }
1066
+ return !1;
1067
+ };
1068
+ return t;
1069
+ }, Ke = n({
1070
+ name: "prefer-state-updater-form",
1071
+ meta: {
1072
+ type: "suggestion",
1073
+ docs: { description: "Prefer the updater-function form of a state setter when the new value depends on the current one" },
1074
+ schema: [],
1075
+ messages: { preferUpdaterForm: "'{{setter}}' argument references '{{state}}' directly — use the updater form `{{setter}}((current) => ...)` instead." }
1076
+ },
1077
+ defaultOptions: [],
1078
+ create(e) {
1079
+ return { CallExpression(t) {
1080
+ if (t.callee.type !== "Identifier" || !/^set[A-Z]/.test(t.callee.name) || t.arguments.length !== 1) return;
1081
+ let [n] = t.arguments;
1082
+ if (["ArrowFunctionExpression", "FunctionExpression"].includes(n.type)) return;
1083
+ let r = He(t.callee.name.slice(3));
1084
+ Ge(r)(n) && e.report({
1085
+ node: t,
1086
+ messageId: "preferUpdaterForm",
1087
+ data: {
1088
+ setter: t.callee.name,
1089
+ state: r
1090
+ }
1091
+ });
1092
+ } };
1093
+ }
1094
+ }), X = /* @__PURE__ */ new Set([
1095
+ "setInterval",
1096
+ "setTimeout",
1097
+ "addEventListener",
1098
+ "subscribe"
1099
+ ]), qe = (e) => !!e && typeof e == "object" && typeof e.type == "string", Je = (e) => e.type === "CallExpression" ? e.callee.type === "Identifier" ? X.has(e.callee.name) : e.callee.type === "MemberExpression" && e.callee.property.type === "Identifier" && X.has(e.callee.property.name) : !1, Z = (e) => {
1100
+ if (!qe(e)) return !1;
1101
+ if (Je(e)) return !0;
1102
+ if ([
1103
+ "FunctionDeclaration",
1104
+ "FunctionExpression",
1105
+ "ArrowFunctionExpression"
1106
+ ].includes(e.type)) return !1;
1107
+ for (let t of Object.keys(e)) {
1108
+ if (t === "parent") continue;
1109
+ let n = e[t];
1110
+ if (Array.isArray(n) ? n.some((e) => Z(e)) : Z(n)) return !0;
1111
+ }
1112
+ return !1;
1113
+ }, Ye = (e) => e.body.some((e) => e.type === "ReturnStatement" && e.argument !== null && ["ArrowFunctionExpression", "FunctionExpression"].includes(e.argument.type)), Q = n({
1114
+ name: "require-effect-cleanup",
1115
+ meta: {
1116
+ type: "suggestion",
1117
+ docs: { description: "Require a cleanup return from a useEffect that registers a timer/listener/subscription" },
1118
+ schema: [],
1119
+ messages: { requireCleanup: "This effect registers a {{what}} but returns no cleanup function to tear it down." }
1120
+ },
1121
+ defaultOptions: [],
1122
+ create(e) {
1123
+ return { CallExpression(t) {
1124
+ if (t.callee.type !== "Identifier" || t.callee.name !== "useEffect") return;
1125
+ let [n] = t.arguments;
1126
+ !n || n.type !== "ArrowFunctionExpression" && n.type !== "FunctionExpression" || n.body.type !== "BlockStatement" || Z(n.body) && (Ye(n.body) || e.report({
1127
+ node: t,
1128
+ messageId: "requireCleanup",
1129
+ data: { what: "interval/listener/subscription" }
1130
+ }));
1131
+ } };
1132
+ }
1133
+ }), Xe = (e) => {
1134
+ let t = 0, n = e;
1135
+ for (; n.type === "MemberExpression" || n.type === "CallExpression";) n.optional && (t += 1), n = n.type === "CallExpression" ? n.callee : n.object;
1136
+ return t;
1137
+ }, Ze = (e) => e.parent.type === "LogicalExpression" && e.parent.operator === "??" && e.parent.left === e, Qe = n({
1138
+ name: "require-fallback-on-deep-chain",
1139
+ meta: {
1140
+ type: "suggestion",
1141
+ docs: { description: "Require a ?? fallback on a deep optional chain" },
1142
+ schema: [{
1143
+ type: "object",
1144
+ properties: { minDepth: {
1145
+ type: "integer",
1146
+ minimum: 2
1147
+ } },
1148
+ additionalProperties: !1
1149
+ }],
1150
+ messages: { requireFallback: "Optional chain is {{depth}} levels deep with no `?? fallback` — add one." }
1151
+ },
1152
+ defaultOptions: [{}],
1153
+ create(e, [t]) {
1154
+ let n = t.minDepth ?? 3;
1155
+ return { ChainExpression(t) {
1156
+ let r = Xe(t.expression);
1157
+ r < n || Ze(t) || e.report({
1158
+ node: t,
1159
+ messageId: "requireFallback",
1160
+ data: { depth: r }
1161
+ });
1162
+ } };
1163
+ }
1164
+ }), $e = n({
1165
+ name: "require-numeric-enum-initializer",
1166
+ meta: {
1167
+ type: "suggestion",
1168
+ docs: { description: "Require an explicit initializer on every enum member" },
1169
+ schema: [],
1170
+ messages: { missingInitializer: "Enum member '{{name}}' has no explicit initializer — assign an explicit value." }
1171
+ },
1172
+ defaultOptions: [],
1173
+ create(e) {
1174
+ return { TSEnumMember(t) {
1175
+ if (t.initializer) return;
1176
+ let n = t.id.type === "Identifier" ? t.id.name : e.sourceCode.getText(t.id);
1177
+ e.report({
1178
+ node: t,
1179
+ messageId: "missingInitializer",
1180
+ data: { name: n }
1181
+ });
1182
+ } };
1183
+ }
1184
+ }), et = /* @__PURE__ */ new Set(["useState", "useRef"]), tt = (e) => {
1185
+ let t = e.typeArguments;
1186
+ return !!t && t.params.length > 0;
1187
+ }, nt = (e) => {
1188
+ if (e.arguments.length === 0) return !0;
1189
+ let [t] = e.arguments;
1190
+ return t.type === "Literal" && t.value === null || t.type === "Identifier" && t.name === "undefined";
1191
+ }, $ = {
1192
+ meta: { name: "@lichens-innovation/eslint-plugin-coding-guide" },
1193
+ rules: {
1194
+ "filename-convention-by-export-shape": c,
1195
+ "hoist-static-component-constants": re,
1196
+ "max-params-project": fe,
1197
+ "no-explicit-undefined-optional": _,
1198
+ "no-exported-mutable-state": v,
1199
+ "no-hook-returning-jsx": S,
1200
+ "no-inline-array-chain-in-jsx": T,
1201
+ "no-inline-curried-handler": D,
1202
+ "no-inline-guard-chain-handler": k,
1203
+ "no-inline-object-param-type": A,
1204
+ "no-inline-render-function": P,
1205
+ "no-jsx-in-variable": F,
1206
+ "no-nested-try": I,
1207
+ "no-non-hook-use-prefix": z,
1208
+ "no-render-fn-in-usecallback": _e,
1209
+ "no-tests-in-dunder-folder": ve,
1210
+ "no-trivial-usememo": ye,
1211
+ "no-unguarded-json-parse": Se,
1212
+ "prefer-element-ref-type": Te,
1213
+ "prefer-includes-over-or-chain": De,
1214
+ "prefer-jsx-short-circuit": Ae,
1215
+ "prefer-nullish-helpers": Me,
1216
+ "prefer-positive-condition": Ne,
1217
+ "prefer-props-with-children": Pe,
1218
+ "prefer-reactnode-over-jsxelement-union": Re,
1219
+ "prefer-role-query-over-testid": Be,
1220
+ "prefer-some-over-find-check": Ve,
1221
+ "prefer-state-updater-form": Ke,
1222
+ "require-effect-cleanup": Q,
1223
+ "require-fallback-on-deep-chain": Qe,
1224
+ "require-numeric-enum-initializer": $e,
1225
+ "require-usestate-useref-generic": n({
1226
+ name: "require-usestate-useref-generic",
1227
+ meta: {
1228
+ type: "suggestion",
1229
+ docs: { description: "Require an explicit generic on useState()/useRef() when the initial value can't infer one" },
1230
+ schema: [],
1231
+ messages: { requireGeneric: "'{{name}}()' has no inferable type from its initial value — add an explicit `<Type>` generic." }
1232
+ },
1233
+ defaultOptions: [],
1234
+ create(e) {
1235
+ return { CallExpression(t) {
1236
+ t.callee.type !== "Identifier" || !et.has(t.callee.name) || tt(t) || nt(t) && e.report({
1237
+ node: t,
1238
+ messageId: "requireGeneric",
1239
+ data: { name: t.callee.name }
1240
+ });
1241
+ } };
1242
+ }
1243
+ }),
1244
+ "todo-ticket-ref": n({
1245
+ name: "todo-ticket-ref",
1246
+ meta: {
1247
+ type: "suggestion",
1248
+ docs: { description: "Require a ticket reference in the TODO comment" },
1249
+ schema: [{
1250
+ type: "object",
1251
+ properties: {
1252
+ pattern: { type: "string" },
1253
+ terms: {
1254
+ type: "array",
1255
+ items: { type: "string" }
1256
+ },
1257
+ commentPattern: { type: "string" },
1258
+ description: { type: "string" },
1259
+ comment: { type: "string" }
1260
+ },
1261
+ additionalProperties: !1
1262
+ }],
1263
+ messages: {
1264
+ missingTicket: "{{ term }} comment doesn't reference a ticket number. Ticket pattern: {{ pattern }}",
1265
+ missingTicketWithCommentPattern: "{{ term }} comment doesn't reference a ticket number. Comment pattern: {{ commentPattern }}",
1266
+ missingTicketWithDescription: "{{ term }} comment doesn't reference a ticket number. {{ description }}"
1267
+ }
1268
+ },
1269
+ defaultOptions: [{}],
1270
+ create(e, [t]) {
1271
+ let n = t.pattern ?? "([A-Z]+-\\d+)", r = t.terms ?? ["TODO"], i = t.commentPattern, a = t.description, o = e.sourceCode.getAllComments(), s = new RegExp(n, "i"), c = {};
1272
+ for (let e of r) c[e] = i ? new RegExp(i, "i") : s;
1273
+ let l = () => a ? "missingTicketWithDescription" : i ? "missingTicketWithCommentPattern" : "missingTicket";
1274
+ return o.forEach((t) => {
1275
+ let o = t.value;
1276
+ for (let s of r) o.includes(s) && (c[s].test(o) || e.report({
1277
+ loc: t.loc,
1278
+ messageId: l(),
1279
+ data: {
1280
+ term: s,
1281
+ pattern: n,
1282
+ commentPattern: i ?? "",
1283
+ description: a ?? ""
1284
+ }
1285
+ }));
1286
+ }), {};
1287
+ }
1288
+ })
1289
+ }
1290
+ }, rt = { recommended: t($) };
1291
+ //#endregion
1292
+ export { rt as configs, $ as default };
1293
+
1294
+ //# sourceMappingURL=index.js.map