eslint-cdk-plugin 3.0.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/dist/index.cjs +223 -31
- package/dist/index.d.ts +13 -77
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +223 -31
- package/package.json +2 -4
- package/src/index.ts +17 -2
- package/src/rules/construct-constructor-property.ts +36 -17
- package/src/rules/no-construct-in-public-property-of-construct.ts +2 -5
- package/src/rules/no-construct-stack-suffix.ts +1 -1
- package/src/rules/no-unused-props.ts +176 -0
- package/src/rules/no-variable-construct-id.ts +1 -1
- package/src/rules/pascal-case-construct-id.ts +1 -1
- package/src/rules/props-name-convention.ts +5 -11
- package/src/rules/require-jsdoc.ts +9 -6
- package/src/rules/require-passing-this.ts +1 -1
- package/src/rules/require-props-default-doc.ts +6 -14
- package/src/utils/getConstructor.ts +16 -0
- package/src/utils/{parseType.ts → getPropertyNames.ts} +20 -0
- package/src/utils/propsUsageTracker.ts +188 -0
- package/src/utils/typeCheck.ts +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,12 +3,18 @@ import { ESLintUtils, AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
|
|
5
5
|
var name = "eslint-cdk-plugin";
|
|
6
|
-
var version = "3.0
|
|
6
|
+
var version = "3.2.0";
|
|
7
7
|
|
|
8
8
|
const createRule = ESLintUtils.RuleCreator(
|
|
9
9
|
(name) => `https://eslint-cdk-plugin.dev/rules/${name}`
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
+
const getConstructor = (node) => {
|
|
13
|
+
return node.body.body.find(
|
|
14
|
+
(member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
12
18
|
const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
|
|
13
19
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
14
20
|
return isTargetSuperClassType(
|
|
@@ -23,7 +29,7 @@ const isConstructType = (type, ignoredClasses = ["App", "Stage", "Stack"]) => {
|
|
|
23
29
|
};
|
|
24
30
|
const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
|
|
25
31
|
if (!type.symbol) return false;
|
|
26
|
-
if (targetSuperClasses.some((suffix) => type.symbol.name
|
|
32
|
+
if (targetSuperClasses.some((suffix) => type.symbol.name === suffix)) {
|
|
27
33
|
return true;
|
|
28
34
|
}
|
|
29
35
|
const baseTypes = type.getBaseTypes() ?? [];
|
|
@@ -38,7 +44,9 @@ const constructConstructorProperty = createRule({
|
|
|
38
44
|
description: "Enforces that constructors of classes extending Construct have the property names 'scope, id' or 'scope, id, props'"
|
|
39
45
|
},
|
|
40
46
|
messages: {
|
|
41
|
-
invalidConstructorProperty: "Constructor of a class extending Construct must have the property names 'scope, id' or 'scope, id, props'"
|
|
47
|
+
invalidConstructorProperty: "Constructor of a class extending Construct must have the property names 'scope, id' or 'scope, id, props'",
|
|
48
|
+
invalidConstructorType: "Constructor of a class extending Construct must have the type 'Construct' for the first parameter",
|
|
49
|
+
invalidConstructorIdType: "Constructor of a class extending Construct must have the type 'string' for the second parameter"
|
|
42
50
|
},
|
|
43
51
|
schema: []
|
|
44
52
|
},
|
|
@@ -49,16 +57,14 @@ const constructConstructorProperty = createRule({
|
|
|
49
57
|
ClassDeclaration(node) {
|
|
50
58
|
const type = parserServices.getTypeAtLocation(node);
|
|
51
59
|
if (!isConstructType(type)) return;
|
|
52
|
-
const constructor = node
|
|
53
|
-
(member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
54
|
-
);
|
|
60
|
+
const constructor = getConstructor(node);
|
|
55
61
|
if (!constructor) return;
|
|
56
|
-
validateConstructorProperty(constructor, context);
|
|
62
|
+
validateConstructorProperty(constructor, context, parserServices);
|
|
57
63
|
}
|
|
58
64
|
};
|
|
59
65
|
}
|
|
60
66
|
});
|
|
61
|
-
const validateConstructorProperty = (constructor, context) => {
|
|
67
|
+
const validateConstructorProperty = (constructor, context, parserServices) => {
|
|
62
68
|
const params = constructor.value.params;
|
|
63
69
|
if (params.length < 2) {
|
|
64
70
|
context.report({
|
|
@@ -68,24 +74,35 @@ const validateConstructorProperty = (constructor, context) => {
|
|
|
68
74
|
return;
|
|
69
75
|
}
|
|
70
76
|
const firstParam = params[0];
|
|
71
|
-
if (firstParam.type
|
|
77
|
+
if (firstParam.type !== AST_NODE_TYPES.Identifier || firstParam.name !== "scope") {
|
|
72
78
|
context.report({
|
|
73
79
|
node: firstParam,
|
|
74
80
|
messageId: "invalidConstructorProperty"
|
|
75
81
|
});
|
|
76
|
-
|
|
82
|
+
} else if (!isConstructType(parserServices.getTypeAtLocation(firstParam))) {
|
|
83
|
+
context.report({
|
|
84
|
+
node: firstParam,
|
|
85
|
+
messageId: "invalidConstructorType"
|
|
86
|
+
});
|
|
77
87
|
}
|
|
78
88
|
const secondParam = params[1];
|
|
79
|
-
if (secondParam.type
|
|
89
|
+
if (secondParam.type !== AST_NODE_TYPES.Identifier || secondParam.name !== "id") {
|
|
80
90
|
context.report({
|
|
81
91
|
node: secondParam,
|
|
82
92
|
messageId: "invalidConstructorProperty"
|
|
83
93
|
});
|
|
84
94
|
return;
|
|
95
|
+
} else if (secondParam.typeAnnotation?.typeAnnotation.type !== AST_NODE_TYPES.TSStringKeyword) {
|
|
96
|
+
context.report({
|
|
97
|
+
node: secondParam,
|
|
98
|
+
messageId: "invalidConstructorIdType"
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
85
101
|
}
|
|
86
|
-
if (params.length < 3)
|
|
102
|
+
if (params.length < 3)
|
|
103
|
+
return;
|
|
87
104
|
const thirdParam = params[2];
|
|
88
|
-
if (thirdParam.type
|
|
105
|
+
if (thirdParam.type !== AST_NODE_TYPES.Identifier || thirdParam.name !== "props") {
|
|
89
106
|
context.report({
|
|
90
107
|
node: thirdParam,
|
|
91
108
|
messageId: "invalidConstructorProperty"
|
|
@@ -161,9 +178,7 @@ const noConstructInPublicPropertyOfConstruct = createRule({
|
|
|
161
178
|
const type = parserServices.getTypeAtLocation(node);
|
|
162
179
|
if (!isConstructOrStackType(type)) return;
|
|
163
180
|
validatePublicPropertyOfConstruct(node, context, parserServices);
|
|
164
|
-
const constructor = node
|
|
165
|
-
(member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
166
|
-
);
|
|
181
|
+
const constructor = getConstructor(node);
|
|
167
182
|
if (!constructor || constructor.value.type !== AST_NODE_TYPES.FunctionExpression) {
|
|
168
183
|
return;
|
|
169
184
|
}
|
|
@@ -231,6 +246,12 @@ const toPascalCase = (str) => {
|
|
|
231
246
|
}).join("");
|
|
232
247
|
};
|
|
233
248
|
|
|
249
|
+
const getPropertyNames = (properties) => {
|
|
250
|
+
return properties.reduce(
|
|
251
|
+
(acc, prop) => prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier ? [...acc, prop.key.name] : acc,
|
|
252
|
+
[]
|
|
253
|
+
);
|
|
254
|
+
};
|
|
234
255
|
const getConstructorPropertyNames = (type) => {
|
|
235
256
|
const declarations = type.symbol?.declarations;
|
|
236
257
|
if (!declarations?.length) return [];
|
|
@@ -752,6 +773,176 @@ const validateConstructId$2 = ({
|
|
|
752
773
|
}
|
|
753
774
|
};
|
|
754
775
|
|
|
776
|
+
const propsUsageTrackerFactory = (propsType) => {
|
|
777
|
+
const getPropsPropertyNames = (propsType2) => {
|
|
778
|
+
const typeProperties = propsType2.getProperties();
|
|
779
|
+
if (typeProperties.length) {
|
|
780
|
+
return typeProperties.reduce(
|
|
781
|
+
(acc, prop) => !isInternalProperty(prop.name) ? [...acc, prop.name] : acc,
|
|
782
|
+
[]
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const symbol = propsType2.getSymbol();
|
|
786
|
+
if (!symbol?.members) return [];
|
|
787
|
+
return Array.from(symbol.members.keys()).reduce((acc, key) => {
|
|
788
|
+
const name = String(key);
|
|
789
|
+
return !isInternalProperty(name) ? [...acc, name] : acc;
|
|
790
|
+
}, []);
|
|
791
|
+
};
|
|
792
|
+
const isInternalProperty = (propertyName) => {
|
|
793
|
+
return propertyName.startsWith("_") || propertyName === "constructor" || propertyName === "prototype";
|
|
794
|
+
};
|
|
795
|
+
const markAsUsed = (propertyName) => {
|
|
796
|
+
if (propUsages.has(propertyName)) propUsages.set(propertyName, true);
|
|
797
|
+
};
|
|
798
|
+
const propUsages = new Map(
|
|
799
|
+
getPropsPropertyNames(propsType).map((name) => [name, false])
|
|
800
|
+
);
|
|
801
|
+
const getUnusedProperties = () => {
|
|
802
|
+
return Array.from(propUsages.entries()).reduce(
|
|
803
|
+
(acc, [name, used]) => !used ? [...acc, name] : acc,
|
|
804
|
+
[]
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
const markAsUsedForMemberExpression = (node, propsParamName) => {
|
|
808
|
+
if (node.object.type === AST_NODE_TYPES.Identifier && node.object.name === propsParamName && node.property.type === AST_NODE_TYPES.Identifier) {
|
|
809
|
+
markAsUsed(node.property.name);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (node.object.type === AST_NODE_TYPES.MemberExpression && node.object.object.type === AST_NODE_TYPES.ThisExpression && node.object.property.type === AST_NODE_TYPES.Identifier && node.object.property.name === "props" && node.property.type === AST_NODE_TYPES.Identifier) {
|
|
813
|
+
markAsUsed(node.property.name);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
const markAsUsedForVariableDeclarator = (node, propsParamName) => {
|
|
818
|
+
if (node.id.type !== AST_NODE_TYPES.ObjectPattern || node.init?.type !== AST_NODE_TYPES.Identifier || node.init.name !== propsParamName) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const propertyNames = getPropertyNames(node.id.properties);
|
|
822
|
+
for (const name of propertyNames) {
|
|
823
|
+
markAsUsed(name);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const markAsUsedForAssignmentExpression = (node, propsParamName) => {
|
|
827
|
+
if (node.right.type !== AST_NODE_TYPES.MemberExpression || node.right.object.type !== AST_NODE_TYPES.Identifier || node.right.object.name !== propsParamName || node.right.property.type !== AST_NODE_TYPES.Identifier) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
markAsUsed(node.right.property.name);
|
|
831
|
+
};
|
|
832
|
+
const markAsUsedForObjectPattern = (node) => {
|
|
833
|
+
for (const propName of getPropertyNames(node.properties)) {
|
|
834
|
+
markAsUsed(propName);
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
if (!propUsages.size) return null;
|
|
838
|
+
return {
|
|
839
|
+
getUnusedProperties,
|
|
840
|
+
markAsUsedForMemberExpression,
|
|
841
|
+
markAsUsedForVariableDeclarator,
|
|
842
|
+
markAsUsedForAssignmentExpression,
|
|
843
|
+
markAsUsedForObjectPattern
|
|
844
|
+
};
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const noUnusedProps = createRule({
|
|
848
|
+
name: "no-unused-props",
|
|
849
|
+
meta: {
|
|
850
|
+
type: "suggestion",
|
|
851
|
+
docs: {
|
|
852
|
+
description: "Enforces that all properties defined in props type are used within the constructor"
|
|
853
|
+
},
|
|
854
|
+
messages: {
|
|
855
|
+
unusedProp: "Property '{{propName}}' is defined in props but never used"
|
|
856
|
+
},
|
|
857
|
+
schema: []
|
|
858
|
+
},
|
|
859
|
+
defaultOptions: [],
|
|
860
|
+
create(context) {
|
|
861
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
862
|
+
return {
|
|
863
|
+
ClassDeclaration(node) {
|
|
864
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
865
|
+
if (!isConstructType(type)) return;
|
|
866
|
+
const constructor = getConstructor(node);
|
|
867
|
+
if (!constructor) return;
|
|
868
|
+
analyzePropsUsage(constructor, context, parserServices);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
const analyzePropsUsage = (constructor, context, parserServices) => {
|
|
874
|
+
const params = constructor.value.params;
|
|
875
|
+
if (params.length < 3) return;
|
|
876
|
+
const propsParam = params[2];
|
|
877
|
+
switch (propsParam.type) {
|
|
878
|
+
case AST_NODE_TYPES.Identifier: {
|
|
879
|
+
const propsType = parserServices.getTypeAtLocation(propsParam);
|
|
880
|
+
const tracker = propsUsageTrackerFactory(propsType);
|
|
881
|
+
if (!tracker || !constructor.value.body) return;
|
|
882
|
+
analyzeConstructorBody(constructor.value.body, propsParam.name, tracker);
|
|
883
|
+
reportUnusedProperties(tracker, propsParam, context);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
case AST_NODE_TYPES.ObjectPattern: {
|
|
887
|
+
const typeAnnotation = propsParam.typeAnnotation?.typeAnnotation;
|
|
888
|
+
if (!typeAnnotation) return;
|
|
889
|
+
const propsType = parserServices.getTypeAtLocation(typeAnnotation);
|
|
890
|
+
const tracker = propsUsageTrackerFactory(propsType);
|
|
891
|
+
if (!tracker) return;
|
|
892
|
+
tracker.markAsUsedForObjectPattern(propsParam);
|
|
893
|
+
reportUnusedProperties(tracker, propsParam, context);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
default:
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const analyzeConstructorBody = (body, propsParamName, tracker) => {
|
|
901
|
+
const visited = /* @__PURE__ */ new Set();
|
|
902
|
+
const visitNode = (node) => {
|
|
903
|
+
if (visited.has(node)) return;
|
|
904
|
+
visited.add(node);
|
|
905
|
+
switch (node.type) {
|
|
906
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
907
|
+
tracker.markAsUsedForMemberExpression(node, propsParamName);
|
|
908
|
+
break;
|
|
909
|
+
case AST_NODE_TYPES.VariableDeclarator:
|
|
910
|
+
tracker.markAsUsedForVariableDeclarator(node, propsParamName);
|
|
911
|
+
break;
|
|
912
|
+
case AST_NODE_TYPES.AssignmentExpression:
|
|
913
|
+
tracker.markAsUsedForAssignmentExpression(node, propsParamName);
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
const children = getChildNodes(node);
|
|
917
|
+
for (const child of children) {
|
|
918
|
+
visitNode(child);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
visitNode(body);
|
|
922
|
+
};
|
|
923
|
+
const reportUnusedProperties = (tracker, propsParam, context) => {
|
|
924
|
+
for (const propName of tracker.getUnusedProperties()) {
|
|
925
|
+
context.report({
|
|
926
|
+
node: propsParam,
|
|
927
|
+
messageId: "unusedProp",
|
|
928
|
+
data: {
|
|
929
|
+
propName
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const getChildNodes = (node) => {
|
|
935
|
+
return Object.entries(node).reduce((acc, [key, value]) => {
|
|
936
|
+
if (["parent", "range", "loc"].includes(key)) return acc;
|
|
937
|
+
if (isESTreeNode(value)) return [...acc, value];
|
|
938
|
+
if (Array.isArray(value)) return [...acc, ...value.filter(isESTreeNode)];
|
|
939
|
+
return acc;
|
|
940
|
+
}, []);
|
|
941
|
+
};
|
|
942
|
+
const isESTreeNode = (value) => {
|
|
943
|
+
return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
944
|
+
};
|
|
945
|
+
|
|
755
946
|
const noVariableConstructId = createRule({
|
|
756
947
|
name: "no-variable-construct-id",
|
|
757
948
|
meta: {
|
|
@@ -888,10 +1079,9 @@ const propsNameConvention = createRule({
|
|
|
888
1079
|
if (!node.id || !node.superClass) return;
|
|
889
1080
|
const type = parserServices.getTypeAtLocation(node.superClass);
|
|
890
1081
|
if (!isConstructType(type)) return;
|
|
891
|
-
const constructor = node
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const propsParam = constructor?.value.params?.[2];
|
|
1082
|
+
const constructor = getConstructor(node);
|
|
1083
|
+
if (!constructor) return;
|
|
1084
|
+
const propsParam = constructor.value.params?.[2];
|
|
895
1085
|
if (propsParam?.type !== AST_NODE_TYPES.Identifier) return;
|
|
896
1086
|
const typeAnnotation = propsParam.typeAnnotation;
|
|
897
1087
|
if (typeAnnotation?.type !== AST_NODE_TYPES.TSTypeAnnotation) return;
|
|
@@ -933,9 +1123,10 @@ const requireJSDoc = createRule({
|
|
|
933
1123
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
934
1124
|
return {
|
|
935
1125
|
TSPropertySignature(node) {
|
|
936
|
-
if (node.key.type !== AST_NODE_TYPES.Identifier
|
|
937
|
-
|
|
938
|
-
|
|
1126
|
+
if (node.key.type !== AST_NODE_TYPES.Identifier) return;
|
|
1127
|
+
const parent = node.parent.parent;
|
|
1128
|
+
if (parent.type !== AST_NODE_TYPES.TSInterfaceDeclaration) return;
|
|
1129
|
+
if (!parent.id.name.endsWith("Props")) return;
|
|
939
1130
|
const sourceCode = context.sourceCode;
|
|
940
1131
|
const comments = sourceCode.getCommentsBefore(node);
|
|
941
1132
|
const hasJSDoc = comments.some(
|
|
@@ -1064,14 +1255,11 @@ const requirePropsDefaultDoc = createRule({
|
|
|
1064
1255
|
create(context) {
|
|
1065
1256
|
return {
|
|
1066
1257
|
TSPropertySignature(node) {
|
|
1258
|
+
if (node.key.type !== AST_NODE_TYPES.Identifier) return;
|
|
1067
1259
|
if (!node.optional) return;
|
|
1068
|
-
const parent = node.parent
|
|
1069
|
-
if (parent
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
if (parent.id.type !== AST_NODE_TYPES.Identifier || !parent.id.name.endsWith("Props")) {
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1260
|
+
const parent = node.parent.parent;
|
|
1261
|
+
if (parent.type !== AST_NODE_TYPES.TSInterfaceDeclaration) return;
|
|
1262
|
+
if (!parent.id.name.endsWith("Props")) return;
|
|
1075
1263
|
const sourceCode = context.sourceCode;
|
|
1076
1264
|
const comments = sourceCode.getCommentsBefore(node);
|
|
1077
1265
|
const hasDefaultDoc = comments.some(
|
|
@@ -1082,7 +1270,7 @@ const requirePropsDefaultDoc = createRule({
|
|
|
1082
1270
|
node,
|
|
1083
1271
|
messageId: "missingDefaultDoc",
|
|
1084
1272
|
data: {
|
|
1085
|
-
propertyName: node.key.
|
|
1273
|
+
propertyName: node.key.name
|
|
1086
1274
|
}
|
|
1087
1275
|
});
|
|
1088
1276
|
}
|
|
@@ -1100,6 +1288,7 @@ const rules = {
|
|
|
1100
1288
|
"no-mutable-property-of-props-interface": noMutablePropertyOfPropsInterface,
|
|
1101
1289
|
"no-mutable-public-property-of-construct": noMutablePublicPropertyOfConstruct,
|
|
1102
1290
|
"no-parent-name-construct-id-match": noParentNameConstructIdMatch,
|
|
1291
|
+
"no-unused-props": noUnusedProps,
|
|
1103
1292
|
"no-variable-construct-id": noVariableConstructId,
|
|
1104
1293
|
"pascal-case-construct-id": pascalCaseConstructId,
|
|
1105
1294
|
"props-name-convention": propsNameConvention,
|
|
@@ -1136,6 +1325,8 @@ const recommended = createFlatConfig({
|
|
|
1136
1325
|
"error",
|
|
1137
1326
|
{ disallowContainingParentName: false }
|
|
1138
1327
|
],
|
|
1328
|
+
// TODO: Enable this rule at v4.0.0
|
|
1329
|
+
// "cdk/no-unused-props": "error",
|
|
1139
1330
|
"cdk/no-variable-construct-id": "error",
|
|
1140
1331
|
"cdk/pascal-case-construct-id": "error",
|
|
1141
1332
|
"cdk/require-passing-this": ["error", { allowNonThisAndDisallowScope: true }]
|
|
@@ -1152,6 +1343,7 @@ const strict = createFlatConfig({
|
|
|
1152
1343
|
"error",
|
|
1153
1344
|
{ disallowContainingParentName: true }
|
|
1154
1345
|
],
|
|
1346
|
+
"cdk/no-unused-props": "error",
|
|
1155
1347
|
"cdk/no-variable-construct-id": "error",
|
|
1156
1348
|
"cdk/pascal-case-construct-id": "error",
|
|
1157
1349
|
"cdk/props-name-convention": "error",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-cdk-plugin",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "eslint plugin for AWS CDK projects",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"standard-version": "^9.5.0",
|
|
40
40
|
"typescript": "^5.8.3",
|
|
41
41
|
"typescript-eslint": "^8.32.1",
|
|
42
|
-
"vitepress-plugin-llms": "^1.1.4",
|
|
43
42
|
"vitest": "^3.1.3"
|
|
44
43
|
},
|
|
45
44
|
"dependencies": {
|
|
@@ -47,8 +46,7 @@
|
|
|
47
46
|
"@typescript-eslint/utils": "^8.32.1"
|
|
48
47
|
},
|
|
49
48
|
"volta": {
|
|
50
|
-
"node": "22.15.0"
|
|
51
|
-
"pnpm": "10.10.0"
|
|
49
|
+
"node": "22.15.0"
|
|
52
50
|
},
|
|
53
51
|
"files": [
|
|
54
52
|
"dist",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import tsParser from "@typescript-eslint/parser";
|
|
2
|
+
import { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
|
|
2
3
|
|
|
3
4
|
import { name, version } from "../package.json";
|
|
4
5
|
|
|
@@ -10,6 +11,7 @@ import { noImportPrivate } from "./rules/no-import-private";
|
|
|
10
11
|
import { noMutablePropertyOfPropsInterface } from "./rules/no-mutable-property-of-props-interface";
|
|
11
12
|
import { noMutablePublicPropertyOfConstruct } from "./rules/no-mutable-public-property-of-construct";
|
|
12
13
|
import { noParentNameConstructIdMatch } from "./rules/no-parent-name-construct-id-match";
|
|
14
|
+
import { noUnusedProps } from "./rules/no-unused-props";
|
|
13
15
|
import { noVariableConstructId } from "./rules/no-variable-construct-id";
|
|
14
16
|
import { pascalCaseConstructId } from "./rules/pascal-case-construct-id";
|
|
15
17
|
import { propsNameConvention } from "./rules/props-name-convention";
|
|
@@ -27,6 +29,7 @@ const rules = {
|
|
|
27
29
|
"no-mutable-property-of-props-interface": noMutablePropertyOfPropsInterface,
|
|
28
30
|
"no-mutable-public-property-of-construct": noMutablePublicPropertyOfConstruct,
|
|
29
31
|
"no-parent-name-construct-id-match": noParentNameConstructIdMatch,
|
|
32
|
+
"no-unused-props": noUnusedProps,
|
|
30
33
|
"no-variable-construct-id": noVariableConstructId,
|
|
31
34
|
"pascal-case-construct-id": pascalCaseConstructId,
|
|
32
35
|
"props-name-convention": propsNameConvention,
|
|
@@ -40,7 +43,13 @@ const cdkPlugin = {
|
|
|
40
43
|
rules,
|
|
41
44
|
};
|
|
42
45
|
|
|
43
|
-
const createFlatConfig = (
|
|
46
|
+
const createFlatConfig = (
|
|
47
|
+
rules: FlatConfig.Rules
|
|
48
|
+
): {
|
|
49
|
+
languageOptions: FlatConfig.LanguageOptions;
|
|
50
|
+
plugins: FlatConfig.Plugins;
|
|
51
|
+
rules: FlatConfig.Rules;
|
|
52
|
+
} => {
|
|
44
53
|
return {
|
|
45
54
|
languageOptions: {
|
|
46
55
|
parser: tsParser,
|
|
@@ -66,6 +75,8 @@ const recommended = createFlatConfig({
|
|
|
66
75
|
"error",
|
|
67
76
|
{ disallowContainingParentName: false },
|
|
68
77
|
],
|
|
78
|
+
// TODO: Enable this rule at v4.0.0
|
|
79
|
+
// "cdk/no-unused-props": "error",
|
|
69
80
|
"cdk/no-variable-construct-id": "error",
|
|
70
81
|
"cdk/pascal-case-construct-id": "error",
|
|
71
82
|
"cdk/require-passing-this": ["error", { allowNonThisAndDisallowScope: true }],
|
|
@@ -83,6 +94,7 @@ const strict = createFlatConfig({
|
|
|
83
94
|
"error",
|
|
84
95
|
{ disallowContainingParentName: true },
|
|
85
96
|
],
|
|
97
|
+
"cdk/no-unused-props": "error",
|
|
86
98
|
"cdk/no-variable-construct-id": "error",
|
|
87
99
|
"cdk/pascal-case-construct-id": "error",
|
|
88
100
|
"cdk/props-name-convention": "error",
|
|
@@ -100,7 +112,10 @@ export { configs, rules };
|
|
|
100
112
|
|
|
101
113
|
export interface EslintCdkPlugin {
|
|
102
114
|
rules: typeof rules;
|
|
103
|
-
configs:
|
|
115
|
+
configs: Readonly<{
|
|
116
|
+
recommended: FlatConfig.Config;
|
|
117
|
+
strict: FlatConfig.Config;
|
|
118
|
+
}>;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
const eslintCdkPlugin: EslintCdkPlugin = {
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AST_NODE_TYPES,
|
|
3
3
|
ESLintUtils,
|
|
4
|
+
ParserServicesWithTypeInformation,
|
|
4
5
|
TSESLint,
|
|
5
6
|
TSESTree,
|
|
6
7
|
} from "@typescript-eslint/utils";
|
|
7
8
|
|
|
8
9
|
import { createRule } from "../utils/createRule";
|
|
10
|
+
import { getConstructor } from "../utils/getConstructor";
|
|
9
11
|
import { isConstructType } from "../utils/typeCheck";
|
|
10
12
|
|
|
11
|
-
type Context = TSESLint.RuleContext<
|
|
13
|
+
type Context = TSESLint.RuleContext<
|
|
14
|
+
| "invalidConstructorProperty"
|
|
15
|
+
| "invalidConstructorType"
|
|
16
|
+
| "invalidConstructorIdType",
|
|
17
|
+
[]
|
|
18
|
+
>;
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Enforces that constructors of classes extending Construct have the property names 'scope, id' or 'scope, id, props'
|
|
@@ -26,6 +33,10 @@ export const constructConstructorProperty = createRule({
|
|
|
26
33
|
messages: {
|
|
27
34
|
invalidConstructorProperty:
|
|
28
35
|
"Constructor of a class extending Construct must have the property names 'scope, id' or 'scope, id, props'",
|
|
36
|
+
invalidConstructorType:
|
|
37
|
+
"Constructor of a class extending Construct must have the type 'Construct' for the first parameter",
|
|
38
|
+
invalidConstructorIdType:
|
|
39
|
+
"Constructor of a class extending Construct must have the type 'string' for the second parameter",
|
|
29
40
|
},
|
|
30
41
|
schema: [],
|
|
31
42
|
},
|
|
@@ -37,17 +48,10 @@ export const constructConstructorProperty = createRule({
|
|
|
37
48
|
const type = parserServices.getTypeAtLocation(node);
|
|
38
49
|
if (!isConstructType(type)) return;
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
const constructor = node.body.body.find(
|
|
42
|
-
(member): member is TSESTree.MethodDefinition =>
|
|
43
|
-
member.type === AST_NODE_TYPES.MethodDefinition &&
|
|
44
|
-
member.kind === "constructor"
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// NOTE: Skip if there's no constructor
|
|
51
|
+
const constructor = getConstructor(node);
|
|
48
52
|
if (!constructor) return;
|
|
49
53
|
|
|
50
|
-
validateConstructorProperty(constructor, context);
|
|
54
|
+
validateConstructorProperty(constructor, context, parserServices);
|
|
51
55
|
},
|
|
52
56
|
};
|
|
53
57
|
},
|
|
@@ -58,7 +62,8 @@ export const constructConstructorProperty = createRule({
|
|
|
58
62
|
*/
|
|
59
63
|
const validateConstructorProperty = (
|
|
60
64
|
constructor: TSESTree.MethodDefinition,
|
|
61
|
-
context: Context
|
|
65
|
+
context: Context,
|
|
66
|
+
parserServices: ParserServicesWithTypeInformation
|
|
62
67
|
): void => {
|
|
63
68
|
const params = constructor.value.params;
|
|
64
69
|
|
|
@@ -74,20 +79,24 @@ const validateConstructorProperty = (
|
|
|
74
79
|
// NOTE: Check if the first parameter is named "scope"
|
|
75
80
|
const firstParam = params[0];
|
|
76
81
|
if (
|
|
77
|
-
firstParam.type
|
|
82
|
+
firstParam.type !== AST_NODE_TYPES.Identifier ||
|
|
78
83
|
firstParam.name !== "scope"
|
|
79
84
|
) {
|
|
80
85
|
context.report({
|
|
81
86
|
node: firstParam,
|
|
82
87
|
messageId: "invalidConstructorProperty",
|
|
83
88
|
});
|
|
84
|
-
|
|
89
|
+
} else if (!isConstructType(parserServices.getTypeAtLocation(firstParam))) {
|
|
90
|
+
context.report({
|
|
91
|
+
node: firstParam,
|
|
92
|
+
messageId: "invalidConstructorType",
|
|
93
|
+
});
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
// NOTE: Check if the second parameter is named "id"
|
|
88
97
|
const secondParam = params[1];
|
|
89
98
|
if (
|
|
90
|
-
secondParam.type
|
|
99
|
+
secondParam.type !== AST_NODE_TYPES.Identifier ||
|
|
91
100
|
secondParam.name !== "id"
|
|
92
101
|
) {
|
|
93
102
|
context.report({
|
|
@@ -95,15 +104,25 @@ const validateConstructorProperty = (
|
|
|
95
104
|
messageId: "invalidConstructorProperty",
|
|
96
105
|
});
|
|
97
106
|
return;
|
|
107
|
+
} else if (
|
|
108
|
+
secondParam.typeAnnotation?.typeAnnotation.type !==
|
|
109
|
+
AST_NODE_TYPES.TSStringKeyword
|
|
110
|
+
) {
|
|
111
|
+
context.report({
|
|
112
|
+
node: secondParam,
|
|
113
|
+
messageId: "invalidConstructorIdType",
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
if (params.length < 3)
|
|
119
|
+
// NOTE: If there's no third parameter, return
|
|
120
|
+
return;
|
|
102
121
|
|
|
103
122
|
// NOTE: Check if the third parameter is named "props"
|
|
104
123
|
const thirdParam = params[2];
|
|
105
124
|
if (
|
|
106
|
-
thirdParam.type
|
|
125
|
+
thirdParam.type !== AST_NODE_TYPES.Identifier ||
|
|
107
126
|
thirdParam.name !== "props"
|
|
108
127
|
) {
|
|
109
128
|
context.report({
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { SYMBOL_FLAGS } from "../constants/tsInternalFlags";
|
|
10
10
|
import { createRule } from "../utils/createRule";
|
|
11
|
+
import { getConstructor } from "../utils/getConstructor";
|
|
11
12
|
import { isConstructOrStackType } from "../utils/typeCheck";
|
|
12
13
|
|
|
13
14
|
type Context = TSESLint.RuleContext<"invalidPublicPropertyOfConstruct", []>;
|
|
@@ -42,11 +43,7 @@ export const noConstructInPublicPropertyOfConstruct = createRule({
|
|
|
42
43
|
validatePublicPropertyOfConstruct(node, context, parserServices);
|
|
43
44
|
|
|
44
45
|
// NOTE: Check constructor parameter properties
|
|
45
|
-
const constructor = node
|
|
46
|
-
(member): member is TSESTree.MethodDefinition =>
|
|
47
|
-
member.type === AST_NODE_TYPES.MethodDefinition &&
|
|
48
|
-
member.kind === "constructor"
|
|
49
|
-
);
|
|
46
|
+
const constructor = getConstructor(node);
|
|
50
47
|
if (
|
|
51
48
|
!constructor ||
|
|
52
49
|
constructor.value.type !== AST_NODE_TYPES.FunctionExpression
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import { toPascalCase } from "../utils/convertString";
|
|
9
9
|
import { createRule } from "../utils/createRule";
|
|
10
|
-
import { getConstructorPropertyNames } from "../utils/
|
|
10
|
+
import { getConstructorPropertyNames } from "../utils/getPropertyNames";
|
|
11
11
|
import { isConstructOrStackType } from "../utils/typeCheck";
|
|
12
12
|
|
|
13
13
|
const SUFFIX_TYPE = {
|