@fictjs/eslint-plugin 0.0.2

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/dist/index.cjs ADDED
@@ -0,0 +1,815 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => index_default,
24
+ noDirectMutation: () => no_direct_mutation_default,
25
+ noEmptyEffect: () => no_empty_effect_default,
26
+ noInlineFunctions: () => no_inline_functions_default,
27
+ noMemoSideEffects: () => no_memo_side_effects_default,
28
+ noNestedComponents: () => no_nested_components_default,
29
+ noStateDestructureWrite: () => no_state_destructure_write_default,
30
+ noStateInLoop: () => no_state_in_loop_default,
31
+ requireComponentReturn: () => require_component_return_default,
32
+ requireListKey: () => require_list_key_default
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/rules/no-direct-mutation.ts
37
+ var rule = {
38
+ meta: {
39
+ type: "suggestion",
40
+ docs: {
41
+ description: "Warn against direct mutation of $state objects",
42
+ recommended: true
43
+ },
44
+ messages: {
45
+ noDirectMutation: "Direct mutation of nested $state properties may not trigger updates. Use spread syntax or $store for deep reactivity."
46
+ },
47
+ schema: []
48
+ },
49
+ create(context) {
50
+ const stateVariables = /* @__PURE__ */ new Set();
51
+ return {
52
+ VariableDeclarator(node) {
53
+ if (node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && node.init.callee.name === "$state" && node.id.type === "Identifier") {
54
+ stateVariables.add(node.id.name);
55
+ }
56
+ },
57
+ AssignmentExpression(node) {
58
+ if (node.left.type === "MemberExpression") {
59
+ const root = getRootIdentifier(node.left);
60
+ if (root && stateVariables.has(root.name)) {
61
+ if (isDeepAccess(node.left)) {
62
+ context.report({
63
+ node,
64
+ messageId: "noDirectMutation"
65
+ });
66
+ }
67
+ }
68
+ }
69
+ }
70
+ };
71
+ }
72
+ };
73
+ function getRootIdentifier(node) {
74
+ let current = node;
75
+ while (current.type === "MemberExpression") {
76
+ current = current.object;
77
+ }
78
+ return current.type === "Identifier" ? current : null;
79
+ }
80
+ function isDeepAccess(node) {
81
+ let depth = 0;
82
+ let current = node;
83
+ while (current.type === "MemberExpression") {
84
+ depth++;
85
+ current = current.object;
86
+ }
87
+ return depth > 1;
88
+ }
89
+ var no_direct_mutation_default = rule;
90
+
91
+ // src/rules/no-empty-effect.ts
92
+ var rule2 = {
93
+ meta: {
94
+ type: "suggestion",
95
+ docs: {
96
+ description: "Disallow $effect bodies that do not read any reactive value (FICT-E001)",
97
+ recommended: true
98
+ },
99
+ messages: {
100
+ emptyEffect: "$effect should reference at least one reactive value (FICT-E001)."
101
+ },
102
+ schema: []
103
+ },
104
+ create(context) {
105
+ const builtinIgnore = /* @__PURE__ */ new Set([
106
+ "console",
107
+ "Math",
108
+ "Date",
109
+ "JSON",
110
+ "Number",
111
+ "String",
112
+ "Boolean",
113
+ "Symbol",
114
+ "BigInt",
115
+ "Reflect",
116
+ "RegExp"
117
+ ]);
118
+ const collectLocals = (node, locals) => {
119
+ if (!node || node.type !== "BlockStatement") return;
120
+ for (const stmt of node.body) {
121
+ if (stmt.type === "VariableDeclaration") {
122
+ for (const decl of stmt.declarations) {
123
+ if (decl.id.type === "Identifier") {
124
+ locals.add(decl.id.name);
125
+ } else if (decl.id.type === "ObjectPattern" || decl.id.type === "ArrayPattern") {
126
+ const visit = (p) => {
127
+ if (!p) return;
128
+ if (p.type === "Identifier") {
129
+ locals.add(p.name);
130
+ } else if (p.type === "RestElement") {
131
+ visit(p.argument);
132
+ } else if (p.type === "ObjectPattern") {
133
+ for (const prop of p.properties) {
134
+ if (prop.type === "Property") {
135
+ visit(prop.value);
136
+ } else if (prop.type === "RestElement") {
137
+ visit(prop.argument);
138
+ }
139
+ }
140
+ } else if (p.type === "ArrayPattern") {
141
+ p.elements.forEach(visit);
142
+ }
143
+ };
144
+ visit(decl.id);
145
+ }
146
+ }
147
+ }
148
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name) {
149
+ locals.add(stmt.id.name);
150
+ }
151
+ }
152
+ };
153
+ const hasOuterReference = (node, locals) => {
154
+ let found = false;
155
+ const visit = (n) => {
156
+ if (found) return;
157
+ if (!n) return;
158
+ if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression") {
159
+ return;
160
+ }
161
+ if (n.type === "Identifier") {
162
+ if (!locals.has(n.name) && !builtinIgnore.has(n.name)) {
163
+ found = true;
164
+ }
165
+ return;
166
+ }
167
+ if (n.type === "MemberExpression") {
168
+ visit(n.object);
169
+ if (n.computed) {
170
+ visit(n.property);
171
+ }
172
+ return;
173
+ }
174
+ for (const key of Object.keys(n)) {
175
+ if (key === "parent") continue;
176
+ const value = n[key];
177
+ if (!value) continue;
178
+ if (Array.isArray(value)) {
179
+ for (const child of value) {
180
+ if (child && typeof child.type === "string") visit(child);
181
+ }
182
+ } else if (value && typeof value.type === "string") {
183
+ visit(value);
184
+ }
185
+ }
186
+ };
187
+ visit(node);
188
+ return found;
189
+ };
190
+ return {
191
+ CallExpression(node) {
192
+ if (node.callee.type === "Identifier" && node.callee.name === "$effect") {
193
+ const firstArg = node.arguments[0];
194
+ if (firstArg && (firstArg.type === "ArrowFunctionExpression" || firstArg.type === "FunctionExpression")) {
195
+ if (firstArg.body.type !== "BlockStatement") {
196
+ return;
197
+ }
198
+ const block = firstArg.body;
199
+ if (block.body.length === 0) {
200
+ context.report({ node, messageId: "emptyEffect" });
201
+ return;
202
+ }
203
+ const locals = /* @__PURE__ */ new Set();
204
+ firstArg.params.forEach((param) => {
205
+ if (param.type === "Identifier") locals.add(param.name);
206
+ });
207
+ collectLocals(block, locals);
208
+ if (!hasOuterReference(block, locals)) {
209
+ context.report({ node, messageId: "emptyEffect" });
210
+ }
211
+ }
212
+ }
213
+ }
214
+ };
215
+ }
216
+ };
217
+ var no_empty_effect_default = rule2;
218
+
219
+ // src/rules/no-inline-functions.ts
220
+ var rule3 = {
221
+ meta: {
222
+ type: "suggestion",
223
+ docs: {
224
+ description: "Disallow inline function definitions in JSX props that may cause unnecessary re-renders",
225
+ recommended: true
226
+ },
227
+ messages: {
228
+ // Message matches DiagnosticCode.FICT_X003
229
+ inlineFunction: "Inline function in JSX props may cause unnecessary re-renders. Consider memoizing with $memo or moving outside the render."
230
+ },
231
+ schema: [
232
+ {
233
+ type: "object",
234
+ properties: {
235
+ allowEventHandlers: {
236
+ type: "boolean",
237
+ description: "Allow inline functions for event handlers (onClick, onChange, etc.)"
238
+ }
239
+ },
240
+ additionalProperties: false
241
+ }
242
+ ]
243
+ },
244
+ create(context) {
245
+ const options = context.options[0] || {};
246
+ const allowEventHandlers = options.allowEventHandlers ?? true;
247
+ const eventHandlerPattern = /^on[A-Z]/;
248
+ return {
249
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSX node type not available in base ESLint types
250
+ JSXAttribute(node) {
251
+ if (node.value && node.value.type === "JSXExpressionContainer" && node.value.expression.type !== "JSXEmptyExpression") {
252
+ const expr = node.value.expression;
253
+ if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
254
+ if (allowEventHandlers && node.name.type === "JSXIdentifier") {
255
+ const attrName = node.name.name;
256
+ if (eventHandlerPattern.test(attrName)) {
257
+ return;
258
+ }
259
+ }
260
+ context.report({
261
+ node: expr,
262
+ messageId: "inlineFunction"
263
+ });
264
+ }
265
+ }
266
+ }
267
+ };
268
+ }
269
+ };
270
+ var no_inline_functions_default = rule3;
271
+
272
+ // src/rules/no-memo-side-effects.ts
273
+ var rule4 = {
274
+ meta: {
275
+ type: "problem",
276
+ docs: {
277
+ description: "Disallow obvious side effects inside $memo callbacks (FICT-M003)",
278
+ recommended: true
279
+ },
280
+ messages: {
281
+ sideEffectInMemo: "Avoid side effects inside $memo. Move mutations/effects outside or wrap them in $effect (FICT-M003)."
282
+ },
283
+ schema: []
284
+ },
285
+ create(context) {
286
+ const hasSideEffect = (node) => {
287
+ let found = false;
288
+ const visit = (n) => {
289
+ if (found) return;
290
+ if (n.type === "AssignmentExpression" || n.type === "UpdateExpression") {
291
+ found = true;
292
+ return;
293
+ }
294
+ if (n.type === "CallExpression") {
295
+ if (n.callee.type === "Identifier" && n.callee.name === "$effect") {
296
+ found = true;
297
+ return;
298
+ }
299
+ }
300
+ if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression") {
301
+ return;
302
+ }
303
+ for (const key of Object.keys(n)) {
304
+ if (key === "parent") continue;
305
+ const value = n[key];
306
+ if (!value) continue;
307
+ if (Array.isArray(value)) {
308
+ for (const child of value) {
309
+ if (child && typeof child.type === "string") visit(child);
310
+ }
311
+ } else if (value && typeof value.type === "string") {
312
+ visit(value);
313
+ }
314
+ }
315
+ };
316
+ visit(node);
317
+ return found;
318
+ };
319
+ return {
320
+ CallExpression(node) {
321
+ if (node.callee.type !== "Identifier" || node.callee.name !== "$memo") return;
322
+ const first = node.arguments[0];
323
+ if (first && (first.type === "ArrowFunctionExpression" || first.type === "FunctionExpression")) {
324
+ const body = first.type === "ArrowFunctionExpression" && first.body.type !== "BlockStatement" ? first.body : first.body;
325
+ if (hasSideEffect(body)) {
326
+ context.report({ node, messageId: "sideEffectInMemo" });
327
+ }
328
+ }
329
+ }
330
+ };
331
+ }
332
+ };
333
+ var no_memo_side_effects_default = rule4;
334
+
335
+ // src/rules/no-nested-components.ts
336
+ var rule5 = {
337
+ meta: {
338
+ type: "problem",
339
+ docs: {
340
+ description: "Disallow defining components inside other components (FICT-C003)",
341
+ recommended: true
342
+ },
343
+ messages: {
344
+ nestedComponent: "Do not define a component inside another component. Move {{name}} to module scope to avoid recreating it on every render."
345
+ },
346
+ schema: []
347
+ },
348
+ create(context) {
349
+ const componentStack = [];
350
+ const isUpperCaseName = (name) => !!name && /^[A-Z]/.test(name);
351
+ const getFunctionName = (node) => {
352
+ if (node.id?.name) {
353
+ return node.id.name;
354
+ }
355
+ const parent = node.parent;
356
+ if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") {
357
+ return parent.id.name;
358
+ }
359
+ return void 0;
360
+ };
361
+ const hasJSX = (node) => {
362
+ let found = false;
363
+ const visit = (n) => {
364
+ if (found) return;
365
+ const t = n.type;
366
+ if (t === "JSXElement" || t === "JSXFragment") {
367
+ found = true;
368
+ return;
369
+ }
370
+ if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression") {
371
+ return;
372
+ }
373
+ for (const key of Object.keys(n)) {
374
+ if (key === "parent") continue;
375
+ const value = n[key];
376
+ if (!value) continue;
377
+ if (Array.isArray(value)) {
378
+ for (const child of value) {
379
+ if (child && typeof child.type === "string") visit(child);
380
+ }
381
+ } else if (value && typeof value.type === "string") {
382
+ visit(value);
383
+ }
384
+ }
385
+ };
386
+ visit(node);
387
+ return found;
388
+ };
389
+ const isComponentLike = (node) => {
390
+ const name = getFunctionName(node);
391
+ if (isUpperCaseName(name)) return true;
392
+ if (node.type === "ArrowFunctionExpression" && node.body) {
393
+ const bodyType = node.body.type;
394
+ if (bodyType === "JSXElement" || bodyType === "JSXFragment") return true;
395
+ if (bodyType === "BlockStatement" && hasJSX(node.body)) return true;
396
+ }
397
+ if (node.type !== "ArrowFunctionExpression" && node.body && hasJSX(node.body))
398
+ return true;
399
+ return false;
400
+ };
401
+ const enterFunction = (node) => {
402
+ const parentIsComponent = componentStack[componentStack.length - 1] ?? false;
403
+ const currentIsComponent = isComponentLike(node);
404
+ if (parentIsComponent && currentIsComponent) {
405
+ const name = getFunctionName(node) ?? "this component";
406
+ context.report({
407
+ node,
408
+ messageId: "nestedComponent",
409
+ data: { name }
410
+ });
411
+ }
412
+ componentStack.push(parentIsComponent || currentIsComponent);
413
+ };
414
+ const exitFunction = () => {
415
+ componentStack.pop();
416
+ };
417
+ return {
418
+ FunctionDeclaration: enterFunction,
419
+ "FunctionDeclaration:exit": exitFunction,
420
+ FunctionExpression: enterFunction,
421
+ "FunctionExpression:exit": exitFunction,
422
+ ArrowFunctionExpression: enterFunction,
423
+ "ArrowFunctionExpression:exit": exitFunction
424
+ };
425
+ }
426
+ };
427
+ var no_nested_components_default = rule5;
428
+
429
+ // src/rules/no-state-in-loop.ts
430
+ var rule6 = {
431
+ meta: {
432
+ type: "problem",
433
+ docs: {
434
+ description: "Disallow $state declarations inside loops",
435
+ recommended: true
436
+ },
437
+ messages: {
438
+ noStateInLoop: "$state should not be declared inside a loop. Move it outside the loop."
439
+ },
440
+ schema: []
441
+ },
442
+ create(context) {
443
+ let loopDepth = 0;
444
+ return {
445
+ ForStatement() {
446
+ loopDepth++;
447
+ },
448
+ "ForStatement:exit"() {
449
+ loopDepth--;
450
+ },
451
+ ForInStatement() {
452
+ loopDepth++;
453
+ },
454
+ "ForInStatement:exit"() {
455
+ loopDepth--;
456
+ },
457
+ ForOfStatement() {
458
+ loopDepth++;
459
+ },
460
+ "ForOfStatement:exit"() {
461
+ loopDepth--;
462
+ },
463
+ WhileStatement() {
464
+ loopDepth++;
465
+ },
466
+ "WhileStatement:exit"() {
467
+ loopDepth--;
468
+ },
469
+ DoWhileStatement() {
470
+ loopDepth++;
471
+ },
472
+ "DoWhileStatement:exit"() {
473
+ loopDepth--;
474
+ },
475
+ CallExpression(node) {
476
+ if (loopDepth > 0 && node.callee.type === "Identifier" && node.callee.name === "$state") {
477
+ context.report({
478
+ node,
479
+ messageId: "noStateInLoop"
480
+ });
481
+ }
482
+ }
483
+ };
484
+ }
485
+ };
486
+ var no_state_in_loop_default = rule6;
487
+
488
+ // src/rules/no-state-destructure-write.ts
489
+ var rule7 = {
490
+ meta: {
491
+ type: "problem",
492
+ docs: {
493
+ description: "Disallow writing to destructured aliases from $state; write via the original state instead.",
494
+ recommended: true
495
+ },
496
+ messages: {
497
+ noWrite: "Do not write to '{name}' (destructured from $state). Update via the original state object (e.g. state.count++ or immutable update)."
498
+ },
499
+ schema: []
500
+ },
501
+ create(context) {
502
+ const stateVars = /* @__PURE__ */ new Set();
503
+ const destructuredAliases = /* @__PURE__ */ new Set();
504
+ const collectIds = (pattern) => {
505
+ const ids = [];
506
+ const visit = (p) => {
507
+ if (p.type === "Identifier") {
508
+ ids.push(p);
509
+ return;
510
+ }
511
+ if (p.type === "RestElement") {
512
+ visit(p.argument);
513
+ return;
514
+ }
515
+ if (p.type === "ObjectPattern") {
516
+ for (const prop of p.properties) {
517
+ if (prop.type === "Property") {
518
+ if (prop.value.type === "Identifier") {
519
+ ids.push(prop.value);
520
+ } else {
521
+ visit(prop.value);
522
+ }
523
+ } else if (prop.type === "RestElement") {
524
+ visit(prop.argument);
525
+ }
526
+ }
527
+ return;
528
+ }
529
+ if (p.type === "ArrayPattern") {
530
+ for (const el of p.elements) {
531
+ if (!el) continue;
532
+ visit(el);
533
+ }
534
+ }
535
+ };
536
+ visit(pattern);
537
+ return ids;
538
+ };
539
+ const markDestructure = (node) => {
540
+ if (!node.id || node.id.type !== "ObjectPattern" && node.id.type !== "ArrayPattern") return;
541
+ const init = node.init;
542
+ if (!init) return;
543
+ if (init.type === "Identifier" && stateVars.has(init.name)) {
544
+ collectIds(node.id).forEach((id) => destructuredAliases.add(id.name));
545
+ }
546
+ };
547
+ const isAliasWrite = (name) => destructuredAliases.has(name);
548
+ return {
549
+ VariableDeclarator(node) {
550
+ if (node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && node.init.callee.name === "$state" && node.id.type === "Identifier") {
551
+ stateVars.add(node.id.name);
552
+ }
553
+ markDestructure(node);
554
+ },
555
+ AssignmentExpression(node) {
556
+ if (node.left.type === "Identifier" && isAliasWrite(node.left.name)) {
557
+ context.report({
558
+ node,
559
+ messageId: "noWrite",
560
+ data: { name: node.left.name }
561
+ });
562
+ }
563
+ },
564
+ UpdateExpression(node) {
565
+ if (node.argument.type === "Identifier" && isAliasWrite(node.argument.name)) {
566
+ context.report({
567
+ node,
568
+ messageId: "noWrite",
569
+ data: { name: node.argument.name }
570
+ });
571
+ }
572
+ }
573
+ };
574
+ }
575
+ };
576
+ var no_state_destructure_write_default = rule7;
577
+
578
+ // src/rules/require-component-return.ts
579
+ var rule8 = {
580
+ meta: {
581
+ type: "problem",
582
+ docs: {
583
+ description: "Require component functions to return a value (FICT-C004)",
584
+ recommended: true
585
+ },
586
+ messages: {
587
+ missingReturn: "Component should return JSX or null/undefined (FICT-C004)."
588
+ },
589
+ schema: []
590
+ },
591
+ create(context) {
592
+ const isUpperCaseName = (name) => !!name && /^[A-Z]/.test(name);
593
+ const getFunctionName = (node) => {
594
+ if (node.id?.name) {
595
+ return node.id.name;
596
+ }
597
+ const parent = node.parent;
598
+ if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") {
599
+ return parent.id.name;
600
+ }
601
+ return void 0;
602
+ };
603
+ const hasReturn = (node) => {
604
+ const visit = (n) => {
605
+ switch (n.type) {
606
+ case "ReturnStatement":
607
+ return true;
608
+ case "BlockStatement":
609
+ return n.body.some((stmt) => visit(stmt));
610
+ case "IfStatement":
611
+ return visit(n.consequent) || !!n.alternate && visit(n.alternate);
612
+ case "SwitchStatement":
613
+ return n.cases.some((c) => c.consequent.some(visit));
614
+ case "WhileStatement":
615
+ case "DoWhileStatement":
616
+ case "ForStatement":
617
+ case "ForInStatement":
618
+ case "ForOfStatement":
619
+ return visit(n.body);
620
+ case "TryStatement": {
621
+ const t = n;
622
+ return t.block && visit(t.block) || !!t.handler && visit(t.handler.body) || !!t.finalizer && visit(t.finalizer);
623
+ }
624
+ default:
625
+ if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassDeclaration" || n.type === "ClassExpression") {
626
+ return false;
627
+ }
628
+ for (const key of Object.keys(n)) {
629
+ if (key === "parent") continue;
630
+ const value = n[key];
631
+ if (!value) continue;
632
+ if (Array.isArray(value)) {
633
+ if (value.some((child) => child && typeof child.type === "string" && visit(child))) {
634
+ return true;
635
+ }
636
+ } else if (value && typeof value.type === "string") {
637
+ if (visit(value)) return true;
638
+ }
639
+ }
640
+ return false;
641
+ }
642
+ };
643
+ return visit(node);
644
+ };
645
+ const enter = (node) => {
646
+ const name = getFunctionName(node);
647
+ const isComponent = isUpperCaseName(name);
648
+ if (!isComponent) return;
649
+ if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
650
+ return;
651
+ }
652
+ const body = node.type === "ArrowFunctionExpression" ? node.body : node.body;
653
+ if (body && body.type === "BlockStatement" && !hasReturn(body)) {
654
+ context.report({
655
+ node,
656
+ messageId: "missingReturn"
657
+ });
658
+ }
659
+ };
660
+ return {
661
+ FunctionDeclaration: enter,
662
+ FunctionExpression: enter,
663
+ ArrowFunctionExpression: enter
664
+ };
665
+ }
666
+ };
667
+ var require_component_return_default = rule8;
668
+
669
+ // src/rules/require-list-key.ts
670
+ var rule9 = {
671
+ meta: {
672
+ type: "problem",
673
+ docs: {
674
+ description: "Require key on elements returned from Array.prototype.map in JSX (FICT-J002)",
675
+ recommended: true
676
+ },
677
+ messages: {
678
+ missingKey: "Elements returned from map() in JSX should have a stable key (FICT-J002). Add key={...}."
679
+ },
680
+ schema: []
681
+ },
682
+ create(context) {
683
+ const hasKeyAttribute = (node) => {
684
+ if (node.type === "JSXFragment") {
685
+ return false;
686
+ }
687
+ if (node.type !== "JSXElement") return false;
688
+ return (node.openingElement?.attributes ?? []).some((attr) => {
689
+ if (!attr || attr.type !== "JSXAttribute") return false;
690
+ return attr.name?.name === "key";
691
+ });
692
+ };
693
+ const collectReturnedJSX = (expr, out) => {
694
+ const target = expr.type === "ReturnStatement" ? expr.argument ?? void 0 : expr;
695
+ if (!target) return;
696
+ const t = target.type;
697
+ if (t === "JSXElement" || t === "JSXFragment") {
698
+ out.push(target);
699
+ return;
700
+ }
701
+ if (t === "ArrayExpression") {
702
+ for (const el of target.elements) {
703
+ if (!el || el.type === "SpreadElement") continue;
704
+ collectReturnedJSX(el, out);
705
+ }
706
+ }
707
+ if (t === "ConditionalExpression") {
708
+ collectReturnedJSX(target.consequent, out);
709
+ collectReturnedJSX(target.alternate, out);
710
+ }
711
+ if (t === "LogicalExpression") {
712
+ collectReturnedJSX(target.right, out);
713
+ }
714
+ };
715
+ const mapReturnsJSXWithoutKey = (call) => {
716
+ const callback = call.arguments[0];
717
+ if (!callback) return null;
718
+ if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
719
+ return null;
720
+ }
721
+ const returned = [];
722
+ if (callback.type === "ArrowFunctionExpression") {
723
+ const body = callback.body;
724
+ const bodyType = body.type;
725
+ if (bodyType === "JSXElement" || bodyType === "JSXFragment") {
726
+ returned.push(body);
727
+ } else if (bodyType === "BlockStatement") {
728
+ for (const stmt of body.body) {
729
+ if (stmt.type === "ReturnStatement") {
730
+ collectReturnedJSX(stmt, returned);
731
+ }
732
+ }
733
+ } else {
734
+ collectReturnedJSX(body, returned);
735
+ }
736
+ } else {
737
+ for (const stmt of callback.body.body) {
738
+ if (stmt.type === "ReturnStatement") {
739
+ collectReturnedJSX(stmt, returned);
740
+ }
741
+ }
742
+ }
743
+ const missing = returned.find((node) => !hasKeyAttribute(node));
744
+ return missing ?? null;
745
+ };
746
+ return {
747
+ CallExpression(node) {
748
+ if (node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && node.callee.property.name === "map") {
749
+ const offending = mapReturnsJSXWithoutKey(node);
750
+ if (offending) {
751
+ context.report({
752
+ node: offending,
753
+ messageId: "missingKey"
754
+ });
755
+ }
756
+ }
757
+ }
758
+ };
759
+ }
760
+ };
761
+ var require_list_key_default = rule9;
762
+
763
+ // src/index.ts
764
+ var plugin = {
765
+ meta: {
766
+ name: "eslint-plugin-fict",
767
+ version: "0.0.1"
768
+ },
769
+ rules: {
770
+ "no-state-in-loop": no_state_in_loop_default,
771
+ "no-direct-mutation": no_direct_mutation_default,
772
+ "no-empty-effect": no_empty_effect_default,
773
+ "no-inline-functions": no_inline_functions_default,
774
+ "no-state-destructure-write": no_state_destructure_write_default,
775
+ "no-nested-components": no_nested_components_default,
776
+ "require-list-key": require_list_key_default,
777
+ "no-memo-side-effects": no_memo_side_effects_default,
778
+ "require-component-return": require_component_return_default
779
+ },
780
+ configs: {
781
+ recommended: {
782
+ plugins: ["fict"],
783
+ rules: {
784
+ "fict/no-state-in-loop": "error",
785
+ "fict/no-direct-mutation": "warn",
786
+ "fict/no-empty-effect": "warn",
787
+ // FICT-E001
788
+ "fict/no-inline-functions": "warn",
789
+ // FICT-X003
790
+ "fict/no-state-destructure-write": "error",
791
+ "fict/no-nested-components": "error",
792
+ // FICT-C003
793
+ "fict/require-list-key": "error",
794
+ // FICT-J002
795
+ "fict/no-memo-side-effects": "warn",
796
+ // FICT-M003
797
+ "fict/require-component-return": "warn"
798
+ // FICT-C004
799
+ }
800
+ }
801
+ }
802
+ };
803
+ var index_default = plugin;
804
+ // Annotate the CommonJS export names for ESM import in node:
805
+ 0 && (module.exports = {
806
+ noDirectMutation,
807
+ noEmptyEffect,
808
+ noInlineFunctions,
809
+ noMemoSideEffects,
810
+ noNestedComponents,
811
+ noStateDestructureWrite,
812
+ noStateInLoop,
813
+ requireComponentReturn,
814
+ requireListKey
815
+ });