eslint-cdk-plugin 3.4.0 → 3.4.1
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/dist/index.cjs +117 -29
- package/dist/index.mjs +117 -29
- package/package.json +6 -8
- package/src/rules/no-construct-in-interface.ts +54 -9
- package/src/rules/no-construct-in-public-property-of-construct.ts +66 -13
- package/src/utils/getArrayElementType.ts +14 -0
- package/src/utils/getGenericTypeArgument.ts +47 -0
- package/src/utils/typecheck/ts-type.ts +12 -0
package/dist/index.cjs
CHANGED
|
@@ -26,7 +26,7 @@ function _interopNamespaceDefault(e) {
|
|
|
26
26
|
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
27
27
|
|
|
28
28
|
var name = "eslint-cdk-plugin";
|
|
29
|
-
var version = "3.4.
|
|
29
|
+
var version = "3.4.1";
|
|
30
30
|
|
|
31
31
|
const createRule = utils.ESLintUtils.RuleCreator(
|
|
32
32
|
(name) => `https://eslint-cdk-plugin.dev/rules/${name}`
|
|
@@ -153,6 +153,46 @@ const SYNTAX_KIND = {
|
|
|
153
153
|
PROPERTY_ACCESS_EXPRESSION: 211
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
+
const getSymbol = (type) => {
|
|
157
|
+
return type.getSymbol?.() ?? type.symbol;
|
|
158
|
+
};
|
|
159
|
+
const isClassType = (type) => {
|
|
160
|
+
return getSymbol(type)?.flags === SYMBOL_FLAGS.CLASS;
|
|
161
|
+
};
|
|
162
|
+
const isArrayType = (type) => {
|
|
163
|
+
const symbol = getSymbol(type);
|
|
164
|
+
if (symbol?.name === "Array") return true;
|
|
165
|
+
if ("target" in type && type.target) {
|
|
166
|
+
const targetSymbol = getSymbol(type.target);
|
|
167
|
+
return targetSymbol?.name === "Array";
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const getArrayElementType = (type) => {
|
|
173
|
+
if (!isArrayType(type)) return void 0;
|
|
174
|
+
if ("typeArguments" in type && Array.isArray(type.typeArguments)) {
|
|
175
|
+
return type.typeArguments[0];
|
|
176
|
+
}
|
|
177
|
+
return void 0;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const getGenericTypeArgument = (type) => {
|
|
181
|
+
if ("aliasSymbol" in type && type.aliasSymbol && "aliasTypeArguments" in type && type.aliasTypeArguments?.length) {
|
|
182
|
+
return type.aliasTypeArguments[0];
|
|
183
|
+
}
|
|
184
|
+
if ("typeArguments" in type && Array.isArray(type.typeArguments) && type.typeArguments?.length) {
|
|
185
|
+
return type.typeArguments[0];
|
|
186
|
+
}
|
|
187
|
+
if ("target" in type && type.target && "typeArguments" in type && Array.isArray(type.typeArguments) && type.typeArguments?.length) {
|
|
188
|
+
return type.typeArguments[0];
|
|
189
|
+
}
|
|
190
|
+
if ("modifiersType" in type && type.modifiersType) {
|
|
191
|
+
return type.modifiersType;
|
|
192
|
+
}
|
|
193
|
+
return void 0;
|
|
194
|
+
};
|
|
195
|
+
|
|
156
196
|
const isClassDeclaration = (node) => {
|
|
157
197
|
return node.kind === SYNTAX_KIND.CLASS_DECLARATION;
|
|
158
198
|
};
|
|
@@ -169,13 +209,6 @@ const isConstructorDeclaration = (node) => {
|
|
|
169
209
|
return node.kind === SYNTAX_KIND.CONSTRUCTOR;
|
|
170
210
|
};
|
|
171
211
|
|
|
172
|
-
const getSymbol = (type) => {
|
|
173
|
-
return type.getSymbol?.() ?? type.symbol;
|
|
174
|
-
};
|
|
175
|
-
const isClassType = (type) => {
|
|
176
|
-
return getSymbol(type)?.flags === SYMBOL_FLAGS.CLASS;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
212
|
const isResourceWithReadonlyInterface = (type) => {
|
|
180
213
|
if (!isResourceType(type) || !type.symbol?.name) return false;
|
|
181
214
|
if (isIgnoreClass(type.symbol.name)) return false;
|
|
@@ -267,17 +300,45 @@ const noConstructInInterface = createRule({
|
|
|
267
300
|
continue;
|
|
268
301
|
}
|
|
269
302
|
const type = parserServices.getTypeAtLocation(property);
|
|
270
|
-
if (
|
|
303
|
+
if (isClassType(type) && isResourceWithReadonlyInterface(type)) {
|
|
304
|
+
context.report({
|
|
305
|
+
node: property,
|
|
306
|
+
messageId: "invalidInterfaceProperty",
|
|
307
|
+
data: {
|
|
308
|
+
propertyName: property.key.name,
|
|
309
|
+
typeName: type.symbol.name
|
|
310
|
+
}
|
|
311
|
+
});
|
|
271
312
|
continue;
|
|
272
313
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
314
|
+
const elementType = getArrayElementType(type);
|
|
315
|
+
if (elementType && isClassType(elementType) && isResourceWithReadonlyInterface(elementType)) {
|
|
316
|
+
context.report({
|
|
317
|
+
node: property,
|
|
318
|
+
messageId: "invalidInterfaceProperty",
|
|
319
|
+
data: {
|
|
320
|
+
propertyName: property.key.name,
|
|
321
|
+
typeName: `${elementType.symbol.name}[]`
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const genericArgument = getGenericTypeArgument(type);
|
|
327
|
+
if (genericArgument && isClassType(genericArgument) && isResourceWithReadonlyInterface(genericArgument)) {
|
|
328
|
+
const wrapperName = (() => {
|
|
329
|
+
if (type.aliasSymbol) return type.aliasSymbol.name;
|
|
330
|
+
if (type.symbol?.name) return type.symbol.name;
|
|
331
|
+
return void 0;
|
|
332
|
+
})();
|
|
333
|
+
context.report({
|
|
334
|
+
node: property,
|
|
335
|
+
messageId: "invalidInterfaceProperty",
|
|
336
|
+
data: {
|
|
337
|
+
propertyName: property.key.name,
|
|
338
|
+
typeName: wrapperName ? `${wrapperName}<${genericArgument.symbol.name}>` : genericArgument.symbol.name
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
281
342
|
}
|
|
282
343
|
}
|
|
283
344
|
};
|
|
@@ -327,15 +388,7 @@ const validatePublicPropertyOfConstruct = (node, context, parserServices) => {
|
|
|
327
388
|
}
|
|
328
389
|
if (!property.typeAnnotation) continue;
|
|
329
390
|
const type = parserServices.getTypeAtLocation(property);
|
|
330
|
-
|
|
331
|
-
context.report({
|
|
332
|
-
node: property,
|
|
333
|
-
messageId: "invalidPublicPropertyOfConstruct",
|
|
334
|
-
data: {
|
|
335
|
-
propertyName: property.key.name,
|
|
336
|
-
typeName: type.symbol.name
|
|
337
|
-
}
|
|
338
|
-
});
|
|
391
|
+
checkAndReportConstructType(type, property, property.key.name, context);
|
|
339
392
|
}
|
|
340
393
|
};
|
|
341
394
|
const validateConstructorParameterProperty = (constructor, context, parserServices) => {
|
|
@@ -348,15 +401,50 @@ const validateConstructorParameterProperty = (constructor, context, parserServic
|
|
|
348
401
|
}
|
|
349
402
|
if (!param.parameter.typeAnnotation) continue;
|
|
350
403
|
const type = parserServices.getTypeAtLocation(param);
|
|
351
|
-
|
|
404
|
+
checkAndReportConstructType(type, param, param.parameter.name, context);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const checkAndReportConstructType = (type, node, propertyName, context) => {
|
|
408
|
+
if (isClassType(type) && isResourceWithReadonlyInterface(type)) {
|
|
352
409
|
context.report({
|
|
353
|
-
node
|
|
410
|
+
node,
|
|
354
411
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
355
412
|
data: {
|
|
356
|
-
propertyName
|
|
413
|
+
propertyName,
|
|
357
414
|
typeName: type.symbol.name
|
|
358
415
|
}
|
|
359
416
|
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const elementType = getArrayElementType(type);
|
|
420
|
+
if (elementType && isClassType(elementType) && isResourceWithReadonlyInterface(elementType)) {
|
|
421
|
+
context.report({
|
|
422
|
+
node,
|
|
423
|
+
messageId: "invalidPublicPropertyOfConstruct",
|
|
424
|
+
data: {
|
|
425
|
+
propertyName,
|
|
426
|
+
typeName: `${elementType.symbol.name}[]`
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const genericArgument = getGenericTypeArgument(type);
|
|
432
|
+
if (genericArgument && isClassType(genericArgument) && isResourceWithReadonlyInterface(genericArgument)) {
|
|
433
|
+
const wrapperName = (() => {
|
|
434
|
+
if ("aliasSymbol" in type && type.aliasSymbol) {
|
|
435
|
+
return type.aliasSymbol.name;
|
|
436
|
+
}
|
|
437
|
+
if (type.symbol?.name) return type.symbol.name;
|
|
438
|
+
return void 0;
|
|
439
|
+
})();
|
|
440
|
+
context.report({
|
|
441
|
+
node,
|
|
442
|
+
messageId: "invalidPublicPropertyOfConstruct",
|
|
443
|
+
data: {
|
|
444
|
+
propertyName,
|
|
445
|
+
typeName: wrapperName ? `${wrapperName}<${genericArgument.symbol.name}>` : genericArgument.symbol.name
|
|
446
|
+
}
|
|
447
|
+
});
|
|
360
448
|
}
|
|
361
449
|
};
|
|
362
450
|
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { ESLintUtils, AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
|
|
5
5
|
var name = "eslint-cdk-plugin";
|
|
6
|
-
var version = "3.4.
|
|
6
|
+
var version = "3.4.1";
|
|
7
7
|
|
|
8
8
|
const createRule = ESLintUtils.RuleCreator(
|
|
9
9
|
(name) => `https://eslint-cdk-plugin.dev/rules/${name}`
|
|
@@ -130,6 +130,46 @@ const SYNTAX_KIND = {
|
|
|
130
130
|
PROPERTY_ACCESS_EXPRESSION: 211
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
+
const getSymbol = (type) => {
|
|
134
|
+
return type.getSymbol?.() ?? type.symbol;
|
|
135
|
+
};
|
|
136
|
+
const isClassType = (type) => {
|
|
137
|
+
return getSymbol(type)?.flags === SYMBOL_FLAGS.CLASS;
|
|
138
|
+
};
|
|
139
|
+
const isArrayType = (type) => {
|
|
140
|
+
const symbol = getSymbol(type);
|
|
141
|
+
if (symbol?.name === "Array") return true;
|
|
142
|
+
if ("target" in type && type.target) {
|
|
143
|
+
const targetSymbol = getSymbol(type.target);
|
|
144
|
+
return targetSymbol?.name === "Array";
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const getArrayElementType = (type) => {
|
|
150
|
+
if (!isArrayType(type)) return void 0;
|
|
151
|
+
if ("typeArguments" in type && Array.isArray(type.typeArguments)) {
|
|
152
|
+
return type.typeArguments[0];
|
|
153
|
+
}
|
|
154
|
+
return void 0;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const getGenericTypeArgument = (type) => {
|
|
158
|
+
if ("aliasSymbol" in type && type.aliasSymbol && "aliasTypeArguments" in type && type.aliasTypeArguments?.length) {
|
|
159
|
+
return type.aliasTypeArguments[0];
|
|
160
|
+
}
|
|
161
|
+
if ("typeArguments" in type && Array.isArray(type.typeArguments) && type.typeArguments?.length) {
|
|
162
|
+
return type.typeArguments[0];
|
|
163
|
+
}
|
|
164
|
+
if ("target" in type && type.target && "typeArguments" in type && Array.isArray(type.typeArguments) && type.typeArguments?.length) {
|
|
165
|
+
return type.typeArguments[0];
|
|
166
|
+
}
|
|
167
|
+
if ("modifiersType" in type && type.modifiersType) {
|
|
168
|
+
return type.modifiersType;
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
171
|
+
};
|
|
172
|
+
|
|
133
173
|
const isClassDeclaration = (node) => {
|
|
134
174
|
return node.kind === SYNTAX_KIND.CLASS_DECLARATION;
|
|
135
175
|
};
|
|
@@ -146,13 +186,6 @@ const isConstructorDeclaration = (node) => {
|
|
|
146
186
|
return node.kind === SYNTAX_KIND.CONSTRUCTOR;
|
|
147
187
|
};
|
|
148
188
|
|
|
149
|
-
const getSymbol = (type) => {
|
|
150
|
-
return type.getSymbol?.() ?? type.symbol;
|
|
151
|
-
};
|
|
152
|
-
const isClassType = (type) => {
|
|
153
|
-
return getSymbol(type)?.flags === SYMBOL_FLAGS.CLASS;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
189
|
const isResourceWithReadonlyInterface = (type) => {
|
|
157
190
|
if (!isResourceType(type) || !type.symbol?.name) return false;
|
|
158
191
|
if (isIgnoreClass(type.symbol.name)) return false;
|
|
@@ -244,17 +277,45 @@ const noConstructInInterface = createRule({
|
|
|
244
277
|
continue;
|
|
245
278
|
}
|
|
246
279
|
const type = parserServices.getTypeAtLocation(property);
|
|
247
|
-
if (
|
|
280
|
+
if (isClassType(type) && isResourceWithReadonlyInterface(type)) {
|
|
281
|
+
context.report({
|
|
282
|
+
node: property,
|
|
283
|
+
messageId: "invalidInterfaceProperty",
|
|
284
|
+
data: {
|
|
285
|
+
propertyName: property.key.name,
|
|
286
|
+
typeName: type.symbol.name
|
|
287
|
+
}
|
|
288
|
+
});
|
|
248
289
|
continue;
|
|
249
290
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
291
|
+
const elementType = getArrayElementType(type);
|
|
292
|
+
if (elementType && isClassType(elementType) && isResourceWithReadonlyInterface(elementType)) {
|
|
293
|
+
context.report({
|
|
294
|
+
node: property,
|
|
295
|
+
messageId: "invalidInterfaceProperty",
|
|
296
|
+
data: {
|
|
297
|
+
propertyName: property.key.name,
|
|
298
|
+
typeName: `${elementType.symbol.name}[]`
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const genericArgument = getGenericTypeArgument(type);
|
|
304
|
+
if (genericArgument && isClassType(genericArgument) && isResourceWithReadonlyInterface(genericArgument)) {
|
|
305
|
+
const wrapperName = (() => {
|
|
306
|
+
if (type.aliasSymbol) return type.aliasSymbol.name;
|
|
307
|
+
if (type.symbol?.name) return type.symbol.name;
|
|
308
|
+
return void 0;
|
|
309
|
+
})();
|
|
310
|
+
context.report({
|
|
311
|
+
node: property,
|
|
312
|
+
messageId: "invalidInterfaceProperty",
|
|
313
|
+
data: {
|
|
314
|
+
propertyName: property.key.name,
|
|
315
|
+
typeName: wrapperName ? `${wrapperName}<${genericArgument.symbol.name}>` : genericArgument.symbol.name
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
258
319
|
}
|
|
259
320
|
}
|
|
260
321
|
};
|
|
@@ -304,15 +365,7 @@ const validatePublicPropertyOfConstruct = (node, context, parserServices) => {
|
|
|
304
365
|
}
|
|
305
366
|
if (!property.typeAnnotation) continue;
|
|
306
367
|
const type = parserServices.getTypeAtLocation(property);
|
|
307
|
-
|
|
308
|
-
context.report({
|
|
309
|
-
node: property,
|
|
310
|
-
messageId: "invalidPublicPropertyOfConstruct",
|
|
311
|
-
data: {
|
|
312
|
-
propertyName: property.key.name,
|
|
313
|
-
typeName: type.symbol.name
|
|
314
|
-
}
|
|
315
|
-
});
|
|
368
|
+
checkAndReportConstructType(type, property, property.key.name, context);
|
|
316
369
|
}
|
|
317
370
|
};
|
|
318
371
|
const validateConstructorParameterProperty = (constructor, context, parserServices) => {
|
|
@@ -325,15 +378,50 @@ const validateConstructorParameterProperty = (constructor, context, parserServic
|
|
|
325
378
|
}
|
|
326
379
|
if (!param.parameter.typeAnnotation) continue;
|
|
327
380
|
const type = parserServices.getTypeAtLocation(param);
|
|
328
|
-
|
|
381
|
+
checkAndReportConstructType(type, param, param.parameter.name, context);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const checkAndReportConstructType = (type, node, propertyName, context) => {
|
|
385
|
+
if (isClassType(type) && isResourceWithReadonlyInterface(type)) {
|
|
329
386
|
context.report({
|
|
330
|
-
node
|
|
387
|
+
node,
|
|
331
388
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
332
389
|
data: {
|
|
333
|
-
propertyName
|
|
390
|
+
propertyName,
|
|
334
391
|
typeName: type.symbol.name
|
|
335
392
|
}
|
|
336
393
|
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const elementType = getArrayElementType(type);
|
|
397
|
+
if (elementType && isClassType(elementType) && isResourceWithReadonlyInterface(elementType)) {
|
|
398
|
+
context.report({
|
|
399
|
+
node,
|
|
400
|
+
messageId: "invalidPublicPropertyOfConstruct",
|
|
401
|
+
data: {
|
|
402
|
+
propertyName,
|
|
403
|
+
typeName: `${elementType.symbol.name}[]`
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const genericArgument = getGenericTypeArgument(type);
|
|
409
|
+
if (genericArgument && isClassType(genericArgument) && isResourceWithReadonlyInterface(genericArgument)) {
|
|
410
|
+
const wrapperName = (() => {
|
|
411
|
+
if ("aliasSymbol" in type && type.aliasSymbol) {
|
|
412
|
+
return type.aliasSymbol.name;
|
|
413
|
+
}
|
|
414
|
+
if (type.symbol?.name) return type.symbol.name;
|
|
415
|
+
return void 0;
|
|
416
|
+
})();
|
|
417
|
+
context.report({
|
|
418
|
+
node,
|
|
419
|
+
messageId: "invalidPublicPropertyOfConstruct",
|
|
420
|
+
data: {
|
|
421
|
+
propertyName,
|
|
422
|
+
typeName: wrapperName ? `${wrapperName}<${genericArgument.symbol.name}>` : genericArgument.symbol.name
|
|
423
|
+
}
|
|
424
|
+
});
|
|
337
425
|
}
|
|
338
426
|
};
|
|
339
427
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-cdk-plugin",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"description": "eslint plugin for AWS CDK projects",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -22,24 +22,22 @@
|
|
|
22
22
|
"lint": "eslint --fix --config eslint.config.js",
|
|
23
23
|
"check": "tsc --noEmit",
|
|
24
24
|
"pack": "pnpm run build && npm pack",
|
|
25
|
-
"release:minor": "standard-version --release-as minor",
|
|
26
|
-
"release:major": "standard-version --release-as major",
|
|
27
|
-
"release:patch": "standard-version --release-as patch",
|
|
28
25
|
"docs:dev": "cd ./docs && pnpm install && pnpm run dev",
|
|
29
26
|
"docs:build": "cd ./docs && pnpm install && pnpm run build",
|
|
30
27
|
"docs:preview": "cd ./docs && pnpm install && pnpm run preview"
|
|
31
28
|
},
|
|
32
29
|
"devDependencies": {
|
|
33
30
|
"@eslint/js": "^9.26.0",
|
|
31
|
+
"@secretlint/secretlint-rule-preset-recommend": "^11.2.4",
|
|
34
32
|
"@types/node": "^22.15.0",
|
|
35
33
|
"@typescript-eslint/rule-tester": "^8.32.1",
|
|
36
|
-
"eslint": "9.
|
|
37
|
-
"eslint-plugin-import": "^2.
|
|
34
|
+
"eslint": "9.36.0",
|
|
35
|
+
"eslint-plugin-import": "^2.32.0",
|
|
38
36
|
"pkgroll": "^2.12.2",
|
|
39
|
-
"
|
|
37
|
+
"secretlint": "^11.2.4",
|
|
40
38
|
"typescript": "^5.8.3",
|
|
41
39
|
"typescript-eslint": "^8.32.1",
|
|
42
|
-
"vitest": "^3.
|
|
40
|
+
"vitest": "^3.2.4"
|
|
43
41
|
},
|
|
44
42
|
"dependencies": {
|
|
45
43
|
"@typescript-eslint/parser": "^8.32.1",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
|
|
2
2
|
|
|
3
3
|
import { createRule } from "../utils/createRule";
|
|
4
|
+
import { getArrayElementType } from "../utils/getArrayElementType";
|
|
5
|
+
import { getGenericTypeArgument } from "../utils/getGenericTypeArgument";
|
|
4
6
|
import { isResourceWithReadonlyInterface } from "../utils/is-resource-with-readonly-interface";
|
|
5
7
|
import { isClassType } from "../utils/typecheck/ts-type";
|
|
6
8
|
|
|
@@ -36,18 +38,61 @@ export const noConstructInInterface = createRule({
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const type = parserServices.getTypeAtLocation(property);
|
|
39
|
-
|
|
41
|
+
|
|
42
|
+
// NOTE: Check if it's a direct class type
|
|
43
|
+
if (isClassType(type) && isResourceWithReadonlyInterface(type)) {
|
|
44
|
+
context.report({
|
|
45
|
+
node: property,
|
|
46
|
+
messageId: "invalidInterfaceProperty",
|
|
47
|
+
data: {
|
|
48
|
+
propertyName: property.key.name,
|
|
49
|
+
typeName: type.symbol.name,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
40
52
|
continue;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
// NOTE: Check if it's an array of class types
|
|
56
|
+
const elementType = getArrayElementType(type);
|
|
57
|
+
if (
|
|
58
|
+
elementType &&
|
|
59
|
+
isClassType(elementType) &&
|
|
60
|
+
isResourceWithReadonlyInterface(elementType)
|
|
61
|
+
) {
|
|
62
|
+
context.report({
|
|
63
|
+
node: property,
|
|
64
|
+
messageId: "invalidInterfaceProperty",
|
|
65
|
+
data: {
|
|
66
|
+
propertyName: property.key.name,
|
|
67
|
+
typeName: `${elementType.symbol.name}[]`,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// NOTE: Check if it's a generic type wrapping a class type
|
|
74
|
+
const genericArgument = getGenericTypeArgument(type);
|
|
75
|
+
if (
|
|
76
|
+
genericArgument &&
|
|
77
|
+
isClassType(genericArgument) &&
|
|
78
|
+
isResourceWithReadonlyInterface(genericArgument)
|
|
79
|
+
) {
|
|
80
|
+
const wrapperName = (() => {
|
|
81
|
+
if (type.aliasSymbol) return type.aliasSymbol.name; // For type aliases like Readonly<T>, Partial<T>
|
|
82
|
+
if (type.symbol?.name) return type.symbol.name; // For other generic types like Array<T>
|
|
83
|
+
return undefined;
|
|
84
|
+
})();
|
|
85
|
+
context.report({
|
|
86
|
+
node: property,
|
|
87
|
+
messageId: "invalidInterfaceProperty",
|
|
88
|
+
data: {
|
|
89
|
+
propertyName: property.key.name,
|
|
90
|
+
typeName: wrapperName
|
|
91
|
+
? `${wrapperName}<${genericArgument.symbol.name}>`
|
|
92
|
+
: genericArgument.symbol.name,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
51
96
|
}
|
|
52
97
|
},
|
|
53
98
|
};
|
|
@@ -5,9 +5,12 @@ import {
|
|
|
5
5
|
TSESLint,
|
|
6
6
|
TSESTree,
|
|
7
7
|
} from "@typescript-eslint/utils";
|
|
8
|
+
import { Type } from "typescript";
|
|
8
9
|
|
|
9
10
|
import { createRule } from "../utils/createRule";
|
|
11
|
+
import { getArrayElementType } from "../utils/getArrayElementType";
|
|
10
12
|
import { getConstructor } from "../utils/getConstructor";
|
|
13
|
+
import { getGenericTypeArgument } from "../utils/getGenericTypeArgument";
|
|
11
14
|
import { isResourceWithReadonlyInterface } from "../utils/is-resource-with-readonly-interface";
|
|
12
15
|
import { isConstructOrStackType } from "../utils/typecheck/cdk";
|
|
13
16
|
import { isClassType } from "../utils/typecheck/ts-type";
|
|
@@ -88,16 +91,7 @@ const validatePublicPropertyOfConstruct = (
|
|
|
88
91
|
if (!property.typeAnnotation) continue;
|
|
89
92
|
|
|
90
93
|
const type = parserServices.getTypeAtLocation(property);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
context.report({
|
|
94
|
-
node: property,
|
|
95
|
-
messageId: "invalidPublicPropertyOfConstruct",
|
|
96
|
-
data: {
|
|
97
|
-
propertyName: property.key.name,
|
|
98
|
-
typeName: type.symbol.name,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
94
|
+
checkAndReportConstructType(type, property, property.key.name, context);
|
|
101
95
|
}
|
|
102
96
|
};
|
|
103
97
|
|
|
@@ -127,15 +121,74 @@ const validateConstructorParameterProperty = (
|
|
|
127
121
|
if (!param.parameter.typeAnnotation) continue;
|
|
128
122
|
|
|
129
123
|
const type = parserServices.getTypeAtLocation(param);
|
|
130
|
-
|
|
124
|
+
checkAndReportConstructType(type, param, param.parameter.name, context);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
131
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Common validation logic for checking if a type is a Construct type
|
|
130
|
+
*/
|
|
131
|
+
const checkAndReportConstructType = (
|
|
132
|
+
type: Type,
|
|
133
|
+
node: TSESTree.Node,
|
|
134
|
+
propertyName: string,
|
|
135
|
+
context: Context
|
|
136
|
+
): void => {
|
|
137
|
+
// NOTE: Check if it's a direct class type
|
|
138
|
+
if (isClassType(type) && isResourceWithReadonlyInterface(type)) {
|
|
132
139
|
context.report({
|
|
133
|
-
node
|
|
140
|
+
node,
|
|
134
141
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
135
142
|
data: {
|
|
136
|
-
propertyName
|
|
143
|
+
propertyName,
|
|
137
144
|
typeName: type.symbol.name,
|
|
138
145
|
},
|
|
139
146
|
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// NOTE: Check if it's an array of class types
|
|
151
|
+
const elementType = getArrayElementType(type);
|
|
152
|
+
if (
|
|
153
|
+
elementType &&
|
|
154
|
+
isClassType(elementType) &&
|
|
155
|
+
isResourceWithReadonlyInterface(elementType)
|
|
156
|
+
) {
|
|
157
|
+
context.report({
|
|
158
|
+
node,
|
|
159
|
+
messageId: "invalidPublicPropertyOfConstruct",
|
|
160
|
+
data: {
|
|
161
|
+
propertyName,
|
|
162
|
+
typeName: `${elementType.symbol.name}[]`,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// NOTE: Check if it's a generic type wrapping a class type
|
|
169
|
+
const genericArgument = getGenericTypeArgument(type);
|
|
170
|
+
if (
|
|
171
|
+
genericArgument &&
|
|
172
|
+
isClassType(genericArgument) &&
|
|
173
|
+
isResourceWithReadonlyInterface(genericArgument)
|
|
174
|
+
) {
|
|
175
|
+
const wrapperName = (() => {
|
|
176
|
+
if ("aliasSymbol" in type && type.aliasSymbol) {
|
|
177
|
+
return type.aliasSymbol.name; // For type aliases like Readonly<T>, Partial<T>
|
|
178
|
+
}
|
|
179
|
+
if (type.symbol?.name) return type.symbol.name; // For other generic types like Array<T>
|
|
180
|
+
return undefined;
|
|
181
|
+
})();
|
|
182
|
+
|
|
183
|
+
context.report({
|
|
184
|
+
node,
|
|
185
|
+
messageId: "invalidPublicPropertyOfConstruct",
|
|
186
|
+
data: {
|
|
187
|
+
propertyName,
|
|
188
|
+
typeName: wrapperName
|
|
189
|
+
? `${wrapperName}<${genericArgument.symbol.name}>`
|
|
190
|
+
: genericArgument.symbol.name,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
140
193
|
}
|
|
141
194
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Type } from "typescript";
|
|
2
|
+
|
|
3
|
+
import { isArrayType } from "./typecheck/ts-type";
|
|
4
|
+
|
|
5
|
+
export const getArrayElementType = (type: Type): Type | undefined => {
|
|
6
|
+
if (!isArrayType(type)) return undefined;
|
|
7
|
+
|
|
8
|
+
// Get type arguments for Array<T>
|
|
9
|
+
if ("typeArguments" in type && Array.isArray(type.typeArguments)) {
|
|
10
|
+
return (type.typeArguments as Type[])[0];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return undefined;
|
|
14
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Type } from "typescript";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the type argument from a generic type reference
|
|
5
|
+
* @param type - The type to check
|
|
6
|
+
* @returns The first type argument if it's a generic type reference, undefined otherwise
|
|
7
|
+
*/
|
|
8
|
+
export const getGenericTypeArgument = (type: Type): Type | undefined => {
|
|
9
|
+
// NOTE: Check for type alias (e.g. Readonly<T>, Partial<T>)
|
|
10
|
+
if (
|
|
11
|
+
"aliasSymbol" in type &&
|
|
12
|
+
type.aliasSymbol &&
|
|
13
|
+
"aliasTypeArguments" in type &&
|
|
14
|
+
type.aliasTypeArguments?.length
|
|
15
|
+
) {
|
|
16
|
+
return type.aliasTypeArguments[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// NOTE: Check if type has typeArguments (generic types like Array<T>, etc.)
|
|
20
|
+
// This works for TypeReference types
|
|
21
|
+
if (
|
|
22
|
+
"typeArguments" in type &&
|
|
23
|
+
Array.isArray(type.typeArguments) &&
|
|
24
|
+
type.typeArguments?.length
|
|
25
|
+
) {
|
|
26
|
+
return type.typeArguments[0] as Type;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// NOTE: Alternative approach: check for target property (some generic types have this)
|
|
30
|
+
if (
|
|
31
|
+
"target" in type &&
|
|
32
|
+
type.target &&
|
|
33
|
+
"typeArguments" in type &&
|
|
34
|
+
Array.isArray(type.typeArguments) &&
|
|
35
|
+
type.typeArguments?.length
|
|
36
|
+
) {
|
|
37
|
+
return type.typeArguments[0] as Type;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// NOTE: For mapped types like Readonly<T> and Partial<T>
|
|
41
|
+
// These are represented differently in TypeScript's type system
|
|
42
|
+
if ("modifiersType" in type && type.modifiersType) {
|
|
43
|
+
return type.modifiersType as Type;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
@@ -13,3 +13,15 @@ const getSymbol = (type: Type): Symbol | undefined => {
|
|
|
13
13
|
export const isClassType = (type: Type): boolean => {
|
|
14
14
|
return getSymbol(type)?.flags === SYMBOL_FLAGS.CLASS;
|
|
15
15
|
};
|
|
16
|
+
|
|
17
|
+
export const isArrayType = (type: Type): boolean => {
|
|
18
|
+
const symbol = getSymbol(type);
|
|
19
|
+
if (symbol?.name === "Array") return true;
|
|
20
|
+
|
|
21
|
+
if ("target" in type && type.target) {
|
|
22
|
+
const targetSymbol = getSymbol(type.target as Type);
|
|
23
|
+
return targetSymbol?.name === "Array";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false;
|
|
27
|
+
};
|