eslint-cdk-plugin 1.1.0 → 2.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/README.md +6 -17
- package/dist/index.cjs +241 -193770
- package/dist/index.d.ts +78 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +241 -193770
- package/package.json +14 -13
- package/src/constants/tsInternalFlags.ts +14 -0
- package/src/index.ts +60 -34
- package/src/rules/construct-constructor-property.ts +117 -0
- package/src/rules/no-class-in-interface.ts +5 -3
- package/src/rules/no-construct-stack-suffix.ts +53 -9
- package/src/rules/no-parent-name-construct-id-match.ts +1 -1
- package/src/rules/no-public-class-fields.ts +9 -3
- package/src/rules/no-variable-construct-id.ts +19 -0
- package/src/rules/pascal-case-construct-id.ts +1 -1
- package/src/rules/require-passing-this.ts +62 -11
- package/src/utils/parseType.ts +33 -1
- package/src/utils/typeCheck.ts +1 -1
- package/src/types/symbolFlags.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-cdk-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "eslint plugin for AWS CDK projects",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"build": "sh scripts/build.sh",
|
|
21
21
|
"test": "vitest --run",
|
|
22
22
|
"lint": "eslint --fix --config eslint.config.js",
|
|
23
|
-
"
|
|
23
|
+
"check": "tsc --noEmit",
|
|
24
|
+
"pack": "pnpm run build && npm pack",
|
|
24
25
|
"release:minor": "standard-version --release-as minor",
|
|
25
26
|
"release:major": "standard-version --release-as major",
|
|
26
27
|
"release:patch": "standard-version --release-as patch",
|
|
@@ -29,23 +30,23 @@
|
|
|
29
30
|
"docs:preview": "cd ./docs && pnpm install && pnpm run preview"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"@eslint/js": "^9.
|
|
33
|
-
"@types/
|
|
34
|
-
"@
|
|
35
|
-
"
|
|
36
|
-
"eslint": "9.10.0",
|
|
33
|
+
"@eslint/js": "^9.22.0",
|
|
34
|
+
"@types/node": "^22.13.10",
|
|
35
|
+
"@typescript-eslint/rule-tester": "^8.26.0",
|
|
36
|
+
"eslint": "9.22.0",
|
|
37
37
|
"eslint-plugin-import": "^2.31.0",
|
|
38
|
-
"pkgroll": "^2.
|
|
38
|
+
"pkgroll": "^2.11.2",
|
|
39
39
|
"standard-version": "^9.5.0",
|
|
40
|
-
"typescript": "^5.
|
|
41
|
-
"typescript-eslint": "^8.
|
|
42
|
-
"vitest": "^
|
|
40
|
+
"typescript": "^5.8.2",
|
|
41
|
+
"typescript-eslint": "^8.26.0",
|
|
42
|
+
"vitest": "^3.0.8"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@typescript-eslint/
|
|
45
|
+
"@typescript-eslint/parser": "^8.26.0",
|
|
46
|
+
"@typescript-eslint/utils": "^8.26.0"
|
|
46
47
|
},
|
|
47
48
|
"volta": {
|
|
48
|
-
"node": "22.
|
|
49
|
+
"node": "22.14.0"
|
|
49
50
|
},
|
|
50
51
|
"files": [
|
|
51
52
|
"dist",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementing `SymbolFlags` defined in typescript on your own, in order not to include TypeScript in dependencies
|
|
3
|
+
*/
|
|
4
|
+
export const SYMBOL_FLAGS = {
|
|
5
|
+
CLASS: 32,
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Implementing `SyntaxKind` defined in typescript on your own, in order not to include TypeScript in dependencies
|
|
10
|
+
*/
|
|
11
|
+
export const SYNTAX_KIND = {
|
|
12
|
+
CLASS_DECLARATION: 263,
|
|
13
|
+
CONSTRUCTOR: 176,
|
|
14
|
+
} as const;
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import tsParser from "@typescript-eslint/parser";
|
|
2
|
+
|
|
3
|
+
import { name, version } from "../package.json";
|
|
4
|
+
|
|
5
|
+
import { constructConstructorProperty } from "./rules/construct-constructor-property";
|
|
1
6
|
import { noClassInInterface } from "./rules/no-class-in-interface";
|
|
2
7
|
import { noConstructStackSuffix } from "./rules/no-construct-stack-suffix";
|
|
3
8
|
import { noImportPrivate } from "./rules/no-import-private";
|
|
@@ -18,50 +23,71 @@ const rules = {
|
|
|
18
23
|
"no-parent-name-construct-id-match": noParentNameConstructIdMatch,
|
|
19
24
|
"no-public-class-fields": noPublicClassFields,
|
|
20
25
|
"pascal-case-construct-id": pascalCaseConstructId,
|
|
21
|
-
"no-mutable-public-fields": noMutablePublicFields,
|
|
22
|
-
"no-mutable-props-interface": noMutablePropsInterface,
|
|
23
26
|
"require-passing-this": requirePassingThis,
|
|
24
27
|
"no-variable-construct-id": noVariableConstructId,
|
|
28
|
+
"no-mutable-public-fields": noMutablePublicFields,
|
|
29
|
+
"no-mutable-props-interface": noMutablePropsInterface,
|
|
30
|
+
"construct-constructor-property": constructConstructorProperty,
|
|
25
31
|
"require-jsdoc": requireJSDoc,
|
|
26
32
|
"require-props-default-doc": requirePropsDefaultDoc,
|
|
27
33
|
"props-name-convention": propsNameConvention,
|
|
28
34
|
"no-import-private": noImportPrivate,
|
|
29
35
|
};
|
|
30
36
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"cdk/props-name-convention": "warn",
|
|
37
|
+
const cdkPlugin = {
|
|
38
|
+
meta: { name, version },
|
|
39
|
+
rules,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createFlatConfig = (rules: Record<string, unknown>) => {
|
|
43
|
+
return {
|
|
44
|
+
languageOptions: {
|
|
45
|
+
parserOptions: {
|
|
46
|
+
projectService: true,
|
|
47
|
+
parser: tsParser,
|
|
48
|
+
sourceType: "module",
|
|
49
|
+
},
|
|
45
50
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
plugins: ["cdk"],
|
|
49
|
-
rules: {
|
|
50
|
-
"cdk/no-class-in-interface": "error",
|
|
51
|
-
"cdk/no-construct-stack-suffix": "error",
|
|
52
|
-
"cdk/no-parent-name-construct-id-match": "error",
|
|
53
|
-
"cdk/no-public-class-fields": "error",
|
|
54
|
-
"cdk/pascal-case-construct-id": "error",
|
|
55
|
-
"cdk/require-passing-this": "error",
|
|
56
|
-
"cdk/no-variable-construct-id": "error",
|
|
57
|
-
"cdk/no-mutable-public-fields": "error",
|
|
58
|
-
"cdk/no-mutable-props-interface": "error",
|
|
59
|
-
"cdk/no-import-private": "error",
|
|
60
|
-
"cdk/require-props-default-doc": "error",
|
|
61
|
-
"cdk/props-name-convention": "error",
|
|
62
|
-
"cdk/require-jsdoc": "error",
|
|
51
|
+
plugins: {
|
|
52
|
+
cdk: cdkPlugin,
|
|
63
53
|
},
|
|
64
|
-
|
|
54
|
+
rules,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const recommended = createFlatConfig({
|
|
59
|
+
"cdk/no-class-in-interface": "error",
|
|
60
|
+
"cdk/no-construct-stack-suffix": "error",
|
|
61
|
+
"cdk/no-parent-name-construct-id-match": "error",
|
|
62
|
+
"cdk/no-public-class-fields": "error",
|
|
63
|
+
"cdk/pascal-case-construct-id": "error",
|
|
64
|
+
"cdk/require-passing-this": ["error", { allowNonThisAndDisallowScope: true }],
|
|
65
|
+
"cdk/no-variable-construct-id": "error",
|
|
66
|
+
"cdk/no-mutable-public-fields": "warn",
|
|
67
|
+
"cdk/no-mutable-props-interface": "warn",
|
|
68
|
+
"cdk/construct-constructor-property": "error",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const strict = createFlatConfig({
|
|
72
|
+
"cdk/no-class-in-interface": "error",
|
|
73
|
+
"cdk/no-construct-stack-suffix": "error",
|
|
74
|
+
"cdk/no-parent-name-construct-id-match": "error",
|
|
75
|
+
"cdk/no-public-class-fields": "error",
|
|
76
|
+
"cdk/pascal-case-construct-id": "error",
|
|
77
|
+
"cdk/require-passing-this": "error",
|
|
78
|
+
"cdk/no-variable-construct-id": "error",
|
|
79
|
+
"cdk/no-mutable-public-fields": "error",
|
|
80
|
+
"cdk/no-mutable-props-interface": "error",
|
|
81
|
+
"cdk/construct-constructor-property": "error",
|
|
82
|
+
"cdk/require-jsdoc": "error",
|
|
83
|
+
"cdk/require-props-default-doc": "error",
|
|
84
|
+
"cdk/props-name-convention": "error",
|
|
85
|
+
"cdk/no-import-private": "error",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const configs = {
|
|
89
|
+
recommended,
|
|
90
|
+
strict,
|
|
65
91
|
};
|
|
66
92
|
|
|
67
93
|
export { configs, rules };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ESLintUtils,
|
|
4
|
+
TSESLint,
|
|
5
|
+
TSESTree,
|
|
6
|
+
} from "@typescript-eslint/utils";
|
|
7
|
+
|
|
8
|
+
import { isConstructType } from "../utils/typeCheck";
|
|
9
|
+
|
|
10
|
+
type Context = TSESLint.RuleContext<"invalidConstructorProperty", []>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enforces that constructors of classes extending Construct have the property names 'scope, id' or 'scope, id, props'
|
|
14
|
+
* @param context - The rule context provided by ESLint
|
|
15
|
+
* @returns An object containing the AST visitor functions
|
|
16
|
+
* @see {@link https://eslint-cdk-plugin.dev/rules/construct-constructor-property} - Documentation
|
|
17
|
+
*/
|
|
18
|
+
export const constructConstructorProperty = ESLintUtils.RuleCreator.withoutDocs(
|
|
19
|
+
{
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem",
|
|
22
|
+
docs: {
|
|
23
|
+
description:
|
|
24
|
+
"Enforces that constructors of classes extending Construct have the property names 'scope, id' or 'scope, id, props'",
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
invalidConstructorProperty:
|
|
28
|
+
"Constructor of a class extending Construct must have the property names 'scope, id' or 'scope, id, props'",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ClassDeclaration(node) {
|
|
38
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
39
|
+
if (!isConstructType(type)) return;
|
|
40
|
+
|
|
41
|
+
// NOTE: Find the constructor method
|
|
42
|
+
const constructor = node.body.body.find(
|
|
43
|
+
(member): member is TSESTree.MethodDefinition =>
|
|
44
|
+
member.type === AST_NODE_TYPES.MethodDefinition &&
|
|
45
|
+
member.kind === "constructor"
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// NOTE: Skip if there's no constructor
|
|
49
|
+
if (!constructor) return;
|
|
50
|
+
|
|
51
|
+
validateConstructorProperty(constructor, context);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates that the constructor has the property names "scope, id" or "scope, id, props"
|
|
60
|
+
*/
|
|
61
|
+
const validateConstructorProperty = (
|
|
62
|
+
constructor: TSESTree.MethodDefinition,
|
|
63
|
+
context: Context
|
|
64
|
+
): void => {
|
|
65
|
+
const params = constructor.value.params;
|
|
66
|
+
|
|
67
|
+
// NOTE: Check if the constructor has at least 2 parameters
|
|
68
|
+
if (params.length < 2) {
|
|
69
|
+
context.report({
|
|
70
|
+
node: constructor,
|
|
71
|
+
messageId: "invalidConstructorProperty",
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// NOTE: Check if the first parameter is named "scope"
|
|
77
|
+
const firstParam = params[0];
|
|
78
|
+
if (
|
|
79
|
+
firstParam.type === AST_NODE_TYPES.Identifier &&
|
|
80
|
+
firstParam.name !== "scope"
|
|
81
|
+
) {
|
|
82
|
+
context.report({
|
|
83
|
+
node: constructor,
|
|
84
|
+
messageId: "invalidConstructorProperty",
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// NOTE: Check if the second parameter is named "id"
|
|
90
|
+
const secondParam = params[1];
|
|
91
|
+
if (
|
|
92
|
+
secondParam.type === AST_NODE_TYPES.Identifier &&
|
|
93
|
+
secondParam.name !== "id"
|
|
94
|
+
) {
|
|
95
|
+
context.report({
|
|
96
|
+
node: constructor,
|
|
97
|
+
messageId: "invalidConstructorProperty",
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// NOTE: If there's no third parameter, return
|
|
103
|
+
if (params.length < 3) return;
|
|
104
|
+
|
|
105
|
+
// NOTE: Check if the third parameter is named "props"
|
|
106
|
+
const thirdParam = params[2];
|
|
107
|
+
if (
|
|
108
|
+
thirdParam.type === AST_NODE_TYPES.Identifier &&
|
|
109
|
+
thirdParam.name !== "props"
|
|
110
|
+
) {
|
|
111
|
+
context.report({
|
|
112
|
+
node: constructor,
|
|
113
|
+
messageId: "invalidConstructorProperty",
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { SYMBOL_FLAGS } from "../constants/tsInternalFlags";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Enforces the use of interface types instead of class in interface properties
|
|
@@ -37,8 +37,10 @@ export const noClassInInterface = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
37
37
|
const type = parserServices.getTypeAtLocation(property);
|
|
38
38
|
if (!type.symbol) continue;
|
|
39
39
|
|
|
40
|
-
// NOTE:
|
|
41
|
-
|
|
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;
|
|
42
44
|
if (!isClass) continue;
|
|
43
45
|
|
|
44
46
|
context.report({
|
|
@@ -9,7 +9,20 @@ import { toPascalCase } from "../utils/convertString";
|
|
|
9
9
|
import { getConstructorPropertyNames } from "../utils/parseType";
|
|
10
10
|
import { isConstructOrStackType } from "../utils/typeCheck";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const SUFFIX_TYPE = {
|
|
13
|
+
CONSTRUCT: "Construct",
|
|
14
|
+
STACK: "Stack",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
type SuffixType = (typeof SUFFIX_TYPE)[keyof typeof SUFFIX_TYPE];
|
|
18
|
+
|
|
19
|
+
type Options = [
|
|
20
|
+
{
|
|
21
|
+
disallowedSuffixes?: SuffixType[];
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
type Context = TSESLint.RuleContext<"noConstructStackSuffix", Options>;
|
|
13
26
|
|
|
14
27
|
/**
|
|
15
28
|
* Enforces that Construct IDs do not end with 'Construct' or 'Stack' suffix
|
|
@@ -28,11 +41,34 @@ export const noConstructStackSuffix = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
28
41
|
noConstructStackSuffix:
|
|
29
42
|
"{{ classType }} ID '{{ id }}' should not include {{ suffix }} suffix.",
|
|
30
43
|
},
|
|
31
|
-
schema: [
|
|
44
|
+
schema: [
|
|
45
|
+
{
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
disallowedSuffixes: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: {
|
|
51
|
+
type: "string",
|
|
52
|
+
enum: [SUFFIX_TYPE.CONSTRUCT, SUFFIX_TYPE.STACK],
|
|
53
|
+
},
|
|
54
|
+
uniqueItems: true,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
32
60
|
},
|
|
33
|
-
defaultOptions: [
|
|
61
|
+
defaultOptions: [
|
|
62
|
+
{
|
|
63
|
+
disallowedSuffixes: [SUFFIX_TYPE.CONSTRUCT, SUFFIX_TYPE.STACK],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
34
66
|
create(context) {
|
|
35
67
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
68
|
+
const options = context.options[0] ?? {
|
|
69
|
+
disallowedSuffixes: [SUFFIX_TYPE.CONSTRUCT, SUFFIX_TYPE.STACK],
|
|
70
|
+
};
|
|
71
|
+
|
|
36
72
|
return {
|
|
37
73
|
NewExpression(node) {
|
|
38
74
|
const type = parserServices.getTypeAtLocation(node);
|
|
@@ -43,7 +79,7 @@ export const noConstructStackSuffix = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
43
79
|
const constructorPropertyNames = getConstructorPropertyNames(type);
|
|
44
80
|
if (constructorPropertyNames[1] !== "id") return;
|
|
45
81
|
|
|
46
|
-
validateConstructId(node, context);
|
|
82
|
+
validateConstructId(node, context, options);
|
|
47
83
|
},
|
|
48
84
|
};
|
|
49
85
|
},
|
|
@@ -54,7 +90,8 @@ export const noConstructStackSuffix = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
54
90
|
*/
|
|
55
91
|
const validateConstructId = (
|
|
56
92
|
node: TSESTree.NewExpression,
|
|
57
|
-
context: Context
|
|
93
|
+
context: Context,
|
|
94
|
+
options: { disallowedSuffixes: SuffixType[] }
|
|
58
95
|
): void => {
|
|
59
96
|
// NOTE: Treat the second argument as ID
|
|
60
97
|
const secondArg = node.arguments[1];
|
|
@@ -66,25 +103,32 @@ const validateConstructId = (
|
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
const formattedConstructId = toPascalCase(secondArg.value);
|
|
106
|
+
const disallowedSuffixes = options.disallowedSuffixes;
|
|
69
107
|
|
|
70
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
disallowedSuffixes.includes(SUFFIX_TYPE.CONSTRUCT) &&
|
|
110
|
+
formattedConstructId.endsWith(SUFFIX_TYPE.CONSTRUCT)
|
|
111
|
+
) {
|
|
71
112
|
context.report({
|
|
72
113
|
node,
|
|
73
114
|
messageId: "noConstructStackSuffix",
|
|
74
115
|
data: {
|
|
75
116
|
classType: "Construct",
|
|
76
117
|
id: secondArg.value,
|
|
77
|
-
suffix:
|
|
118
|
+
suffix: SUFFIX_TYPE.CONSTRUCT,
|
|
78
119
|
},
|
|
79
120
|
});
|
|
80
|
-
} else if (
|
|
121
|
+
} else if (
|
|
122
|
+
disallowedSuffixes.includes(SUFFIX_TYPE.STACK) &&
|
|
123
|
+
formattedConstructId.endsWith(SUFFIX_TYPE.STACK)
|
|
124
|
+
) {
|
|
81
125
|
context.report({
|
|
82
126
|
node,
|
|
83
127
|
messageId: "noConstructStackSuffix",
|
|
84
128
|
data: {
|
|
85
129
|
classType: "Stack",
|
|
86
130
|
id: secondArg.value,
|
|
87
|
-
suffix:
|
|
131
|
+
suffix: SUFFIX_TYPE.STACK,
|
|
88
132
|
},
|
|
89
133
|
});
|
|
90
134
|
}
|
|
@@ -297,7 +297,7 @@ const validateConstructId = ({
|
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
const formattedConstructId = toPascalCase(secondArg.value
|
|
300
|
+
const formattedConstructId = toPascalCase(secondArg.value);
|
|
301
301
|
const formattedParentClassName = toPascalCase(parentClassName);
|
|
302
302
|
if (formattedParentClassName !== formattedConstructId) return;
|
|
303
303
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TSESTree,
|
|
7
7
|
} from "@typescript-eslint/utils";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { SYMBOL_FLAGS } from "../constants/tsInternalFlags";
|
|
10
10
|
import { isConstructOrStackType } from "../utils/typeCheck";
|
|
11
11
|
|
|
12
12
|
type Context = TSESLint.RuleContext<"noPublicClassFields", []>;
|
|
@@ -91,7 +91,10 @@ const validateClassMember = (
|
|
|
91
91
|
const type = parserServices.getTypeAtLocation(member);
|
|
92
92
|
if (!type.symbol) continue;
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
|
|
95
|
+
// Therefore, the type information structures do not match.
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
97
|
+
const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
|
|
95
98
|
if (!isClass) continue;
|
|
96
99
|
|
|
97
100
|
context.report({
|
|
@@ -133,7 +136,10 @@ const validateConstructorParameterProperty = (
|
|
|
133
136
|
const type = parserServices.getTypeAtLocation(param);
|
|
134
137
|
if (!type.symbol) continue;
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
// NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
|
|
140
|
+
// Therefore, the type information structures do not match.
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
142
|
+
const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
|
|
137
143
|
if (!isClass) continue;
|
|
138
144
|
|
|
139
145
|
context.report({
|
|
@@ -93,6 +93,25 @@ const isInsideLoop = (node: TSESTree.Node): boolean => {
|
|
|
93
93
|
) {
|
|
94
94
|
return true;
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// NOTE: Check for array methods like forEach, map, etc.
|
|
98
|
+
if (
|
|
99
|
+
current.type === AST_NODE_TYPES.CallExpression &&
|
|
100
|
+
current.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
101
|
+
current.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
102
|
+
[
|
|
103
|
+
"forEach",
|
|
104
|
+
"map",
|
|
105
|
+
"filter",
|
|
106
|
+
"reduce",
|
|
107
|
+
"flatMap",
|
|
108
|
+
"some",
|
|
109
|
+
"every",
|
|
110
|
+
].includes(current.callee.property.name)
|
|
111
|
+
) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
current = current.parent;
|
|
97
116
|
}
|
|
98
117
|
return false;
|
|
@@ -93,7 +93,7 @@ const validateConstructId = (
|
|
|
93
93
|
node,
|
|
94
94
|
messageId: "pascalCaseConstructId",
|
|
95
95
|
fix: (fixer) => {
|
|
96
|
-
const pascalCaseValue = toPascalCase(secondArg.value
|
|
96
|
+
const pascalCaseValue = toPascalCase(secondArg.value);
|
|
97
97
|
return fixer.replaceText(secondArg, `${quote}${pascalCaseValue}${quote}`);
|
|
98
98
|
},
|
|
99
99
|
});
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ESLintUtils,
|
|
4
|
+
TSESLint,
|
|
5
|
+
} from "@typescript-eslint/utils";
|
|
2
6
|
|
|
3
7
|
import { getConstructorPropertyNames } from "../utils/parseType";
|
|
4
8
|
import { isConstructType } from "../utils/typeCheck";
|
|
5
9
|
|
|
10
|
+
type Options = [
|
|
11
|
+
{
|
|
12
|
+
allowNonThisAndDisallowScope?: boolean;
|
|
13
|
+
}
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
type Context = TSESLint.RuleContext<"requirePassingThis", Options>;
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* Enforces that `this` is passed to the constructor
|
|
8
20
|
* @param context - The rule context provided by ESLint
|
|
@@ -18,11 +30,29 @@ export const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
18
30
|
messages: {
|
|
19
31
|
requirePassingThis: "Require passing `this` in a constructor.",
|
|
20
32
|
},
|
|
21
|
-
schema: [
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
allowNonThisAndDisallowScope: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
22
45
|
fixable: "code",
|
|
23
46
|
},
|
|
24
|
-
defaultOptions: [
|
|
25
|
-
|
|
47
|
+
defaultOptions: [
|
|
48
|
+
{
|
|
49
|
+
allowNonThisAndDisallowScope: false,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
create(context: Context) {
|
|
53
|
+
const options = context.options[0] || {
|
|
54
|
+
allowNonThisAndDisallowScope: false,
|
|
55
|
+
};
|
|
26
56
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
27
57
|
return {
|
|
28
58
|
NewExpression(node) {
|
|
@@ -31,18 +61,39 @@ export const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
31
61
|
if (!isConstructType(type) || !node.arguments.length) return;
|
|
32
62
|
|
|
33
63
|
const argument = node.arguments[0];
|
|
64
|
+
|
|
65
|
+
// NOTE: If the first argument is already `this`, it's valid
|
|
34
66
|
if (argument.type === AST_NODE_TYPES.ThisExpression) return;
|
|
35
67
|
|
|
68
|
+
// NOTE: If the first argument is not `scope`, it's valid
|
|
36
69
|
const constructorPropertyNames = getConstructorPropertyNames(type);
|
|
37
70
|
if (constructorPropertyNames[0] !== "scope") return;
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
// NOTE: If `allowNonThisAndDisallowScope` is false, require `this` for all cases
|
|
73
|
+
if (!options.allowNonThisAndDisallowScope) {
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: "requirePassingThis",
|
|
77
|
+
fix: (fixer) => {
|
|
78
|
+
return fixer.replaceText(argument, "this");
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// NOTE: If `allowNonThisAndDisallowScope` is true, allow non-`this` values except `scope` variable
|
|
84
|
+
// Check if the argument is the `scope` variable
|
|
85
|
+
if (
|
|
86
|
+
argument.type === AST_NODE_TYPES.Identifier &&
|
|
87
|
+
argument.name === "scope"
|
|
88
|
+
) {
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: "requirePassingThis",
|
|
92
|
+
fix: (fixer) => {
|
|
93
|
+
return fixer.replaceText(argument, "this");
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
46
97
|
},
|
|
47
98
|
};
|
|
48
99
|
},
|
package/src/utils/parseType.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ClassDeclaration,
|
|
3
|
+
ConstructorDeclaration,
|
|
4
|
+
Declaration,
|
|
5
|
+
Node,
|
|
6
|
+
Type,
|
|
7
|
+
} from "typescript";
|
|
8
|
+
|
|
9
|
+
import { SYNTAX_KIND } from "../constants/tsInternalFlags";
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
12
|
* Parses type to get the property names of the class constructor.
|
|
@@ -18,3 +26,27 @@ export const getConstructorPropertyNames = (type: Type): string[] => {
|
|
|
18
26
|
|
|
19
27
|
return constructor.parameters.map((param) => param.name.getText());
|
|
20
28
|
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Implementing `isClassDeclaration` defined in typescript on your own, in order not to include TypeScript in dependencies
|
|
32
|
+
*/
|
|
33
|
+
const isClassDeclaration = (
|
|
34
|
+
declaration: Declaration
|
|
35
|
+
): declaration is ClassDeclaration => {
|
|
36
|
+
// NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
|
|
37
|
+
// Therefore, the type information structures do not match.
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
39
|
+
return declaration.kind === SYNTAX_KIND.CLASS_DECLARATION;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Implementing `isConstructorDeclaration` defined in typescript on your own, in order not to include TypeScript in dependencies
|
|
44
|
+
*/
|
|
45
|
+
const isConstructorDeclaration = (
|
|
46
|
+
node: Node
|
|
47
|
+
): node is ConstructorDeclaration => {
|
|
48
|
+
// NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
|
|
49
|
+
// Therefore, the type information structures do not match.
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
51
|
+
return node.kind === SYNTAX_KIND.CONSTRUCTOR;
|
|
52
|
+
};
|
package/src/utils/typeCheck.ts
CHANGED