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/dist/index.cjs
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var utils = require('@typescript-eslint/utils');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
|
|
8
|
+
function _interopNamespaceDefault(e) {
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
26
|
+
|
|
27
|
+
var SymbolFlags = /* @__PURE__ */ ((SymbolFlags2) => {
|
|
28
|
+
SymbolFlags2[SymbolFlags2["Class"] = 32] = "Class";
|
|
29
|
+
return SymbolFlags2;
|
|
30
|
+
})(SymbolFlags || {});
|
|
31
|
+
|
|
32
|
+
const noClassInInterface = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
33
|
+
meta: {
|
|
34
|
+
type: "problem",
|
|
35
|
+
docs: {
|
|
36
|
+
description: "Disallow class types in interface properties"
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
noClassInInterfaceProps: "Interface property '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead."
|
|
40
|
+
},
|
|
41
|
+
schema: []
|
|
42
|
+
},
|
|
43
|
+
defaultOptions: [],
|
|
44
|
+
create(context) {
|
|
45
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
46
|
+
return {
|
|
47
|
+
TSInterfaceDeclaration(node) {
|
|
48
|
+
for (const property of node.body.body) {
|
|
49
|
+
if (property.type !== utils.AST_NODE_TYPES.TSPropertySignature || property.key.type !== utils.AST_NODE_TYPES.Identifier) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const type = parserServices.getTypeAtLocation(property);
|
|
53
|
+
if (!type.symbol) continue;
|
|
54
|
+
const isClass = type.symbol.flags === SymbolFlags.Class;
|
|
55
|
+
if (!isClass) continue;
|
|
56
|
+
context.report({
|
|
57
|
+
node: property,
|
|
58
|
+
messageId: "noClassInInterfaceProps",
|
|
59
|
+
data: {
|
|
60
|
+
propertyName: property.key.name,
|
|
61
|
+
typeName: type.symbol.name
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const toPascalCase = (str) => {
|
|
71
|
+
return str.split(/[-_\s]/).map((word) => {
|
|
72
|
+
return word.replace(/([A-Z])/g, " $1").split(/\s+/).map(
|
|
73
|
+
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
74
|
+
).join("");
|
|
75
|
+
}).join("");
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isConstructOrStackType = (type) => {
|
|
79
|
+
return isTargetSuperClassType(
|
|
80
|
+
type,
|
|
81
|
+
["Construct", "Stack"],
|
|
82
|
+
isConstructOrStackType
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
const isConstructType = (type) => {
|
|
86
|
+
return isTargetSuperClassType(type, ["Construct"], isConstructType);
|
|
87
|
+
};
|
|
88
|
+
const isStackType = (type) => {
|
|
89
|
+
return isTargetSuperClassType(type, ["Stack"], isStackType);
|
|
90
|
+
};
|
|
91
|
+
const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
|
|
92
|
+
if (!type.symbol) return false;
|
|
93
|
+
if (targetSuperClasses.some((suffix) => type.symbol.name.endsWith(suffix))) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const baseTypes = type.getBaseTypes() || [];
|
|
97
|
+
return baseTypes.some((baseType) => typeCheckFunction(baseType));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const noConstructStackSuffix = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
101
|
+
meta: {
|
|
102
|
+
type: "problem",
|
|
103
|
+
docs: {
|
|
104
|
+
description: "Effort to avoid using 'Construct' and 'Stack' suffix in construct id."
|
|
105
|
+
},
|
|
106
|
+
messages: {
|
|
107
|
+
noConstructStackSuffix: "{{ classType }} ID '{{ id }}' should not include {{ suffix }} suffix."
|
|
108
|
+
},
|
|
109
|
+
schema: []
|
|
110
|
+
},
|
|
111
|
+
defaultOptions: [],
|
|
112
|
+
create(context) {
|
|
113
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
114
|
+
return {
|
|
115
|
+
NewExpression(node) {
|
|
116
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
117
|
+
if (!isConstructOrStackType(type) || node.arguments.length < 2) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
validateConstructId$3(node, context);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const validateConstructId$3 = (node, context) => {
|
|
126
|
+
const secondArg = node.arguments[1];
|
|
127
|
+
if (secondArg.type !== utils.AST_NODE_TYPES.Literal || typeof secondArg.value !== "string") {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const formattedConstructId = toPascalCase(secondArg.value);
|
|
131
|
+
if (formattedConstructId.endsWith("Construct")) {
|
|
132
|
+
context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: "noConstructStackSuffix",
|
|
135
|
+
data: {
|
|
136
|
+
classType: "Construct",
|
|
137
|
+
id: secondArg.value,
|
|
138
|
+
suffix: "Construct"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
} else if (formattedConstructId.endsWith("Stack")) {
|
|
142
|
+
context.report({
|
|
143
|
+
node,
|
|
144
|
+
messageId: "noConstructStackSuffix",
|
|
145
|
+
data: {
|
|
146
|
+
classType: "Stack",
|
|
147
|
+
id: secondArg.value,
|
|
148
|
+
suffix: "Stack"
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const noImportPrivate = {
|
|
155
|
+
meta: {
|
|
156
|
+
type: "problem",
|
|
157
|
+
docs: {
|
|
158
|
+
description: "Cannot import modules from private dir at different levels of the hierarchy."
|
|
159
|
+
},
|
|
160
|
+
messages: {
|
|
161
|
+
noImportPrivate: "Cannot import modules from private dir at different levels of the hierarchy."
|
|
162
|
+
},
|
|
163
|
+
schema: []
|
|
164
|
+
},
|
|
165
|
+
create(context) {
|
|
166
|
+
return {
|
|
167
|
+
ImportDeclaration(node) {
|
|
168
|
+
const importPath = node.source.value?.toString() ?? "";
|
|
169
|
+
const currentFilePath = context.filename;
|
|
170
|
+
const currentDirPath = path__namespace.dirname(currentFilePath);
|
|
171
|
+
if (!importPath.includes("/private")) return;
|
|
172
|
+
const absoluteCurrentDirPath = path__namespace.resolve(currentDirPath);
|
|
173
|
+
const absoluteImportPath = path__namespace.resolve(currentDirPath, importPath);
|
|
174
|
+
const importDirBeforePrivate = absoluteImportPath.split("/private")[0];
|
|
175
|
+
const currentDirSegments = getDirSegments(absoluteCurrentDirPath);
|
|
176
|
+
const importDirSegments = getDirSegments(importDirBeforePrivate);
|
|
177
|
+
if (currentDirSegments.length !== importDirSegments.length || currentDirSegments.some(
|
|
178
|
+
(segment, index) => segment !== importDirSegments[index]
|
|
179
|
+
)) {
|
|
180
|
+
context.report({ node, messageId: "noImportPrivate" });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const getDirSegments = (dirPath) => {
|
|
187
|
+
return dirPath.split(path__namespace.sep).filter((segment) => segment !== "");
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const noMutablePropsInterface = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
191
|
+
meta: {
|
|
192
|
+
type: "problem",
|
|
193
|
+
docs: {
|
|
194
|
+
description: "Disallow mutable properties in Props interfaces"
|
|
195
|
+
},
|
|
196
|
+
fixable: "code",
|
|
197
|
+
messages: {
|
|
198
|
+
noMutablePropsInterface: "Property '{{ propertyName }}' in Props interface should be readonly."
|
|
199
|
+
},
|
|
200
|
+
schema: []
|
|
201
|
+
},
|
|
202
|
+
defaultOptions: [],
|
|
203
|
+
create(context) {
|
|
204
|
+
return {
|
|
205
|
+
TSInterfaceDeclaration(node) {
|
|
206
|
+
const sourceCode = context.sourceCode;
|
|
207
|
+
if (!node.id.name.endsWith("Props")) return;
|
|
208
|
+
for (const property of node.body.body) {
|
|
209
|
+
if (property.type !== utils.AST_NODE_TYPES.TSPropertySignature || property.key.type !== utils.AST_NODE_TYPES.Identifier) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (property.readonly) continue;
|
|
213
|
+
context.report({
|
|
214
|
+
node: property,
|
|
215
|
+
messageId: "noMutablePropsInterface",
|
|
216
|
+
data: {
|
|
217
|
+
propertyName: property.key.name
|
|
218
|
+
},
|
|
219
|
+
fix: (fixer) => {
|
|
220
|
+
const propertyText = sourceCode.getText(property);
|
|
221
|
+
return fixer.replaceText(property, `readonly ${propertyText}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const noMutablePublicFields = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
231
|
+
meta: {
|
|
232
|
+
type: "problem",
|
|
233
|
+
docs: {
|
|
234
|
+
description: "Disallow mutable public class fields"
|
|
235
|
+
},
|
|
236
|
+
fixable: "code",
|
|
237
|
+
messages: {
|
|
238
|
+
noMutablePublicFields: "Public field '{{ propertyName }}' should be readonly. Consider adding the 'readonly' modifier."
|
|
239
|
+
},
|
|
240
|
+
schema: []
|
|
241
|
+
},
|
|
242
|
+
defaultOptions: [],
|
|
243
|
+
create(context) {
|
|
244
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
245
|
+
return {
|
|
246
|
+
ClassDeclaration(node) {
|
|
247
|
+
const sourceCode = context.sourceCode;
|
|
248
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
249
|
+
if (!isConstructOrStackType(type)) return;
|
|
250
|
+
for (const member of node.body.body) {
|
|
251
|
+
if (member.type !== utils.AST_NODE_TYPES.PropertyDefinition || member.key.type !== utils.AST_NODE_TYPES.Identifier) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (["private", "protected"].includes(member.accessibility ?? "")) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (member.readonly) continue;
|
|
258
|
+
context.report({
|
|
259
|
+
node: member,
|
|
260
|
+
messageId: "noMutablePublicFields",
|
|
261
|
+
data: {
|
|
262
|
+
propertyName: member.key.name
|
|
263
|
+
},
|
|
264
|
+
fix: (fixer) => {
|
|
265
|
+
const accessibility = member.accessibility ? "public " : "";
|
|
266
|
+
const paramText = sourceCode.getText(member);
|
|
267
|
+
const [key, value] = paramText.split(":");
|
|
268
|
+
const replacedKey = key.startsWith("public ") ? key.replace("public ", "") : key;
|
|
269
|
+
return fixer.replaceText(
|
|
270
|
+
member,
|
|
271
|
+
`${accessibility}readonly ${replacedKey}:${value}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const noParentNameConstructIdMatch = utils.ESLintUtils.RuleCreator.withoutDocs(
|
|
282
|
+
{
|
|
283
|
+
meta: {
|
|
284
|
+
type: "problem",
|
|
285
|
+
docs: {
|
|
286
|
+
description: "Enforce that construct IDs does not match the parent construct name."
|
|
287
|
+
},
|
|
288
|
+
messages: {
|
|
289
|
+
noParentNameConstructIdMatch: "Construct ID '{{ constructId }}' should not match parent construct name '{{ parentConstructName }}'. Use a more specific identifier."
|
|
290
|
+
},
|
|
291
|
+
schema: []
|
|
292
|
+
},
|
|
293
|
+
defaultOptions: [],
|
|
294
|
+
create(context) {
|
|
295
|
+
return {
|
|
296
|
+
ClassBody(node) {
|
|
297
|
+
const parent = node.parent;
|
|
298
|
+
if (parent?.type !== utils.AST_NODE_TYPES.ClassDeclaration) return;
|
|
299
|
+
const parentClassName = parent.id?.name;
|
|
300
|
+
if (!parentClassName) return;
|
|
301
|
+
for (const body of node.body) {
|
|
302
|
+
if (body.type !== utils.AST_NODE_TYPES.MethodDefinition || !["method", "constructor"].includes(body.kind) || body.value.type !== utils.AST_NODE_TYPES.FunctionExpression) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
validateConstructorBody({
|
|
306
|
+
node,
|
|
307
|
+
expression: body.value,
|
|
308
|
+
parentClassName,
|
|
309
|
+
context
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
const validateConstructorBody = ({
|
|
318
|
+
node,
|
|
319
|
+
expression,
|
|
320
|
+
parentClassName,
|
|
321
|
+
context
|
|
322
|
+
}) => {
|
|
323
|
+
for (const statement of expression.body.body) {
|
|
324
|
+
switch (statement.type) {
|
|
325
|
+
case utils.AST_NODE_TYPES.VariableDeclaration: {
|
|
326
|
+
const newExpression = statement.declarations[0].init;
|
|
327
|
+
if (newExpression?.type !== utils.AST_NODE_TYPES.NewExpression) continue;
|
|
328
|
+
validateConstructId$2({
|
|
329
|
+
node,
|
|
330
|
+
context,
|
|
331
|
+
expression: newExpression,
|
|
332
|
+
parentClassName
|
|
333
|
+
});
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
case utils.AST_NODE_TYPES.ExpressionStatement: {
|
|
337
|
+
if (statement.expression?.type !== utils.AST_NODE_TYPES.NewExpression) break;
|
|
338
|
+
validateStatement({
|
|
339
|
+
node,
|
|
340
|
+
statement,
|
|
341
|
+
parentClassName,
|
|
342
|
+
context
|
|
343
|
+
});
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case utils.AST_NODE_TYPES.IfStatement: {
|
|
347
|
+
traverseStatements({
|
|
348
|
+
node,
|
|
349
|
+
context,
|
|
350
|
+
parentClassName,
|
|
351
|
+
statement: statement.consequent
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case utils.AST_NODE_TYPES.SwitchStatement: {
|
|
356
|
+
for (const switchCase of statement.cases) {
|
|
357
|
+
for (const statement2 of switchCase.consequent) {
|
|
358
|
+
traverseStatements({
|
|
359
|
+
node,
|
|
360
|
+
context,
|
|
361
|
+
parentClassName,
|
|
362
|
+
statement: statement2
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const traverseStatements = ({
|
|
372
|
+
node,
|
|
373
|
+
statement,
|
|
374
|
+
parentClassName,
|
|
375
|
+
context
|
|
376
|
+
}) => {
|
|
377
|
+
switch (statement.type) {
|
|
378
|
+
case utils.AST_NODE_TYPES.BlockStatement: {
|
|
379
|
+
for (const body of statement.body) {
|
|
380
|
+
validateStatement({
|
|
381
|
+
node,
|
|
382
|
+
statement: body,
|
|
383
|
+
parentClassName,
|
|
384
|
+
context
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case utils.AST_NODE_TYPES.ExpressionStatement: {
|
|
390
|
+
const newExpression = statement.expression;
|
|
391
|
+
if (newExpression?.type !== utils.AST_NODE_TYPES.NewExpression) break;
|
|
392
|
+
validateStatement({
|
|
393
|
+
node,
|
|
394
|
+
statement,
|
|
395
|
+
parentClassName,
|
|
396
|
+
context
|
|
397
|
+
});
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case utils.AST_NODE_TYPES.VariableDeclaration: {
|
|
401
|
+
const newExpression = statement.declarations[0].init;
|
|
402
|
+
if (newExpression?.type !== utils.AST_NODE_TYPES.NewExpression) break;
|
|
403
|
+
validateConstructId$2({
|
|
404
|
+
node,
|
|
405
|
+
context,
|
|
406
|
+
expression: newExpression,
|
|
407
|
+
parentClassName
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const validateStatement = ({
|
|
414
|
+
node,
|
|
415
|
+
statement,
|
|
416
|
+
parentClassName,
|
|
417
|
+
context
|
|
418
|
+
}) => {
|
|
419
|
+
switch (statement.type) {
|
|
420
|
+
case utils.AST_NODE_TYPES.VariableDeclaration: {
|
|
421
|
+
const newExpression = statement.declarations[0].init;
|
|
422
|
+
if (newExpression?.type !== utils.AST_NODE_TYPES.NewExpression) break;
|
|
423
|
+
validateConstructId$2({
|
|
424
|
+
node,
|
|
425
|
+
context,
|
|
426
|
+
expression: newExpression,
|
|
427
|
+
parentClassName
|
|
428
|
+
});
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case utils.AST_NODE_TYPES.ExpressionStatement: {
|
|
432
|
+
const newExpression = statement.expression;
|
|
433
|
+
if (newExpression?.type !== utils.AST_NODE_TYPES.NewExpression) break;
|
|
434
|
+
validateConstructId$2({
|
|
435
|
+
node,
|
|
436
|
+
context,
|
|
437
|
+
expression: newExpression,
|
|
438
|
+
parentClassName
|
|
439
|
+
});
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
case utils.AST_NODE_TYPES.IfStatement: {
|
|
443
|
+
validateIfStatement({
|
|
444
|
+
node,
|
|
445
|
+
statement,
|
|
446
|
+
parentClassName,
|
|
447
|
+
context
|
|
448
|
+
});
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case utils.AST_NODE_TYPES.SwitchStatement: {
|
|
452
|
+
validateSwitchStatement({
|
|
453
|
+
node,
|
|
454
|
+
statement,
|
|
455
|
+
parentClassName,
|
|
456
|
+
context
|
|
457
|
+
});
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
const validateIfStatement = ({
|
|
463
|
+
node,
|
|
464
|
+
statement,
|
|
465
|
+
parentClassName,
|
|
466
|
+
context
|
|
467
|
+
}) => {
|
|
468
|
+
traverseStatements({
|
|
469
|
+
node,
|
|
470
|
+
context,
|
|
471
|
+
parentClassName,
|
|
472
|
+
statement: statement.consequent
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
const validateSwitchStatement = ({
|
|
476
|
+
node,
|
|
477
|
+
statement,
|
|
478
|
+
parentClassName,
|
|
479
|
+
context
|
|
480
|
+
}) => {
|
|
481
|
+
for (const caseStatement of statement.cases) {
|
|
482
|
+
for (const _consequent of caseStatement.consequent) {
|
|
483
|
+
traverseStatements({
|
|
484
|
+
node,
|
|
485
|
+
context,
|
|
486
|
+
parentClassName,
|
|
487
|
+
statement: _consequent
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const validateConstructId$2 = ({
|
|
493
|
+
node,
|
|
494
|
+
context,
|
|
495
|
+
expression,
|
|
496
|
+
parentClassName
|
|
497
|
+
}) => {
|
|
498
|
+
if (expression.arguments.length < 2) return;
|
|
499
|
+
const secondArg = expression.arguments[1];
|
|
500
|
+
if (secondArg.type !== utils.AST_NODE_TYPES.Literal || typeof secondArg.value !== "string") {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const formattedConstructId = toPascalCase(secondArg.value);
|
|
504
|
+
const formattedParentClassName = toPascalCase(parentClassName);
|
|
505
|
+
if (formattedParentClassName !== formattedConstructId) return;
|
|
506
|
+
context.report({
|
|
507
|
+
node,
|
|
508
|
+
messageId: "noParentNameConstructIdMatch",
|
|
509
|
+
data: {
|
|
510
|
+
constructId: secondArg.value,
|
|
511
|
+
parentConstructName: parentClassName
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const noPublicClassFields = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
517
|
+
meta: {
|
|
518
|
+
type: "problem",
|
|
519
|
+
docs: {
|
|
520
|
+
description: "Disallow class types in public class fields"
|
|
521
|
+
},
|
|
522
|
+
messages: {
|
|
523
|
+
noPublicClassFields: "Public field '{{ propertyName }}' should not use class type '{{ typeName }}'. Consider using an interface or type alias instead."
|
|
524
|
+
},
|
|
525
|
+
schema: []
|
|
526
|
+
},
|
|
527
|
+
defaultOptions: [],
|
|
528
|
+
create(context) {
|
|
529
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
530
|
+
return {
|
|
531
|
+
ClassDeclaration(node) {
|
|
532
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
533
|
+
if (!isConstructOrStackType(type)) return;
|
|
534
|
+
validateClassMember(node, context, parserServices);
|
|
535
|
+
const constructor = node.body.body.find(
|
|
536
|
+
(member) => member.type === utils.AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
|
|
537
|
+
);
|
|
538
|
+
if (!constructor || constructor.value.type !== utils.AST_NODE_TYPES.FunctionExpression) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
validateConstructorParameterProperty(
|
|
542
|
+
constructor,
|
|
543
|
+
context,
|
|
544
|
+
parserServices
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
const validateClassMember = (node, context, parserServices) => {
|
|
551
|
+
for (const member of node.body.body) {
|
|
552
|
+
if (member.type !== utils.AST_NODE_TYPES.PropertyDefinition || member.key.type !== utils.AST_NODE_TYPES.Identifier) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (["private", "protected"].includes(member.accessibility ?? "")) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (!member.typeAnnotation) continue;
|
|
559
|
+
const type = parserServices.getTypeAtLocation(member);
|
|
560
|
+
if (!type.symbol) continue;
|
|
561
|
+
const isClass = type.symbol.flags === SymbolFlags.Class;
|
|
562
|
+
if (!isClass) continue;
|
|
563
|
+
context.report({
|
|
564
|
+
node: member,
|
|
565
|
+
messageId: "noPublicClassFields",
|
|
566
|
+
data: {
|
|
567
|
+
propertyName: member.key.name,
|
|
568
|
+
typeName: type.symbol.name
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const validateConstructorParameterProperty = (constructor, context, parserServices) => {
|
|
574
|
+
for (const param of constructor.value.params) {
|
|
575
|
+
if (param.type !== utils.AST_NODE_TYPES.TSParameterProperty || param.parameter.type !== utils.AST_NODE_TYPES.Identifier) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (["private", "protected"].includes(param.accessibility ?? "")) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (!param.parameter.typeAnnotation) continue;
|
|
582
|
+
const type = parserServices.getTypeAtLocation(param);
|
|
583
|
+
if (!type.symbol) continue;
|
|
584
|
+
const isClass = type.symbol.flags === SymbolFlags.Class;
|
|
585
|
+
if (!isClass) continue;
|
|
586
|
+
context.report({
|
|
587
|
+
node: param,
|
|
588
|
+
messageId: "noPublicClassFields",
|
|
589
|
+
data: {
|
|
590
|
+
propertyName: param.parameter.name,
|
|
591
|
+
typeName: type.symbol.name
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const noVariableConstructId = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
598
|
+
meta: {
|
|
599
|
+
type: "problem",
|
|
600
|
+
docs: {
|
|
601
|
+
description: `Enforce using literal strings for Construct ID.`
|
|
602
|
+
},
|
|
603
|
+
messages: {
|
|
604
|
+
noVariableConstructId: "Shouldn't use a parameter as a Construct ID."
|
|
605
|
+
},
|
|
606
|
+
schema: []
|
|
607
|
+
},
|
|
608
|
+
defaultOptions: [],
|
|
609
|
+
create(context) {
|
|
610
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
611
|
+
return {
|
|
612
|
+
NewExpression(node) {
|
|
613
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
614
|
+
if (!isConstructType(type) || isStackType(type) || node.arguments.length < 2) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
validateConstructId$1(node, context);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
const validateConstructId$1 = (node, context) => {
|
|
623
|
+
if (node.arguments.length < 2 || isInsideLoop(node)) return;
|
|
624
|
+
const secondArg = node.arguments[1];
|
|
625
|
+
if (secondArg.type === utils.AST_NODE_TYPES.Literal && typeof secondArg.value === "string") {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (secondArg.type === utils.AST_NODE_TYPES.TemplateLiteral && !secondArg.expressions.length) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
context.report({
|
|
632
|
+
node,
|
|
633
|
+
messageId: "noVariableConstructId"
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
const isInsideLoop = (node) => {
|
|
637
|
+
let current = node.parent;
|
|
638
|
+
while (current) {
|
|
639
|
+
if (current.type === utils.AST_NODE_TYPES.ForStatement || current.type === utils.AST_NODE_TYPES.ForInStatement || current.type === utils.AST_NODE_TYPES.ForOfStatement || current.type === utils.AST_NODE_TYPES.WhileStatement || current.type === utils.AST_NODE_TYPES.DoWhileStatement) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
current = current.parent;
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const QUOTE_TYPE = {
|
|
648
|
+
SINGLE: "'",
|
|
649
|
+
DOUBLE: '"'
|
|
650
|
+
};
|
|
651
|
+
const pascalCaseConstructId = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
652
|
+
meta: {
|
|
653
|
+
type: "problem",
|
|
654
|
+
docs: {
|
|
655
|
+
description: "Enforce PascalCase for Construct ID."
|
|
656
|
+
},
|
|
657
|
+
messages: {
|
|
658
|
+
pascalCaseConstructId: "Construct ID must be PascalCase."
|
|
659
|
+
},
|
|
660
|
+
schema: [],
|
|
661
|
+
fixable: "code"
|
|
662
|
+
},
|
|
663
|
+
defaultOptions: [],
|
|
664
|
+
create(context) {
|
|
665
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
666
|
+
return {
|
|
667
|
+
NewExpression(node) {
|
|
668
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
669
|
+
if (!isConstructOrStackType(type) || node.arguments.length < 2) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
validateConstructId(node, context);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
const isPascalCase = (str) => {
|
|
678
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(str);
|
|
679
|
+
};
|
|
680
|
+
const validateConstructId = (node, context) => {
|
|
681
|
+
if (node.arguments.length < 2) return;
|
|
682
|
+
const secondArg = node.arguments[1];
|
|
683
|
+
if (secondArg.type !== utils.AST_NODE_TYPES.Literal || typeof secondArg.value !== "string") {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const quote = secondArg.raw?.startsWith('"') ? QUOTE_TYPE.DOUBLE : QUOTE_TYPE.SINGLE;
|
|
687
|
+
if (isPascalCase(secondArg.value)) return;
|
|
688
|
+
context.report({
|
|
689
|
+
node,
|
|
690
|
+
messageId: "pascalCaseConstructId",
|
|
691
|
+
fix: (fixer) => {
|
|
692
|
+
const pascalCaseValue = toPascalCase(secondArg.value);
|
|
693
|
+
return fixer.replaceText(secondArg, `${quote}${pascalCaseValue}${quote}`);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const requirePassingThis = utils.ESLintUtils.RuleCreator.withoutDocs({
|
|
699
|
+
meta: {
|
|
700
|
+
type: "problem",
|
|
701
|
+
docs: {
|
|
702
|
+
description: "Require passing `this` in a constructor."
|
|
703
|
+
},
|
|
704
|
+
messages: {
|
|
705
|
+
requirePassingThis: "Require passing `this` in a constructor."
|
|
706
|
+
},
|
|
707
|
+
schema: [],
|
|
708
|
+
fixable: "code"
|
|
709
|
+
},
|
|
710
|
+
defaultOptions: [],
|
|
711
|
+
create(context) {
|
|
712
|
+
const parserServices = utils.ESLintUtils.getParserServices(context);
|
|
713
|
+
return {
|
|
714
|
+
NewExpression(node) {
|
|
715
|
+
const type = parserServices.getTypeAtLocation(node);
|
|
716
|
+
if (!isConstructType(type) || isStackType(type) || !node.arguments.length) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const argument = node.arguments[0];
|
|
720
|
+
if (argument.type === utils.AST_NODE_TYPES.ThisExpression) return;
|
|
721
|
+
context.report({
|
|
722
|
+
node,
|
|
723
|
+
messageId: "requirePassingThis",
|
|
724
|
+
fix: (fixer) => {
|
|
725
|
+
return fixer.replaceText(argument, "this");
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const rules = {
|
|
734
|
+
"no-class-in-interface": noClassInInterface,
|
|
735
|
+
"no-construct-stack-suffix": noConstructStackSuffix,
|
|
736
|
+
"no-parent-name-construct-id-match": noParentNameConstructIdMatch,
|
|
737
|
+
"no-public-class-fields": noPublicClassFields,
|
|
738
|
+
"pascal-case-construct-id": pascalCaseConstructId,
|
|
739
|
+
"no-mutable-public-fields": noMutablePublicFields,
|
|
740
|
+
"no-mutable-props-interface": noMutablePropsInterface,
|
|
741
|
+
"require-passing-this": requirePassingThis,
|
|
742
|
+
"no-variable-construct-id": noVariableConstructId,
|
|
743
|
+
"no-import-private": noImportPrivate
|
|
744
|
+
};
|
|
745
|
+
const configs = {
|
|
746
|
+
recommended: {
|
|
747
|
+
plugins: ["cdk"],
|
|
748
|
+
rules: {
|
|
749
|
+
"cdk/no-class-in-interface": "error",
|
|
750
|
+
"cdk/no-construct-stack-suffix": "error",
|
|
751
|
+
"cdk/no-parent-name-construct-id-match": "error",
|
|
752
|
+
"cdk/no-public-class-fields": "error",
|
|
753
|
+
"cdk/pascal-case-construct-id": "error",
|
|
754
|
+
"cdk/require-passing-this": "error",
|
|
755
|
+
"cdk/no-variable-construct-id": "error",
|
|
756
|
+
"cdk/no-mutable-public-fields": "warn",
|
|
757
|
+
"cdk/no-mutable-props-interface": "warn"
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
strict: {
|
|
761
|
+
plugins: ["cdk"],
|
|
762
|
+
rules: {
|
|
763
|
+
"cdk/no-class-in-interface": "error",
|
|
764
|
+
"cdk/no-construct-stack-suffix": "error",
|
|
765
|
+
"cdk/no-parent-name-construct-id-match": "error",
|
|
766
|
+
"cdk/no-public-class-fields": "error",
|
|
767
|
+
"cdk/pascal-case-construct-id": "error",
|
|
768
|
+
"cdk/require-passing-this": "error",
|
|
769
|
+
"cdk/no-variable-construct-id": "error",
|
|
770
|
+
"cdk/no-mutable-public-fields": "error",
|
|
771
|
+
"cdk/no-mutable-props-interface": "error",
|
|
772
|
+
"cdk/no-import-private": "error"
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
const eslintCdkPlugin = {
|
|
777
|
+
rules,
|
|
778
|
+
configs
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
exports.configs = configs;
|
|
782
|
+
exports.default = eslintCdkPlugin;
|
|
783
|
+
exports.rules = rules;
|