eslint-cdk-plugin 1.0.1 → 1.0.3

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.
Files changed (58) hide show
  1. package/README.md +29 -2
  2. package/dist/index.cjs +783 -0
  3. package/dist/index.d.ts +51 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.mjs +755 -39
  6. package/package.json +22 -9
  7. package/src/index.ts +69 -0
  8. package/src/rules/no-class-in-interface.ts +56 -0
  9. package/src/rules/no-construct-stack-suffix.ts +86 -0
  10. package/src/rules/no-import-private.ts +60 -0
  11. package/src/rules/no-mutable-props-interface.ts +58 -0
  12. package/src/rules/no-mutable-public-fields.ts +75 -0
  13. package/src/rules/no-parent-name-construct-id-match.ts +312 -0
  14. package/src/rules/no-public-class-fields.ts +148 -0
  15. package/src/rules/no-variable-construct-id.ts +101 -0
  16. package/src/rules/pascal-case-construct-id.ts +95 -0
  17. package/src/rules/require-passing-this.ts +51 -0
  18. package/src/types/symbolFlags.ts +6 -0
  19. package/src/utils/convertString.ts +20 -0
  20. package/src/utils/typeCheck.ts +57 -0
  21. package/dist/index.d.mts +0 -31
  22. package/dist/index.mjs.map +0 -1
  23. package/dist/no-class-in-interface-props.d.mts +0 -2
  24. package/dist/no-class-in-interface-props.mjs +0 -45
  25. package/dist/no-class-in-interface-props.mjs.map +0 -1
  26. package/dist/no-construct-stack-suffix.d.mts +0 -2
  27. package/dist/no-construct-stack-suffix.mjs +0 -64
  28. package/dist/no-construct-stack-suffix.mjs.map +0 -1
  29. package/dist/no-import-private.d.mts +0 -2
  30. package/dist/no-import-private.mjs +0 -37
  31. package/dist/no-import-private.mjs.map +0 -1
  32. package/dist/no-mutable-props-interface.d.mts +0 -2
  33. package/dist/no-mutable-props-interface.mjs +0 -44
  34. package/dist/no-mutable-props-interface.mjs.map +0 -1
  35. package/dist/no-mutable-public-fields.d.mts +0 -2
  36. package/dist/no-mutable-public-fields.mjs +0 -57
  37. package/dist/no-mutable-public-fields.mjs.map +0 -1
  38. package/dist/no-parent-name-construct-id-match.d.mts +0 -2
  39. package/dist/no-parent-name-construct-id-match.mjs +0 -218
  40. package/dist/no-parent-name-construct-id-match.mjs.map +0 -1
  41. package/dist/no-public-class-fields.d.mts +0 -2
  42. package/dist/no-public-class-fields.mjs +0 -105
  43. package/dist/no-public-class-fields.mjs.map +0 -1
  44. package/dist/no-variable-construct-id.d.mts +0 -2
  45. package/dist/no-variable-construct-id.mjs +0 -63
  46. package/dist/no-variable-construct-id.mjs.map +0 -1
  47. package/dist/pascal-case-construct-id.d.mts +0 -2
  48. package/dist/pascal-case-construct-id.mjs +0 -62
  49. package/dist/pascal-case-construct-id.mjs.map +0 -1
  50. package/dist/require-passing-this.d.mts +0 -2
  51. package/dist/require-passing-this.mjs +0 -39
  52. package/dist/require-passing-this.mjs.map +0 -1
  53. package/dist/utils/convertString.d.mts +0 -1
  54. package/dist/utils/convertString.mjs +0 -13
  55. package/dist/utils/convertString.mjs.map +0 -1
  56. package/dist/utils/typeCheck.d.mts +0 -3
  57. package/dist/utils/typeCheck.mjs +0 -16
  58. package/dist/utils/typeCheck.mjs.map +0 -1
package/package.json CHANGED
@@ -1,13 +1,26 @@
1
1
  {
2
2
  "name": "eslint-cdk-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "eslint plugin for AWS CDK projects",
5
- "main": "dist/index.mjs",
5
+ "main": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
6
7
  "type": "module",
8
+ "exports": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.cjs"
13
+ },
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.mjs"
17
+ }
18
+ },
7
19
  "scripts": {
8
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
20
+ "build": "sh scripts/build.sh",
9
21
  "test": "vitest --run",
10
- "lint": "eslint --fix --config eslint.config.mjs",
22
+ "lint": "eslint --fix --config eslint.config.js",
23
+ "type-check": "tsc --noEmit",
11
24
  "release:minor": "standard-version --release-as minor",
12
25
  "release:major": "standard-version --release-as major",
13
26
  "release:patch": "standard-version --release-as patch",
@@ -20,21 +33,24 @@
20
33
  "@types/estree": "^1.0.6",
21
34
  "@types/node": "^22.9.0",
22
35
  "@typescript-eslint/rule-tester": "^8.14.0",
36
+ "eslint": "9.10.0",
23
37
  "eslint-plugin-import": "^2.31.0",
38
+ "pkgroll": "^2.6.0",
24
39
  "standard-version": "^9.5.0",
25
40
  "typescript": "^5.6.3",
26
41
  "typescript-eslint": "^8.14.0",
27
42
  "vitest": "^2.1.5"
28
43
  },
29
44
  "dependencies": {
30
- "@typescript-eslint/utils": "^8.14.0",
31
- "eslint": "9.10.0"
45
+ "@typescript-eslint/utils": "^8.14.0"
32
46
  },
33
47
  "volta": {
34
48
  "node": "22.11.0"
35
49
  },
36
50
  "files": [
37
51
  "dist",
52
+ "src",
53
+ "!src/__tests__",
38
54
  "README.md",
39
55
  "LICENSE"
40
56
  ],
@@ -50,9 +66,6 @@
50
66
  "type": "git",
51
67
  "url": "https://github.com/ren-yamanashi/eslint-cdk-plugin.git"
52
68
  },
53
- "engines": {
54
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
55
- },
56
69
  "author": {
57
70
  "name": "ren-yamanashi"
58
71
  },
package/src/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { noClassInInterface } from "./rules/no-class-in-interface";
2
+ import { noConstructStackSuffix } from "./rules/no-construct-stack-suffix";
3
+ import { noImportPrivate } from "./rules/no-import-private";
4
+ import { noMutablePropsInterface } from "./rules/no-mutable-props-interface";
5
+ import { noMutablePublicFields } from "./rules/no-mutable-public-fields";
6
+ import { noParentNameConstructIdMatch } from "./rules/no-parent-name-construct-id-match";
7
+ import { noPublicClassFields } from "./rules/no-public-class-fields";
8
+ import { noVariableConstructId } from "./rules/no-variable-construct-id";
9
+ import { pascalCaseConstructId } from "./rules/pascal-case-construct-id";
10
+ import { requirePassingThis } from "./rules/require-passing-this";
11
+
12
+ const rules = {
13
+ "no-class-in-interface": noClassInInterface,
14
+ "no-construct-stack-suffix": noConstructStackSuffix,
15
+ "no-parent-name-construct-id-match": noParentNameConstructIdMatch,
16
+ "no-public-class-fields": noPublicClassFields,
17
+ "pascal-case-construct-id": pascalCaseConstructId,
18
+ "no-mutable-public-fields": noMutablePublicFields,
19
+ "no-mutable-props-interface": noMutablePropsInterface,
20
+ "require-passing-this": requirePassingThis,
21
+ "no-variable-construct-id": noVariableConstructId,
22
+ "no-import-private": noImportPrivate,
23
+ };
24
+
25
+ const configs = {
26
+ recommended: {
27
+ plugins: ["cdk"],
28
+ rules: {
29
+ "cdk/no-class-in-interface": "error",
30
+ "cdk/no-construct-stack-suffix": "error",
31
+ "cdk/no-parent-name-construct-id-match": "error",
32
+ "cdk/no-public-class-fields": "error",
33
+ "cdk/pascal-case-construct-id": "error",
34
+ "cdk/require-passing-this": "error",
35
+ "cdk/no-variable-construct-id": "error",
36
+ "cdk/no-mutable-public-fields": "warn",
37
+ "cdk/no-mutable-props-interface": "warn",
38
+ },
39
+ },
40
+ strict: {
41
+ plugins: ["cdk"],
42
+ rules: {
43
+ "cdk/no-class-in-interface": "error",
44
+ "cdk/no-construct-stack-suffix": "error",
45
+ "cdk/no-parent-name-construct-id-match": "error",
46
+ "cdk/no-public-class-fields": "error",
47
+ "cdk/pascal-case-construct-id": "error",
48
+ "cdk/require-passing-this": "error",
49
+ "cdk/no-variable-construct-id": "error",
50
+ "cdk/no-mutable-public-fields": "error",
51
+ "cdk/no-mutable-props-interface": "error",
52
+ "cdk/no-import-private": "error",
53
+ },
54
+ },
55
+ };
56
+
57
+ export { configs, rules };
58
+
59
+ export interface EslintCdkPlugin {
60
+ rules: typeof rules;
61
+ configs: typeof configs;
62
+ }
63
+
64
+ const eslintCdkPlugin: EslintCdkPlugin = {
65
+ rules,
66
+ configs,
67
+ };
68
+
69
+ export default eslintCdkPlugin;
@@ -0,0 +1,56 @@
1
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ import { SymbolFlags } from "../types/symbolFlags";
4
+
5
+ /**
6
+ * Enforces the use of interface types instead of class in interface properties
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-class-in-interface} - Documentation
10
+ */
11
+ export const noClassInInterface = ESLintUtils.RuleCreator.withoutDocs({
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: "Disallow class types in interface properties",
16
+ },
17
+ messages: {
18
+ noClassInInterfaceProps:
19
+ "Interface property '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead.",
20
+ },
21
+ schema: [],
22
+ },
23
+ defaultOptions: [],
24
+ create(context) {
25
+ const parserServices = ESLintUtils.getParserServices(context);
26
+ return {
27
+ TSInterfaceDeclaration(node) {
28
+ for (const property of node.body.body) {
29
+ // NOTE: check property signature
30
+ if (
31
+ property.type !== AST_NODE_TYPES.TSPropertySignature ||
32
+ property.key.type !== AST_NODE_TYPES.Identifier
33
+ ) {
34
+ continue;
35
+ }
36
+
37
+ const type = parserServices.getTypeAtLocation(property);
38
+ if (!type.symbol) continue;
39
+
40
+ // NOTE: check class type
41
+ const isClass = type.symbol.flags === SymbolFlags.Class;
42
+ if (!isClass) continue;
43
+
44
+ context.report({
45
+ node: property,
46
+ messageId: "noClassInInterfaceProps",
47
+ data: {
48
+ propertyName: property.key.name,
49
+ typeName: type.symbol.name,
50
+ },
51
+ });
52
+ }
53
+ },
54
+ };
55
+ },
56
+ });
@@ -0,0 +1,86 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ ESLintUtils,
4
+ TSESLint,
5
+ TSESTree,
6
+ } from "@typescript-eslint/utils";
7
+
8
+ import { toPascalCase } from "../utils/convertString";
9
+ import { isConstructOrStackType } from "../utils/typeCheck";
10
+
11
+ type Context = TSESLint.RuleContext<"noConstructStackSuffix", []>;
12
+
13
+ /**
14
+ * Enforces that Construct IDs do not end with 'Construct' or 'Stack' suffix
15
+ * @param context - The rule context provided by ESLint
16
+ * @returns An object containing the AST visitor functions
17
+ * @see {@link https://eslint-cdk-plugin.dev/rules/no-construct-stack-suffix} - Documentation
18
+ */
19
+ export const noConstructStackSuffix = ESLintUtils.RuleCreator.withoutDocs({
20
+ meta: {
21
+ type: "problem",
22
+ docs: {
23
+ description:
24
+ "Effort to avoid using 'Construct' and 'Stack' suffix in construct id.",
25
+ },
26
+ messages: {
27
+ noConstructStackSuffix:
28
+ "{{ classType }} ID '{{ id }}' should not include {{ suffix }} suffix.",
29
+ },
30
+ schema: [],
31
+ },
32
+ defaultOptions: [],
33
+ create(context) {
34
+ const parserServices = ESLintUtils.getParserServices(context);
35
+ return {
36
+ NewExpression(node) {
37
+ const type = parserServices.getTypeAtLocation(node);
38
+ if (!isConstructOrStackType(type) || node.arguments.length < 2) {
39
+ return;
40
+ }
41
+ validateConstructId(node, context);
42
+ },
43
+ };
44
+ },
45
+ });
46
+
47
+ /**
48
+ * Validate that construct ID does not end with "Construct" or "Stack"
49
+ */
50
+ const validateConstructId = (
51
+ node: TSESTree.NewExpression,
52
+ context: Context
53
+ ): void => {
54
+ // NOTE: Treat the second argument as ID
55
+ const secondArg = node.arguments[1];
56
+ if (
57
+ secondArg.type !== AST_NODE_TYPES.Literal ||
58
+ typeof secondArg.value !== "string"
59
+ ) {
60
+ return;
61
+ }
62
+
63
+ const formattedConstructId = toPascalCase(secondArg.value);
64
+
65
+ if (formattedConstructId.endsWith("Construct")) {
66
+ context.report({
67
+ node,
68
+ messageId: "noConstructStackSuffix",
69
+ data: {
70
+ classType: "Construct",
71
+ id: secondArg.value,
72
+ suffix: "Construct",
73
+ },
74
+ });
75
+ } else if (formattedConstructId.endsWith("Stack")) {
76
+ context.report({
77
+ node,
78
+ messageId: "noConstructStackSuffix",
79
+ data: {
80
+ classType: "Stack",
81
+ id: secondArg.value,
82
+ suffix: "Stack",
83
+ },
84
+ });
85
+ }
86
+ };
@@ -0,0 +1,60 @@
1
+ import * as path from "path";
2
+
3
+ import { Rule } from "eslint";
4
+
5
+ /**
6
+ * Disallow importing modules from private directories at different levels of the hierarchy.
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-import-private} - Documentation
10
+ */
11
+ export const noImportPrivate: Rule.RuleModule = {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description:
16
+ "Cannot import modules from private dir at different levels of the hierarchy.",
17
+ },
18
+ messages: {
19
+ noImportPrivate:
20
+ "Cannot import modules from private dir at different levels of the hierarchy.",
21
+ },
22
+ schema: [],
23
+ },
24
+ create(context) {
25
+ return {
26
+ ImportDeclaration(node) {
27
+ const importPath = node.source.value?.toString() ?? "";
28
+ const currentFilePath = context.filename;
29
+ const currentDirPath = path.dirname(currentFilePath);
30
+
31
+ if (!importPath.includes("/private")) return;
32
+ const absoluteCurrentDirPath = path.resolve(currentDirPath);
33
+ const absoluteImportPath = path.resolve(currentDirPath, importPath);
34
+
35
+ // NOTE: Get the directory from the import path up to the private directory
36
+ const importDirBeforePrivate = absoluteImportPath.split("/private")[0];
37
+
38
+ const currentDirSegments = getDirSegments(absoluteCurrentDirPath);
39
+ const importDirSegments = getDirSegments(importDirBeforePrivate);
40
+ if (
41
+ currentDirSegments.length !== importDirSegments.length ||
42
+ currentDirSegments.some(
43
+ (segment, index) => segment !== importDirSegments[index]
44
+ )
45
+ ) {
46
+ context.report({ node, messageId: "noImportPrivate" });
47
+ }
48
+ },
49
+ };
50
+ },
51
+ };
52
+
53
+ /**
54
+ * Split the directory path into segments (split at `/`)
55
+ * @param dirPath - The directory path to split
56
+ * @returns The segments of the directory path
57
+ */
58
+ const getDirSegments = (dirPath: string): string[] => {
59
+ return dirPath.split(path.sep).filter((segment) => segment !== "");
60
+ };
@@ -0,0 +1,58 @@
1
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ /**
4
+ * Disallow mutable properties in Props interfaces
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-props-interface} - Documentation
8
+ */
9
+ export const noMutablePropsInterface = ESLintUtils.RuleCreator.withoutDocs({
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description: "Disallow mutable properties in Props interfaces",
14
+ },
15
+ fixable: "code",
16
+ messages: {
17
+ noMutablePropsInterface:
18
+ "Property '{{ propertyName }}' in Props interface should be readonly.",
19
+ },
20
+ schema: [],
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ return {
25
+ TSInterfaceDeclaration(node) {
26
+ const sourceCode = context.sourceCode;
27
+
28
+ // NOTE: Interface name check for "Props"
29
+ if (!node.id.name.endsWith("Props")) return;
30
+
31
+ for (const property of node.body.body) {
32
+ // NOTE: check property signature
33
+ if (
34
+ property.type !== AST_NODE_TYPES.TSPropertySignature ||
35
+ property.key.type !== AST_NODE_TYPES.Identifier
36
+ ) {
37
+ continue;
38
+ }
39
+
40
+ // NOTE: Skip if already readonly
41
+ if (property.readonly) continue;
42
+
43
+ context.report({
44
+ node: property,
45
+ messageId: "noMutablePropsInterface",
46
+ data: {
47
+ propertyName: property.key.name,
48
+ },
49
+ fix: (fixer) => {
50
+ const propertyText = sourceCode.getText(property);
51
+ return fixer.replaceText(property, `readonly ${propertyText}`);
52
+ },
53
+ });
54
+ }
55
+ },
56
+ };
57
+ },
58
+ });
@@ -0,0 +1,75 @@
1
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ import { isConstructOrStackType } from "../utils/typeCheck";
4
+
5
+ /**
6
+ * Disallow mutable public class fields
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-fields} - Documentation
10
+ */
11
+ export const noMutablePublicFields = ESLintUtils.RuleCreator.withoutDocs({
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: "Disallow mutable public class fields",
16
+ },
17
+ fixable: "code",
18
+ messages: {
19
+ noMutablePublicFields:
20
+ "Public field '{{ propertyName }}' should be readonly. Consider adding the 'readonly' modifier.",
21
+ },
22
+ schema: [],
23
+ },
24
+ defaultOptions: [],
25
+ create(context) {
26
+ const parserServices = ESLintUtils.getParserServices(context);
27
+
28
+ return {
29
+ ClassDeclaration(node) {
30
+ const sourceCode = context.sourceCode;
31
+ const type = parserServices.getTypeAtLocation(node);
32
+ if (!isConstructOrStackType(type)) return;
33
+
34
+ for (const member of node.body.body) {
35
+ // NOTE: check property definition
36
+ if (
37
+ member.type !== AST_NODE_TYPES.PropertyDefinition ||
38
+ member.key.type !== AST_NODE_TYPES.Identifier
39
+ ) {
40
+ continue;
41
+ }
42
+
43
+ // NOTE: Skip private and protected fields
44
+ if (["private", "protected"].includes(member.accessibility ?? "")) {
45
+ continue;
46
+ }
47
+
48
+ // NOTE: Skip if readonly is present
49
+ if (member.readonly) continue;
50
+
51
+ context.report({
52
+ node: member,
53
+ messageId: "noMutablePublicFields",
54
+ data: {
55
+ propertyName: member.key.name,
56
+ },
57
+ fix: (fixer) => {
58
+ const accessibility = member.accessibility ? "public " : "";
59
+ const paramText = sourceCode.getText(member);
60
+ const [key, value] = paramText.split(":");
61
+ const replacedKey = key.startsWith("public ")
62
+ ? key.replace("public ", "")
63
+ : key;
64
+
65
+ return fixer.replaceText(
66
+ member,
67
+ `${accessibility}readonly ${replacedKey}:${value}`
68
+ );
69
+ },
70
+ });
71
+ }
72
+ },
73
+ };
74
+ },
75
+ });