eslint-cdk-plugin 3.1.0 → 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/dist/index.cjs +201 -22
- package/dist/index.d.ts +12 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +201 -22
- package/package.json +1 -1
- package/src/index.ts +17 -2
- package/src/rules/construct-constructor-property.ts +2 -8
- 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/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.
|
|
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(
|
|
@@ -51,9 +57,7 @@ const constructConstructorProperty = createRule({
|
|
|
51
57
|
ClassDeclaration(node) {
|
|
52
58
|
const type = parserServices.getTypeAtLocation(node);
|
|
53
59
|
if (!isConstructType(type)) return;
|
|
54
|
-
const constructor = node
|
|
55
|
-
(member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
56
|
-
);
|
|
60
|
+
const constructor = getConstructor(node);
|
|
57
61
|
if (!constructor) return;
|
|
58
62
|
validateConstructorProperty(constructor, context, parserServices);
|
|
59
63
|
}
|
|
@@ -174,9 +178,7 @@ const noConstructInPublicPropertyOfConstruct = createRule({
|
|
|
174
178
|
const type = parserServices.getTypeAtLocation(node);
|
|
175
179
|
if (!isConstructOrStackType(type)) return;
|
|
176
180
|
validatePublicPropertyOfConstruct(node, context, parserServices);
|
|
177
|
-
const constructor = node
|
|
178
|
-
(member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
179
|
-
);
|
|
181
|
+
const constructor = getConstructor(node);
|
|
180
182
|
if (!constructor || constructor.value.type !== AST_NODE_TYPES.FunctionExpression) {
|
|
181
183
|
return;
|
|
182
184
|
}
|
|
@@ -244,6 +246,12 @@ const toPascalCase = (str) => {
|
|
|
244
246
|
}).join("");
|
|
245
247
|
};
|
|
246
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
|
+
};
|
|
247
255
|
const getConstructorPropertyNames = (type) => {
|
|
248
256
|
const declarations = type.symbol?.declarations;
|
|
249
257
|
if (!declarations?.length) return [];
|
|
@@ -765,6 +773,176 @@ const validateConstructId$2 = ({
|
|
|
765
773
|
}
|
|
766
774
|
};
|
|
767
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
|
+
|
|
768
946
|
const noVariableConstructId = createRule({
|
|
769
947
|
name: "no-variable-construct-id",
|
|
770
948
|
meta: {
|
|
@@ -901,10 +1079,9 @@ const propsNameConvention = createRule({
|
|
|
901
1079
|
if (!node.id || !node.superClass) return;
|
|
902
1080
|
const type = parserServices.getTypeAtLocation(node.superClass);
|
|
903
1081
|
if (!isConstructType(type)) return;
|
|
904
|
-
const constructor = node
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const propsParam = constructor?.value.params?.[2];
|
|
1082
|
+
const constructor = getConstructor(node);
|
|
1083
|
+
if (!constructor) return;
|
|
1084
|
+
const propsParam = constructor.value.params?.[2];
|
|
908
1085
|
if (propsParam?.type !== AST_NODE_TYPES.Identifier) return;
|
|
909
1086
|
const typeAnnotation = propsParam.typeAnnotation;
|
|
910
1087
|
if (typeAnnotation?.type !== AST_NODE_TYPES.TSTypeAnnotation) return;
|
|
@@ -946,9 +1123,10 @@ const requireJSDoc = createRule({
|
|
|
946
1123
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
947
1124
|
return {
|
|
948
1125
|
TSPropertySignature(node) {
|
|
949
|
-
if (node.key.type !== AST_NODE_TYPES.Identifier
|
|
950
|
-
|
|
951
|
-
|
|
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;
|
|
952
1130
|
const sourceCode = context.sourceCode;
|
|
953
1131
|
const comments = sourceCode.getCommentsBefore(node);
|
|
954
1132
|
const hasJSDoc = comments.some(
|
|
@@ -1077,14 +1255,11 @@ const requirePropsDefaultDoc = createRule({
|
|
|
1077
1255
|
create(context) {
|
|
1078
1256
|
return {
|
|
1079
1257
|
TSPropertySignature(node) {
|
|
1258
|
+
if (node.key.type !== AST_NODE_TYPES.Identifier) return;
|
|
1080
1259
|
if (!node.optional) return;
|
|
1081
|
-
const parent = node.parent
|
|
1082
|
-
if (parent
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
if (parent.id.type !== AST_NODE_TYPES.Identifier || !parent.id.name.endsWith("Props")) {
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1260
|
+
const parent = node.parent.parent;
|
|
1261
|
+
if (parent.type !== AST_NODE_TYPES.TSInterfaceDeclaration) return;
|
|
1262
|
+
if (!parent.id.name.endsWith("Props")) return;
|
|
1088
1263
|
const sourceCode = context.sourceCode;
|
|
1089
1264
|
const comments = sourceCode.getCommentsBefore(node);
|
|
1090
1265
|
const hasDefaultDoc = comments.some(
|
|
@@ -1095,7 +1270,7 @@ const requirePropsDefaultDoc = createRule({
|
|
|
1095
1270
|
node,
|
|
1096
1271
|
messageId: "missingDefaultDoc",
|
|
1097
1272
|
data: {
|
|
1098
|
-
propertyName: node.key.
|
|
1273
|
+
propertyName: node.key.name
|
|
1099
1274
|
}
|
|
1100
1275
|
});
|
|
1101
1276
|
}
|
|
@@ -1113,6 +1288,7 @@ const rules = {
|
|
|
1113
1288
|
"no-mutable-property-of-props-interface": noMutablePropertyOfPropsInterface,
|
|
1114
1289
|
"no-mutable-public-property-of-construct": noMutablePublicPropertyOfConstruct,
|
|
1115
1290
|
"no-parent-name-construct-id-match": noParentNameConstructIdMatch,
|
|
1291
|
+
"no-unused-props": noUnusedProps,
|
|
1116
1292
|
"no-variable-construct-id": noVariableConstructId,
|
|
1117
1293
|
"pascal-case-construct-id": pascalCaseConstructId,
|
|
1118
1294
|
"props-name-convention": propsNameConvention,
|
|
@@ -1149,6 +1325,8 @@ const recommended = createFlatConfig({
|
|
|
1149
1325
|
"error",
|
|
1150
1326
|
{ disallowContainingParentName: false }
|
|
1151
1327
|
],
|
|
1328
|
+
// TODO: Enable this rule at v4.0.0
|
|
1329
|
+
// "cdk/no-unused-props": "error",
|
|
1152
1330
|
"cdk/no-variable-construct-id": "error",
|
|
1153
1331
|
"cdk/pascal-case-construct-id": "error",
|
|
1154
1332
|
"cdk/require-passing-this": ["error", { allowNonThisAndDisallowScope: true }]
|
|
@@ -1165,6 +1343,7 @@ const strict = createFlatConfig({
|
|
|
1165
1343
|
"error",
|
|
1166
1344
|
{ disallowContainingParentName: true }
|
|
1167
1345
|
],
|
|
1346
|
+
"cdk/no-unused-props": "error",
|
|
1168
1347
|
"cdk/no-variable-construct-id": "error",
|
|
1169
1348
|
"cdk/pascal-case-construct-id": "error",
|
|
1170
1349
|
"cdk/props-name-convention": "error",
|
package/package.json
CHANGED
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 = {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@typescript-eslint/utils";
|
|
8
8
|
|
|
9
9
|
import { createRule } from "../utils/createRule";
|
|
10
|
+
import { getConstructor } from "../utils/getConstructor";
|
|
10
11
|
import { isConstructType } from "../utils/typeCheck";
|
|
11
12
|
|
|
12
13
|
type Context = TSESLint.RuleContext<
|
|
@@ -47,14 +48,7 @@ export const constructConstructorProperty = createRule({
|
|
|
47
48
|
const type = parserServices.getTypeAtLocation(node);
|
|
48
49
|
if (!isConstructType(type)) return;
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
const constructor = node.body.body.find(
|
|
52
|
-
(member): member is TSESTree.MethodDefinition =>
|
|
53
|
-
member.type === AST_NODE_TYPES.MethodDefinition &&
|
|
54
|
-
member.kind === "constructor"
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// NOTE: Skip if there's no constructor
|
|
51
|
+
const constructor = getConstructor(node);
|
|
58
52
|
if (!constructor) return;
|
|
59
53
|
|
|
60
54
|
validateConstructorProperty(constructor, context, parserServices);
|
|
@@ -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 = {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ESLintUtils,
|
|
4
|
+
ParserServicesWithTypeInformation,
|
|
5
|
+
TSESLint,
|
|
6
|
+
TSESTree,
|
|
7
|
+
} from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { createRule } from "../utils/createRule";
|
|
10
|
+
import { getConstructor } from "../utils/getConstructor";
|
|
11
|
+
import {
|
|
12
|
+
IPropsUsageTracker,
|
|
13
|
+
propsUsageTrackerFactory,
|
|
14
|
+
} from "../utils/propsUsageTracker";
|
|
15
|
+
import { isConstructType } from "../utils/typeCheck";
|
|
16
|
+
|
|
17
|
+
type Context = TSESLint.RuleContext<"unusedProp", []>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enforces that all properties defined in props type are used within the constructor
|
|
21
|
+
* @param context - The rule context provided by ESLint
|
|
22
|
+
* @returns An object containing the AST visitor functions
|
|
23
|
+
*/
|
|
24
|
+
export const noUnusedProps = createRule({
|
|
25
|
+
name: "no-unused-props",
|
|
26
|
+
meta: {
|
|
27
|
+
type: "suggestion",
|
|
28
|
+
docs: {
|
|
29
|
+
description:
|
|
30
|
+
"Enforces that all properties defined in props type are used within the constructor",
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
unusedProp: "Property '{{propName}}' is defined in props but never used",
|
|
34
|
+
},
|
|
35
|
+
schema: [],
|
|
36
|
+
},
|
|
37
|
+
defaultOptions: [],
|
|
38
|
+
create(context) {
|
|
39
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
ClassDeclaration(node) {
|
|
43
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
44
|
+
if (!isConstructType(type)) return;
|
|
45
|
+
|
|
46
|
+
const constructor = getConstructor(node);
|
|
47
|
+
if (!constructor) return;
|
|
48
|
+
|
|
49
|
+
analyzePropsUsage(constructor, context, parserServices);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Analyzes props usage in the constructor
|
|
57
|
+
*/
|
|
58
|
+
const analyzePropsUsage = (
|
|
59
|
+
constructor: TSESTree.MethodDefinition,
|
|
60
|
+
context: Context,
|
|
61
|
+
parserServices: ParserServicesWithTypeInformation
|
|
62
|
+
): void => {
|
|
63
|
+
const params = constructor.value.params;
|
|
64
|
+
|
|
65
|
+
// NOTE: Check if constructor has props parameter (3rd parameter)
|
|
66
|
+
if (params.length < 3) return;
|
|
67
|
+
|
|
68
|
+
const propsParam = params[2];
|
|
69
|
+
|
|
70
|
+
switch (propsParam.type) {
|
|
71
|
+
case AST_NODE_TYPES.Identifier: {
|
|
72
|
+
// NOTE: Standard props parameter (e.g. props: MyConstructProps)
|
|
73
|
+
const propsType = parserServices.getTypeAtLocation(propsParam);
|
|
74
|
+
const tracker = propsUsageTrackerFactory(propsType);
|
|
75
|
+
if (!tracker || !constructor.value.body) return;
|
|
76
|
+
|
|
77
|
+
analyzeConstructorBody(constructor.value.body, propsParam.name, tracker);
|
|
78
|
+
reportUnusedProperties(tracker, propsParam, context);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
case AST_NODE_TYPES.ObjectPattern: {
|
|
82
|
+
// NOTE: Inline destructuring (e.g. { bucketName, enableVersioning }: MyConstructProps)
|
|
83
|
+
const typeAnnotation = propsParam.typeAnnotation?.typeAnnotation;
|
|
84
|
+
if (!typeAnnotation) return;
|
|
85
|
+
|
|
86
|
+
const propsType = parserServices.getTypeAtLocation(typeAnnotation);
|
|
87
|
+
const tracker = propsUsageTrackerFactory(propsType);
|
|
88
|
+
if (!tracker) return;
|
|
89
|
+
|
|
90
|
+
tracker.markAsUsedForObjectPattern(propsParam);
|
|
91
|
+
reportUnusedProperties(tracker, propsParam, context);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Analyzes constructor body for props usage patterns
|
|
101
|
+
*/
|
|
102
|
+
const analyzeConstructorBody = (
|
|
103
|
+
body: TSESTree.BlockStatement,
|
|
104
|
+
propsParamName: string,
|
|
105
|
+
tracker: IPropsUsageTracker
|
|
106
|
+
): void => {
|
|
107
|
+
const visited = new Set<TSESTree.Node>();
|
|
108
|
+
|
|
109
|
+
const visitNode = (node: TSESTree.Node): void => {
|
|
110
|
+
if (visited.has(node)) return;
|
|
111
|
+
visited.add(node);
|
|
112
|
+
|
|
113
|
+
switch (node.type) {
|
|
114
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
115
|
+
tracker.markAsUsedForMemberExpression(node, propsParamName);
|
|
116
|
+
break;
|
|
117
|
+
case AST_NODE_TYPES.VariableDeclarator:
|
|
118
|
+
tracker.markAsUsedForVariableDeclarator(node, propsParamName);
|
|
119
|
+
break;
|
|
120
|
+
case AST_NODE_TYPES.AssignmentExpression:
|
|
121
|
+
tracker.markAsUsedForAssignmentExpression(node, propsParamName);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// NOTE: Recursively visit child nodes
|
|
126
|
+
const children = getChildNodes(node);
|
|
127
|
+
for (const child of children) {
|
|
128
|
+
visitNode(child);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
visitNode(body);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reports unused properties to ESLint
|
|
137
|
+
*/
|
|
138
|
+
const reportUnusedProperties = (
|
|
139
|
+
tracker: IPropsUsageTracker,
|
|
140
|
+
propsParam: TSESTree.Parameter,
|
|
141
|
+
context: Context
|
|
142
|
+
): void => {
|
|
143
|
+
for (const propName of tracker.getUnusedProperties()) {
|
|
144
|
+
context.report({
|
|
145
|
+
node: propsParam,
|
|
146
|
+
messageId: "unusedProp",
|
|
147
|
+
data: {
|
|
148
|
+
propName,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Safely gets child nodes from a TSESTree.Node
|
|
156
|
+
*/
|
|
157
|
+
const getChildNodes = (node: TSESTree.Node): TSESTree.Node[] => {
|
|
158
|
+
return Object.entries(node).reduce<TSESTree.Node[]>((acc, [key, value]) => {
|
|
159
|
+
if (["parent", "range", "loc"].includes(key)) return acc; // Keys to skip to avoid circular references and unnecessary properties
|
|
160
|
+
if (isESTreeNode(value)) return [...acc, value];
|
|
161
|
+
if (Array.isArray(value)) return [...acc, ...value.filter(isESTreeNode)];
|
|
162
|
+
return acc;
|
|
163
|
+
}, []);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Type guard to check if a value is a TSESTree.Node
|
|
168
|
+
*/
|
|
169
|
+
const isESTreeNode = (value: unknown): value is TSESTree.Node => {
|
|
170
|
+
return (
|
|
171
|
+
value !== null &&
|
|
172
|
+
typeof value === "object" &&
|
|
173
|
+
"type" in value &&
|
|
174
|
+
typeof (value as { type: unknown }).type === "string"
|
|
175
|
+
);
|
|
176
|
+
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "@typescript-eslint/utils";
|
|
7
7
|
|
|
8
8
|
import { createRule } from "../utils/createRule";
|
|
9
|
-
import { getConstructorPropertyNames } from "../utils/
|
|
9
|
+
import { getConstructorPropertyNames } from "../utils/getPropertyNames";
|
|
10
10
|
import { isConstructType } from "../utils/typeCheck";
|
|
11
11
|
|
|
12
12
|
type Context = TSESLint.RuleContext<"invalidConstructId", []>;
|
|
@@ -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 QUOTE_TYPE = {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AST_NODE_TYPES,
|
|
3
|
-
ESLintUtils,
|
|
4
|
-
TSESTree,
|
|
5
|
-
} from "@typescript-eslint/utils";
|
|
1
|
+
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
|
|
6
2
|
|
|
7
3
|
import { createRule } from "../utils/createRule";
|
|
4
|
+
import { getConstructor } from "../utils/getConstructor";
|
|
8
5
|
import { isConstructType } from "../utils/typeCheck";
|
|
9
6
|
|
|
10
7
|
/**
|
|
@@ -37,13 +34,10 @@ export const propsNameConvention = createRule({
|
|
|
37
34
|
if (!isConstructType(type)) return;
|
|
38
35
|
|
|
39
36
|
// NOTE: check constructor parameter
|
|
40
|
-
const constructor = node
|
|
41
|
-
|
|
42
|
-
member.type === AST_NODE_TYPES.MethodDefinition &&
|
|
43
|
-
member.kind === "constructor"
|
|
44
|
-
);
|
|
37
|
+
const constructor = getConstructor(node);
|
|
38
|
+
if (!constructor) return;
|
|
45
39
|
|
|
46
|
-
const propsParam = constructor
|
|
40
|
+
const propsParam = constructor.value.params?.[2];
|
|
47
41
|
if (propsParam?.type !== AST_NODE_TYPES.Identifier) return;
|
|
48
42
|
|
|
49
43
|
const typeAnnotation = propsParam.typeAnnotation;
|