eslint-cdk-plugin 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ren Yamanashi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # eslint-plugin-cdk
2
+
3
+ ESLint plugin for [AWS CDK](https://github.com/aws/aws-cdk).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # npm
9
+ npm install -D @nigg/eslint-plugin-cdk
10
+
11
+ # yarn
12
+ yarn add -D @nigg/eslint-plugin-cdk
13
+
14
+ # pnpm
15
+ pnpm install -D @nigg/eslint-plugin-cdk
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Use recommended config
21
+
22
+ ```js
23
+ // eslint.config.mjs
24
+ import eslintPluginCdk from "@nigg/eslint-plugin-cdk";
25
+ export default [
26
+ {
27
+ plugins: {
28
+ cdk: eslintPluginCdk,
29
+ },
30
+ rules: {
31
+ ...eslintPluginCdk.configs.recommended.rules,
32
+ },
33
+ },
34
+ ];
35
+ ```
36
+
37
+ ### For more detailed documentation, see [docs for eslint-plugin-cdk](https://eslint-plugin-cdk.dev/)
package/dist/index.mjs ADDED
@@ -0,0 +1,35 @@
1
+ import { noClassInInterfaceProps } from "./no-class-in-interface.mjs";
2
+ import { noConstructStackSuffix } from "./no-construct-stack-suffix.mjs";
3
+ import { noImportPrivate } from "./no-import-private.mjs";
4
+ import { noMutablePropsInterface } from "./no-mutable-props-interface.mjs";
5
+ import { noMutablePublicFields } from "./no-mutable-public-fields.mjs";
6
+ import { noParentNameConstructIdMatch } from "./no-parent-name-construct-id-match.mjs";
7
+ import { noPublicClassFields } from "./no-public-class-fields.mjs";
8
+ import { pascalCaseConstructId } from "./pascal-case-construct-id.mjs";
9
+ var plugin = {
10
+ rules: {
11
+ "no-class-in-interface": noClassInInterfaceProps,
12
+ "no-construct-stack-suffix": noConstructStackSuffix,
13
+ "no-import-private": noImportPrivate,
14
+ "no-parent-name-construct-id-match": noParentNameConstructIdMatch,
15
+ "no-public-class-fields": noPublicClassFields,
16
+ "pascal-case-construct-id": pascalCaseConstructId,
17
+ "no-mutable-public-fields": noMutablePublicFields,
18
+ "no-mutable-props-interface": noMutablePropsInterface,
19
+ },
20
+ configs: {
21
+ recommended: {
22
+ plugins: ["cdk"],
23
+ rules: {
24
+ "cdk/no-class-in-interface": "error",
25
+ "cdk/no-construct-stack-suffix": "error",
26
+ "cdk/no-parent-name-construct-id-match": "error",
27
+ "cdk/no-public-class-fields": "error",
28
+ "cdk/pascal-case-construct-id": "error",
29
+ "cdk/no-mutable-public-fields": "warn",
30
+ "cdk/no-mutable-props-interface": "warn",
31
+ },
32
+ },
33
+ },
34
+ };
35
+ export default plugin;
@@ -0,0 +1,46 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ import { SymbolFlags } from "typescript";
3
+ export var noClassInInterfaceProps = ESLintUtils.RuleCreator.withoutDocs({
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "Disallow class types in interface properties",
8
+ },
9
+ messages: {
10
+ noClassInInterfaceProps: "Interface property '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead.",
11
+ },
12
+ schema: [],
13
+ },
14
+ defaultOptions: [],
15
+ create: function (context) {
16
+ var parserServices = ESLintUtils.getParserServices(context);
17
+ var checker = parserServices.program.getTypeChecker();
18
+ return {
19
+ TSInterfaceDeclaration: function (node) {
20
+ for (var _i = 0, _a = node.body.body; _i < _a.length; _i++) {
21
+ var property = _a[_i];
22
+ if (property.type !== "TSPropertySignature" ||
23
+ property.key.type !== "Identifier") {
24
+ continue;
25
+ }
26
+ var tsNode = parserServices.esTreeNodeToTSNodeMap.get(property);
27
+ var type = checker.getTypeAtLocation(tsNode);
28
+ if (!type.symbol)
29
+ continue;
30
+ // NOTE: check class type
31
+ var isClass = type.symbol.flags === SymbolFlags.Class;
32
+ if (!isClass)
33
+ continue;
34
+ context.report({
35
+ node: property,
36
+ messageId: "noClassInInterfaceProps",
37
+ data: {
38
+ propertyName: property.key.name,
39
+ typeName: type.symbol.name,
40
+ },
41
+ });
42
+ }
43
+ },
44
+ };
45
+ },
46
+ });
@@ -0,0 +1,192 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ import { toPascalCase } from "./utils/convertString.mjs";
3
+ export var noConstructStackSuffix = ESLintUtils.RuleCreator.withoutDocs({
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "Effort to avoid using 'Construct' and 'Stack' suffix in construct id.",
8
+ },
9
+ messages: {
10
+ noConstructStackSuffix: "{{ classType }} ID '{{ id }}' should not include {{ suffix }} suffix.",
11
+ },
12
+ schema: [],
13
+ },
14
+ defaultOptions: [],
15
+ create: function (context) {
16
+ return {
17
+ ClassBody: function (node) {
18
+ var _a;
19
+ var parent = node.parent;
20
+ if ((parent === null || parent === void 0 ? void 0 : parent.type) !== "ClassDeclaration")
21
+ return;
22
+ var className = (_a = parent.id) === null || _a === void 0 ? void 0 : _a.name;
23
+ if (!className)
24
+ return;
25
+ for (var _i = 0, _b = node.body; _i < _b.length; _i++) {
26
+ var body = _b[_i];
27
+ if (body.type !== "MethodDefinition" ||
28
+ !["method", "constructor"].includes(body.kind) ||
29
+ body.value.type !== "FunctionExpression") {
30
+ continue;
31
+ }
32
+ validateConstructorBody(node, body.value, context);
33
+ }
34
+ },
35
+ };
36
+ },
37
+ });
38
+ /**
39
+ * Validate the constructor body for the parent class
40
+ * - validate each statement in the constructor body
41
+ */
42
+ var validateConstructorBody = function (node, expression, context) {
43
+ var _a;
44
+ for (var _i = 0, _b = expression.body.body; _i < _b.length; _i++) {
45
+ var statement = _b[_i];
46
+ switch (statement.type) {
47
+ case "VariableDeclaration": {
48
+ var newExpression = statement.declarations[0].init;
49
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
50
+ continue;
51
+ validateConstructId(node, context, newExpression);
52
+ break;
53
+ }
54
+ case "ExpressionStatement": {
55
+ if (((_a = statement.expression) === null || _a === void 0 ? void 0 : _a.type) !== "NewExpression")
56
+ break;
57
+ validateStatement(node, statement, context);
58
+ break;
59
+ }
60
+ case "IfStatement": {
61
+ traverseStatements(node, statement.consequent, context);
62
+ break;
63
+ }
64
+ case "SwitchStatement": {
65
+ for (var _c = 0, _d = statement.cases; _c < _d.length; _c++) {
66
+ var switchCase = _d[_c];
67
+ for (var _e = 0, _f = switchCase.consequent; _e < _f.length; _e++) {
68
+ var statement_1 = _f[_e];
69
+ traverseStatements(node, statement_1, context);
70
+ }
71
+ }
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ };
77
+ /**
78
+ * Recursively traverse and validate statements in the AST
79
+ * - Handles BlockStatement, ExpressionStatement, and VariableDeclaration
80
+ * - Validates construct IDs
81
+ */
82
+ var traverseStatements = function (node, statement, context) {
83
+ switch (statement.type) {
84
+ case "BlockStatement": {
85
+ for (var _i = 0, _a = statement.body; _i < _a.length; _i++) {
86
+ var body = _a[_i];
87
+ validateStatement(node, body, context);
88
+ }
89
+ break;
90
+ }
91
+ case "ExpressionStatement": {
92
+ var newExpression = statement.expression;
93
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
94
+ break;
95
+ validateStatement(node, statement, context);
96
+ break;
97
+ }
98
+ case "VariableDeclaration": {
99
+ var newExpression = statement.declarations[0].init;
100
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
101
+ break;
102
+ validateConstructId(node, context, newExpression);
103
+ break;
104
+ }
105
+ }
106
+ };
107
+ /**
108
+ * Validate a single statement in the AST
109
+ * - Handles different types of statements (Variable, Expression, If, Switch)
110
+ * - Extracts and validates construct IDs from new expressions
111
+ */
112
+ var validateStatement = function (node, body, context) {
113
+ switch (body.type) {
114
+ case "VariableDeclaration": {
115
+ var newExpression = body.declarations[0].init;
116
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
117
+ break;
118
+ validateConstructId(node, context, newExpression);
119
+ break;
120
+ }
121
+ case "ExpressionStatement": {
122
+ var newExpression = body.expression;
123
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
124
+ break;
125
+ validateConstructId(node, context, newExpression);
126
+ break;
127
+ }
128
+ case "IfStatement": {
129
+ validateIfStatement(node, body, context);
130
+ break;
131
+ }
132
+ case "SwitchStatement": {
133
+ validateSwitchStatement(node, body, context);
134
+ break;
135
+ }
136
+ }
137
+ };
138
+ /**
139
+ * Validate the `if` statement
140
+ * - Validate recursively if `if` statements are nested
141
+ */
142
+ var validateIfStatement = function (node, ifStatement, context) {
143
+ traverseStatements(node, ifStatement.consequent, context);
144
+ };
145
+ /**
146
+ * Validate the `switch` statement
147
+ * - Validate recursively if `switch` statements are nested
148
+ */
149
+ var validateSwitchStatement = function (node, switchStatement, context) {
150
+ for (var _i = 0, _a = switchStatement.cases; _i < _a.length; _i++) {
151
+ var statement = _a[_i];
152
+ for (var _b = 0, _c = statement.consequent; _b < _c.length; _b++) {
153
+ var _consequent = _c[_b];
154
+ traverseStatements(node, _consequent, context);
155
+ }
156
+ }
157
+ };
158
+ /**
159
+ * Validate that construct ID does not end with "Construct" or "Stack"
160
+ */
161
+ var validateConstructId = function (node, context, expression) {
162
+ if (expression.arguments.length < 2)
163
+ return;
164
+ // NOTE: Treat the second argument as ID
165
+ var secondArg = expression.arguments[1];
166
+ if (secondArg.type !== "Literal" || typeof secondArg.value !== "string") {
167
+ return;
168
+ }
169
+ var formattedConstructId = toPascalCase(secondArg.value);
170
+ if (formattedConstructId.endsWith("Construct")) {
171
+ context.report({
172
+ node: node,
173
+ messageId: "noConstructStackSuffix",
174
+ data: {
175
+ classType: "Construct",
176
+ id: secondArg.value,
177
+ suffix: "Construct",
178
+ },
179
+ });
180
+ }
181
+ else if (formattedConstructId.endsWith("Stack")) {
182
+ context.report({
183
+ node: node,
184
+ messageId: "noConstructStackSuffix",
185
+ data: {
186
+ classType: "Stack",
187
+ id: secondArg.value,
188
+ suffix: "Stack",
189
+ },
190
+ });
191
+ }
192
+ };
@@ -0,0 +1,43 @@
1
+ import * as path from "path";
2
+ /**
3
+ * Split the directory path into segments (split at `/`)
4
+ * @param dirPath - The directory path to split
5
+ * @returns The segments of the directory path
6
+ */
7
+ var getDirSegments = function (dirPath) {
8
+ return dirPath.split(path.sep).filter(function (segment) { return segment !== ""; });
9
+ };
10
+ export var noImportPrivate = {
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description: "Cannot import modules from private dir at different levels of the hierarchy.",
15
+ },
16
+ messages: {
17
+ noImportPrivate: "Cannot import modules from private dir at different levels of the hierarchy.",
18
+ },
19
+ schema: [],
20
+ },
21
+ create: function (context) {
22
+ return {
23
+ ImportDeclaration: function (node) {
24
+ var _a, _b;
25
+ var importPath = (_b = (_a = node.source.value) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "";
26
+ var currentFilePath = context.filename;
27
+ var currentDirPath = path.dirname(currentFilePath);
28
+ if (importPath.includes("/private")) {
29
+ var absoluteCurrentDirPath = path.resolve(currentDirPath);
30
+ var absoluteImportPath = path.resolve(currentDirPath, importPath);
31
+ // NOTE: Get the directory from the import path up to the private directory
32
+ var importDirBeforePrivate = absoluteImportPath.split("/private")[0];
33
+ var currentDirSegments = getDirSegments(absoluteCurrentDirPath);
34
+ var importDirSegments_1 = getDirSegments(importDirBeforePrivate);
35
+ if (currentDirSegments.length !== importDirSegments_1.length ||
36
+ currentDirSegments.some(function (segment, index) { return segment !== importDirSegments_1[index]; })) {
37
+ context.report({ node: node, messageId: "noImportPrivate" });
38
+ }
39
+ }
40
+ },
41
+ };
42
+ },
43
+ };
@@ -0,0 +1,49 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ export var noMutablePropsInterface = ESLintUtils.RuleCreator.withoutDocs({
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Disallow mutable properties in Props interfaces",
7
+ },
8
+ fixable: "code",
9
+ messages: {
10
+ noMutablePropsInterface: "Property '{{ propertyName }}' in Props interface should be readonly.",
11
+ },
12
+ schema: [],
13
+ },
14
+ defaultOptions: [],
15
+ create: function (context) {
16
+ var sourceCode = context.sourceCode;
17
+ return {
18
+ TSInterfaceDeclaration: function (node) {
19
+ // NOTE: Interface name check for "Props"
20
+ if (!node.id.name.endsWith("Props"))
21
+ return;
22
+ var _loop_1 = function (property) {
23
+ if (property.type !== "TSPropertySignature" ||
24
+ property.key.type !== "Identifier") {
25
+ return "continue";
26
+ }
27
+ // NOTE: Skip if already readonly
28
+ if (property.readonly)
29
+ return "continue";
30
+ context.report({
31
+ node: property,
32
+ messageId: "noMutablePropsInterface",
33
+ data: {
34
+ propertyName: property.key.name,
35
+ },
36
+ fix: function (fixer) {
37
+ var propertyText = sourceCode.getText(property);
38
+ return fixer.replaceText(property, "readonly ".concat(propertyText));
39
+ },
40
+ });
41
+ };
42
+ for (var _i = 0, _a = node.body.body; _i < _a.length; _i++) {
43
+ var property = _a[_i];
44
+ _loop_1(property);
45
+ }
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,56 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ export var noMutablePublicFields = ESLintUtils.RuleCreator.withoutDocs({
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Disallow mutable public class fields",
7
+ },
8
+ fixable: "code",
9
+ messages: {
10
+ noMutablePublicFields: "Public field '{{ propertyName }}' should be readonly. Consider adding the 'readonly' modifier.",
11
+ },
12
+ schema: [],
13
+ },
14
+ defaultOptions: [],
15
+ create: function (context) {
16
+ var sourceCode = context.sourceCode;
17
+ return {
18
+ ClassDeclaration: function (node) {
19
+ var _a;
20
+ var _loop_1 = function (member) {
21
+ if (member.type !== "PropertyDefinition" ||
22
+ member.key.type !== "Identifier") {
23
+ return "continue";
24
+ }
25
+ // NOTE: Skip private and protected fields
26
+ if (["private", "protected"].includes((_a = member.accessibility) !== null && _a !== void 0 ? _a : "")) {
27
+ return "continue";
28
+ }
29
+ // NOTE: Skip if readonly is present
30
+ if (member.readonly)
31
+ return "continue";
32
+ context.report({
33
+ node: member,
34
+ messageId: "noMutablePublicFields",
35
+ data: {
36
+ propertyName: member.key.name,
37
+ },
38
+ fix: function (fixer) {
39
+ var accessibility = member.accessibility ? "public " : "";
40
+ var paramText = sourceCode.getText(member);
41
+ var _a = paramText.split(":"), key = _a[0], value = _a[1];
42
+ var replacedKey = key.startsWith("public ")
43
+ ? key.replace("public ", "")
44
+ : key;
45
+ return fixer.replaceText(member, "".concat(accessibility, "readonly ").concat(replacedKey, ":").concat(value));
46
+ },
47
+ });
48
+ };
49
+ for (var _i = 0, _b = node.body.body; _i < _b.length; _i++) {
50
+ var member = _b[_i];
51
+ _loop_1(member);
52
+ }
53
+ },
54
+ };
55
+ },
56
+ });
@@ -0,0 +1,257 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ import { toPascalCase } from "./utils/convertString.mjs";
3
+ export var noParentNameConstructIdMatch = ESLintUtils.RuleCreator.withoutDocs({
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "Enforce that construct IDs does not match the parent construct name.",
8
+ },
9
+ messages: {
10
+ noParentNameConstructIdMatch: "Construct ID '{{ constructId }}' should not match parent construct name '{{ parentConstructName }}'. Use a more specific identifier.",
11
+ },
12
+ schema: [],
13
+ },
14
+ defaultOptions: [],
15
+ create: function (context) {
16
+ return {
17
+ ClassBody: function (node) {
18
+ var _a;
19
+ var parent = node.parent;
20
+ if ((parent === null || parent === void 0 ? void 0 : parent.type) !== "ClassDeclaration")
21
+ return;
22
+ var parentClassName = (_a = parent.id) === null || _a === void 0 ? void 0 : _a.name;
23
+ if (!parentClassName)
24
+ return;
25
+ for (var _i = 0, _b = node.body; _i < _b.length; _i++) {
26
+ var body = _b[_i];
27
+ if (body.type !== "MethodDefinition" ||
28
+ !["method", "constructor"].includes(body.kind) ||
29
+ body.value.type !== "FunctionExpression") {
30
+ continue;
31
+ }
32
+ validateConstructorBody({
33
+ node: node,
34
+ expression: body.value,
35
+ parentClassName: parentClassName,
36
+ context: context,
37
+ });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ });
43
+ /**
44
+ * Validate the constructor body for the parent class
45
+ * - validate each statement in the constructor body
46
+ */
47
+ var validateConstructorBody = function (_a) {
48
+ var _b;
49
+ var node = _a.node, expression = _a.expression, parentClassName = _a.parentClassName, context = _a.context;
50
+ for (var _i = 0, _c = expression.body.body; _i < _c.length; _i++) {
51
+ var statement = _c[_i];
52
+ switch (statement.type) {
53
+ case "VariableDeclaration": {
54
+ var newExpression = statement.declarations[0].init;
55
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
56
+ continue;
57
+ validateConstructId({
58
+ node: node,
59
+ context: context,
60
+ expression: newExpression,
61
+ parentClassName: parentClassName,
62
+ });
63
+ break;
64
+ }
65
+ case "ExpressionStatement": {
66
+ if (((_b = statement.expression) === null || _b === void 0 ? void 0 : _b.type) !== "NewExpression")
67
+ break;
68
+ validateStatement({
69
+ node: node,
70
+ body: statement,
71
+ parentClassName: parentClassName,
72
+ context: context,
73
+ });
74
+ break;
75
+ }
76
+ case "IfStatement": {
77
+ traverseStatements({
78
+ node: node,
79
+ context: context,
80
+ parentClassName: parentClassName,
81
+ statement: statement.consequent,
82
+ });
83
+ break;
84
+ }
85
+ case "SwitchStatement": {
86
+ for (var _d = 0, _e = statement.cases; _d < _e.length; _d++) {
87
+ var switchCase = _e[_d];
88
+ for (var _f = 0, _g = switchCase.consequent; _f < _g.length; _f++) {
89
+ var statement_1 = _g[_f];
90
+ traverseStatements({
91
+ node: node,
92
+ context: context,
93
+ parentClassName: parentClassName,
94
+ statement: statement_1,
95
+ });
96
+ }
97
+ }
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ };
103
+ /**
104
+ * Recursively traverse and validate statements in the AST
105
+ * - Handles BlockStatement, ExpressionStatement, and VariableDeclaration
106
+ * - Validates construct IDs against parent class name
107
+ */
108
+ var traverseStatements = function (_a) {
109
+ var node = _a.node, statement = _a.statement, parentClassName = _a.parentClassName, context = _a.context;
110
+ switch (statement.type) {
111
+ case "BlockStatement": {
112
+ for (var _i = 0, _b = statement.body; _i < _b.length; _i++) {
113
+ var body = _b[_i];
114
+ validateStatement({
115
+ node: node,
116
+ body: body,
117
+ parentClassName: parentClassName,
118
+ context: context,
119
+ });
120
+ }
121
+ break;
122
+ }
123
+ case "ExpressionStatement": {
124
+ var newExpression = statement.expression;
125
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
126
+ break;
127
+ validateStatement({
128
+ node: node,
129
+ body: statement,
130
+ parentClassName: parentClassName,
131
+ context: context,
132
+ });
133
+ break;
134
+ }
135
+ case "VariableDeclaration": {
136
+ var newExpression = statement.declarations[0].init;
137
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
138
+ break;
139
+ validateConstructId({
140
+ node: node,
141
+ context: context,
142
+ expression: newExpression,
143
+ parentClassName: parentClassName,
144
+ });
145
+ break;
146
+ }
147
+ }
148
+ };
149
+ /**
150
+ * Validate a single statement in the AST
151
+ * - Handles different types of statements (Variable, Expression, If, Switch)
152
+ * - Extracts and validates construct IDs from new expressions
153
+ */
154
+ var validateStatement = function (_a) {
155
+ var node = _a.node, body = _a.body, parentClassName = _a.parentClassName, context = _a.context;
156
+ switch (body.type) {
157
+ case "VariableDeclaration": {
158
+ var newExpression = body.declarations[0].init;
159
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
160
+ break;
161
+ validateConstructId({
162
+ node: node,
163
+ context: context,
164
+ expression: newExpression,
165
+ parentClassName: parentClassName,
166
+ });
167
+ break;
168
+ }
169
+ case "ExpressionStatement": {
170
+ var newExpression = body.expression;
171
+ if ((newExpression === null || newExpression === void 0 ? void 0 : newExpression.type) !== "NewExpression")
172
+ break;
173
+ validateConstructId({
174
+ node: node,
175
+ context: context,
176
+ expression: newExpression,
177
+ parentClassName: parentClassName,
178
+ });
179
+ break;
180
+ }
181
+ case "IfStatement": {
182
+ validateIfStatement({
183
+ node: node,
184
+ ifStatement: body,
185
+ parentClassName: parentClassName,
186
+ context: context,
187
+ });
188
+ break;
189
+ }
190
+ case "SwitchStatement": {
191
+ validateSwitchStatement({
192
+ node: node,
193
+ switchStatement: body,
194
+ parentClassName: parentClassName,
195
+ context: context,
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ };
201
+ /**
202
+ * Validate the `if` statement
203
+ * - Validate recursively if `if` statements are nested
204
+ */
205
+ var validateIfStatement = function (_a) {
206
+ var node = _a.node, ifStatement = _a.ifStatement, parentClassName = _a.parentClassName, context = _a.context;
207
+ traverseStatements({
208
+ node: node,
209
+ context: context,
210
+ parentClassName: parentClassName,
211
+ statement: ifStatement.consequent,
212
+ });
213
+ };
214
+ /**
215
+ * Validate the `switch` statement
216
+ * - Validate recursively if `switch` statements are nested
217
+ */
218
+ var validateSwitchStatement = function (_a) {
219
+ var node = _a.node, switchStatement = _a.switchStatement, parentClassName = _a.parentClassName, context = _a.context;
220
+ for (var _i = 0, _b = switchStatement.cases; _i < _b.length; _i++) {
221
+ var statement = _b[_i];
222
+ for (var _c = 0, _d = statement.consequent; _c < _d.length; _c++) {
223
+ var _consequent = _d[_c];
224
+ traverseStatements({
225
+ node: node,
226
+ context: context,
227
+ parentClassName: parentClassName,
228
+ statement: _consequent,
229
+ });
230
+ }
231
+ }
232
+ };
233
+ /**
234
+ * Validate that parent construct name and child id do not match
235
+ */
236
+ var validateConstructId = function (_a) {
237
+ var node = _a.node, context = _a.context, expression = _a.expression, parentClassName = _a.parentClassName;
238
+ if (expression.arguments.length < 2)
239
+ return;
240
+ // NOTE: Treat the second argument as ID
241
+ var secondArg = expression.arguments[1];
242
+ if (secondArg.type !== "Literal" || typeof secondArg.value !== "string") {
243
+ return;
244
+ }
245
+ var formattedConstructId = toPascalCase(secondArg.value);
246
+ var formattedParentClassName = toPascalCase(parentClassName);
247
+ if (formattedParentClassName === formattedConstructId) {
248
+ context.report({
249
+ node: node,
250
+ messageId: "noParentNameConstructIdMatch",
251
+ data: {
252
+ constructId: secondArg.value,
253
+ parentConstructName: parentClassName,
254
+ },
255
+ });
256
+ }
257
+ };
@@ -0,0 +1,111 @@
1
+ import { ESLintUtils, } from "@typescript-eslint/utils";
2
+ import { SymbolFlags } from "typescript";
3
+ export var noPublicClassFields = ESLintUtils.RuleCreator.withoutDocs({
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "Disallow class types in public class fields",
8
+ },
9
+ messages: {
10
+ noPublicClassFields: "Public field '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead.",
11
+ },
12
+ schema: [],
13
+ },
14
+ defaultOptions: [],
15
+ create: function (context) {
16
+ var parserServices = ESLintUtils.getParserServices(context);
17
+ var typeChecker = parserServices.program.getTypeChecker();
18
+ return {
19
+ ClassDeclaration: function (node) {
20
+ // NOTE: Check class members
21
+ validateClassMember({
22
+ node: node,
23
+ context: context,
24
+ parserServices: parserServices,
25
+ typeChecker: typeChecker,
26
+ });
27
+ // NOTE: Check constructor parameter properties
28
+ var constructor = node.body.body.find(function (member) {
29
+ return member.type === "MethodDefinition" && member.kind === "constructor";
30
+ });
31
+ if (!constructor || constructor.value.type !== "FunctionExpression") {
32
+ return;
33
+ }
34
+ validateConstructorParameterProperty({
35
+ constructor: constructor,
36
+ context: context,
37
+ parserServices: parserServices,
38
+ typeChecker: typeChecker,
39
+ });
40
+ },
41
+ };
42
+ },
43
+ });
44
+ var validateClassMember = function (_a) {
45
+ var _b;
46
+ var node = _a.node, context = _a.context, parserServices = _a.parserServices, typeChecker = _a.typeChecker;
47
+ for (var _i = 0, _c = node.body.body; _i < _c.length; _i++) {
48
+ var member = _c[_i];
49
+ if (member.type !== "PropertyDefinition" ||
50
+ member.key.type !== "Identifier") {
51
+ continue;
52
+ }
53
+ // NOTE: Skip private and protected fields
54
+ if (["private", "protected"].includes((_b = member.accessibility) !== null && _b !== void 0 ? _b : "")) {
55
+ continue;
56
+ }
57
+ // NOTE: Skip fields without type annotation
58
+ if (!member.typeAnnotation) {
59
+ continue;
60
+ }
61
+ var tsNode = parserServices.esTreeNodeToTSNodeMap.get(member);
62
+ var type = typeChecker.getTypeAtLocation(tsNode);
63
+ if (!type.symbol)
64
+ continue;
65
+ var isClass = type.symbol.flags === SymbolFlags.Class;
66
+ if (!isClass)
67
+ continue;
68
+ context.report({
69
+ node: member,
70
+ messageId: "noPublicClassFields",
71
+ data: {
72
+ propertyName: member.key.name,
73
+ typeName: type.symbol.name,
74
+ },
75
+ });
76
+ }
77
+ };
78
+ var validateConstructorParameterProperty = function (_a) {
79
+ var _b;
80
+ var constructor = _a.constructor, context = _a.context, parserServices = _a.parserServices, typeChecker = _a.typeChecker;
81
+ for (var _i = 0, _c = constructor.value.params; _i < _c.length; _i++) {
82
+ var param = _c[_i];
83
+ if (param.type !== "TSParameterProperty" ||
84
+ param.parameter.type !== "Identifier") {
85
+ continue;
86
+ }
87
+ // NOTE: Skip private and protected parameters
88
+ if (["private", "protected"].includes((_b = param.accessibility) !== null && _b !== void 0 ? _b : "")) {
89
+ continue;
90
+ }
91
+ // NOTE: Skip parameters without type annotation
92
+ if (!param.parameter.typeAnnotation) {
93
+ continue;
94
+ }
95
+ var tsNode = parserServices.esTreeNodeToTSNodeMap.get(param);
96
+ var type = typeChecker.getTypeAtLocation(tsNode);
97
+ if (!type.symbol)
98
+ continue;
99
+ var isClass = type.symbol.flags === SymbolFlags.Class;
100
+ if (!isClass)
101
+ continue;
102
+ context.report({
103
+ node: param,
104
+ messageId: "noPublicClassFields",
105
+ data: {
106
+ propertyName: param.parameter.name,
107
+ typeName: type.symbol.name,
108
+ },
109
+ });
110
+ }
111
+ };
@@ -0,0 +1,69 @@
1
+ import { toPascalCase } from "./utils/convertString.mjs";
2
+ var QUOTE_TYPE = {
3
+ SINGLE: "'",
4
+ DOUBLE: '"',
5
+ };
6
+ /**
7
+ * check if the string is PascalCase
8
+ * @param str - The string to check
9
+ * @returns true if the string is PascalCase, false otherwise
10
+ */
11
+ var isPascalCase = function (str) {
12
+ return /^[A-Z][a-zA-Z0-9]*$/.test(str);
13
+ };
14
+ var validateConstructId = function (node, context, args) {
15
+ var _a;
16
+ if (args.length < 2)
17
+ return;
18
+ // NOTE: Treat the second argument as ID
19
+ var secondArg = args[1];
20
+ if (secondArg.type !== "Literal" || typeof secondArg.value !== "string") {
21
+ return;
22
+ }
23
+ var quote = ((_a = secondArg.raw) === null || _a === void 0 ? void 0 : _a.startsWith('"'))
24
+ ? QUOTE_TYPE.DOUBLE
25
+ : QUOTE_TYPE.SINGLE;
26
+ if (!isPascalCase(secondArg.value)) {
27
+ context.report({
28
+ node: node,
29
+ messageId: "pascalCaseConstructId",
30
+ fix: function (fixer) {
31
+ var pascalCaseValue = toPascalCase(secondArg.value);
32
+ return fixer.replaceText(secondArg, "".concat(quote).concat(pascalCaseValue).concat(quote));
33
+ },
34
+ });
35
+ }
36
+ };
37
+ export var pascalCaseConstructId = {
38
+ meta: {
39
+ type: "problem",
40
+ docs: {
41
+ description: "Enforce PascalCase for Construct ID.",
42
+ },
43
+ messages: {
44
+ pascalCaseConstructId: "Construct ID must be PascalCase.",
45
+ },
46
+ schema: [],
47
+ fixable: "code",
48
+ },
49
+ create: function (context) {
50
+ return {
51
+ ExpressionStatement: function (node) {
52
+ if (node.expression.type !== "NewExpression")
53
+ return;
54
+ validateConstructId(node, context, node.expression.arguments);
55
+ },
56
+ VariableDeclaration: function (node) {
57
+ var _a;
58
+ if (!node.declarations.length)
59
+ return;
60
+ for (var _i = 0, _b = node.declarations; _i < _b.length; _i++) {
61
+ var declaration = _b[_i];
62
+ if (((_a = declaration.init) === null || _a === void 0 ? void 0 : _a.type) !== "NewExpression")
63
+ return;
64
+ validateConstructId(node, context, declaration.init.arguments);
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Convert a string to PascalCase
3
+ * @param str - The string to convert
4
+ * @returns The PascalCase string
5
+ */
6
+ export var toPascalCase = function (str) {
7
+ return str
8
+ .split(/[-_\s]/)
9
+ .map(function (word) {
10
+ // Consider camelCase, split by uppercase letters
11
+ return word
12
+ .replace(/([A-Z])/g, " $1")
13
+ .split(/\s+/)
14
+ .map(function (part) { return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); })
15
+ .join("");
16
+ })
17
+ .join("");
18
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "eslint-cdk-plugin",
3
+ "version": "0.1.0",
4
+ "description": "eslint plugin for AWS CDK projects",
5
+ "main": "dist/index.mjs",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "rm -rf dist && tsc src/*.*ts --outDir dist --skipLibCheck",
9
+ "test": "vitest --run",
10
+ "lint": "eslint --fix --config eslint.config.mjs",
11
+ "release:minor": "standard-version --release-as minor",
12
+ "release:major": "standard-version --release-as major",
13
+ "release:patch": "standard-version --release-as patch",
14
+ "docs:dev": "cd ./docs && pnpm install && pnpm run dev",
15
+ "docs:build": "cd ./docs && pnpm install && pnpm run build",
16
+ "docs:preview": "cd ./docs && pnpm install && pnpm run preview"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.15.0",
20
+ "@types/estree": "^1.0.6",
21
+ "@types/node": "^22.9.0",
22
+ "@typescript-eslint/rule-tester": "^8.14.0",
23
+ "eslint-plugin-import": "^2.31.0",
24
+ "standard-version": "^9.5.0",
25
+ "typescript": "^5.6.3",
26
+ "typescript-eslint": "^8.14.0",
27
+ "vitest": "^2.1.5"
28
+ },
29
+ "dependencies": {
30
+ "@typescript-eslint/utils": "^8.14.0",
31
+ "eslint": "9.10.0"
32
+ },
33
+ "volta": {
34
+ "node": "22.11.0"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "keywords": [
42
+ "eslint",
43
+ "eslintplugin",
44
+ "eslint-plugin",
45
+ "aws",
46
+ "cdk"
47
+ ],
48
+ "homepage": "https://eslint-plugin-cdk.dev/",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/ren-yamanashi/eslint-plugin-cdk.git"
52
+ },
53
+ "engines": {
54
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
55
+ },
56
+ "author": {
57
+ "name": "ren-yamanashi"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "license": "MIT"
63
+ }