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
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
|
|
10
|
+
type Context = TSESLint.RuleContext<"noParentNameConstructIdMatch", []>;
|
|
11
|
+
|
|
12
|
+
type ValidateStatementArgs<T extends TSESTree.Statement> = {
|
|
13
|
+
node: TSESTree.ClassBody;
|
|
14
|
+
statement: T;
|
|
15
|
+
parentClassName: string;
|
|
16
|
+
context: Context;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ValidateExpressionArgs<T extends TSESTree.Expression> = {
|
|
20
|
+
node: TSESTree.ClassBody;
|
|
21
|
+
expression: T;
|
|
22
|
+
parentClassName: string;
|
|
23
|
+
context: Context;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enforce that construct IDs does not match the parent construct name.
|
|
28
|
+
* @param context - The rule context provided by ESLint
|
|
29
|
+
* @returns An object containing the AST visitor functions
|
|
30
|
+
* @see {@link https://eslint-cdk-plugin.dev/rules/no-parent-name-construct-id-match} - Documentation
|
|
31
|
+
*/
|
|
32
|
+
export const noParentNameConstructIdMatch = ESLintUtils.RuleCreator.withoutDocs(
|
|
33
|
+
{
|
|
34
|
+
meta: {
|
|
35
|
+
type: "problem",
|
|
36
|
+
docs: {
|
|
37
|
+
description:
|
|
38
|
+
"Enforce that construct IDs does not match the parent construct name.",
|
|
39
|
+
},
|
|
40
|
+
messages: {
|
|
41
|
+
noParentNameConstructIdMatch:
|
|
42
|
+
"Construct ID '{{ constructId }}' should not match parent construct name '{{ parentConstructName }}'. Use a more specific identifier.",
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
},
|
|
46
|
+
defaultOptions: [],
|
|
47
|
+
create(context) {
|
|
48
|
+
return {
|
|
49
|
+
ClassBody(node) {
|
|
50
|
+
const parent = node.parent;
|
|
51
|
+
if (parent?.type !== AST_NODE_TYPES.ClassDeclaration) return;
|
|
52
|
+
|
|
53
|
+
const parentClassName = parent.id?.name;
|
|
54
|
+
if (!parentClassName) return;
|
|
55
|
+
|
|
56
|
+
for (const body of node.body) {
|
|
57
|
+
// NOTE: Ignore if neither method nor constructor.
|
|
58
|
+
if (
|
|
59
|
+
body.type !== AST_NODE_TYPES.MethodDefinition ||
|
|
60
|
+
!["method", "constructor"].includes(body.kind) ||
|
|
61
|
+
body.value.type !== AST_NODE_TYPES.FunctionExpression
|
|
62
|
+
) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
validateConstructorBody({
|
|
66
|
+
node,
|
|
67
|
+
expression: body.value,
|
|
68
|
+
parentClassName,
|
|
69
|
+
context,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate the constructor body for the parent class
|
|
80
|
+
* - validate each statement in the constructor body
|
|
81
|
+
*/
|
|
82
|
+
const validateConstructorBody = ({
|
|
83
|
+
node,
|
|
84
|
+
expression,
|
|
85
|
+
parentClassName,
|
|
86
|
+
context,
|
|
87
|
+
}: ValidateExpressionArgs<TSESTree.FunctionExpression>): void => {
|
|
88
|
+
for (const statement of expression.body.body) {
|
|
89
|
+
switch (statement.type) {
|
|
90
|
+
case AST_NODE_TYPES.VariableDeclaration: {
|
|
91
|
+
const newExpression = statement.declarations[0].init;
|
|
92
|
+
if (newExpression?.type !== AST_NODE_TYPES.NewExpression) continue;
|
|
93
|
+
validateConstructId({
|
|
94
|
+
node,
|
|
95
|
+
context,
|
|
96
|
+
expression: newExpression,
|
|
97
|
+
parentClassName,
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case AST_NODE_TYPES.ExpressionStatement: {
|
|
102
|
+
if (statement.expression?.type !== AST_NODE_TYPES.NewExpression) break;
|
|
103
|
+
validateStatement({
|
|
104
|
+
node,
|
|
105
|
+
statement,
|
|
106
|
+
parentClassName,
|
|
107
|
+
context,
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case AST_NODE_TYPES.IfStatement: {
|
|
112
|
+
traverseStatements({
|
|
113
|
+
node,
|
|
114
|
+
context,
|
|
115
|
+
parentClassName,
|
|
116
|
+
statement: statement.consequent,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case AST_NODE_TYPES.SwitchStatement: {
|
|
121
|
+
for (const switchCase of statement.cases) {
|
|
122
|
+
for (const statement of switchCase.consequent) {
|
|
123
|
+
traverseStatements({
|
|
124
|
+
node,
|
|
125
|
+
context,
|
|
126
|
+
parentClassName,
|
|
127
|
+
statement,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Recursively traverse and validate statements in the AST
|
|
139
|
+
* - Handles BlockStatement, ExpressionStatement, and VariableDeclaration
|
|
140
|
+
* - Validates construct IDs against parent class name
|
|
141
|
+
*/
|
|
142
|
+
const traverseStatements = ({
|
|
143
|
+
node,
|
|
144
|
+
statement,
|
|
145
|
+
parentClassName,
|
|
146
|
+
context,
|
|
147
|
+
}: ValidateStatementArgs<TSESTree.Statement>) => {
|
|
148
|
+
switch (statement.type) {
|
|
149
|
+
case AST_NODE_TYPES.BlockStatement: {
|
|
150
|
+
for (const body of statement.body) {
|
|
151
|
+
validateStatement({
|
|
152
|
+
node,
|
|
153
|
+
statement: body,
|
|
154
|
+
parentClassName,
|
|
155
|
+
context,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case AST_NODE_TYPES.ExpressionStatement: {
|
|
161
|
+
const newExpression = statement.expression;
|
|
162
|
+
if (newExpression?.type !== AST_NODE_TYPES.NewExpression) break;
|
|
163
|
+
validateStatement({
|
|
164
|
+
node,
|
|
165
|
+
statement,
|
|
166
|
+
parentClassName,
|
|
167
|
+
context,
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case AST_NODE_TYPES.VariableDeclaration: {
|
|
172
|
+
const newExpression = statement.declarations[0].init;
|
|
173
|
+
if (newExpression?.type !== AST_NODE_TYPES.NewExpression) break;
|
|
174
|
+
validateConstructId({
|
|
175
|
+
node,
|
|
176
|
+
context,
|
|
177
|
+
expression: newExpression,
|
|
178
|
+
parentClassName,
|
|
179
|
+
});
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Validate a single statement in the AST
|
|
187
|
+
* - Handles different types of statements (Variable, Expression, If, Switch)
|
|
188
|
+
* - Extracts and validates construct IDs from new expressions
|
|
189
|
+
*/
|
|
190
|
+
const validateStatement = ({
|
|
191
|
+
node,
|
|
192
|
+
statement,
|
|
193
|
+
parentClassName,
|
|
194
|
+
context,
|
|
195
|
+
}: ValidateStatementArgs<TSESTree.Statement>): void => {
|
|
196
|
+
switch (statement.type) {
|
|
197
|
+
case AST_NODE_TYPES.VariableDeclaration: {
|
|
198
|
+
const newExpression = statement.declarations[0].init;
|
|
199
|
+
if (newExpression?.type !== AST_NODE_TYPES.NewExpression) break;
|
|
200
|
+
validateConstructId({
|
|
201
|
+
node,
|
|
202
|
+
context,
|
|
203
|
+
expression: newExpression,
|
|
204
|
+
parentClassName,
|
|
205
|
+
});
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case AST_NODE_TYPES.ExpressionStatement: {
|
|
209
|
+
const newExpression = statement.expression;
|
|
210
|
+
if (newExpression?.type !== AST_NODE_TYPES.NewExpression) break;
|
|
211
|
+
validateConstructId({
|
|
212
|
+
node,
|
|
213
|
+
context,
|
|
214
|
+
expression: newExpression,
|
|
215
|
+
parentClassName,
|
|
216
|
+
});
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case AST_NODE_TYPES.IfStatement: {
|
|
220
|
+
validateIfStatement({
|
|
221
|
+
node,
|
|
222
|
+
statement,
|
|
223
|
+
parentClassName,
|
|
224
|
+
context,
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case AST_NODE_TYPES.SwitchStatement: {
|
|
229
|
+
validateSwitchStatement({
|
|
230
|
+
node,
|
|
231
|
+
statement,
|
|
232
|
+
parentClassName,
|
|
233
|
+
context,
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate the `if` statement
|
|
242
|
+
* - Validate recursively if `if` statements are nested
|
|
243
|
+
*/
|
|
244
|
+
const validateIfStatement = ({
|
|
245
|
+
node,
|
|
246
|
+
statement,
|
|
247
|
+
parentClassName,
|
|
248
|
+
context,
|
|
249
|
+
}: ValidateStatementArgs<TSESTree.IfStatement>): void => {
|
|
250
|
+
traverseStatements({
|
|
251
|
+
node,
|
|
252
|
+
context,
|
|
253
|
+
parentClassName,
|
|
254
|
+
statement: statement.consequent,
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validate the `switch` statement
|
|
260
|
+
* - Validate recursively if `switch` statements are nested
|
|
261
|
+
*/
|
|
262
|
+
const validateSwitchStatement = ({
|
|
263
|
+
node,
|
|
264
|
+
statement,
|
|
265
|
+
parentClassName,
|
|
266
|
+
context,
|
|
267
|
+
}: ValidateStatementArgs<TSESTree.SwitchStatement>): void => {
|
|
268
|
+
for (const caseStatement of statement.cases) {
|
|
269
|
+
for (const _consequent of caseStatement.consequent) {
|
|
270
|
+
traverseStatements({
|
|
271
|
+
node,
|
|
272
|
+
context,
|
|
273
|
+
parentClassName,
|
|
274
|
+
statement: _consequent,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate that parent construct name and child id do not match
|
|
282
|
+
*/
|
|
283
|
+
const validateConstructId = ({
|
|
284
|
+
node,
|
|
285
|
+
context,
|
|
286
|
+
expression,
|
|
287
|
+
parentClassName,
|
|
288
|
+
}: ValidateExpressionArgs<TSESTree.NewExpression>): void => {
|
|
289
|
+
if (expression.arguments.length < 2) return;
|
|
290
|
+
|
|
291
|
+
// NOTE: Treat the second argument as ID
|
|
292
|
+
const secondArg = expression.arguments[1];
|
|
293
|
+
if (
|
|
294
|
+
secondArg.type !== AST_NODE_TYPES.Literal ||
|
|
295
|
+
typeof secondArg.value !== "string"
|
|
296
|
+
) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const formattedConstructId = toPascalCase(secondArg.value as string);
|
|
301
|
+
const formattedParentClassName = toPascalCase(parentClassName);
|
|
302
|
+
if (formattedParentClassName !== formattedConstructId) return;
|
|
303
|
+
|
|
304
|
+
context.report({
|
|
305
|
+
node,
|
|
306
|
+
messageId: "noParentNameConstructIdMatch",
|
|
307
|
+
data: {
|
|
308
|
+
constructId: secondArg.value,
|
|
309
|
+
parentConstructName: parentClassName,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ESLintUtils,
|
|
4
|
+
ParserServicesWithTypeInformation,
|
|
5
|
+
TSESLint,
|
|
6
|
+
TSESTree,
|
|
7
|
+
} from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { SymbolFlags } from "../types/symbolFlags";
|
|
10
|
+
import { isConstructOrStackType } from "../utils/typeCheck";
|
|
11
|
+
|
|
12
|
+
type Context = TSESLint.RuleContext<"noPublicClassFields", []>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Disallow class types in public class fields
|
|
16
|
+
* @param context - The rule context provided by ESLint
|
|
17
|
+
* @returns An object containing the AST visitor functions
|
|
18
|
+
* @see {@link https://eslint-cdk-plugin.dev/rules/no-public-class-fields} - Documentation
|
|
19
|
+
*/
|
|
20
|
+
export const noPublicClassFields = ESLintUtils.RuleCreator.withoutDocs({
|
|
21
|
+
meta: {
|
|
22
|
+
type: "problem",
|
|
23
|
+
docs: {
|
|
24
|
+
description: "Disallow class types in public class fields",
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
noPublicClassFields:
|
|
28
|
+
"Public field '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead.",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
35
|
+
return {
|
|
36
|
+
ClassDeclaration(node) {
|
|
37
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
38
|
+
if (!isConstructOrStackType(type)) return;
|
|
39
|
+
|
|
40
|
+
// NOTE: Check class members
|
|
41
|
+
validateClassMember(node, context, parserServices);
|
|
42
|
+
|
|
43
|
+
// NOTE: Check constructor parameter properties
|
|
44
|
+
const constructor = node.body.body.find(
|
|
45
|
+
(member): member is TSESTree.MethodDefinition =>
|
|
46
|
+
member.type === AST_NODE_TYPES.MethodDefinition &&
|
|
47
|
+
member.kind === "constructor"
|
|
48
|
+
);
|
|
49
|
+
if (
|
|
50
|
+
!constructor ||
|
|
51
|
+
constructor.value.type !== AST_NODE_TYPES.FunctionExpression
|
|
52
|
+
) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validateConstructorParameterProperty(
|
|
57
|
+
constructor,
|
|
58
|
+
context,
|
|
59
|
+
parserServices
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* check the public variable of the class
|
|
68
|
+
* - if it is a class type, report an error
|
|
69
|
+
*/
|
|
70
|
+
const validateClassMember = (
|
|
71
|
+
node: TSESTree.ClassDeclaration,
|
|
72
|
+
context: Context,
|
|
73
|
+
parserServices: ParserServicesWithTypeInformation
|
|
74
|
+
) => {
|
|
75
|
+
for (const member of node.body.body) {
|
|
76
|
+
if (
|
|
77
|
+
member.type !== AST_NODE_TYPES.PropertyDefinition ||
|
|
78
|
+
member.key.type !== AST_NODE_TYPES.Identifier
|
|
79
|
+
) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// NOTE: Skip private and protected fields
|
|
84
|
+
if (["private", "protected"].includes(member.accessibility ?? "")) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// NOTE: Skip fields without type annotation
|
|
89
|
+
if (!member.typeAnnotation) continue;
|
|
90
|
+
|
|
91
|
+
const type = parserServices.getTypeAtLocation(member);
|
|
92
|
+
if (!type.symbol) continue;
|
|
93
|
+
|
|
94
|
+
const isClass = type.symbol.flags === SymbolFlags.Class;
|
|
95
|
+
if (!isClass) continue;
|
|
96
|
+
|
|
97
|
+
context.report({
|
|
98
|
+
node: member,
|
|
99
|
+
messageId: "noPublicClassFields",
|
|
100
|
+
data: {
|
|
101
|
+
propertyName: member.key.name,
|
|
102
|
+
typeName: type.symbol.name,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* check the constructor parameter property
|
|
110
|
+
* - if it is a class type, report an error
|
|
111
|
+
*/
|
|
112
|
+
const validateConstructorParameterProperty = (
|
|
113
|
+
constructor: TSESTree.MethodDefinition,
|
|
114
|
+
context: Context,
|
|
115
|
+
parserServices: ParserServicesWithTypeInformation
|
|
116
|
+
) => {
|
|
117
|
+
for (const param of constructor.value.params) {
|
|
118
|
+
if (
|
|
119
|
+
param.type !== AST_NODE_TYPES.TSParameterProperty ||
|
|
120
|
+
param.parameter.type !== AST_NODE_TYPES.Identifier
|
|
121
|
+
) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// NOTE: Skip private and protected parameters
|
|
126
|
+
if (["private", "protected"].includes(param.accessibility ?? "")) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// NOTE: Skip parameters without type annotation
|
|
131
|
+
if (!param.parameter.typeAnnotation) continue;
|
|
132
|
+
|
|
133
|
+
const type = parserServices.getTypeAtLocation(param);
|
|
134
|
+
if (!type.symbol) continue;
|
|
135
|
+
|
|
136
|
+
const isClass = type.symbol.flags === SymbolFlags.Class;
|
|
137
|
+
if (!isClass) continue;
|
|
138
|
+
|
|
139
|
+
context.report({
|
|
140
|
+
node: param,
|
|
141
|
+
messageId: "noPublicClassFields",
|
|
142
|
+
data: {
|
|
143
|
+
propertyName: param.parameter.name,
|
|
144
|
+
typeName: type.symbol.name,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ESLintUtils,
|
|
4
|
+
TSESLint,
|
|
5
|
+
TSESTree,
|
|
6
|
+
} from "@typescript-eslint/utils";
|
|
7
|
+
|
|
8
|
+
import { isConstructType, isStackType } from "../utils/typeCheck";
|
|
9
|
+
|
|
10
|
+
type Context = TSESLint.RuleContext<"noVariableConstructId", []>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enforce using literal strings for Construct ID.
|
|
14
|
+
* @param context - The rule context provided by ESLint
|
|
15
|
+
* @returns An object containing the AST visitor functions
|
|
16
|
+
*/
|
|
17
|
+
export const noVariableConstructId = ESLintUtils.RuleCreator.withoutDocs({
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: `Enforce using literal strings for Construct ID.`,
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
noVariableConstructId: "Shouldn't use a parameter as a Construct ID.",
|
|
25
|
+
},
|
|
26
|
+
schema: [],
|
|
27
|
+
},
|
|
28
|
+
defaultOptions: [],
|
|
29
|
+
create(context) {
|
|
30
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
31
|
+
return {
|
|
32
|
+
NewExpression(node) {
|
|
33
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
!isConstructType(type) ||
|
|
37
|
+
isStackType(type) ||
|
|
38
|
+
node.arguments.length < 2
|
|
39
|
+
) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
validateConstructId(node, context);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if the construct ID is a literal string
|
|
51
|
+
*/
|
|
52
|
+
const validateConstructId = (
|
|
53
|
+
node: TSESTree.NewExpression,
|
|
54
|
+
context: Context
|
|
55
|
+
) => {
|
|
56
|
+
if (node.arguments.length < 2 || isInsideLoop(node)) return;
|
|
57
|
+
|
|
58
|
+
// NOTE: Treat the second argument as ID
|
|
59
|
+
const secondArg = node.arguments[1];
|
|
60
|
+
|
|
61
|
+
// NOTE: When id is string literal, it's OK
|
|
62
|
+
if (
|
|
63
|
+
secondArg.type === AST_NODE_TYPES.Literal &&
|
|
64
|
+
typeof secondArg.value === "string"
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// NOTE: When id is template literal, only those without expressions are OK
|
|
70
|
+
if (
|
|
71
|
+
secondArg.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
72
|
+
!secondArg.expressions.length
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: "noVariableConstructId",
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if the node is inside a loop statement
|
|
85
|
+
*/
|
|
86
|
+
const isInsideLoop = (node: TSESTree.Node): boolean => {
|
|
87
|
+
let current = node.parent;
|
|
88
|
+
while (current) {
|
|
89
|
+
if (
|
|
90
|
+
current.type === AST_NODE_TYPES.ForStatement ||
|
|
91
|
+
current.type === AST_NODE_TYPES.ForInStatement ||
|
|
92
|
+
current.type === AST_NODE_TYPES.ForOfStatement ||
|
|
93
|
+
current.type === AST_NODE_TYPES.WhileStatement ||
|
|
94
|
+
current.type === AST_NODE_TYPES.DoWhileStatement
|
|
95
|
+
) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
current = current.parent;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
const QUOTE_TYPE = {
|
|
12
|
+
SINGLE: "'",
|
|
13
|
+
DOUBLE: '"',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
type QuoteType = (typeof QUOTE_TYPE)[keyof typeof QUOTE_TYPE];
|
|
17
|
+
|
|
18
|
+
type Context = TSESLint.RuleContext<"pascalCaseConstructId", []>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
/**
|
|
22
|
+
* Enforce PascalCase for Construct ID.
|
|
23
|
+
* @param context - The rule context provided by ESLint
|
|
24
|
+
* @returns An object containing the AST visitor functions
|
|
25
|
+
* @see {@link https://eslint-cdk-plugin.dev/rules/pascal-case-construct-id} - Documentation
|
|
26
|
+
*/
|
|
27
|
+
export const pascalCaseConstructId = ESLintUtils.RuleCreator.withoutDocs({
|
|
28
|
+
meta: {
|
|
29
|
+
type: "problem",
|
|
30
|
+
docs: {
|
|
31
|
+
description: "Enforce PascalCase for Construct ID.",
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
pascalCaseConstructId: "Construct ID must be PascalCase.",
|
|
35
|
+
},
|
|
36
|
+
schema: [],
|
|
37
|
+
fixable: "code",
|
|
38
|
+
},
|
|
39
|
+
defaultOptions: [],
|
|
40
|
+
create(context) {
|
|
41
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
42
|
+
return {
|
|
43
|
+
NewExpression(node) {
|
|
44
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
45
|
+
if (!isConstructOrStackType(type) || node.arguments.length < 2) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
validateConstructId(node, context);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* check if the string is PascalCase
|
|
56
|
+
* @param str - The string to check
|
|
57
|
+
* @returns true if the string is PascalCase, false otherwise
|
|
58
|
+
*/
|
|
59
|
+
const isPascalCase = (str: string) => {
|
|
60
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(str);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check the construct ID is PascalCase
|
|
65
|
+
*/
|
|
66
|
+
const validateConstructId = (
|
|
67
|
+
node: TSESTree.NewExpression,
|
|
68
|
+
context: Context
|
|
69
|
+
) => {
|
|
70
|
+
if (node.arguments.length < 2) return;
|
|
71
|
+
|
|
72
|
+
// NOTE: Treat the second argument as ID
|
|
73
|
+
const secondArg = node.arguments[1];
|
|
74
|
+
if (
|
|
75
|
+
secondArg.type !== AST_NODE_TYPES.Literal ||
|
|
76
|
+
typeof secondArg.value !== "string"
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const quote: QuoteType = secondArg.raw?.startsWith('"')
|
|
82
|
+
? QUOTE_TYPE.DOUBLE
|
|
83
|
+
: QUOTE_TYPE.SINGLE;
|
|
84
|
+
|
|
85
|
+
if (isPascalCase(secondArg.value)) return;
|
|
86
|
+
|
|
87
|
+
context.report({
|
|
88
|
+
node,
|
|
89
|
+
messageId: "pascalCaseConstructId",
|
|
90
|
+
fix: (fixer) => {
|
|
91
|
+
const pascalCaseValue = toPascalCase(secondArg.value as string);
|
|
92
|
+
return fixer.replaceText(secondArg, `${quote}${pascalCaseValue}${quote}`);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { isConstructType, isStackType } from "../utils/typeCheck";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enforces that `this` is passed to the constructor
|
|
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/require-passing-this} - Documentation
|
|
10
|
+
*/
|
|
11
|
+
export const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Require passing `this` in a constructor.",
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
requirePassingThis: "Require passing `this` in a constructor.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
fixable: "code",
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
26
|
+
return {
|
|
27
|
+
NewExpression(node) {
|
|
28
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
!isConstructType(type) ||
|
|
32
|
+
isStackType(type) ||
|
|
33
|
+
!node.arguments.length
|
|
34
|
+
) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const argument = node.arguments[0];
|
|
39
|
+
if (argument.type === AST_NODE_TYPES.ThisExpression) return;
|
|
40
|
+
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: "requirePassingThis",
|
|
44
|
+
fix: (fixer) => {
|
|
45
|
+
return fixer.replaceText(argument, "this");
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|