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.
- package/README.md +29 -2
- package/dist/index.cjs +783 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +755 -39
- package/package.json +22 -9
- package/src/index.ts +69 -0
- package/src/rules/no-class-in-interface.ts +56 -0
- package/src/rules/no-construct-stack-suffix.ts +86 -0
- package/src/rules/no-import-private.ts +60 -0
- package/src/rules/no-mutable-props-interface.ts +58 -0
- package/src/rules/no-mutable-public-fields.ts +75 -0
- package/src/rules/no-parent-name-construct-id-match.ts +312 -0
- package/src/rules/no-public-class-fields.ts +148 -0
- package/src/rules/no-variable-construct-id.ts +101 -0
- package/src/rules/pascal-case-construct-id.ts +95 -0
- package/src/rules/require-passing-this.ts +51 -0
- package/src/types/symbolFlags.ts +6 -0
- package/src/utils/convertString.ts +20 -0
- package/src/utils/typeCheck.ts +57 -0
- package/dist/index.d.mts +0 -31
- package/dist/index.mjs.map +0 -1
- package/dist/no-class-in-interface-props.d.mts +0 -2
- package/dist/no-class-in-interface-props.mjs +0 -45
- package/dist/no-class-in-interface-props.mjs.map +0 -1
- package/dist/no-construct-stack-suffix.d.mts +0 -2
- package/dist/no-construct-stack-suffix.mjs +0 -64
- package/dist/no-construct-stack-suffix.mjs.map +0 -1
- package/dist/no-import-private.d.mts +0 -2
- package/dist/no-import-private.mjs +0 -37
- package/dist/no-import-private.mjs.map +0 -1
- package/dist/no-mutable-props-interface.d.mts +0 -2
- package/dist/no-mutable-props-interface.mjs +0 -44
- package/dist/no-mutable-props-interface.mjs.map +0 -1
- package/dist/no-mutable-public-fields.d.mts +0 -2
- package/dist/no-mutable-public-fields.mjs +0 -57
- package/dist/no-mutable-public-fields.mjs.map +0 -1
- package/dist/no-parent-name-construct-id-match.d.mts +0 -2
- package/dist/no-parent-name-construct-id-match.mjs +0 -218
- package/dist/no-parent-name-construct-id-match.mjs.map +0 -1
- package/dist/no-public-class-fields.d.mts +0 -2
- package/dist/no-public-class-fields.mjs +0 -105
- package/dist/no-public-class-fields.mjs.map +0 -1
- package/dist/no-variable-construct-id.d.mts +0 -2
- package/dist/no-variable-construct-id.mjs +0 -63
- package/dist/no-variable-construct-id.mjs.map +0 -1
- package/dist/pascal-case-construct-id.d.mts +0 -2
- package/dist/pascal-case-construct-id.mjs +0 -62
- package/dist/pascal-case-construct-id.mjs.map +0 -1
- package/dist/require-passing-this.d.mts +0 -2
- package/dist/require-passing-this.mjs +0 -39
- package/dist/require-passing-this.mjs.map +0 -1
- package/dist/utils/convertString.d.mts +0 -1
- package/dist/utils/convertString.mjs +0 -13
- package/dist/utils/convertString.mjs.map +0 -1
- package/dist/utils/typeCheck.d.mts +0 -3
- package/dist/utils/typeCheck.mjs +0 -16
- 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.
|
|
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": "
|
|
20
|
+
"build": "sh scripts/build.sh",
|
|
9
21
|
"test": "vitest --run",
|
|
10
|
-
"lint": "eslint --fix --config eslint.config.
|
|
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
|
+
});
|