eslint-cdk-plugin 3.3.0 → 3.4.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/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.3.0";
29
+ var version = "3.4.0";
30
30
 
31
31
  const createRule = utils.ESLintUtils.RuleCreator(
32
32
  (name) => `https://eslint-cdk-plugin.dev/rules/${name}`
@@ -55,6 +55,10 @@ const isConstructType = (type, ignoredClasses = [
55
55
  if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
56
56
  return isTargetSuperClassType(type, ["Construct"], isConstructType);
57
57
  };
58
+ const isResourceType = (type, ignoredClasses = []) => {
59
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
60
+ return isTargetSuperClassType(type, ["Resource"], isResourceType);
61
+ };
58
62
  const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
59
63
  if (!type.symbol) return false;
60
64
  if (targetSuperClasses.some((suffix) => type.symbol.name === suffix)) {
@@ -140,11 +144,105 @@ const validateConstructorProperty = (constructor, context, parserServices) => {
140
144
  };
141
145
 
142
146
  const SYMBOL_FLAGS = {
143
- CLASS: 32
144
- };
147
+ CLASS: 32};
145
148
  const SYNTAX_KIND = {
146
149
  CLASS_DECLARATION: 263,
147
- CONSTRUCTOR: 176
150
+ CONSTRUCTOR: 176,
151
+ IMPLEMENTS_KEYWORD: 119,
152
+ IDENTIFIER: 80,
153
+ PROPERTY_ACCESS_EXPRESSION: 211
154
+ };
155
+
156
+ const isClassDeclaration = (node) => {
157
+ return node.kind === SYNTAX_KIND.CLASS_DECLARATION;
158
+ };
159
+ const isIdentifier = (node) => {
160
+ return node.kind === SYNTAX_KIND.IDENTIFIER;
161
+ };
162
+ const isPropertyAccessExpression = (node) => {
163
+ return node.kind === SYNTAX_KIND.PROPERTY_ACCESS_EXPRESSION;
164
+ };
165
+ const checkHeritageClauseIsImplements = (node) => {
166
+ return node.token === SYNTAX_KIND.IMPLEMENTS_KEYWORD;
167
+ };
168
+ const isConstructorDeclaration = (node) => {
169
+ return node.kind === SYNTAX_KIND.CONSTRUCTOR;
170
+ };
171
+
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
+ const isResourceWithReadonlyInterface = (type) => {
180
+ if (!isResourceType(type) || !type.symbol?.name) return false;
181
+ if (isIgnoreClass(type.symbol.name)) return false;
182
+ return hasMatchingInterfaceInHierarchy(type);
183
+ };
184
+ const hasMatchingInterfaceInHierarchy = (type) => {
185
+ const processedTypes = /* @__PURE__ */ new Set();
186
+ const checkTypeAndBases = (currentType) => {
187
+ const symbol = currentType.getSymbol?.() ?? currentType.symbol;
188
+ if (!symbol?.name) return false;
189
+ if (processedTypes.has(symbol.name)) return false;
190
+ processedTypes.add(symbol.name);
191
+ const currentClassName = symbol.name;
192
+ if (isIgnoreClass(currentClassName)) return false;
193
+ const directInterfaces = getDirectImplementedInterfaceNames(currentType);
194
+ if (directInterfaces.some(
195
+ (interfaceName) => checkInterfaceMatchClassName(interfaceName, currentClassName)
196
+ )) {
197
+ return true;
198
+ }
199
+ const baseTypes = currentType.getBaseTypes?.() ?? [];
200
+ return baseTypes.some(
201
+ (baseType) => isClassType(baseType) && checkTypeAndBases(baseType)
202
+ );
203
+ };
204
+ return checkTypeAndBases(type);
205
+ };
206
+ const checkInterfaceMatchClassName = (interfaceName, classname) => {
207
+ const simpleInterfaceName = interfaceName.includes(".") ? interfaceName.split(".").pop() ?? interfaceName : interfaceName;
208
+ if (simpleInterfaceName === `I${classname}`) return true;
209
+ const classNameWithoutBase = classname.replace(/^Base|Base$/g, "");
210
+ if (classNameWithoutBase && simpleInterfaceName === `I${classNameWithoutBase}`) {
211
+ return true;
212
+ }
213
+ const baseVMatch = /^(.+)BaseV(\d+)$/.exec(classname);
214
+ if (!baseVMatch) return false;
215
+ const [, baseName, version] = baseVMatch;
216
+ return simpleInterfaceName === `I${baseName}V${version}`;
217
+ };
218
+ const getDirectImplementedInterfaceNames = (type) => {
219
+ const symbol = type.getSymbol?.() ?? type.symbol;
220
+ if (!symbol?.name) return [];
221
+ const declarations = symbol.getDeclarations ? symbol.getDeclarations() : symbol.declarations;
222
+ if (!declarations?.length) return [];
223
+ return declarations.reduce((acc, decl) => {
224
+ if (!isClassDeclaration(decl)) return acc;
225
+ const heritageClauses = decl.heritageClauses;
226
+ if (!heritageClauses) return acc;
227
+ return heritageClauses.reduce((hcAcc, hc) => {
228
+ if (!checkHeritageClauseIsImplements(hc)) return hcAcc;
229
+ return hc.types.reduce((typeAcc, type2) => {
230
+ const expression = type2.expression;
231
+ if (!expression) return typeAcc;
232
+ if (isIdentifier(expression)) return [...typeAcc, expression.text];
233
+ if (!isPropertyAccessExpression(expression)) return typeAcc;
234
+ const namespace = expression.expression;
235
+ const interfaceName = expression.name;
236
+ if (isIdentifier(namespace) && isIdentifier(interfaceName)) {
237
+ return [...typeAcc, `${namespace.text}.${interfaceName.text}`];
238
+ }
239
+ return typeAcc;
240
+ }, []);
241
+ }, []);
242
+ }, []);
243
+ };
244
+ const isIgnoreClass = (className) => {
245
+ return className === "Resource" || className === "Construct";
148
246
  };
149
247
 
150
248
  const noConstructInInterface = createRule({
@@ -169,9 +267,9 @@ const noConstructInInterface = createRule({
169
267
  continue;
170
268
  }
171
269
  const type = parserServices.getTypeAtLocation(property);
172
- if (!isConstructOrStackType(type)) continue;
173
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
174
- if (!isClass) continue;
270
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) {
271
+ continue;
272
+ }
175
273
  context.report({
176
274
  node: property,
177
275
  messageId: "invalidInterfaceProperty",
@@ -229,9 +327,7 @@ const validatePublicPropertyOfConstruct = (node, context, parserServices) => {
229
327
  }
230
328
  if (!property.typeAnnotation) continue;
231
329
  const type = parserServices.getTypeAtLocation(property);
232
- if (!isConstructOrStackType(type)) continue;
233
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
234
- if (!isClass) continue;
330
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
235
331
  context.report({
236
332
  node: property,
237
333
  messageId: "invalidPublicPropertyOfConstruct",
@@ -252,9 +348,7 @@ const validateConstructorParameterProperty = (constructor, context, parserServic
252
348
  }
253
349
  if (!param.parameter.typeAnnotation) continue;
254
350
  const type = parserServices.getTypeAtLocation(param);
255
- if (!isConstructOrStackType(type)) continue;
256
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
257
- if (!isClass) continue;
351
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
258
352
  context.report({
259
353
  node: param,
260
354
  messageId: "invalidPublicPropertyOfConstruct",
@@ -291,12 +385,6 @@ const getConstructorPropertyNames = (type) => {
291
385
  if (!constructor?.parameters.length) return [];
292
386
  return constructor.parameters.map((param) => param.name.getText());
293
387
  };
294
- const isClassDeclaration = (declaration) => {
295
- return declaration.kind === SYNTAX_KIND.CLASS_DECLARATION;
296
- };
297
- const isConstructorDeclaration = (node) => {
298
- return node.kind === SYNTAX_KIND.CONSTRUCTOR;
299
- };
300
388
 
301
389
  const SUFFIX_TYPE = {
302
390
  CONSTRUCT: "Construct",
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.3.0";
6
+ var version = "3.4.0";
7
7
 
8
8
  const createRule = ESLintUtils.RuleCreator(
9
9
  (name) => `https://eslint-cdk-plugin.dev/rules/${name}`
@@ -32,6 +32,10 @@ const isConstructType = (type, ignoredClasses = [
32
32
  if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
33
33
  return isTargetSuperClassType(type, ["Construct"], isConstructType);
34
34
  };
35
+ const isResourceType = (type, ignoredClasses = []) => {
36
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
37
+ return isTargetSuperClassType(type, ["Resource"], isResourceType);
38
+ };
35
39
  const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
36
40
  if (!type.symbol) return false;
37
41
  if (targetSuperClasses.some((suffix) => type.symbol.name === suffix)) {
@@ -117,11 +121,105 @@ const validateConstructorProperty = (constructor, context, parserServices) => {
117
121
  };
118
122
 
119
123
  const SYMBOL_FLAGS = {
120
- CLASS: 32
121
- };
124
+ CLASS: 32};
122
125
  const SYNTAX_KIND = {
123
126
  CLASS_DECLARATION: 263,
124
- CONSTRUCTOR: 176
127
+ CONSTRUCTOR: 176,
128
+ IMPLEMENTS_KEYWORD: 119,
129
+ IDENTIFIER: 80,
130
+ PROPERTY_ACCESS_EXPRESSION: 211
131
+ };
132
+
133
+ const isClassDeclaration = (node) => {
134
+ return node.kind === SYNTAX_KIND.CLASS_DECLARATION;
135
+ };
136
+ const isIdentifier = (node) => {
137
+ return node.kind === SYNTAX_KIND.IDENTIFIER;
138
+ };
139
+ const isPropertyAccessExpression = (node) => {
140
+ return node.kind === SYNTAX_KIND.PROPERTY_ACCESS_EXPRESSION;
141
+ };
142
+ const checkHeritageClauseIsImplements = (node) => {
143
+ return node.token === SYNTAX_KIND.IMPLEMENTS_KEYWORD;
144
+ };
145
+ const isConstructorDeclaration = (node) => {
146
+ return node.kind === SYNTAX_KIND.CONSTRUCTOR;
147
+ };
148
+
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
+ const isResourceWithReadonlyInterface = (type) => {
157
+ if (!isResourceType(type) || !type.symbol?.name) return false;
158
+ if (isIgnoreClass(type.symbol.name)) return false;
159
+ return hasMatchingInterfaceInHierarchy(type);
160
+ };
161
+ const hasMatchingInterfaceInHierarchy = (type) => {
162
+ const processedTypes = /* @__PURE__ */ new Set();
163
+ const checkTypeAndBases = (currentType) => {
164
+ const symbol = currentType.getSymbol?.() ?? currentType.symbol;
165
+ if (!symbol?.name) return false;
166
+ if (processedTypes.has(symbol.name)) return false;
167
+ processedTypes.add(symbol.name);
168
+ const currentClassName = symbol.name;
169
+ if (isIgnoreClass(currentClassName)) return false;
170
+ const directInterfaces = getDirectImplementedInterfaceNames(currentType);
171
+ if (directInterfaces.some(
172
+ (interfaceName) => checkInterfaceMatchClassName(interfaceName, currentClassName)
173
+ )) {
174
+ return true;
175
+ }
176
+ const baseTypes = currentType.getBaseTypes?.() ?? [];
177
+ return baseTypes.some(
178
+ (baseType) => isClassType(baseType) && checkTypeAndBases(baseType)
179
+ );
180
+ };
181
+ return checkTypeAndBases(type);
182
+ };
183
+ const checkInterfaceMatchClassName = (interfaceName, classname) => {
184
+ const simpleInterfaceName = interfaceName.includes(".") ? interfaceName.split(".").pop() ?? interfaceName : interfaceName;
185
+ if (simpleInterfaceName === `I${classname}`) return true;
186
+ const classNameWithoutBase = classname.replace(/^Base|Base$/g, "");
187
+ if (classNameWithoutBase && simpleInterfaceName === `I${classNameWithoutBase}`) {
188
+ return true;
189
+ }
190
+ const baseVMatch = /^(.+)BaseV(\d+)$/.exec(classname);
191
+ if (!baseVMatch) return false;
192
+ const [, baseName, version] = baseVMatch;
193
+ return simpleInterfaceName === `I${baseName}V${version}`;
194
+ };
195
+ const getDirectImplementedInterfaceNames = (type) => {
196
+ const symbol = type.getSymbol?.() ?? type.symbol;
197
+ if (!symbol?.name) return [];
198
+ const declarations = symbol.getDeclarations ? symbol.getDeclarations() : symbol.declarations;
199
+ if (!declarations?.length) return [];
200
+ return declarations.reduce((acc, decl) => {
201
+ if (!isClassDeclaration(decl)) return acc;
202
+ const heritageClauses = decl.heritageClauses;
203
+ if (!heritageClauses) return acc;
204
+ return heritageClauses.reduce((hcAcc, hc) => {
205
+ if (!checkHeritageClauseIsImplements(hc)) return hcAcc;
206
+ return hc.types.reduce((typeAcc, type2) => {
207
+ const expression = type2.expression;
208
+ if (!expression) return typeAcc;
209
+ if (isIdentifier(expression)) return [...typeAcc, expression.text];
210
+ if (!isPropertyAccessExpression(expression)) return typeAcc;
211
+ const namespace = expression.expression;
212
+ const interfaceName = expression.name;
213
+ if (isIdentifier(namespace) && isIdentifier(interfaceName)) {
214
+ return [...typeAcc, `${namespace.text}.${interfaceName.text}`];
215
+ }
216
+ return typeAcc;
217
+ }, []);
218
+ }, []);
219
+ }, []);
220
+ };
221
+ const isIgnoreClass = (className) => {
222
+ return className === "Resource" || className === "Construct";
125
223
  };
126
224
 
127
225
  const noConstructInInterface = createRule({
@@ -146,9 +244,9 @@ const noConstructInInterface = createRule({
146
244
  continue;
147
245
  }
148
246
  const type = parserServices.getTypeAtLocation(property);
149
- if (!isConstructOrStackType(type)) continue;
150
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
151
- if (!isClass) continue;
247
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) {
248
+ continue;
249
+ }
152
250
  context.report({
153
251
  node: property,
154
252
  messageId: "invalidInterfaceProperty",
@@ -206,9 +304,7 @@ const validatePublicPropertyOfConstruct = (node, context, parserServices) => {
206
304
  }
207
305
  if (!property.typeAnnotation) continue;
208
306
  const type = parserServices.getTypeAtLocation(property);
209
- if (!isConstructOrStackType(type)) continue;
210
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
211
- if (!isClass) continue;
307
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
212
308
  context.report({
213
309
  node: property,
214
310
  messageId: "invalidPublicPropertyOfConstruct",
@@ -229,9 +325,7 @@ const validateConstructorParameterProperty = (constructor, context, parserServic
229
325
  }
230
326
  if (!param.parameter.typeAnnotation) continue;
231
327
  const type = parserServices.getTypeAtLocation(param);
232
- if (!isConstructOrStackType(type)) continue;
233
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
234
- if (!isClass) continue;
328
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
235
329
  context.report({
236
330
  node: param,
237
331
  messageId: "invalidPublicPropertyOfConstruct",
@@ -268,12 +362,6 @@ const getConstructorPropertyNames = (type) => {
268
362
  if (!constructor?.parameters.length) return [];
269
363
  return constructor.parameters.map((param) => param.name.getText());
270
364
  };
271
- const isClassDeclaration = (declaration) => {
272
- return declaration.kind === SYNTAX_KIND.CLASS_DECLARATION;
273
- };
274
- const isConstructorDeclaration = (node) => {
275
- return node.kind === SYNTAX_KIND.CONSTRUCTOR;
276
- };
277
365
 
278
366
  const SUFFIX_TYPE = {
279
367
  CONSTRUCT: "Construct",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-cdk-plugin",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "eslint plugin for AWS CDK projects",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export const SYMBOL_FLAGS = {
5
5
  CLASS: 32,
6
+ Interface: 64,
6
7
  } as const;
7
8
 
8
9
  /**
@@ -11,4 +12,7 @@ export const SYMBOL_FLAGS = {
11
12
  export const SYNTAX_KIND = {
12
13
  CLASS_DECLARATION: 263,
13
14
  CONSTRUCTOR: 176,
15
+ IMPLEMENTS_KEYWORD: 119,
16
+ IDENTIFIER: 80,
17
+ PROPERTY_ACCESS_EXPRESSION: 211,
14
18
  } as const;
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  import { createRule } from "../utils/createRule";
10
10
  import { getConstructor } from "../utils/getConstructor";
11
- import { isConstructType } from "../utils/typeCheck";
11
+ import { isConstructType } from "../utils/typecheck/cdk";
12
12
 
13
13
  type Context = TSESLint.RuleContext<
14
14
  | "invalidConstructorProperty"
@@ -1,8 +1,8 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
 
3
- import { SYMBOL_FLAGS } from "../constants/tsInternalFlags";
4
3
  import { createRule } from "../utils/createRule";
5
- import { isConstructOrStackType } from "../utils/typeCheck";
4
+ import { isResourceWithReadonlyInterface } from "../utils/is-resource-with-readonly-interface";
5
+ import { isClassType } from "../utils/typecheck/ts-type";
6
6
 
7
7
  /**
8
8
  * Enforces the use of interface types instead of CDK Construct types in interface properties
@@ -28,7 +28,6 @@ export const noConstructInInterface = createRule({
28
28
  return {
29
29
  TSInterfaceDeclaration(node) {
30
30
  for (const property of node.body.body) {
31
- // NOTE: check property signature
32
31
  if (
33
32
  property.type !== AST_NODE_TYPES.TSPropertySignature ||
34
33
  property.key.type !== AST_NODE_TYPES.Identifier
@@ -37,13 +36,9 @@ export const noConstructInInterface = createRule({
37
36
  }
38
37
 
39
38
  const type = parserServices.getTypeAtLocation(property);
40
- if (!isConstructOrStackType(type)) continue;
41
-
42
- // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
43
- // Therefore, the type information structures do not match.
44
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
45
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
46
- if (!isClass) continue;
39
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) {
40
+ continue;
41
+ }
47
42
 
48
43
  context.report({
49
44
  node: property,
@@ -6,10 +6,11 @@ import {
6
6
  TSESTree,
7
7
  } from "@typescript-eslint/utils";
8
8
 
9
- import { SYMBOL_FLAGS } from "../constants/tsInternalFlags";
10
9
  import { createRule } from "../utils/createRule";
11
10
  import { getConstructor } from "../utils/getConstructor";
12
- import { isConstructOrStackType } from "../utils/typeCheck";
11
+ import { isResourceWithReadonlyInterface } from "../utils/is-resource-with-readonly-interface";
12
+ import { isConstructOrStackType } from "../utils/typecheck/cdk";
13
+ import { isClassType } from "../utils/typecheck/ts-type";
13
14
 
14
15
  type Context = TSESLint.RuleContext<"invalidPublicPropertyOfConstruct", []>;
15
16
 
@@ -87,13 +88,7 @@ const validatePublicPropertyOfConstruct = (
87
88
  if (!property.typeAnnotation) continue;
88
89
 
89
90
  const type = parserServices.getTypeAtLocation(property);
90
- if (!isConstructOrStackType(type)) continue;
91
-
92
- // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
93
- // Therefore, the type information structures do not match.
94
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
95
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
96
- if (!isClass) continue;
91
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
97
92
 
98
93
  context.report({
99
94
  node: property,
@@ -132,13 +127,7 @@ const validateConstructorParameterProperty = (
132
127
  if (!param.parameter.typeAnnotation) continue;
133
128
 
134
129
  const type = parserServices.getTypeAtLocation(param);
135
- if (!isConstructOrStackType(type)) continue;
136
-
137
- // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
138
- // Therefore, the type information structures do not match.
139
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
140
- const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
141
- if (!isClass) continue;
130
+ if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
142
131
 
143
132
  context.report({
144
133
  node: param,
@@ -8,7 +8,7 @@ import {
8
8
  import { toPascalCase } from "../utils/convertString";
9
9
  import { createRule } from "../utils/createRule";
10
10
  import { getConstructorPropertyNames } from "../utils/getPropertyNames";
11
- import { isConstructOrStackType } from "../utils/typeCheck";
11
+ import { isConstructOrStackType } from "../utils/typecheck/cdk";
12
12
 
13
13
  const SUFFIX_TYPE = {
14
14
  CONSTRUCT: "Construct",
@@ -1,7 +1,7 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
 
3
3
  import { createRule } from "../utils/createRule";
4
- import { isConstructOrStackType } from "../utils/typeCheck";
4
+ import { isConstructOrStackType } from "../utils/typecheck/cdk";
5
5
 
6
6
  /**
7
7
  * Disallow mutable public properties of Construct
@@ -8,7 +8,10 @@ import {
8
8
 
9
9
  import { toPascalCase } from "../utils/convertString";
10
10
  import { createRule } from "../utils/createRule";
11
- import { isConstructOrStackType, isConstructType } from "../utils/typeCheck";
11
+ import {
12
+ isConstructOrStackType,
13
+ isConstructType,
14
+ } from "../utils/typecheck/cdk";
12
15
 
13
16
  type Options = [
14
17
  {
@@ -12,7 +12,7 @@ import {
12
12
  IPropsUsageTracker,
13
13
  propsUsageTrackerFactory,
14
14
  } from "../utils/propsUsageTracker";
15
- import { isConstructType } from "../utils/typeCheck";
15
+ import { isConstructType } from "../utils/typecheck/cdk";
16
16
 
17
17
  type Context = TSESLint.RuleContext<"unusedProp", []>;
18
18
 
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  import { createRule } from "../utils/createRule";
9
9
  import { getConstructorPropertyNames } from "../utils/getPropertyNames";
10
- import { isConstructType } from "../utils/typeCheck";
10
+ import { isConstructType } from "../utils/typecheck/cdk";
11
11
 
12
12
  type Context = TSESLint.RuleContext<"invalidConstructId", []>;
13
13
 
@@ -8,7 +8,7 @@ import {
8
8
  import { toPascalCase } from "../utils/convertString";
9
9
  import { createRule } from "../utils/createRule";
10
10
  import { getConstructorPropertyNames } from "../utils/getPropertyNames";
11
- import { isConstructOrStackType } from "../utils/typeCheck";
11
+ import { isConstructOrStackType } from "../utils/typecheck/cdk";
12
12
 
13
13
  const QUOTE_TYPE = {
14
14
  SINGLE: "'",
@@ -2,7 +2,7 @@ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
 
3
3
  import { createRule } from "../utils/createRule";
4
4
  import { getConstructor } from "../utils/getConstructor";
5
- import { isConstructType } from "../utils/typeCheck";
5
+ import { isConstructType } from "../utils/typecheck/cdk";
6
6
 
7
7
  /**
8
8
  * Enforces a naming convention for props interfaces in Construct classes
@@ -5,7 +5,7 @@ import {
5
5
  } from "@typescript-eslint/utils";
6
6
 
7
7
  import { createRule } from "../utils/createRule";
8
- import { isConstructType } from "../utils/typeCheck";
8
+ import { isConstructType } from "../utils/typecheck/cdk";
9
9
 
10
10
  /**
11
11
  * Require JSDoc comments for interface properties and public properties in Construct classes
@@ -6,7 +6,7 @@ import {
6
6
 
7
7
  import { createRule } from "../utils/createRule";
8
8
  import { getConstructorPropertyNames } from "../utils/getPropertyNames";
9
- import { isConstructType } from "../utils/typeCheck";
9
+ import { isConstructType } from "../utils/typecheck/cdk";
10
10
 
11
11
  type Options = [
12
12
  {
@@ -1,13 +1,10 @@
1
1
  import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
2
- import {
3
- ClassDeclaration,
4
- ConstructorDeclaration,
5
- Declaration,
6
- Node,
7
- Type,
8
- } from "typescript";
2
+ import { Type } from "typescript";
9
3
 
10
- import { SYNTAX_KIND } from "../constants/tsInternalFlags";
4
+ import {
5
+ isClassDeclaration,
6
+ isConstructorDeclaration,
7
+ } from "./typecheck/ts-node";
11
8
 
12
9
  /**
13
10
  * Retrieves the property names from an array of properties.
@@ -46,27 +43,3 @@ export const getConstructorPropertyNames = (type: Type): string[] => {
46
43
 
47
44
  return constructor.parameters.map((param) => param.name.getText());
48
45
  };
49
-
50
- /**
51
- * Implementing `isClassDeclaration` defined in typescript on your own, in order not to include TypeScript in dependencies
52
- */
53
- const isClassDeclaration = (
54
- declaration: Declaration
55
- ): declaration is ClassDeclaration => {
56
- // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
57
- // Therefore, the type information structures do not match.
58
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
59
- return declaration.kind === SYNTAX_KIND.CLASS_DECLARATION;
60
- };
61
-
62
- /**
63
- * Implementing `isConstructorDeclaration` defined in typescript on your own, in order not to include TypeScript in dependencies
64
- */
65
- const isConstructorDeclaration = (
66
- node: Node
67
- ): node is ConstructorDeclaration => {
68
- // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
69
- // Therefore, the type information structures do not match.
70
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
71
- return node.kind === SYNTAX_KIND.CONSTRUCTOR;
72
- };
@@ -0,0 +1,165 @@
1
+ import { Type } from "typescript";
2
+
3
+ import { isResourceType } from "./typecheck/cdk";
4
+ import {
5
+ checkHeritageClauseIsImplements,
6
+ isClassDeclaration,
7
+ isIdentifier,
8
+ isPropertyAccessExpression,
9
+ } from "./typecheck/ts-node";
10
+ import { isClassType } from "./typecheck/ts-type";
11
+
12
+ /**
13
+ * Checks if the type is an AWS resource Construct that implements a read-only resource interface
14
+ *
15
+ * This function validates that:
16
+ * 1. The type extends from Resource (AWS CDK resource)
17
+ * 2. The type or any of its base classes implements an interface following CDK's read-only interface naming convention
18
+ * - Pattern 1: Class name with I prefix (e.g., Bucket -> IBucket)
19
+ * - Pattern 2: Class name without Base suffix/prefix with I prefix (e.g., BucketBase -> IBucket, BaseService -> IService)
20
+ *
21
+ * @param type - The TypeScript type to check
22
+ * @returns true if the type is a resource Construct with a matching read-only interface, false otherwise
23
+ *
24
+ * @example
25
+ * // Returns true for:
26
+ * class Bucket extends Resource implements IBucket { ... }
27
+ * class BucketBase extends Resource implements IBucket { ... }
28
+ * class BaseService extends Resource implements IService { ... }
29
+ * class TableBaseV2 extends Resource implements ITableV2 { ... }
30
+ * class S3OriginAccessControl extends OriginAccessControlBase { ... } // where OriginAccessControlBase implements IOriginAccessControl
31
+ *
32
+ * // Returns false for:
33
+ * class CustomResource extends Resource { ... } // No matching interface
34
+ * class EdgeFunction extends Resource implements IVersion { ... } // Interface doesn't match naming pattern
35
+ */
36
+ export const isResourceWithReadonlyInterface = (type: Type): boolean => {
37
+ if (!isResourceType(type) || !type.symbol?.name) return false;
38
+ if (isIgnoreClass(type.symbol.name)) return false;
39
+ return hasMatchingInterfaceInHierarchy(type);
40
+ };
41
+
42
+ /**
43
+ * Checks if a type or any of its base classes implements an interface matching its class name
44
+ * @param type - The TypeScript type to check
45
+ * @returns true if any class in the hierarchy implements a matching interface
46
+ * @private
47
+ */
48
+ const hasMatchingInterfaceInHierarchy = (type: Type): boolean => {
49
+ const processedTypes = new Set<string>();
50
+
51
+ const checkTypeAndBases = (currentType: Type): boolean => {
52
+ const symbol = currentType.getSymbol?.() ?? currentType.symbol;
53
+ if (!symbol?.name) return false;
54
+
55
+ // Skip if already processed
56
+ if (processedTypes.has(symbol.name)) return false;
57
+ processedTypes.add(symbol.name);
58
+
59
+ const currentClassName = symbol.name;
60
+ if (isIgnoreClass(currentClassName)) return false;
61
+
62
+ const directInterfaces = getDirectImplementedInterfaceNames(currentType);
63
+
64
+ // NOTE: Check if any interface matches this class name
65
+ if (
66
+ directInterfaces.some((interfaceName) =>
67
+ checkInterfaceMatchClassName(interfaceName, currentClassName)
68
+ )
69
+ ) {
70
+ return true;
71
+ }
72
+
73
+ const baseTypes = currentType.getBaseTypes?.() ?? [];
74
+ return baseTypes.some(
75
+ (baseType) => isClassType(baseType) && checkTypeAndBases(baseType)
76
+ );
77
+ };
78
+
79
+ return checkTypeAndBases(type);
80
+ };
81
+
82
+ /**
83
+ * Checks if the provided interface name matches the class name according to specific patterns
84
+ *
85
+ * Patterns:
86
+ * 1. Class name with I prefix (e.g., Bucket -> IBucket)
87
+ * 2. Class name without Base suffix/prefix with I prefix (e.g., BucketBase -> IBucket, BaseService -> IService)
88
+ * 3. Class name with BaseV{number} suffix with I prefix (e.g., TableBaseV2 -> ITableV2)
89
+ *
90
+ * @param interfaceName - The name of the interface to check
91
+ * @param classname - The name of the class to compare against
92
+ * @returns boolean - true if the interface name matches the class name patterns, false otherwise
93
+ */
94
+ const checkInterfaceMatchClassName = (
95
+ interfaceName: string,
96
+ classname: string
97
+ ) => {
98
+ const simpleInterfaceName = interfaceName.includes(".")
99
+ ? interfaceName.split(".").pop() ?? interfaceName
100
+ : interfaceName;
101
+
102
+ // Pattern 1: Class name with I prefix
103
+ if (simpleInterfaceName === `I${classname}`) return true;
104
+
105
+ // Pattern 2: Class name without Base suffix/prefix with I prefix
106
+ const classNameWithoutBase = classname.replace(/^Base|Base$/g, "");
107
+ if (
108
+ classNameWithoutBase &&
109
+ simpleInterfaceName === `I${classNameWithoutBase}`
110
+ ) {
111
+ return true;
112
+ }
113
+
114
+ // Pattern 3: Class name with BaseV{number} suffix with I prefix (e.g., TableBaseV2 -> ITableV2)
115
+ const baseVMatch = /^(.+)BaseV(\d+)$/.exec(classname);
116
+ if (!baseVMatch) return false;
117
+ const [, baseName, version] = baseVMatch;
118
+ return simpleInterfaceName === `I${baseName}V${version}`;
119
+ };
120
+
121
+ /**
122
+ * Retrieves interface names directly implemented by a type (not including inherited interfaces)
123
+ * @param type - The TypeScript type to analyze
124
+ * @returns Array of interface names directly implemented by this type
125
+ * @private
126
+ */
127
+ const getDirectImplementedInterfaceNames = (type: Type): string[] => {
128
+ const symbol = type.getSymbol?.() ?? type.symbol;
129
+ if (!symbol?.name) return [];
130
+
131
+ const declarations = symbol.getDeclarations
132
+ ? symbol.getDeclarations()
133
+ : symbol.declarations;
134
+ if (!declarations?.length) return [];
135
+
136
+ return declarations.reduce<string[]>((acc, decl) => {
137
+ if (!isClassDeclaration(decl)) return acc;
138
+
139
+ const heritageClauses = decl.heritageClauses;
140
+ if (!heritageClauses) return acc;
141
+
142
+ return heritageClauses.reduce<string[]>((hcAcc, hc) => {
143
+ if (!checkHeritageClauseIsImplements(hc)) return hcAcc;
144
+
145
+ return hc.types.reduce<string[]>((typeAcc, type) => {
146
+ const expression = type.expression;
147
+ if (!expression) return typeAcc;
148
+ if (isIdentifier(expression)) return [...typeAcc, expression.text];
149
+ if (!isPropertyAccessExpression(expression)) return typeAcc;
150
+
151
+ const namespace = expression.expression;
152
+ const interfaceName = expression.name;
153
+ if (isIdentifier(namespace) && isIdentifier(interfaceName)) {
154
+ return [...typeAcc, `${namespace.text}.${interfaceName.text}`];
155
+ }
156
+
157
+ return typeAcc;
158
+ }, []);
159
+ }, []);
160
+ }, []);
161
+ };
162
+
163
+ const isIgnoreClass = (className: string): boolean => {
164
+ return className === "Resource" || className === "Construct";
165
+ };
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typescript";
2
2
 
3
- type SuperClassType = "Construct" | "Stack";
3
+ type SuperClassType = "Construct" | "Stack" | "Resource";
4
4
 
5
5
  /**
6
6
  * Check if the type extends Construct or Stack
@@ -39,6 +39,14 @@ export const isConstructType = (
39
39
  return isTargetSuperClassType(type, ["Construct"], isConstructType);
40
40
  };
41
41
 
42
+ export const isResourceType = (
43
+ type: Type,
44
+ ignoredClasses: readonly string[] = [] // App, Stage, CfnOutput, Stack are not extended Resource, so no need to ignore them
45
+ ): boolean => {
46
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
47
+ return isTargetSuperClassType(type, ["Resource"], isResourceType);
48
+ };
49
+
42
50
  /**
43
51
  * Check if the type extends target super class
44
52
  * @param type - The type to check
@@ -0,0 +1,42 @@
1
+ import {
2
+ ClassDeclaration,
3
+ ConstructorDeclaration,
4
+ HeritageClause,
5
+ Identifier,
6
+ Node,
7
+ PropertyAccessExpression,
8
+ } from "typescript";
9
+
10
+ import { SYNTAX_KIND } from "../../constants/tsInternalFlags";
11
+
12
+ // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
13
+ // Therefore, the type information structures do not match.
14
+ /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
15
+
16
+ // NOTE: Implementing type check method defined in typescript on your own, in order not to include TypeScript in dependencies
17
+
18
+ export const isClassDeclaration = (node: Node): node is ClassDeclaration => {
19
+ return node.kind === SYNTAX_KIND.CLASS_DECLARATION;
20
+ };
21
+
22
+ export const isIdentifier = (node: Node): node is Identifier => {
23
+ return node.kind === SYNTAX_KIND.IDENTIFIER;
24
+ };
25
+
26
+ export const isPropertyAccessExpression = (
27
+ node: Node
28
+ ): node is PropertyAccessExpression => {
29
+ return node.kind === SYNTAX_KIND.PROPERTY_ACCESS_EXPRESSION;
30
+ };
31
+
32
+ export const checkHeritageClauseIsImplements = (
33
+ node: HeritageClause
34
+ ): boolean => {
35
+ return node.token === SYNTAX_KIND.IMPLEMENTS_KEYWORD;
36
+ };
37
+
38
+ export const isConstructorDeclaration = (
39
+ node: Node
40
+ ): node is ConstructorDeclaration => {
41
+ return node.kind === SYNTAX_KIND.CONSTRUCTOR;
42
+ };
@@ -0,0 +1,15 @@
1
+ import { Symbol, Type } from "typescript";
2
+
3
+ import { SYMBOL_FLAGS } from "../../constants/tsInternalFlags";
4
+
5
+ // NOTE: In order not to make it dependent on the typescript library, it defines its own unions.
6
+ // Therefore, the type information structures do not match.
7
+ /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
8
+
9
+ const getSymbol = (type: Type): Symbol | undefined => {
10
+ return type.getSymbol?.() ?? type.symbol;
11
+ };
12
+
13
+ export const isClassType = (type: Type): boolean => {
14
+ return getSymbol(type)?.flags === SYMBOL_FLAGS.CLASS;
15
+ };