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/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.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(
@@ -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.endsWith(suffix))) {
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.body.body.find(
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 === AST_NODE_TYPES.Identifier && firstParam.name !== "scope") {
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
- return;
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 === AST_NODE_TYPES.Identifier && secondParam.name !== "id") {
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) return;
102
+ if (params.length < 3)
103
+ return;
87
104
  const thirdParam = params[2];
88
- if (thirdParam.type === AST_NODE_TYPES.Identifier && thirdParam.name !== "props") {
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.body.body.find(
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.body.body.find(
892
- (member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
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 || node.parent?.type !== AST_NODE_TYPES.TSInterfaceBody) {
937
- return;
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?.parent;
1069
- if (parent?.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
1070
- return;
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.type === AST_NODE_TYPES.Identifier ? node.key.name : "unknown"
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",
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 = (rules: Record<string, unknown>) => {
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: typeof 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<"invalidConstructorProperty", []>;
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
- // NOTE: Find the constructor method
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 === AST_NODE_TYPES.Identifier &&
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
- return;
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 === AST_NODE_TYPES.Identifier &&
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
- // NOTE: If there's no third parameter, return
101
- if (params.length < 3) return;
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 === AST_NODE_TYPES.Identifier &&
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.body.body.find(
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/parseType";
10
+ import { getConstructorPropertyNames } from "../utils/getPropertyNames";
11
11
  import { isConstructOrStackType } from "../utils/typeCheck";
12
12
 
13
13
  const SUFFIX_TYPE = {