@wsxjs/eslint-plugin-wsx 0.0.20 → 0.0.22
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 +236 -1
- package/dist/index.mjs +236 -1
- package/package.json +2 -2
- package/src/configs/recommended.ts +2 -0
- package/src/index.ts +4 -0
- package/src/rules/lifecycle-must-call-super.ts +155 -0
- package/src/rules/no-duplicate-keys.ts +192 -0
package/dist/index.js
CHANGED
|
@@ -550,6 +550,237 @@ var i18nAfterAutoRegister = {
|
|
|
550
550
|
}
|
|
551
551
|
};
|
|
552
552
|
|
|
553
|
+
// src/rules/no-duplicate-keys.ts
|
|
554
|
+
var noDuplicateKeys = {
|
|
555
|
+
meta: {
|
|
556
|
+
type: "problem",
|
|
557
|
+
docs: {
|
|
558
|
+
description: "disallow using the same key in different parent containers",
|
|
559
|
+
category: "Possible Errors",
|
|
560
|
+
recommended: true
|
|
561
|
+
},
|
|
562
|
+
messages: {
|
|
563
|
+
duplicateKey: 'Duplicate key "{{key}}" found in different parent containers ({{parent1}} and {{parent2}}). This will cause DOM cache conflicts. Use unique key prefixes like key="{{parent1}}-{{key}}" and key="{{parent2}}-{{key}}".'
|
|
564
|
+
},
|
|
565
|
+
schema: []
|
|
566
|
+
// 无配置选项
|
|
567
|
+
},
|
|
568
|
+
create(context) {
|
|
569
|
+
const functionKeyMap = /* @__PURE__ */ new Map();
|
|
570
|
+
const functionStack = [];
|
|
571
|
+
function getParentJSXName(node) {
|
|
572
|
+
let parent = node.parent;
|
|
573
|
+
while (parent) {
|
|
574
|
+
if (parent.type === "JSXElement") {
|
|
575
|
+
const openingElement = parent.openingElement;
|
|
576
|
+
if (openingElement) {
|
|
577
|
+
const name = openingElement.name;
|
|
578
|
+
if (name) {
|
|
579
|
+
const nameValue = name.name || name;
|
|
580
|
+
if (typeof nameValue === "string") {
|
|
581
|
+
return nameValue;
|
|
582
|
+
}
|
|
583
|
+
if (nameValue.name) {
|
|
584
|
+
return nameValue.name;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
parent = parent.parent;
|
|
590
|
+
}
|
|
591
|
+
return "unknown";
|
|
592
|
+
}
|
|
593
|
+
function getKeyValue(attr) {
|
|
594
|
+
const value = attr.value;
|
|
595
|
+
if (!value) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (value.type === "Literal") {
|
|
599
|
+
return String(value.value);
|
|
600
|
+
}
|
|
601
|
+
if (value.type === "JSXExpressionContainer") {
|
|
602
|
+
const expression = value.expression;
|
|
603
|
+
if (expression.type === "Identifier") {
|
|
604
|
+
return expression.name;
|
|
605
|
+
}
|
|
606
|
+
if (expression.type === "TemplateLiteral") {
|
|
607
|
+
const quasis = expression.quasis || [];
|
|
608
|
+
if (quasis.length > 0 && quasis[0].value) {
|
|
609
|
+
return quasis[0].value.raw || quasis[0].value.cooked;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
// Track function/method entry
|
|
617
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
618
|
+
":function"(node) {
|
|
619
|
+
functionStack.push(node);
|
|
620
|
+
functionKeyMap.set(node, /* @__PURE__ */ new Map());
|
|
621
|
+
},
|
|
622
|
+
// Track function/method exit
|
|
623
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
624
|
+
":function:exit"(_node) {
|
|
625
|
+
functionStack.pop();
|
|
626
|
+
},
|
|
627
|
+
// Check JSX elements for key attributes
|
|
628
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
629
|
+
JSXOpeningElement(node) {
|
|
630
|
+
if (functionStack.length === 0) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const currentFunction = functionStack[functionStack.length - 1];
|
|
634
|
+
const keyMap = functionKeyMap.get(currentFunction);
|
|
635
|
+
if (!keyMap) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const keyAttr = (node.attributes || []).find((attr) => {
|
|
639
|
+
return attr.type === "JSXAttribute" && attr.name && attr.name.name === "key";
|
|
640
|
+
});
|
|
641
|
+
if (!keyAttr) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const keyValue = getKeyValue(keyAttr);
|
|
645
|
+
if (!keyValue) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const parentName = getParentJSXName(node);
|
|
649
|
+
const existing = keyMap.get(keyValue);
|
|
650
|
+
if (existing && existing.parentName !== parentName) {
|
|
651
|
+
context.report({
|
|
652
|
+
node: keyAttr,
|
|
653
|
+
messageId: "duplicateKey",
|
|
654
|
+
data: {
|
|
655
|
+
key: keyValue,
|
|
656
|
+
parent1: existing.parentName,
|
|
657
|
+
parent2: parentName
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
} else if (!existing) {
|
|
661
|
+
keyMap.set(keyValue, {
|
|
662
|
+
key: keyValue,
|
|
663
|
+
parentName,
|
|
664
|
+
node: keyAttr
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/rules/lifecycle-must-call-super.ts
|
|
673
|
+
var lifecycleMustCallSuper = {
|
|
674
|
+
meta: {
|
|
675
|
+
type: "problem",
|
|
676
|
+
docs: {
|
|
677
|
+
description: "Enforce that lifecycle methods (onConnected, onDisconnected, onRendered) call super",
|
|
678
|
+
category: "Possible Errors",
|
|
679
|
+
recommended: true
|
|
680
|
+
},
|
|
681
|
+
messages: {
|
|
682
|
+
mustCallSuper: "Lifecycle method '{{methodName}}' must call 'super.{{methodName}}()' to ensure proper initialization."
|
|
683
|
+
},
|
|
684
|
+
schema: []
|
|
685
|
+
},
|
|
686
|
+
defaultOptions: [],
|
|
687
|
+
create(context) {
|
|
688
|
+
const lifecycleMethods = /* @__PURE__ */ new Set(["onConnected", "onDisconnected", "onRendered"]);
|
|
689
|
+
function checkForSuperCall(node, methodName, visited = /* @__PURE__ */ new WeakSet()) {
|
|
690
|
+
if (visited.has(node)) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
visited.add(node);
|
|
694
|
+
if (node.type === "CallExpression") {
|
|
695
|
+
const callee = node.callee;
|
|
696
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Super" && callee.property.type === "Identifier" && callee.property.name === methodName) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const nodeAny = node;
|
|
701
|
+
const keysToCheck = [
|
|
702
|
+
"body",
|
|
703
|
+
"consequent",
|
|
704
|
+
"alternate",
|
|
705
|
+
"block",
|
|
706
|
+
"handler",
|
|
707
|
+
"finalizer",
|
|
708
|
+
"argument",
|
|
709
|
+
"callee",
|
|
710
|
+
"expression",
|
|
711
|
+
"declarations",
|
|
712
|
+
"init",
|
|
713
|
+
"test",
|
|
714
|
+
"update",
|
|
715
|
+
"cases",
|
|
716
|
+
"discriminant",
|
|
717
|
+
"object",
|
|
718
|
+
"property",
|
|
719
|
+
"elements",
|
|
720
|
+
"properties",
|
|
721
|
+
"value",
|
|
722
|
+
"key"
|
|
723
|
+
];
|
|
724
|
+
for (const key of keysToCheck) {
|
|
725
|
+
const child = nodeAny[key];
|
|
726
|
+
if (Array.isArray(child)) {
|
|
727
|
+
for (const item of child) {
|
|
728
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
729
|
+
if (checkForSuperCall(
|
|
730
|
+
item,
|
|
731
|
+
methodName,
|
|
732
|
+
visited
|
|
733
|
+
)) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} else if (child && typeof child === "object" && "type" in child) {
|
|
739
|
+
if (checkForSuperCall(child, methodName, visited)) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
MethodDefinition(node) {
|
|
748
|
+
if (node.accessibility !== "protected") {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const methodName = node.key.type === "Identifier" ? node.key.name : null;
|
|
752
|
+
if (!methodName || !lifecycleMethods.has(methodName)) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const methodValue = node.value;
|
|
756
|
+
if (!methodValue || methodValue.type !== "FunctionExpression") {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const body = methodValue.body;
|
|
760
|
+
if (!body || body.type !== "BlockStatement") {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
let hasSuperCall = false;
|
|
764
|
+
for (const statement of body.body) {
|
|
765
|
+
if (checkForSuperCall(statement, methodName)) {
|
|
766
|
+
hasSuperCall = true;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (!hasSuperCall) {
|
|
771
|
+
context.report({
|
|
772
|
+
node: node.key,
|
|
773
|
+
messageId: "mustCallSuper",
|
|
774
|
+
data: {
|
|
775
|
+
methodName
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
553
784
|
// src/configs/recommended.ts
|
|
554
785
|
var recommendedConfig = {
|
|
555
786
|
parser: "@typescript-eslint/parser",
|
|
@@ -575,6 +806,8 @@ var recommendedConfig = {
|
|
|
575
806
|
"wsx/no-null-render": "error",
|
|
576
807
|
"wsx/no-inner-html": "error",
|
|
577
808
|
"wsx/i18n-after-autoregister": "error",
|
|
809
|
+
"wsx/no-duplicate-keys": "error",
|
|
810
|
+
"wsx/lifecycle-must-call-super": "error",
|
|
578
811
|
// TypeScript 规则(推荐)
|
|
579
812
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
580
813
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -744,7 +977,9 @@ var plugin = {
|
|
|
744
977
|
"require-jsx-import-source": requireJsxImportSource,
|
|
745
978
|
"no-null-render": noNullRender,
|
|
746
979
|
"no-inner-html": noInnerHTML,
|
|
747
|
-
"i18n-after-autoregister": i18nAfterAutoRegister
|
|
980
|
+
"i18n-after-autoregister": i18nAfterAutoRegister,
|
|
981
|
+
"no-duplicate-keys": noDuplicateKeys,
|
|
982
|
+
"lifecycle-must-call-super": lifecycleMustCallSuper
|
|
748
983
|
},
|
|
749
984
|
// 配置预设
|
|
750
985
|
configs: {
|
package/dist/index.mjs
CHANGED
|
@@ -521,6 +521,237 @@ var i18nAfterAutoRegister = {
|
|
|
521
521
|
}
|
|
522
522
|
};
|
|
523
523
|
|
|
524
|
+
// src/rules/no-duplicate-keys.ts
|
|
525
|
+
var noDuplicateKeys = {
|
|
526
|
+
meta: {
|
|
527
|
+
type: "problem",
|
|
528
|
+
docs: {
|
|
529
|
+
description: "disallow using the same key in different parent containers",
|
|
530
|
+
category: "Possible Errors",
|
|
531
|
+
recommended: true
|
|
532
|
+
},
|
|
533
|
+
messages: {
|
|
534
|
+
duplicateKey: 'Duplicate key "{{key}}" found in different parent containers ({{parent1}} and {{parent2}}). This will cause DOM cache conflicts. Use unique key prefixes like key="{{parent1}}-{{key}}" and key="{{parent2}}-{{key}}".'
|
|
535
|
+
},
|
|
536
|
+
schema: []
|
|
537
|
+
// 无配置选项
|
|
538
|
+
},
|
|
539
|
+
create(context) {
|
|
540
|
+
const functionKeyMap = /* @__PURE__ */ new Map();
|
|
541
|
+
const functionStack = [];
|
|
542
|
+
function getParentJSXName(node) {
|
|
543
|
+
let parent = node.parent;
|
|
544
|
+
while (parent) {
|
|
545
|
+
if (parent.type === "JSXElement") {
|
|
546
|
+
const openingElement = parent.openingElement;
|
|
547
|
+
if (openingElement) {
|
|
548
|
+
const name = openingElement.name;
|
|
549
|
+
if (name) {
|
|
550
|
+
const nameValue = name.name || name;
|
|
551
|
+
if (typeof nameValue === "string") {
|
|
552
|
+
return nameValue;
|
|
553
|
+
}
|
|
554
|
+
if (nameValue.name) {
|
|
555
|
+
return nameValue.name;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
parent = parent.parent;
|
|
561
|
+
}
|
|
562
|
+
return "unknown";
|
|
563
|
+
}
|
|
564
|
+
function getKeyValue(attr) {
|
|
565
|
+
const value = attr.value;
|
|
566
|
+
if (!value) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
if (value.type === "Literal") {
|
|
570
|
+
return String(value.value);
|
|
571
|
+
}
|
|
572
|
+
if (value.type === "JSXExpressionContainer") {
|
|
573
|
+
const expression = value.expression;
|
|
574
|
+
if (expression.type === "Identifier") {
|
|
575
|
+
return expression.name;
|
|
576
|
+
}
|
|
577
|
+
if (expression.type === "TemplateLiteral") {
|
|
578
|
+
const quasis = expression.quasis || [];
|
|
579
|
+
if (quasis.length > 0 && quasis[0].value) {
|
|
580
|
+
return quasis[0].value.raw || quasis[0].value.cooked;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
// Track function/method entry
|
|
588
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
589
|
+
":function"(node) {
|
|
590
|
+
functionStack.push(node);
|
|
591
|
+
functionKeyMap.set(node, /* @__PURE__ */ new Map());
|
|
592
|
+
},
|
|
593
|
+
// Track function/method exit
|
|
594
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
595
|
+
":function:exit"(_node) {
|
|
596
|
+
functionStack.pop();
|
|
597
|
+
},
|
|
598
|
+
// Check JSX elements for key attributes
|
|
599
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
600
|
+
JSXOpeningElement(node) {
|
|
601
|
+
if (functionStack.length === 0) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const currentFunction = functionStack[functionStack.length - 1];
|
|
605
|
+
const keyMap = functionKeyMap.get(currentFunction);
|
|
606
|
+
if (!keyMap) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const keyAttr = (node.attributes || []).find((attr) => {
|
|
610
|
+
return attr.type === "JSXAttribute" && attr.name && attr.name.name === "key";
|
|
611
|
+
});
|
|
612
|
+
if (!keyAttr) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const keyValue = getKeyValue(keyAttr);
|
|
616
|
+
if (!keyValue) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const parentName = getParentJSXName(node);
|
|
620
|
+
const existing = keyMap.get(keyValue);
|
|
621
|
+
if (existing && existing.parentName !== parentName) {
|
|
622
|
+
context.report({
|
|
623
|
+
node: keyAttr,
|
|
624
|
+
messageId: "duplicateKey",
|
|
625
|
+
data: {
|
|
626
|
+
key: keyValue,
|
|
627
|
+
parent1: existing.parentName,
|
|
628
|
+
parent2: parentName
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
} else if (!existing) {
|
|
632
|
+
keyMap.set(keyValue, {
|
|
633
|
+
key: keyValue,
|
|
634
|
+
parentName,
|
|
635
|
+
node: keyAttr
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// src/rules/lifecycle-must-call-super.ts
|
|
644
|
+
var lifecycleMustCallSuper = {
|
|
645
|
+
meta: {
|
|
646
|
+
type: "problem",
|
|
647
|
+
docs: {
|
|
648
|
+
description: "Enforce that lifecycle methods (onConnected, onDisconnected, onRendered) call super",
|
|
649
|
+
category: "Possible Errors",
|
|
650
|
+
recommended: true
|
|
651
|
+
},
|
|
652
|
+
messages: {
|
|
653
|
+
mustCallSuper: "Lifecycle method '{{methodName}}' must call 'super.{{methodName}}()' to ensure proper initialization."
|
|
654
|
+
},
|
|
655
|
+
schema: []
|
|
656
|
+
},
|
|
657
|
+
defaultOptions: [],
|
|
658
|
+
create(context) {
|
|
659
|
+
const lifecycleMethods = /* @__PURE__ */ new Set(["onConnected", "onDisconnected", "onRendered"]);
|
|
660
|
+
function checkForSuperCall(node, methodName, visited = /* @__PURE__ */ new WeakSet()) {
|
|
661
|
+
if (visited.has(node)) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
visited.add(node);
|
|
665
|
+
if (node.type === "CallExpression") {
|
|
666
|
+
const callee = node.callee;
|
|
667
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Super" && callee.property.type === "Identifier" && callee.property.name === methodName) {
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const nodeAny = node;
|
|
672
|
+
const keysToCheck = [
|
|
673
|
+
"body",
|
|
674
|
+
"consequent",
|
|
675
|
+
"alternate",
|
|
676
|
+
"block",
|
|
677
|
+
"handler",
|
|
678
|
+
"finalizer",
|
|
679
|
+
"argument",
|
|
680
|
+
"callee",
|
|
681
|
+
"expression",
|
|
682
|
+
"declarations",
|
|
683
|
+
"init",
|
|
684
|
+
"test",
|
|
685
|
+
"update",
|
|
686
|
+
"cases",
|
|
687
|
+
"discriminant",
|
|
688
|
+
"object",
|
|
689
|
+
"property",
|
|
690
|
+
"elements",
|
|
691
|
+
"properties",
|
|
692
|
+
"value",
|
|
693
|
+
"key"
|
|
694
|
+
];
|
|
695
|
+
for (const key of keysToCheck) {
|
|
696
|
+
const child = nodeAny[key];
|
|
697
|
+
if (Array.isArray(child)) {
|
|
698
|
+
for (const item of child) {
|
|
699
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
700
|
+
if (checkForSuperCall(
|
|
701
|
+
item,
|
|
702
|
+
methodName,
|
|
703
|
+
visited
|
|
704
|
+
)) {
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} else if (child && typeof child === "object" && "type" in child) {
|
|
710
|
+
if (checkForSuperCall(child, methodName, visited)) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
MethodDefinition(node) {
|
|
719
|
+
if (node.accessibility !== "protected") {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const methodName = node.key.type === "Identifier" ? node.key.name : null;
|
|
723
|
+
if (!methodName || !lifecycleMethods.has(methodName)) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const methodValue = node.value;
|
|
727
|
+
if (!methodValue || methodValue.type !== "FunctionExpression") {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const body = methodValue.body;
|
|
731
|
+
if (!body || body.type !== "BlockStatement") {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
let hasSuperCall = false;
|
|
735
|
+
for (const statement of body.body) {
|
|
736
|
+
if (checkForSuperCall(statement, methodName)) {
|
|
737
|
+
hasSuperCall = true;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (!hasSuperCall) {
|
|
742
|
+
context.report({
|
|
743
|
+
node: node.key,
|
|
744
|
+
messageId: "mustCallSuper",
|
|
745
|
+
data: {
|
|
746
|
+
methodName
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
524
755
|
// src/configs/recommended.ts
|
|
525
756
|
var recommendedConfig = {
|
|
526
757
|
parser: "@typescript-eslint/parser",
|
|
@@ -546,6 +777,8 @@ var recommendedConfig = {
|
|
|
546
777
|
"wsx/no-null-render": "error",
|
|
547
778
|
"wsx/no-inner-html": "error",
|
|
548
779
|
"wsx/i18n-after-autoregister": "error",
|
|
780
|
+
"wsx/no-duplicate-keys": "error",
|
|
781
|
+
"wsx/lifecycle-must-call-super": "error",
|
|
549
782
|
// TypeScript 规则(推荐)
|
|
550
783
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
551
784
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -715,7 +948,9 @@ var plugin = {
|
|
|
715
948
|
"require-jsx-import-source": requireJsxImportSource,
|
|
716
949
|
"no-null-render": noNullRender,
|
|
717
950
|
"no-inner-html": noInnerHTML,
|
|
718
|
-
"i18n-after-autoregister": i18nAfterAutoRegister
|
|
951
|
+
"i18n-after-autoregister": i18nAfterAutoRegister,
|
|
952
|
+
"no-duplicate-keys": noDuplicateKeys,
|
|
953
|
+
"lifecycle-must-call-super": lifecycleMustCallSuper
|
|
719
954
|
},
|
|
720
955
|
// 配置预设
|
|
721
956
|
configs: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/eslint-plugin-wsx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "ESLint plugin for WSXJS",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"web-components"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@wsxjs/wsx-core": "0.0.
|
|
28
|
+
"@wsxjs/wsx-core": "0.0.22"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"tsup": "^8.0.0",
|
|
@@ -29,6 +29,8 @@ export const recommendedConfig: WSXConfig = {
|
|
|
29
29
|
"wsx/no-null-render": "error",
|
|
30
30
|
"wsx/no-inner-html": "error",
|
|
31
31
|
"wsx/i18n-after-autoregister": "error",
|
|
32
|
+
"wsx/no-duplicate-keys": "error",
|
|
33
|
+
"wsx/lifecycle-must-call-super": "error",
|
|
32
34
|
|
|
33
35
|
// TypeScript 规则(推荐)
|
|
34
36
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { requireJsxImportSource } from "./rules/require-jsx-import-source";
|
|
|
13
13
|
import { noNullRender } from "./rules/no-null-render";
|
|
14
14
|
import { noInnerHTML } from "./rules/no-inner-html";
|
|
15
15
|
import { i18nAfterAutoRegister } from "./rules/i18n-after-autoregister";
|
|
16
|
+
import { noDuplicateKeys } from "./rules/no-duplicate-keys";
|
|
17
|
+
import { lifecycleMustCallSuper } from "./rules/lifecycle-must-call-super";
|
|
16
18
|
import { recommendedConfig } from "./configs/recommended";
|
|
17
19
|
import { createFlatConfig } from "./configs/flat";
|
|
18
20
|
import { WSXPlugin } from "./types";
|
|
@@ -34,6 +36,8 @@ const plugin: WSXPlugin = {
|
|
|
34
36
|
"no-null-render": noNullRender,
|
|
35
37
|
"no-inner-html": noInnerHTML,
|
|
36
38
|
"i18n-after-autoregister": i18nAfterAutoRegister,
|
|
39
|
+
"no-duplicate-keys": noDuplicateKeys,
|
|
40
|
+
"lifecycle-must-call-super": lifecycleMustCallSuper,
|
|
37
41
|
},
|
|
38
42
|
|
|
39
43
|
// 配置预设
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:lifecycle-must-call-super
|
|
3
|
+
*
|
|
4
|
+
* 确保生命周期方法(onConnected, onDisconnected, onRendered)调用 super
|
|
5
|
+
* 这些方法在 BaseComponent 中是可选的,但如果被重写,应该调用 super
|
|
6
|
+
* 以确保正确的初始化和清理
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Rule } from "eslint";
|
|
10
|
+
import { WSXRuleModule } from "../types";
|
|
11
|
+
|
|
12
|
+
export const lifecycleMustCallSuper: WSXRuleModule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description:
|
|
17
|
+
"Enforce that lifecycle methods (onConnected, onDisconnected, onRendered) call super",
|
|
18
|
+
category: "Possible Errors",
|
|
19
|
+
recommended: true,
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
mustCallSuper:
|
|
23
|
+
"Lifecycle method '{{methodName}}' must call 'super.{{methodName}}()' to ensure proper initialization.",
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
defaultOptions: [],
|
|
28
|
+
create(context: Rule.RuleContext) {
|
|
29
|
+
const lifecycleMethods = new Set(["onConnected", "onDisconnected", "onRendered"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 递归检查节点中是否有 super.methodName() 调用
|
|
33
|
+
* 使用 visited 集合避免重复检查同一个节点(防止循环引用导致的无限递归)
|
|
34
|
+
*/
|
|
35
|
+
function checkForSuperCall(
|
|
36
|
+
node: import("estree").Node,
|
|
37
|
+
methodName: string,
|
|
38
|
+
visited = new WeakSet<import("estree").Node>()
|
|
39
|
+
): boolean {
|
|
40
|
+
// 避免重复检查同一个节点
|
|
41
|
+
if (visited.has(node)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
visited.add(node);
|
|
45
|
+
|
|
46
|
+
if (node.type === "CallExpression") {
|
|
47
|
+
const callee = node.callee;
|
|
48
|
+
if (
|
|
49
|
+
callee.type === "MemberExpression" &&
|
|
50
|
+
callee.object.type === "Super" &&
|
|
51
|
+
callee.property.type === "Identifier" &&
|
|
52
|
+
callee.property.name === methodName
|
|
53
|
+
) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 递归检查子节点(只检查常见的子节点类型,避免遍历所有属性)
|
|
59
|
+
const nodeAny = node as unknown as Record<string, unknown>;
|
|
60
|
+
const keysToCheck = [
|
|
61
|
+
"body",
|
|
62
|
+
"consequent",
|
|
63
|
+
"alternate",
|
|
64
|
+
"block",
|
|
65
|
+
"handler",
|
|
66
|
+
"finalizer",
|
|
67
|
+
"argument",
|
|
68
|
+
"callee",
|
|
69
|
+
"expression",
|
|
70
|
+
"declarations",
|
|
71
|
+
"init",
|
|
72
|
+
"test",
|
|
73
|
+
"update",
|
|
74
|
+
"cases",
|
|
75
|
+
"discriminant",
|
|
76
|
+
"object",
|
|
77
|
+
"property",
|
|
78
|
+
"elements",
|
|
79
|
+
"properties",
|
|
80
|
+
"value",
|
|
81
|
+
"key",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const key of keysToCheck) {
|
|
85
|
+
const child = nodeAny[key];
|
|
86
|
+
if (Array.isArray(child)) {
|
|
87
|
+
for (const item of child) {
|
|
88
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
89
|
+
if (
|
|
90
|
+
checkForSuperCall(
|
|
91
|
+
item as import("estree").Node,
|
|
92
|
+
methodName,
|
|
93
|
+
visited
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (child && typeof child === "object" && "type" in child) {
|
|
101
|
+
if (checkForSuperCall(child as import("estree").Node, methodName, visited)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
MethodDefinition(node: import("estree").MethodDefinition) {
|
|
112
|
+
// 只检查 protected 方法
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
if ((node as any).accessibility !== "protected") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const methodName = node.key.type === "Identifier" ? node.key.name : null;
|
|
119
|
+
if (!methodName || !lifecycleMethods.has(methodName)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 检查方法体中是否调用了 super
|
|
124
|
+
const methodValue = node.value;
|
|
125
|
+
if (!methodValue || methodValue.type !== "FunctionExpression") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const body = methodValue.body;
|
|
130
|
+
if (!body || body.type !== "BlockStatement") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 检查是否有 super 调用
|
|
135
|
+
let hasSuperCall = false;
|
|
136
|
+
for (const statement of body.body) {
|
|
137
|
+
if (checkForSuperCall(statement, methodName)) {
|
|
138
|
+
hasSuperCall = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!hasSuperCall) {
|
|
144
|
+
context.report({
|
|
145
|
+
node: node.key,
|
|
146
|
+
messageId: "mustCallSuper",
|
|
147
|
+
data: {
|
|
148
|
+
methodName,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:no-duplicate-keys
|
|
3
|
+
*
|
|
4
|
+
* 检测同一个 key 在不同父容器中使用的情况
|
|
5
|
+
* 这会导致 DOM 缓存冲突,元素被错误地移动到错误的容器中
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Rule } from "eslint";
|
|
9
|
+
import { WSXRuleModule } from "../types";
|
|
10
|
+
|
|
11
|
+
interface KeyUsage {
|
|
12
|
+
key: string;
|
|
13
|
+
parentName: string;
|
|
14
|
+
node: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const noDuplicateKeys: WSXRuleModule = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "disallow using the same key in different parent containers",
|
|
22
|
+
category: "Possible Errors",
|
|
23
|
+
recommended: true,
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
duplicateKey:
|
|
27
|
+
'Duplicate key "{{key}}" found in different parent containers ({{parent1}} and {{parent2}}). ' +
|
|
28
|
+
"This will cause DOM cache conflicts. " +
|
|
29
|
+
'Use unique key prefixes like key="{{parent1}}-{{key}}" and key="{{parent2}}-{{key}}".',
|
|
30
|
+
},
|
|
31
|
+
schema: [], // 无配置选项
|
|
32
|
+
},
|
|
33
|
+
create(context: Rule.RuleContext) {
|
|
34
|
+
// Map<FunctionNode, Map<KeyValue, KeyUsage>>
|
|
35
|
+
const functionKeyMap = new Map<unknown, Map<string, KeyUsage>>();
|
|
36
|
+
const functionStack: unknown[] = [];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the parent JSX element name
|
|
40
|
+
*/
|
|
41
|
+
function getParentJSXName(node: unknown): string {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
let parent = (node as any).parent;
|
|
44
|
+
|
|
45
|
+
// Traverse up to find the parent JSX element
|
|
46
|
+
while (parent) {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
if ((parent as any).type === "JSXElement") {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
const openingElement = (parent as any).openingElement;
|
|
51
|
+
if (openingElement) {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
const name = (openingElement as any).name;
|
|
54
|
+
if (name) {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
const nameValue = (name as any).name || name;
|
|
57
|
+
if (typeof nameValue === "string") {
|
|
58
|
+
return nameValue;
|
|
59
|
+
}
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
if ((nameValue as any).name) {
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
return (nameValue as any).name;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
parent = (parent as any).parent;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return "unknown";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extracts the key value from JSX attribute
|
|
77
|
+
*/
|
|
78
|
+
function getKeyValue(attr: unknown): string | null {
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
const value = (attr as any).value;
|
|
81
|
+
|
|
82
|
+
if (!value) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// String literal: key="value"
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
if ((value as any).type === "Literal") {
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
return String((value as any).value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Template literal: key={`prefix-${id}`}
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
if ((value as any).type === "JSXExpressionContainer") {
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
const expression = (value as any).expression;
|
|
98
|
+
|
|
99
|
+
// Simple identifier: key={itemId}
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
if ((expression as any).type === "Identifier") {
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
return (expression as any).name;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Template literal: key={`prefix-${id}`}
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
if ((expression as any).type === "TemplateLiteral") {
|
|
109
|
+
// Extract the static parts
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
const quasis = (expression as any).quasis || [];
|
|
112
|
+
if (quasis.length > 0 && quasis[0].value) {
|
|
113
|
+
return quasis[0].value.raw || quasis[0].value.cooked;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
// Track function/method entry
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
":function"(node: any) {
|
|
125
|
+
functionStack.push(node);
|
|
126
|
+
functionKeyMap.set(node, new Map());
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Track function/method exit
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
+
":function:exit"(_node: any) {
|
|
132
|
+
functionStack.pop();
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Check JSX elements for key attributes
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
JSXOpeningElement(node: any) {
|
|
138
|
+
if (functionStack.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const currentFunction = functionStack[functionStack.length - 1];
|
|
143
|
+
const keyMap = functionKeyMap.get(currentFunction);
|
|
144
|
+
|
|
145
|
+
if (!keyMap) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Find the key attribute
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
const keyAttr = (node.attributes || []).find((attr: any) => {
|
|
152
|
+
return attr.type === "JSXAttribute" && attr.name && attr.name.name === "key";
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!keyAttr) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const keyValue = getKeyValue(keyAttr);
|
|
160
|
+
|
|
161
|
+
if (!keyValue) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const parentName = getParentJSXName(node);
|
|
166
|
+
|
|
167
|
+
// Check if this key was already used in a different parent
|
|
168
|
+
const existing = keyMap.get(keyValue);
|
|
169
|
+
|
|
170
|
+
if (existing && existing.parentName !== parentName) {
|
|
171
|
+
// Found duplicate key in different parent!
|
|
172
|
+
context.report({
|
|
173
|
+
node: keyAttr,
|
|
174
|
+
messageId: "duplicateKey",
|
|
175
|
+
data: {
|
|
176
|
+
key: keyValue,
|
|
177
|
+
parent1: existing.parentName,
|
|
178
|
+
parent2: parentName,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
} else if (!existing) {
|
|
182
|
+
// First time seeing this key, record it
|
|
183
|
+
keyMap.set(keyValue, {
|
|
184
|
+
key: keyValue,
|
|
185
|
+
parentName,
|
|
186
|
+
node: keyAttr,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
};
|