@slip-stream-kit/eslint-plugin 0.1.17 → 0.1.20
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.js +634 -32
- package/dist/index.js.map +4 -4
- package/dist/rules/{component-arrow-function.d.ts → component-arrow-function/component-arrow-function.d.ts} +0 -1
- package/dist/rules/component-arrow-function/index.d.ts +1 -0
- package/dist/rules/{component-file-order.d.ts → component-file-order/component-file-order.d.ts} +0 -1
- package/dist/rules/component-file-order/index.d.ts +1 -0
- package/dist/rules/max-components-per-file/index.d.ts +1 -0
- package/dist/rules/max-components-per-file/max-components-per-file.d.ts +2 -0
- package/dist/rules/max-jsx-return-size/index.d.ts +1 -0
- package/dist/rules/max-jsx-return-size/max-jsx-return-size.d.ts +2 -0
- package/dist/rules/props-destructuring-blank-line/index.d.ts +1 -0
- package/dist/rules/{props-destructuring-blank-line.d.ts → props-destructuring-blank-line/props-destructuring-blank-line.d.ts} +0 -1
- package/dist/rules/props-destructuring-newline/index.d.ts +1 -0
- package/dist/rules/{props-destructuring-newline.d.ts → props-destructuring-newline/props-destructuring-newline.d.ts} +0 -1
- package/dist/rules/props-type-name/index.d.ts +1 -0
- package/dist/rules/{props-type-name.d.ts → props-type-name/props-type-name.d.ts} +0 -1
- package/dist/rules/props-type-reference/index.d.ts +1 -0
- package/dist/rules/{props-type-reference.d.ts → props-type-reference/props-type-reference.d.ts} +0 -1
- package/dist/rules/require-component-stories/index.d.ts +1 -0
- package/dist/rules/{require-component-stories.d.ts → require-component-stories/require-component-stories.d.ts} +0 -1
- package/dist/rules/require-jsdoc-example/index.d.ts +1 -0
- package/dist/rules/require-jsdoc-example/require-jsdoc-example.d.ts +2 -0
- package/dist/utils/cognitive-complexity.d.ts +14 -0
- package/dist/utils/component.d.ts +14 -0
- package/package.json +3 -3
- package/readme.md +159 -0
package/dist/index.js
CHANGED
|
@@ -40,18 +40,18 @@ var getComponentName = (node) => {
|
|
|
40
40
|
}
|
|
41
41
|
return null;
|
|
42
42
|
};
|
|
43
|
-
var
|
|
43
|
+
var collectOwnReturnArguments = (node) => {
|
|
44
44
|
if (node.body.type !== "BlockStatement") {
|
|
45
|
-
return
|
|
45
|
+
return [node.body];
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
const args = [];
|
|
48
48
|
const visit = (current) => {
|
|
49
|
-
if (
|
|
49
|
+
if (!current || NESTED_SCOPES.has(current.type)) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
if (current.type === "ReturnStatement") {
|
|
53
|
-
if (
|
|
54
|
-
|
|
53
|
+
if (current.argument) {
|
|
54
|
+
args.push(current.argument);
|
|
55
55
|
}
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
@@ -81,7 +81,10 @@ var returnsJsx = (node) => {
|
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
83
|
node.body.body.forEach(visit);
|
|
84
|
-
return
|
|
84
|
+
return args;
|
|
85
|
+
};
|
|
86
|
+
var returnsJsx = (node) => {
|
|
87
|
+
return collectOwnReturnArguments(node).some(isJsxNode);
|
|
85
88
|
};
|
|
86
89
|
var isComponent = (node) => {
|
|
87
90
|
const name = getComponentName(node);
|
|
@@ -226,7 +229,7 @@ var matchesAnyGlob = (filename, patterns) => {
|
|
|
226
229
|
});
|
|
227
230
|
};
|
|
228
231
|
|
|
229
|
-
// src/rules/component-arrow-function.ts
|
|
232
|
+
// src/rules/component-arrow-function/component-arrow-function.ts
|
|
230
233
|
var ANONYMOUS_NAME = "component";
|
|
231
234
|
var reportName = (fn) => {
|
|
232
235
|
return getComponentName(fn) ?? ANONYMOUS_NAME;
|
|
@@ -272,7 +275,7 @@ var componentArrowFunction = {
|
|
|
272
275
|
docs: {
|
|
273
276
|
description: "Enforce that React components are declared as arrow functions, not `function` declarations or function expressions.",
|
|
274
277
|
recommended: true,
|
|
275
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
278
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#component-arrow-function"
|
|
276
279
|
},
|
|
277
280
|
schema: [
|
|
278
281
|
{
|
|
@@ -317,7 +320,7 @@ var componentArrowFunction = {
|
|
|
317
320
|
}
|
|
318
321
|
};
|
|
319
322
|
|
|
320
|
-
// src/rules/component-file-order.ts
|
|
323
|
+
// src/rules/component-file-order/component-file-order.ts
|
|
321
324
|
var PROPS_SUFFIX = "Props";
|
|
322
325
|
var getDirectivePrologueCount = (body) => {
|
|
323
326
|
let count = 0;
|
|
@@ -371,6 +374,7 @@ var hasStrayBefore = (body, boundary, directiveCount, skipIndex) => {
|
|
|
371
374
|
return index < boundary && index >= directiveCount && index !== skipIndex && statement.type !== "ImportDeclaration";
|
|
372
375
|
});
|
|
373
376
|
};
|
|
377
|
+
var ANONYMOUS_COMPONENT = "component";
|
|
374
378
|
var findImportOrderViolations = (importIndices, importBoundary) => {
|
|
375
379
|
return importIndices.filter((importIndex) => {
|
|
376
380
|
return importIndex > importBoundary;
|
|
@@ -393,7 +397,11 @@ var findAdjacencyViolations = (components, declIndexByName) => {
|
|
|
393
397
|
}
|
|
394
398
|
const propsIndex = declIndexByName.get(propsName);
|
|
395
399
|
if (propsIndex !== void 0 && propsIndex !== component.index - 1) {
|
|
396
|
-
violations.push({
|
|
400
|
+
violations.push({
|
|
401
|
+
index: propsIndex,
|
|
402
|
+
messageId: "interfaceImmediatelyBeforeComponent",
|
|
403
|
+
data: { interface: propsName, component: component.name ?? ANONYMOUS_COMPONENT }
|
|
404
|
+
});
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
407
|
return violations;
|
|
@@ -404,14 +412,24 @@ var findAnchorViolations = (body, directiveCount, importIndices, first, firstPro
|
|
|
404
412
|
return importIndex < firstPropsIndex;
|
|
405
413
|
});
|
|
406
414
|
if (importsBeforeInterface && hasStrayBefore(body, firstPropsIndex, directiveCount, first.index)) {
|
|
407
|
-
violations.push({
|
|
415
|
+
violations.push({
|
|
416
|
+
index: firstPropsIndex,
|
|
417
|
+
messageId: "interfaceImmediatelyAfterImports",
|
|
418
|
+
// `firstPropsIndex !== undefined` implies `firstPropsName !== null` (it is derived from it).
|
|
419
|
+
data: { interface: firstPropsName, component: first.name ?? ANONYMOUS_COMPONENT }
|
|
420
|
+
});
|
|
408
421
|
}
|
|
409
422
|
const firstPropsImported = firstPropsIndex === void 0 && firstPropsName !== null && importedNames.has(firstPropsName);
|
|
410
423
|
const importsBeforeComponent = importIndices.every((importIndex) => {
|
|
411
424
|
return importIndex < first.index;
|
|
412
425
|
});
|
|
413
426
|
if (firstPropsImported && importsBeforeComponent && hasStrayBefore(body, first.index, directiveCount)) {
|
|
414
|
-
violations.push({
|
|
427
|
+
violations.push({
|
|
428
|
+
index: first.index,
|
|
429
|
+
messageId: "componentImmediatelyAfterImports",
|
|
430
|
+
// `firstPropsImported` requires `firstPropsName !== null`.
|
|
431
|
+
data: { interface: firstPropsName, component: first.name ?? ANONYMOUS_COMPONENT }
|
|
432
|
+
});
|
|
415
433
|
}
|
|
416
434
|
return violations;
|
|
417
435
|
};
|
|
@@ -421,7 +439,7 @@ var componentFileOrder = {
|
|
|
421
439
|
docs: {
|
|
422
440
|
description: "Enforce a strict top-level order in React component files: imports first, then \u2014 for each component \u2014 its props interface/type declared immediately before the component.",
|
|
423
441
|
recommended: true,
|
|
424
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
442
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#component-file-order"
|
|
425
443
|
},
|
|
426
444
|
schema: [
|
|
427
445
|
{
|
|
@@ -442,10 +460,14 @@ var componentFileOrder = {
|
|
|
442
460
|
}
|
|
443
461
|
],
|
|
444
462
|
messages: {
|
|
463
|
+
// Generic on purpose: fires on a misplaced import, and the constraint is "before *every*
|
|
464
|
+
// component", so naming a single component would mislead in a multi-component file.
|
|
445
465
|
importsFirst: "Imports must come before the component interface and declaration.",
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
466
|
+
// "props type" (not "interface"): the rule matches both `interface` and `type` alias props
|
|
467
|
+
// declarations, so the neutral term reads correctly for either.
|
|
468
|
+
interfaceImmediatelyBeforeComponent: "Declare the props type `{{interface}}` immediately before component `{{component}}` (no declarations between them).",
|
|
469
|
+
interfaceImmediatelyAfterImports: "Declare the props type `{{interface}}` (for component `{{component}}`) immediately after the imports, with no other declarations in between.",
|
|
470
|
+
componentImmediatelyAfterImports: "Props type `{{interface}}` is imported, so declare component `{{component}}` immediately after the imports, with no other declarations in between."
|
|
449
471
|
}
|
|
450
472
|
},
|
|
451
473
|
create(context) {
|
|
@@ -484,14 +506,274 @@ var componentFileOrder = {
|
|
|
484
506
|
)
|
|
485
507
|
];
|
|
486
508
|
for (const violation of violations) {
|
|
487
|
-
context.report({ node: body[violation.index], messageId: violation.messageId });
|
|
509
|
+
context.report({ node: body[violation.index], messageId: violation.messageId, data: violation.data });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/rules/max-components-per-file/max-components-per-file.ts
|
|
517
|
+
var DEFAULT_MAX_COMPONENTS = 4;
|
|
518
|
+
var ANONYMOUS_NAME2 = "component";
|
|
519
|
+
var componentFunctionsIn = (declaration) => {
|
|
520
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
521
|
+
return isComponent(declaration) ? [declaration] : [];
|
|
522
|
+
}
|
|
523
|
+
if (declaration.type === "VariableDeclaration") {
|
|
524
|
+
return declaration.declarations.map((declarator) => {
|
|
525
|
+
return getComponentFunction(declarator.init);
|
|
526
|
+
}).filter((fn2) => {
|
|
527
|
+
return fn2 !== null && isComponent(fn2);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const fn = getComponentFunction(declaration);
|
|
531
|
+
return fn && isComponent(fn) ? [fn] : [];
|
|
532
|
+
};
|
|
533
|
+
var collectComponents = (body) => {
|
|
534
|
+
return body.flatMap((statement) => {
|
|
535
|
+
const declaration = unwrapExport(statement);
|
|
536
|
+
return declaration ? componentFunctionsIn(declaration) : [];
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
var maxComponentsPerFile = {
|
|
540
|
+
meta: {
|
|
541
|
+
type: "suggestion",
|
|
542
|
+
docs: {
|
|
543
|
+
description: "Limit the number of React components declared in a single file; move extra components into their own files.",
|
|
544
|
+
recommended: true,
|
|
545
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#max-components-per-file"
|
|
546
|
+
},
|
|
547
|
+
schema: [
|
|
548
|
+
{
|
|
549
|
+
type: "object",
|
|
550
|
+
properties: {
|
|
551
|
+
maxComponents: {
|
|
552
|
+
type: "integer",
|
|
553
|
+
minimum: 1,
|
|
554
|
+
description: "Maximum number of component declarations a single file may contain before the rule reports."
|
|
555
|
+
},
|
|
556
|
+
paths: {
|
|
557
|
+
type: "array",
|
|
558
|
+
items: { type: "string" },
|
|
559
|
+
description: "Optional glob patterns. When provided, the rule only runs for files whose path matches one of them."
|
|
560
|
+
},
|
|
561
|
+
ignore: {
|
|
562
|
+
type: "array",
|
|
563
|
+
items: { type: "string" },
|
|
564
|
+
description: "Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`."
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
additionalProperties: false
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
messages: {
|
|
571
|
+
// File-scoped problem → reported once, anchored to the first component over
|
|
572
|
+
// the limit so a human or AI fix loop has a concrete node to move out.
|
|
573
|
+
tooManyComponents: "This file declares {{count}} components (max {{max}}); move components such as {{name}} into separate files."
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
create(context) {
|
|
577
|
+
const options = context.options[0] ?? {};
|
|
578
|
+
const max = options.maxComponents ?? DEFAULT_MAX_COMPONENTS;
|
|
579
|
+
const paths = options.paths ?? [];
|
|
580
|
+
const ignore = options.ignore ?? [];
|
|
581
|
+
if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
|
|
582
|
+
return {};
|
|
583
|
+
}
|
|
584
|
+
if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {
|
|
585
|
+
return {};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
Program(program) {
|
|
589
|
+
const components = collectComponents(program.body);
|
|
590
|
+
if (components.length <= max) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const offender = components[max];
|
|
594
|
+
if (!offender) {
|
|
595
|
+
return;
|
|
488
596
|
}
|
|
597
|
+
const name = getComponentName(offender) ?? ANONYMOUS_NAME2;
|
|
598
|
+
context.report({
|
|
599
|
+
node: offender,
|
|
600
|
+
messageId: "tooManyComponents",
|
|
601
|
+
data: { count: components.length, max, name }
|
|
602
|
+
});
|
|
489
603
|
}
|
|
490
604
|
};
|
|
491
605
|
}
|
|
492
606
|
};
|
|
493
607
|
|
|
494
|
-
// src/rules/
|
|
608
|
+
// src/rules/max-jsx-return-size/max-jsx-return-size.ts
|
|
609
|
+
var DEFAULT_MAX_ELEMENTS = 20;
|
|
610
|
+
var ANONYMOUS_NAME3 = "component";
|
|
611
|
+
var isNode = (value) => {
|
|
612
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
613
|
+
};
|
|
614
|
+
var childNodes = (node, visitorKeys) => {
|
|
615
|
+
const children = [];
|
|
616
|
+
const keys = visitorKeys[node.type] ?? Object.keys(node);
|
|
617
|
+
for (const key of keys) {
|
|
618
|
+
if (key === "parent") {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const value = node[key];
|
|
622
|
+
if (Array.isArray(value)) {
|
|
623
|
+
children.push(...value.filter(isNode));
|
|
624
|
+
} else if (isNode(value)) {
|
|
625
|
+
children.push(value);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return children;
|
|
629
|
+
};
|
|
630
|
+
var countJsxElements = (node, visitorKeys) => {
|
|
631
|
+
const self = node.type === "JSXElement" ? 1 : 0;
|
|
632
|
+
return childNodes(node, visitorKeys).reduce((total, child) => {
|
|
633
|
+
return total + countJsxElements(child, visitorKeys);
|
|
634
|
+
}, self);
|
|
635
|
+
};
|
|
636
|
+
var jsxNameToString = (node) => {
|
|
637
|
+
const name = node;
|
|
638
|
+
switch (name?.type) {
|
|
639
|
+
case "JSXIdentifier":
|
|
640
|
+
return typeof name.name === "string" ? name.name : "element";
|
|
641
|
+
case "JSXMemberExpression":
|
|
642
|
+
return `${jsxNameToString(name.object)}.${jsxNameToString(name.property)}`;
|
|
643
|
+
case "JSXNamespacedName":
|
|
644
|
+
return `${jsxNameToString(name.namespace)}:${jsxNameToString(name.name)}`;
|
|
645
|
+
default:
|
|
646
|
+
return "element";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var topLevelElements = (root, visitorKeys) => {
|
|
650
|
+
const elements = [];
|
|
651
|
+
const walk2 = (node, isRoot) => {
|
|
652
|
+
if (!isRoot && node.type === "JSXElement") {
|
|
653
|
+
elements.push(node);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
for (const child of childNodes(node, visitorKeys)) {
|
|
657
|
+
walk2(child, false);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
walk2(root, true);
|
|
661
|
+
return elements;
|
|
662
|
+
};
|
|
663
|
+
var largestBlock = (root, visitorKeys) => {
|
|
664
|
+
let best = null;
|
|
665
|
+
for (const element2 of topLevelElements(root, visitorKeys)) {
|
|
666
|
+
const count = countJsxElements(element2, visitorKeys);
|
|
667
|
+
if (!best || count > best.count) {
|
|
668
|
+
best = { node: element2, count };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!best) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const element = best.node;
|
|
675
|
+
return { name: jsxNameToString(element.openingElement?.name), line: element.loc?.start.line ?? 0, count: best.count };
|
|
676
|
+
};
|
|
677
|
+
var componentFunctionsIn2 = (declaration) => {
|
|
678
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
679
|
+
return isComponent(declaration) ? [declaration] : [];
|
|
680
|
+
}
|
|
681
|
+
if (declaration.type === "VariableDeclaration") {
|
|
682
|
+
return declaration.declarations.map((declarator) => {
|
|
683
|
+
return getComponentFunction(declarator.init);
|
|
684
|
+
}).filter((fn2) => {
|
|
685
|
+
return fn2 !== null && isComponent(fn2);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const fn = getComponentFunction(declaration);
|
|
689
|
+
return fn && isComponent(fn) ? [fn] : [];
|
|
690
|
+
};
|
|
691
|
+
var collectComponents2 = (body) => {
|
|
692
|
+
return body.flatMap((statement) => {
|
|
693
|
+
const declaration = unwrapExport(statement);
|
|
694
|
+
return declaration ? componentFunctionsIn2(declaration) : [];
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
var maxJsxReturnSize = {
|
|
698
|
+
meta: {
|
|
699
|
+
type: "suggestion",
|
|
700
|
+
docs: {
|
|
701
|
+
description: "Warn when a component return renders too many JSX elements; extract parts into variables or sub-components.",
|
|
702
|
+
recommended: true,
|
|
703
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#max-jsx-return-size"
|
|
704
|
+
},
|
|
705
|
+
schema: [
|
|
706
|
+
{
|
|
707
|
+
type: "object",
|
|
708
|
+
properties: {
|
|
709
|
+
maxElements: {
|
|
710
|
+
type: "integer",
|
|
711
|
+
minimum: 1,
|
|
712
|
+
description: "Maximum number of JSX elements a single return may render before the rule reports."
|
|
713
|
+
},
|
|
714
|
+
paths: {
|
|
715
|
+
type: "array",
|
|
716
|
+
items: { type: "string" },
|
|
717
|
+
description: "Optional glob patterns. When provided, the rule only runs for files whose path matches one of them."
|
|
718
|
+
},
|
|
719
|
+
ignore: {
|
|
720
|
+
type: "array",
|
|
721
|
+
items: { type: "string" },
|
|
722
|
+
description: "Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`."
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
additionalProperties: false
|
|
726
|
+
}
|
|
727
|
+
],
|
|
728
|
+
messages: {
|
|
729
|
+
// Points at the single biggest block so a human or AI fix loop knows
|
|
730
|
+
// exactly what to lift out.
|
|
731
|
+
tooManyElements: "{{name}} renders {{count}} JSX elements in one return (max {{max}}). Extract the largest block \u2014 <{{largest}}> at line {{line}} ({{largestCount}} elements) \u2014 into a variable or a sub-component.",
|
|
732
|
+
// Fallback when no single block dominates (e.g. many flat siblings): there is
|
|
733
|
+
// nothing meaningful to point at, so advise splitting.
|
|
734
|
+
tooManyElementsFlat: "{{name}} renders {{count}} JSX elements in one return (max {{max}}). Split it into smaller sub-components or extract groups of elements into variables."
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
create(context) {
|
|
738
|
+
const options = context.options[0] ?? {};
|
|
739
|
+
const max = options.maxElements ?? DEFAULT_MAX_ELEMENTS;
|
|
740
|
+
const paths = options.paths ?? [];
|
|
741
|
+
const ignore = options.ignore ?? [];
|
|
742
|
+
if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
|
|
743
|
+
return {};
|
|
744
|
+
}
|
|
745
|
+
if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
const visitorKeys = context.sourceCode.visitorKeys;
|
|
749
|
+
const checkComponent = (fn) => {
|
|
750
|
+
const name = getComponentName(fn) ?? ANONYMOUS_NAME3;
|
|
751
|
+
for (const argument of collectOwnReturnArguments(fn)) {
|
|
752
|
+
const count = countJsxElements(argument, visitorKeys);
|
|
753
|
+
if (count <= max) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const largest = largestBlock(argument, visitorKeys);
|
|
757
|
+
if (largest && largest.count >= 2) {
|
|
758
|
+
context.report({
|
|
759
|
+
node: argument,
|
|
760
|
+
messageId: "tooManyElements",
|
|
761
|
+
data: { count, max, name, largest: largest.name, line: largest.line, largestCount: largest.count }
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
context.report({ node: argument, messageId: "tooManyElementsFlat", data: { count, max, name } });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
return {
|
|
769
|
+
Program(program) {
|
|
770
|
+
collectComponents2(program.body).forEach(checkComponent);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// src/rules/props-destructuring-blank-line/props-destructuring-blank-line.ts
|
|
495
777
|
var isPropsDestructuring = (statement) => {
|
|
496
778
|
if (statement.type !== "VariableDeclaration") {
|
|
497
779
|
return false;
|
|
@@ -506,7 +788,7 @@ var propsDestructuringBlankLine = {
|
|
|
506
788
|
docs: {
|
|
507
789
|
description: "Require a blank line after the `const { ... } = props` destructuring statement at the top of a React component body.",
|
|
508
790
|
recommended: true,
|
|
509
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
791
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-destructuring-blank-line"
|
|
510
792
|
},
|
|
511
793
|
fixable: "whitespace",
|
|
512
794
|
schema: [],
|
|
@@ -554,7 +836,7 @@ var propsDestructuringBlankLine = {
|
|
|
554
836
|
}
|
|
555
837
|
};
|
|
556
838
|
|
|
557
|
-
// src/rules/props-destructuring-newline.ts
|
|
839
|
+
// src/rules/props-destructuring-newline/props-destructuring-newline.ts
|
|
558
840
|
var collectBoundNames = (node, names) => {
|
|
559
841
|
if (!node) {
|
|
560
842
|
return;
|
|
@@ -588,7 +870,7 @@ var propsDestructuringNewline = {
|
|
|
588
870
|
docs: {
|
|
589
871
|
description: "Require React components to accept a single props parameter and destructure it on its own line in the body, rather than destructuring inline in the parameter list.",
|
|
590
872
|
recommended: true,
|
|
591
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
873
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-destructuring-newline"
|
|
592
874
|
},
|
|
593
875
|
fixable: "code",
|
|
594
876
|
schema: [],
|
|
@@ -665,7 +947,7 @@ ${baseIndent}}`
|
|
|
665
947
|
}
|
|
666
948
|
};
|
|
667
949
|
|
|
668
|
-
// src/rules/props-type-name.ts
|
|
950
|
+
// src/rules/props-type-name/props-type-name.ts
|
|
669
951
|
var PROPS_SUFFIX2 = "Props";
|
|
670
952
|
var propsTypeName = {
|
|
671
953
|
meta: {
|
|
@@ -673,7 +955,7 @@ var propsTypeName = {
|
|
|
673
955
|
docs: {
|
|
674
956
|
description: "Require a React component's props type to be named `<ComponentName>Props` (e.g. `ButtonProps` for `Button`).",
|
|
675
957
|
recommended: true,
|
|
676
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
958
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-type-name"
|
|
677
959
|
},
|
|
678
960
|
schema: [
|
|
679
961
|
{
|
|
@@ -737,7 +1019,7 @@ var propsTypeName = {
|
|
|
737
1019
|
}
|
|
738
1020
|
};
|
|
739
1021
|
|
|
740
|
-
// src/rules/props-type-reference.ts
|
|
1022
|
+
// src/rules/props-type-reference/props-type-reference.ts
|
|
741
1023
|
var INLINE_OBJECT_TYPE = "TSTypeLiteral";
|
|
742
1024
|
var propsTypeReference = {
|
|
743
1025
|
meta: {
|
|
@@ -745,7 +1027,7 @@ var propsTypeReference = {
|
|
|
745
1027
|
docs: {
|
|
746
1028
|
description: "Require a React component's props parameter to use a named type (e.g. `ButtonProps`) instead of an inline object type literal.",
|
|
747
1029
|
recommended: true,
|
|
748
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
1030
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-type-reference"
|
|
749
1031
|
},
|
|
750
1032
|
schema: [
|
|
751
1033
|
{
|
|
@@ -803,7 +1085,7 @@ var propsTypeReference = {
|
|
|
803
1085
|
}
|
|
804
1086
|
};
|
|
805
1087
|
|
|
806
|
-
// src/rules/require-component-stories.ts
|
|
1088
|
+
// src/rules/require-component-stories/require-component-stories.ts
|
|
807
1089
|
import { existsSync } from "node:fs";
|
|
808
1090
|
import path2 from "node:path";
|
|
809
1091
|
|
|
@@ -879,7 +1161,7 @@ var deriveExpectedStoryPaths = (filePath, opts) => {
|
|
|
879
1161
|
});
|
|
880
1162
|
};
|
|
881
1163
|
|
|
882
|
-
// src/rules/require-component-stories.ts
|
|
1164
|
+
// src/rules/require-component-stories/require-component-stories.ts
|
|
883
1165
|
var toStoryPathOptions = (options) => {
|
|
884
1166
|
const picked = {};
|
|
885
1167
|
if (options.storiesDir !== void 0) {
|
|
@@ -905,7 +1187,7 @@ var requireComponentStories = {
|
|
|
905
1187
|
docs: {
|
|
906
1188
|
description: "Require a co-located Storybook story for every dumb component.",
|
|
907
1189
|
recommended: true,
|
|
908
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
1190
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#require-component-stories"
|
|
909
1191
|
},
|
|
910
1192
|
schema: [
|
|
911
1193
|
{
|
|
@@ -1004,6 +1286,282 @@ var requireComponentStories = {
|
|
|
1004
1286
|
}
|
|
1005
1287
|
};
|
|
1006
1288
|
|
|
1289
|
+
// src/utils/cognitive-complexity.ts
|
|
1290
|
+
var NON_CHILD_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "type"]);
|
|
1291
|
+
var LOOP_TYPES = /* @__PURE__ */ new Set(["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement"]);
|
|
1292
|
+
var NESTED_FUNCTION_TYPES = /* @__PURE__ */ new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
1293
|
+
var isNode2 = (value) => {
|
|
1294
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
1295
|
+
};
|
|
1296
|
+
var childrenOf = (node) => {
|
|
1297
|
+
const children = [];
|
|
1298
|
+
for (const key of Object.keys(node)) {
|
|
1299
|
+
if (NON_CHILD_KEYS.has(key)) {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const value = node[key];
|
|
1303
|
+
if (Array.isArray(value)) {
|
|
1304
|
+
children.push(...value.filter(isNode2));
|
|
1305
|
+
} else if (isNode2(value)) {
|
|
1306
|
+
children.push(value);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return children;
|
|
1310
|
+
};
|
|
1311
|
+
var flattenLogicalOperators = (node, operators) => {
|
|
1312
|
+
if (node.type !== "LogicalExpression") {
|
|
1313
|
+
return [node];
|
|
1314
|
+
}
|
|
1315
|
+
const left = flattenLogicalOperators(node.left, operators);
|
|
1316
|
+
operators.push(node.operator);
|
|
1317
|
+
const right = flattenLogicalOperators(node.right, operators);
|
|
1318
|
+
return [...left, ...right];
|
|
1319
|
+
};
|
|
1320
|
+
var countOperatorRuns = (operators) => {
|
|
1321
|
+
let runs = 0;
|
|
1322
|
+
let previous = null;
|
|
1323
|
+
for (const operator of operators) {
|
|
1324
|
+
if (operator !== previous) {
|
|
1325
|
+
runs += 1;
|
|
1326
|
+
previous = operator;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return runs;
|
|
1330
|
+
};
|
|
1331
|
+
var recurseChildren = (ctx, node, nesting) => {
|
|
1332
|
+
for (const child of childrenOf(node)) {
|
|
1333
|
+
walk(ctx, child, nesting);
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
var handleLogical = (ctx, node, nesting) => {
|
|
1337
|
+
const operators = [];
|
|
1338
|
+
const operands = flattenLogicalOperators(node, operators);
|
|
1339
|
+
ctx.score += countOperatorRuns(operators);
|
|
1340
|
+
for (const operand of operands) {
|
|
1341
|
+
walk(ctx, operand, nesting);
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
var handleAlternate = (ctx, alternate, nesting) => {
|
|
1345
|
+
if (!alternate) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (alternate.type === "IfStatement") {
|
|
1349
|
+
ctx.score += 1;
|
|
1350
|
+
walk(ctx, alternate.test, nesting);
|
|
1351
|
+
walk(ctx, alternate.consequent, nesting + 1);
|
|
1352
|
+
handleAlternate(ctx, alternate.alternate, nesting);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
ctx.score += 1;
|
|
1356
|
+
walk(ctx, alternate, nesting + 1);
|
|
1357
|
+
};
|
|
1358
|
+
var handleIf = (ctx, node, nesting) => {
|
|
1359
|
+
ctx.score += 1 + nesting;
|
|
1360
|
+
walk(ctx, node.test, nesting);
|
|
1361
|
+
walk(ctx, node.consequent, nesting + 1);
|
|
1362
|
+
handleAlternate(ctx, node.alternate, nesting);
|
|
1363
|
+
};
|
|
1364
|
+
var handleTernary = (ctx, node, nesting) => {
|
|
1365
|
+
ctx.score += 1 + nesting;
|
|
1366
|
+
walk(ctx, node.test, nesting);
|
|
1367
|
+
walk(ctx, node.consequent, nesting + 1);
|
|
1368
|
+
walk(ctx, node.alternate, nesting + 1);
|
|
1369
|
+
};
|
|
1370
|
+
var handleSwitch = (ctx, node, nesting) => {
|
|
1371
|
+
ctx.score += 1 + nesting;
|
|
1372
|
+
walk(ctx, node.discriminant, nesting);
|
|
1373
|
+
for (const switchCase of node.cases) {
|
|
1374
|
+
if (switchCase.test) {
|
|
1375
|
+
walk(ctx, switchCase.test, nesting);
|
|
1376
|
+
}
|
|
1377
|
+
for (const statement of switchCase.consequent) {
|
|
1378
|
+
walk(ctx, statement, nesting + 1);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
var handleLoop = (ctx, node, nesting) => {
|
|
1383
|
+
ctx.score += 1 + nesting;
|
|
1384
|
+
const body = node.body;
|
|
1385
|
+
for (const child of childrenOf(node)) {
|
|
1386
|
+
walk(ctx, child, child === body ? nesting + 1 : nesting);
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
var handleTry = (ctx, node, nesting) => {
|
|
1390
|
+
walk(ctx, node.block, nesting);
|
|
1391
|
+
if (node.handler) {
|
|
1392
|
+
ctx.score += 1 + nesting;
|
|
1393
|
+
walk(ctx, node.handler.body, nesting + 1);
|
|
1394
|
+
}
|
|
1395
|
+
if (node.finalizer) {
|
|
1396
|
+
walk(ctx, node.finalizer, nesting);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
var handleCall = (ctx, node, nesting) => {
|
|
1400
|
+
if (ctx.name && node.callee.type === "Identifier" && node.callee.name === ctx.name) {
|
|
1401
|
+
ctx.score += 1;
|
|
1402
|
+
}
|
|
1403
|
+
recurseChildren(ctx, node, nesting);
|
|
1404
|
+
};
|
|
1405
|
+
function walk(ctx, node, nesting) {
|
|
1406
|
+
const type = node.type;
|
|
1407
|
+
if (type === "LogicalExpression") {
|
|
1408
|
+
handleLogical(ctx, node, nesting);
|
|
1409
|
+
} else if (type === "IfStatement") {
|
|
1410
|
+
handleIf(ctx, node, nesting);
|
|
1411
|
+
} else if (type === "ConditionalExpression") {
|
|
1412
|
+
handleTernary(ctx, node, nesting);
|
|
1413
|
+
} else if (type === "SwitchStatement") {
|
|
1414
|
+
handleSwitch(ctx, node, nesting);
|
|
1415
|
+
} else if (LOOP_TYPES.has(type)) {
|
|
1416
|
+
handleLoop(ctx, node, nesting);
|
|
1417
|
+
} else if (type === "TryStatement") {
|
|
1418
|
+
handleTry(ctx, node, nesting);
|
|
1419
|
+
} else if (NESTED_FUNCTION_TYPES.has(type)) {
|
|
1420
|
+
walk(ctx, node.body, nesting + 1);
|
|
1421
|
+
} else if (type === "CallExpression") {
|
|
1422
|
+
handleCall(ctx, node, nesting);
|
|
1423
|
+
} else {
|
|
1424
|
+
recurseChildren(ctx, node, nesting);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
var cognitiveComplexity = (fn, enclosingName) => {
|
|
1428
|
+
const fallbackName = fn.type === "FunctionDeclaration" ? fn.id?.name ?? null : null;
|
|
1429
|
+
const ctx = { score: 0, name: enclosingName ?? fallbackName };
|
|
1430
|
+
walk(ctx, fn.body, 0);
|
|
1431
|
+
return ctx.score;
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// src/rules/require-jsdoc-example/require-jsdoc-example.ts
|
|
1435
|
+
var DEFAULT_MIN_COMPLEXITY = 8;
|
|
1436
|
+
var DEFAULT_EXAMPLE_COMPLEXITY = 12;
|
|
1437
|
+
var isTargetFn = (node) => {
|
|
1438
|
+
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
1439
|
+
};
|
|
1440
|
+
var getExportInfo = (statement) => {
|
|
1441
|
+
if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") {
|
|
1442
|
+
return { exportStmt: statement, declaration: statement.declaration ?? null };
|
|
1443
|
+
}
|
|
1444
|
+
return { exportStmt: null, declaration: statement };
|
|
1445
|
+
};
|
|
1446
|
+
var targetsFromDeclaration = (declaration, anchors) => {
|
|
1447
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
1448
|
+
return declaration.id ? [{ fn: declaration, name: declaration.id.name, anchors }] : [];
|
|
1449
|
+
}
|
|
1450
|
+
if (declaration.type === "VariableDeclaration") {
|
|
1451
|
+
return declaration.declarations.flatMap((declarator) => {
|
|
1452
|
+
return declarator.id.type === "Identifier" && isTargetFn(declarator.init) ? [{ fn: declarator.init, name: declarator.id.name, anchors }] : [];
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return [];
|
|
1456
|
+
};
|
|
1457
|
+
var collectTargets = (body) => {
|
|
1458
|
+
return body.flatMap((statement) => {
|
|
1459
|
+
const { exportStmt, declaration } = getExportInfo(statement);
|
|
1460
|
+
if (!declaration) {
|
|
1461
|
+
return [];
|
|
1462
|
+
}
|
|
1463
|
+
const anchors = exportStmt ? [exportStmt, declaration] : [declaration];
|
|
1464
|
+
return targetsFromDeclaration(declaration, anchors);
|
|
1465
|
+
});
|
|
1466
|
+
};
|
|
1467
|
+
var hasJsdocBlock = (sourceCode, anchors) => {
|
|
1468
|
+
return anchors.some((anchor) => {
|
|
1469
|
+
return sourceCode.getCommentsBefore(anchor).some((comment) => {
|
|
1470
|
+
return comment.type === "Block" && comment.value.startsWith("*");
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1473
|
+
};
|
|
1474
|
+
var hasJsdocExample = (sourceCode, anchors) => {
|
|
1475
|
+
return anchors.some((anchor) => {
|
|
1476
|
+
return sourceCode.getCommentsBefore(anchor).some((comment) => {
|
|
1477
|
+
return comment.type === "Block" && comment.value.startsWith("*") && comment.value.includes("@example");
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
};
|
|
1481
|
+
var decideViolation = (target, settings, sourceCode) => {
|
|
1482
|
+
const complexity = cognitiveComplexity(target.fn, target.name);
|
|
1483
|
+
const { minComplexity, exampleComplexity } = settings;
|
|
1484
|
+
if (complexity < minComplexity) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
const { name } = target;
|
|
1488
|
+
if (!hasJsdocBlock(sourceCode, target.anchors)) {
|
|
1489
|
+
return { messageId: "missingJsdoc", data: { name, complexity, minComplexity } };
|
|
1490
|
+
}
|
|
1491
|
+
if (complexity >= exampleComplexity && !hasJsdocExample(sourceCode, target.anchors)) {
|
|
1492
|
+
return { messageId: "missingExample", data: { name, complexity, exampleComplexity } };
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
};
|
|
1496
|
+
var requireJsdocExample = {
|
|
1497
|
+
meta: {
|
|
1498
|
+
type: "suggestion",
|
|
1499
|
+
docs: {
|
|
1500
|
+
description: "Graduated JSDoc requirement by cognitive complexity: at or above `minComplexity` a function must carry a leading JSDoc block, and at or above `exampleComplexity` that block must also include an `@example` tag.",
|
|
1501
|
+
recommended: true,
|
|
1502
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#require-jsdoc-example"
|
|
1503
|
+
},
|
|
1504
|
+
schema: [
|
|
1505
|
+
{
|
|
1506
|
+
type: "object",
|
|
1507
|
+
properties: {
|
|
1508
|
+
minComplexity: {
|
|
1509
|
+
type: "integer",
|
|
1510
|
+
minimum: 1,
|
|
1511
|
+
description: "Cognitive complexity at or above which a function must carry a leading JSDoc block."
|
|
1512
|
+
},
|
|
1513
|
+
exampleComplexity: {
|
|
1514
|
+
type: "integer",
|
|
1515
|
+
minimum: 1,
|
|
1516
|
+
description: "Cognitive complexity at or above which the JSDoc block must also include an `@example` tag."
|
|
1517
|
+
},
|
|
1518
|
+
paths: {
|
|
1519
|
+
type: "array",
|
|
1520
|
+
items: { type: "string" },
|
|
1521
|
+
description: "Optional glob patterns. When provided, the rule only runs for files whose path matches one of them."
|
|
1522
|
+
},
|
|
1523
|
+
ignore: {
|
|
1524
|
+
type: "array",
|
|
1525
|
+
items: { type: "string" },
|
|
1526
|
+
description: "Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`."
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
additionalProperties: false
|
|
1530
|
+
}
|
|
1531
|
+
],
|
|
1532
|
+
messages: {
|
|
1533
|
+
missingJsdoc: 'function "{{name}}" has cognitive complexity {{complexity}} (>= {{minComplexity}}); add a JSDoc block documenting it.',
|
|
1534
|
+
missingExample: 'function "{{name}}" has cognitive complexity {{complexity}} (>= {{exampleComplexity}}); its JSDoc block needs an `@example` documenting usage.'
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
create(context) {
|
|
1538
|
+
const options = context.options[0] ?? {};
|
|
1539
|
+
const paths = options.paths ?? [];
|
|
1540
|
+
const ignore = options.ignore ?? [];
|
|
1541
|
+
if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
|
|
1542
|
+
return {};
|
|
1543
|
+
}
|
|
1544
|
+
if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {
|
|
1545
|
+
return {};
|
|
1546
|
+
}
|
|
1547
|
+
const settings = {
|
|
1548
|
+
minComplexity: options.minComplexity ?? DEFAULT_MIN_COMPLEXITY,
|
|
1549
|
+
exampleComplexity: options.exampleComplexity ?? DEFAULT_EXAMPLE_COMPLEXITY
|
|
1550
|
+
};
|
|
1551
|
+
const { sourceCode } = context;
|
|
1552
|
+
return {
|
|
1553
|
+
Program(program) {
|
|
1554
|
+
for (const target of collectTargets(program.body)) {
|
|
1555
|
+
const violation = decideViolation(target, settings, sourceCode);
|
|
1556
|
+
if (violation) {
|
|
1557
|
+
context.report({ node: target.fn, messageId: violation.messageId, data: violation.data });
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1007
1565
|
// src/rules/index.ts
|
|
1008
1566
|
var rules = {
|
|
1009
1567
|
"props-destructuring-newline": propsDestructuringNewline,
|
|
@@ -1012,7 +1570,10 @@ var rules = {
|
|
|
1012
1570
|
"props-type-name": propsTypeName,
|
|
1013
1571
|
"component-file-order": componentFileOrder,
|
|
1014
1572
|
"component-arrow-function": componentArrowFunction,
|
|
1015
|
-
"
|
|
1573
|
+
"max-components-per-file": maxComponentsPerFile,
|
|
1574
|
+
"max-jsx-return-size": maxJsxReturnSize,
|
|
1575
|
+
"require-component-stories": requireComponentStories,
|
|
1576
|
+
"require-jsdoc-example": requireJsdocExample
|
|
1016
1577
|
};
|
|
1017
1578
|
|
|
1018
1579
|
// src/index.ts
|
|
@@ -1020,7 +1581,7 @@ var PLUGIN_NAME = "@wl";
|
|
|
1020
1581
|
var plugin = {
|
|
1021
1582
|
meta: {
|
|
1022
1583
|
name: "@wl/eslint-plugin",
|
|
1023
|
-
version: "0.1.
|
|
1584
|
+
version: "0.1.20"
|
|
1024
1585
|
},
|
|
1025
1586
|
rules,
|
|
1026
1587
|
configs: {}
|
|
@@ -1040,7 +1601,16 @@ plugin.configs.recommended = [
|
|
|
1040
1601
|
// Pages and routes are excluded: route/page modules commonly use `function`
|
|
1041
1602
|
// declarations (and framework conventions like default-exported page functions).
|
|
1042
1603
|
[`${PLUGIN_NAME}/component-arrow-function`]: ["error", { ignore: ["**/pages/**", "**/routes/**"] }],
|
|
1043
|
-
[`${PLUGIN_NAME}/require-component-stories`]: "error"
|
|
1604
|
+
[`${PLUGIN_NAME}/require-component-stories`]: "error",
|
|
1605
|
+
// Advisory: flags returns that render too many JSX elements; extract into a
|
|
1606
|
+
// variable or sub-component. Warn-class by nature (`type: 'suggestion'`);
|
|
1607
|
+
// severity confirmed against a repo-wide dry run at the default ceiling.
|
|
1608
|
+
[`${PLUGIN_NAME}/max-jsx-return-size`]: "error",
|
|
1609
|
+
// Caps component declarations per file; extra components belong in their own
|
|
1610
|
+
// files. Pages and routes are excluded (framework conventions co-locate
|
|
1611
|
+
// route trees and default-exported page functions). Ceiling confirmed
|
|
1612
|
+
// against a repo-wide dry run at the default.
|
|
1613
|
+
[`${PLUGIN_NAME}/max-components-per-file`]: ["error", { ignore: ["**/pages/**", "**/routes/**"] }]
|
|
1044
1614
|
}
|
|
1045
1615
|
},
|
|
1046
1616
|
// Storybook stories legitimately deviate from the component conventions: the
|
|
@@ -1055,6 +1625,38 @@ plugin.configs.recommended = [
|
|
|
1055
1625
|
[`${PLUGIN_NAME}/component-file-order`]: "off",
|
|
1056
1626
|
[`${PLUGIN_NAME}/props-type-name`]: "off"
|
|
1057
1627
|
}
|
|
1628
|
+
},
|
|
1629
|
+
// Dumb presentational files (`*-component.tsx`) follow the one-component-per-file
|
|
1630
|
+
// convention the props/order/stories rules already assume, so they get a tighter
|
|
1631
|
+
// ceiling of 1. This MUST come AFTER the global `**/*.tsx` block: flat config
|
|
1632
|
+
// REPLACES rule options across matching blocks (it does not merge), and `ignore`
|
|
1633
|
+
// is re-declared here so pages/routes dumb-components keep their exemption.
|
|
1634
|
+
// A repo-wide dry run found zero `*-component.tsx` files declaring >1 component.
|
|
1635
|
+
{
|
|
1636
|
+
files: ["**/*-component.tsx"],
|
|
1637
|
+
rules: {
|
|
1638
|
+
[`${PLUGIN_NAME}/max-components-per-file`]: [
|
|
1639
|
+
"error",
|
|
1640
|
+
{ maxComponents: 1, ignore: ["**/pages/**", "**/routes/**"] }
|
|
1641
|
+
]
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
// `require-jsdoc-example` targets named functions, which overwhelmingly live in
|
|
1645
|
+
// plain `.ts` lib/util modules (not just `.tsx`), so it gets its OWN block scoped
|
|
1646
|
+
// to both extensions — the tsx-only blocks above would never reach where it
|
|
1647
|
+
// matters. Severity is `warn` (not `error`) so first adoption does not break
|
|
1648
|
+
// consumers' CI; the graduated defaults (minComplexity 8 → require a JSDoc block,
|
|
1649
|
+
// exampleComplexity 12 → also require `@example`) are left implicit. Flat config
|
|
1650
|
+
// REPLACES rule options across matching blocks, so this rule lives only here and
|
|
1651
|
+
// relies on no option merging.
|
|
1652
|
+
{
|
|
1653
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
1654
|
+
plugins: {
|
|
1655
|
+
[PLUGIN_NAME]: plugin
|
|
1656
|
+
},
|
|
1657
|
+
rules: {
|
|
1658
|
+
[`${PLUGIN_NAME}/require-jsdoc-example`]: "warn"
|
|
1659
|
+
}
|
|
1058
1660
|
}
|
|
1059
1661
|
];
|
|
1060
1662
|
var meta = plugin.meta;
|