eslint-cdk-plugin 2.2.0 → 3.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-cdk-plugin",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "description": "eslint plugin for AWS CDK projects",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -30,23 +30,24 @@
30
30
  "docs:preview": "cd ./docs && pnpm install && pnpm run preview"
31
31
  },
32
32
  "devDependencies": {
33
- "@eslint/js": "^9.22.0",
34
- "@types/node": "^22.13.10",
35
- "@typescript-eslint/rule-tester": "^8.26.0",
33
+ "@eslint/js": "^9.26.0",
34
+ "@types/node": "^22.15.0",
35
+ "@typescript-eslint/rule-tester": "^8.32.1",
36
36
  "eslint": "9.22.0",
37
37
  "eslint-plugin-import": "^2.31.0",
38
- "pkgroll": "^2.11.2",
38
+ "pkgroll": "^2.12.2",
39
39
  "standard-version": "^9.5.0",
40
- "typescript": "^5.8.2",
41
- "typescript-eslint": "^8.26.0",
42
- "vitest": "^3.0.8"
40
+ "typescript": "^5.8.3",
41
+ "typescript-eslint": "^8.32.1",
42
+ "vitepress-plugin-llms": "^1.1.4",
43
+ "vitest": "^3.1.3"
43
44
  },
44
45
  "dependencies": {
45
- "@typescript-eslint/parser": "^8.26.0",
46
- "@typescript-eslint/utils": "^8.26.0"
46
+ "@typescript-eslint/parser": "^8.32.1",
47
+ "@typescript-eslint/utils": "^8.32.1"
47
48
  },
48
49
  "volta": {
49
- "node": "22.14.0"
50
+ "node": "22.15.0"
50
51
  },
51
52
  "files": [
52
53
  "dist",
package/src/index.ts CHANGED
@@ -3,13 +3,13 @@ import tsParser from "@typescript-eslint/parser";
3
3
  import { name, version } from "../package.json";
4
4
 
5
5
  import { constructConstructorProperty } from "./rules/construct-constructor-property";
6
- import { noClassInInterface } from "./rules/no-class-in-interface";
6
+ import { noConstructInInterface } from "./rules/no-construct-in-interface";
7
+ import { noConstructInPublicPropertyOfConstruct } from "./rules/no-construct-in-public-property-of-construct";
7
8
  import { noConstructStackSuffix } from "./rules/no-construct-stack-suffix";
8
9
  import { noImportPrivate } from "./rules/no-import-private";
9
- import { noMutablePropsInterface } from "./rules/no-mutable-props-interface";
10
- import { noMutablePublicFields } from "./rules/no-mutable-public-fields";
10
+ import { noMutablePropertyOfPropsInterface } from "./rules/no-mutable-property-of-props-interface";
11
+ import { noMutablePublicPropertyOfConstruct } from "./rules/no-mutable-public-property-of-construct";
11
12
  import { noParentNameConstructIdMatch } from "./rules/no-parent-name-construct-id-match";
12
- import { noPublicClassFields } from "./rules/no-public-class-fields";
13
13
  import { noVariableConstructId } from "./rules/no-variable-construct-id";
14
14
  import { pascalCaseConstructId } from "./rules/pascal-case-construct-id";
15
15
  import { propsNameConvention } from "./rules/props-name-convention";
@@ -18,20 +18,21 @@ import { requirePassingThis } from "./rules/require-passing-this";
18
18
  import { requirePropsDefaultDoc } from "./rules/require-props-default-doc";
19
19
 
20
20
  const rules = {
21
- "no-class-in-interface": noClassInInterface,
21
+ "construct-constructor-property": constructConstructorProperty,
22
+ "no-construct-in-interface": noConstructInInterface,
23
+ "no-construct-in-public-property-of-construct":
24
+ noConstructInPublicPropertyOfConstruct,
22
25
  "no-construct-stack-suffix": noConstructStackSuffix,
26
+ "no-import-private": noImportPrivate,
27
+ "no-mutable-property-of-props-interface": noMutablePropertyOfPropsInterface,
28
+ "no-mutable-public-property-of-construct": noMutablePublicPropertyOfConstruct,
23
29
  "no-parent-name-construct-id-match": noParentNameConstructIdMatch,
24
- "no-public-class-fields": noPublicClassFields,
25
- "pascal-case-construct-id": pascalCaseConstructId,
26
- "require-passing-this": requirePassingThis,
27
30
  "no-variable-construct-id": noVariableConstructId,
28
- "no-mutable-public-fields": noMutablePublicFields,
29
- "no-mutable-props-interface": noMutablePropsInterface,
30
- "construct-constructor-property": constructConstructorProperty,
31
+ "pascal-case-construct-id": pascalCaseConstructId,
32
+ "props-name-convention": propsNameConvention,
31
33
  "require-jsdoc": requireJSDoc,
34
+ "require-passing-this": requirePassingThis,
32
35
  "require-props-default-doc": requirePropsDefaultDoc,
33
- "props-name-convention": propsNameConvention,
34
- "no-import-private": noImportPrivate,
35
36
  };
36
37
 
37
38
  const cdkPlugin = {
@@ -55,39 +56,39 @@ const createFlatConfig = (rules: Record<string, unknown>) => {
55
56
  };
56
57
 
57
58
  const recommended = createFlatConfig({
58
- "cdk/no-class-in-interface": "error",
59
+ "cdk/construct-constructor-property": "error",
60
+ "cdk/no-construct-in-interface": "error",
61
+ "cdk/no-construct-in-public-property-of-construct": "error",
59
62
  "cdk/no-construct-stack-suffix": "error",
63
+ "cdk/no-mutable-property-of-props-interface": "warn",
64
+ "cdk/no-mutable-public-property-of-construct": "warn",
60
65
  "cdk/no-parent-name-construct-id-match": [
61
66
  "error",
62
67
  { disallowContainingParentName: false },
63
68
  ],
64
- "cdk/no-public-class-fields": "error",
69
+ "cdk/no-variable-construct-id": "error",
65
70
  "cdk/pascal-case-construct-id": "error",
66
71
  "cdk/require-passing-this": ["error", { allowNonThisAndDisallowScope: true }],
67
- "cdk/no-variable-construct-id": "error",
68
- "cdk/no-mutable-public-fields": "warn",
69
- "cdk/no-mutable-props-interface": "warn",
70
- "cdk/construct-constructor-property": "error",
71
72
  });
72
73
 
73
74
  const strict = createFlatConfig({
74
- "cdk/no-class-in-interface": "error",
75
+ "cdk/construct-constructor-property": "error",
76
+ "cdk/no-construct-in-interface": "error",
77
+ "cdk/no-construct-in-public-property-of-construct": "error",
75
78
  "cdk/no-construct-stack-suffix": "error",
79
+ "cdk/no-import-private": "error",
80
+ "cdk/no-mutable-property-of-props-interface": "error",
81
+ "cdk/no-mutable-public-property-of-construct": "error",
76
82
  "cdk/no-parent-name-construct-id-match": [
77
83
  "error",
78
84
  { disallowContainingParentName: true },
79
85
  ],
80
- "cdk/no-public-class-fields": "error",
81
- "cdk/pascal-case-construct-id": "error",
82
- "cdk/require-passing-this": "error",
83
86
  "cdk/no-variable-construct-id": "error",
84
- "cdk/no-mutable-public-fields": "error",
85
- "cdk/no-mutable-props-interface": "error",
86
- "cdk/construct-constructor-property": "error",
87
+ "cdk/pascal-case-construct-id": "error",
88
+ "cdk/props-name-convention": "error",
87
89
  "cdk/require-jsdoc": "error",
90
+ "cdk/require-passing-this": "error",
88
91
  "cdk/require-props-default-doc": "error",
89
- "cdk/props-name-convention": "error",
90
- "cdk/no-import-private": "error",
91
92
  });
92
93
 
93
94
  const configs = {
@@ -67,7 +67,7 @@ const validateConstructorProperty = (
67
67
  // NOTE: Check if the constructor has at least 2 parameters
68
68
  if (params.length < 2) {
69
69
  context.report({
70
- node: constructor,
70
+ node: constructor.value,
71
71
  messageId: "invalidConstructorProperty",
72
72
  });
73
73
  return;
@@ -80,7 +80,7 @@ const validateConstructorProperty = (
80
80
  firstParam.name !== "scope"
81
81
  ) {
82
82
  context.report({
83
- node: constructor,
83
+ node: firstParam,
84
84
  messageId: "invalidConstructorProperty",
85
85
  });
86
86
  return;
@@ -93,7 +93,7 @@ const validateConstructorProperty = (
93
93
  secondParam.name !== "id"
94
94
  ) {
95
95
  context.report({
96
- node: constructor,
96
+ node: secondParam,
97
97
  messageId: "invalidConstructorProperty",
98
98
  });
99
99
  return;
@@ -109,7 +109,7 @@ const validateConstructorProperty = (
109
109
  thirdParam.name !== "props"
110
110
  ) {
111
111
  context.report({
112
- node: constructor,
112
+ node: thirdParam,
113
113
  messageId: "invalidConstructorProperty",
114
114
  });
115
115
  return;
@@ -1,22 +1,22 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
 
3
- import { SYMBOL_FLAGS } from "../constants/tsInternalFlags";
3
+ import { isConstructOrStackType } from "../utils/typeCheck";
4
4
 
5
5
  /**
6
- * Enforces the use of interface types instead of class in interface properties
6
+ * Enforces the use of interface types instead of CDK Construct types in interface properties
7
7
  * @param context - The rule context provided by ESLint
8
8
  * @returns An object containing the AST visitor functions
9
9
  * @see {@link https://eslint-cdk-plugin.dev/rules/no-class-in-interface} - Documentation
10
10
  */
11
- export const noClassInInterface = ESLintUtils.RuleCreator.withoutDocs({
11
+ export const noConstructInInterface = ESLintUtils.RuleCreator.withoutDocs({
12
12
  meta: {
13
13
  type: "problem",
14
14
  docs: {
15
- description: "Disallow class types in interface properties",
15
+ description: "Disallow CDK Construct types in interface properties",
16
16
  },
17
17
  messages: {
18
- noClassInInterfaceProps:
19
- "Interface property '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead.",
18
+ invalidInterfaceProperty:
19
+ "Interface property '{{ propertyName }}' should not use CDK Construct type '{{ typeName }}'. Consider using an interface or type alias instead.",
20
20
  },
21
21
  schema: [],
22
22
  },
@@ -35,17 +35,11 @@ export const noClassInInterface = ESLintUtils.RuleCreator.withoutDocs({
35
35
  }
36
36
 
37
37
  const type = parserServices.getTypeAtLocation(property);
38
- if (!type.symbol) continue;
39
-
40
- // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
41
- // Therefore, the type information structures do not match.
42
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
43
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
44
- if (!isClass) continue;
38
+ if (!isConstructOrStackType(type)) continue;
45
39
 
46
40
  context.report({
47
41
  node: property,
48
- messageId: "noClassInInterfaceProps",
42
+ messageId: "invalidInterfaceProperty",
49
43
  data: {
50
44
  propertyName: property.key.name,
51
45
  typeName: type.symbol.name,
@@ -0,0 +1,145 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ ESLintUtils,
4
+ ParserServicesWithTypeInformation,
5
+ TSESLint,
6
+ TSESTree,
7
+ } from "@typescript-eslint/utils";
8
+
9
+ import { isConstructOrStackType } from "../utils/typeCheck";
10
+
11
+ type Context = TSESLint.RuleContext<
12
+ "invalidPublicPropertyOfConstruct",
13
+ []
14
+ >;
15
+
16
+ /**
17
+ * Disallow Construct types in public property of Construct
18
+ * @param context - The rule context provided by ESLint
19
+ * @returns An object containing the AST visitor functions
20
+ * @see {@link https://eslint-cdk-plugin.dev/rules/no-construct-in-public-property-of-construct} - Documentation
21
+ */
22
+ export const noConstructInPublicPropertyOfConstruct =
23
+ ESLintUtils.RuleCreator.withoutDocs({
24
+ meta: {
25
+ type: "problem",
26
+ docs: {
27
+ description: "Disallow Construct types in public property of Construct",
28
+ },
29
+ messages: {
30
+ invalidPublicPropertyOfConstruct:
31
+ "Public property '{{ propertyName }}' of Construct should not use Construct type '{{ typeName }}'. Consider using an interface or type alias instead.",
32
+ },
33
+ schema: [],
34
+ },
35
+ defaultOptions: [],
36
+ create(context) {
37
+ const parserServices = ESLintUtils.getParserServices(context);
38
+ return {
39
+ ClassDeclaration(node) {
40
+ const type = parserServices.getTypeAtLocation(node);
41
+ if (!isConstructOrStackType(type)) return;
42
+
43
+ // NOTE: Check class members
44
+ validatePublicPropertyOfConstruct(node, context, parserServices);
45
+
46
+ // NOTE: Check constructor parameter properties
47
+ const constructor = node.body.body.find(
48
+ (member): member is TSESTree.MethodDefinition =>
49
+ member.type === AST_NODE_TYPES.MethodDefinition &&
50
+ member.kind === "constructor"
51
+ );
52
+ if (
53
+ !constructor ||
54
+ constructor.value.type !== AST_NODE_TYPES.FunctionExpression
55
+ ) {
56
+ return;
57
+ }
58
+
59
+ validateConstructorParameterProperty(
60
+ constructor,
61
+ context,
62
+ parserServices
63
+ );
64
+ },
65
+ };
66
+ },
67
+ });
68
+
69
+ /**
70
+ * check the public property of Construct
71
+ * - if it is a Construct type, report an error
72
+ */
73
+ const validatePublicPropertyOfConstruct = (
74
+ node: TSESTree.ClassDeclaration,
75
+ context: Context,
76
+ parserServices: ParserServicesWithTypeInformation
77
+ ) => {
78
+ for (const property of node.body.body) {
79
+ if (
80
+ property.type !== AST_NODE_TYPES.PropertyDefinition ||
81
+ property.key.type !== AST_NODE_TYPES.Identifier
82
+ ) {
83
+ continue;
84
+ }
85
+
86
+ // NOTE: Skip private and protected fields
87
+ if (["private", "protected"].includes(property.accessibility ?? "")) {
88
+ continue;
89
+ }
90
+
91
+ // NOTE: Skip fields without type annotation
92
+ if (!property.typeAnnotation) continue;
93
+
94
+ const type = parserServices.getTypeAtLocation(property);
95
+ if (!isConstructOrStackType(type)) continue;
96
+
97
+ context.report({
98
+ node: property,
99
+ messageId: "invalidPublicPropertyOfConstruct",
100
+ data: {
101
+ propertyName: property.key.name,
102
+ typeName: type.symbol.name,
103
+ },
104
+ });
105
+ }
106
+ };
107
+
108
+ /**
109
+ * check the constructor parameter property
110
+ * - if it is a Construct type, report an error
111
+ */
112
+ const validateConstructorParameterProperty = (
113
+ constructor: TSESTree.MethodDefinition,
114
+ context: Context,
115
+ parserServices: ParserServicesWithTypeInformation
116
+ ) => {
117
+ for (const param of constructor.value.params) {
118
+ if (
119
+ param.type !== AST_NODE_TYPES.TSParameterProperty ||
120
+ param.parameter.type !== AST_NODE_TYPES.Identifier
121
+ ) {
122
+ continue;
123
+ }
124
+
125
+ // NOTE: Skip private and protected parameters
126
+ if (["private", "protected"].includes(param.accessibility ?? "")) {
127
+ continue;
128
+ }
129
+
130
+ // NOTE: Skip parameters without type annotation
131
+ if (!param.parameter.typeAnnotation) continue;
132
+
133
+ const type = parserServices.getTypeAtLocation(param);
134
+ if (!isConstructOrStackType(type)) continue;
135
+
136
+ context.report({
137
+ node: param,
138
+ messageId: "invalidPublicPropertyOfConstruct",
139
+ data: {
140
+ propertyName: param.parameter.name,
141
+ typeName: type.symbol.name,
142
+ },
143
+ });
144
+ }
145
+ };
@@ -22,7 +22,7 @@ type Options = [
22
22
  }
23
23
  ];
24
24
 
25
- type Context = TSESLint.RuleContext<"noConstructStackSuffix", Options>;
25
+ type Context = TSESLint.RuleContext<"invalidConstructId", Options>;
26
26
 
27
27
  /**
28
28
  * Enforces that Construct IDs do not end with 'Construct' or 'Stack' suffix
@@ -38,7 +38,7 @@ export const noConstructStackSuffix = ESLintUtils.RuleCreator.withoutDocs({
38
38
  "Effort to avoid using 'Construct' and 'Stack' suffix in construct id.",
39
39
  },
40
40
  messages: {
41
- noConstructStackSuffix:
41
+ invalidConstructId:
42
42
  "{{ classType }} ID '{{ id }}' should not include {{ suffix }} suffix.",
43
43
  },
44
44
  schema: [
@@ -110,8 +110,8 @@ const validateConstructId = (
110
110
  formattedConstructId.endsWith(SUFFIX_TYPE.CONSTRUCT)
111
111
  ) {
112
112
  context.report({
113
- node,
114
- messageId: "noConstructStackSuffix",
113
+ node: secondArg,
114
+ messageId: "invalidConstructId",
115
115
  data: {
116
116
  classType: "Construct",
117
117
  id: secondArg.value,
@@ -123,8 +123,8 @@ const validateConstructId = (
123
123
  formattedConstructId.endsWith(SUFFIX_TYPE.STACK)
124
124
  ) {
125
125
  context.report({
126
- node,
127
- messageId: "noConstructStackSuffix",
126
+ node: secondArg,
127
+ messageId: "invalidConstructId",
128
128
  data: {
129
129
  classType: "Stack",
130
130
  id: secondArg.value,
@@ -16,7 +16,7 @@ export const noImportPrivate: Rule.RuleModule = {
16
16
  "Cannot import modules from private dir at different levels of the hierarchy.",
17
17
  },
18
18
  messages: {
19
- noImportPrivate:
19
+ invalidImportPath:
20
20
  "Cannot import modules from private dir at different levels of the hierarchy.",
21
21
  },
22
22
  schema: [],
@@ -43,7 +43,7 @@ export const noImportPrivate: Rule.RuleModule = {
43
43
  (segment, index) => segment !== importDirSegments[index]
44
44
  )
45
45
  ) {
46
- context.report({ node, messageId: "noImportPrivate" });
46
+ context.report({ node, messageId: "invalidImportPath" });
47
47
  }
48
48
  },
49
49
  };
@@ -0,0 +1,60 @@
1
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ /**
4
+ * Disallow mutable properties of Construct Props (interface)
5
+ * @param context - The rule context provided by ESLint
6
+ * @returns An object containing the AST visitor functions
7
+ * @see {@link https://eslint-cdk-plugin.dev/rules/no-mutable-property-of-props-interface} - Documentation
8
+ */
9
+ export const noMutablePropertyOfPropsInterface =
10
+ ESLintUtils.RuleCreator.withoutDocs({
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description:
15
+ "Disallow mutable properties of Construct Props (interface)",
16
+ },
17
+ fixable: "code",
18
+ messages: {
19
+ invalidPropertyOfPropsInterface:
20
+ "Property '{{ propertyName }}' of Construct Props should be readonly.",
21
+ },
22
+ schema: [],
23
+ },
24
+ defaultOptions: [],
25
+ create(context) {
26
+ return {
27
+ TSInterfaceDeclaration(node) {
28
+ const sourceCode = context.sourceCode;
29
+
30
+ // NOTE: Interface name check for "Props"
31
+ if (!node.id.name.endsWith("Props")) return;
32
+
33
+ for (const property of node.body.body) {
34
+ // NOTE: check property signature
35
+ if (
36
+ property.type !== AST_NODE_TYPES.TSPropertySignature ||
37
+ property.key.type !== AST_NODE_TYPES.Identifier
38
+ ) {
39
+ continue;
40
+ }
41
+
42
+ // NOTE: Skip if already readonly
43
+ if (property.readonly) continue;
44
+
45
+ context.report({
46
+ node: property,
47
+ messageId: "invalidPropertyOfPropsInterface",
48
+ data: {
49
+ propertyName: property.key.name,
50
+ },
51
+ fix: (fixer) => {
52
+ const propertyText = sourceCode.getText(property);
53
+ return fixer.replaceText(property, `readonly ${propertyText}`);
54
+ },
55
+ });
56
+ }
57
+ },
58
+ };
59
+ },
60
+ });
@@ -0,0 +1,76 @@
1
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ import { isConstructOrStackType } from "../utils/typeCheck";
4
+
5
+ /**
6
+ * Disallow mutable public properties of Construct
7
+ * @param context - The rule context provided by ESLint
8
+ * @returns An object containing the AST visitor functions
9
+ * @see {@link https://eslint-cdk-plugin.dev/rules/no-mutable-public-property-of-construct} - Documentation
10
+ */
11
+ export const noMutablePublicPropertyOfConstruct =
12
+ ESLintUtils.RuleCreator.withoutDocs({
13
+ meta: {
14
+ type: "problem",
15
+ docs: {
16
+ description: "Disallow mutable public properties of Construct",
17
+ },
18
+ fixable: "code",
19
+ messages: {
20
+ invalidPublicPropertyOfConstruct:
21
+ "Public property '{{ propertyName }}' should be readonly. Consider adding the 'readonly' modifier.",
22
+ },
23
+ schema: [],
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ const parserServices = ESLintUtils.getParserServices(context);
28
+
29
+ return {
30
+ ClassDeclaration(node) {
31
+ const sourceCode = context.sourceCode;
32
+ const type = parserServices.getTypeAtLocation(node);
33
+ if (!isConstructOrStackType(type)) return;
34
+
35
+ for (const member of node.body.body) {
36
+ // NOTE: check property definition
37
+ if (
38
+ member.type !== AST_NODE_TYPES.PropertyDefinition ||
39
+ member.key.type !== AST_NODE_TYPES.Identifier
40
+ ) {
41
+ continue;
42
+ }
43
+
44
+ // NOTE: Skip private and protected fields
45
+ if (["private", "protected"].includes(member.accessibility ?? "")) {
46
+ continue;
47
+ }
48
+
49
+ // NOTE: Skip if readonly is present
50
+ if (member.readonly) continue;
51
+
52
+ context.report({
53
+ node: member,
54
+ messageId: "invalidPublicPropertyOfConstruct",
55
+ data: {
56
+ propertyName: member.key.name,
57
+ },
58
+ fix: (fixer) => {
59
+ const accessibility = member.accessibility ? "public " : "";
60
+ const paramText = sourceCode.getText(member);
61
+ const [key, value] = paramText.split(":");
62
+ const replacedKey = key.startsWith("public ")
63
+ ? key.replace("public ", "")
64
+ : key;
65
+
66
+ return fixer.replaceText(
67
+ member,
68
+ `${accessibility}readonly ${replacedKey}:${value}`
69
+ );
70
+ },
71
+ });
72
+ }
73
+ },
74
+ };
75
+ },
76
+ });