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.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.1.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(
@@ -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.body.body.find(
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.body.body.find(
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.body.body.find(
905
- (member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
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 || node.parent?.type !== AST_NODE_TYPES.TSInterfaceBody) {
950
- return;
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?.parent;
1082
- if (parent?.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
1083
- return;
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.type === AST_NODE_TYPES.Identifier ? node.key.name : "unknown"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-cdk-plugin",
3
- "version": "3.1.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",
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 = {
@@ -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
- // NOTE: Find the constructor method
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.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 = {
@@ -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/parseType";
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/parseType";
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.body.body.find(
41
- (member): member is TSESTree.MethodDefinition =>
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?.value.params?.[2];
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;