eslint-cdk-plugin 3.2.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 +116 -22
- package/dist/index.mjs +116 -22
- package/package.json +1 -1
- package/src/constants/tsInternalFlags.ts +4 -0
- package/src/rules/construct-constructor-property.ts +1 -1
- package/src/rules/no-construct-in-interface.ts +5 -10
- package/src/rules/no-construct-in-public-property-of-construct.ts +5 -16
- package/src/rules/no-construct-stack-suffix.ts +1 -1
- package/src/rules/no-mutable-public-property-of-construct.ts +1 -1
- package/src/rules/no-parent-name-construct-id-match.ts +4 -1
- package/src/rules/no-unused-props.ts +1 -1
- package/src/rules/no-variable-construct-id.ts +1 -1
- package/src/rules/pascal-case-construct-id.ts +1 -1
- package/src/rules/props-name-convention.ts +1 -1
- package/src/rules/require-jsdoc.ts +3 -2
- package/src/rules/require-passing-this.ts +1 -1
- package/src/utils/getPropertyNames.ts +5 -32
- package/src/utils/is-resource-with-readonly-interface.ts +165 -0
- package/src/utils/{typeCheck.ts → typecheck/cdk.ts} +16 -3
- package/src/utils/typecheck/ts-node.ts +42 -0
- package/src/utils/typecheck/ts-type.ts +15 -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.
|
|
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}`
|
|
@@ -38,7 +38,7 @@ const getConstructor = (node) => {
|
|
|
38
38
|
);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
|
|
41
|
+
const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage", "CfnOutput"]) => {
|
|
42
42
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
43
43
|
return isTargetSuperClassType(
|
|
44
44
|
type,
|
|
@@ -46,10 +46,19 @@ const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
|
|
|
46
46
|
isConstructOrStackType
|
|
47
47
|
);
|
|
48
48
|
};
|
|
49
|
-
const isConstructType = (type, ignoredClasses = [
|
|
49
|
+
const isConstructType = (type, ignoredClasses = [
|
|
50
|
+
"App",
|
|
51
|
+
"Stage",
|
|
52
|
+
"CfnOutput",
|
|
53
|
+
"Stack"
|
|
54
|
+
]) => {
|
|
50
55
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
51
56
|
return isTargetSuperClassType(type, ["Construct"], isConstructType);
|
|
52
57
|
};
|
|
58
|
+
const isResourceType = (type, ignoredClasses = []) => {
|
|
59
|
+
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
60
|
+
return isTargetSuperClassType(type, ["Resource"], isResourceType);
|
|
61
|
+
};
|
|
53
62
|
const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
|
|
54
63
|
if (!type.symbol) return false;
|
|
55
64
|
if (targetSuperClasses.some((suffix) => type.symbol.name === suffix)) {
|
|
@@ -135,11 +144,105 @@ const validateConstructorProperty = (constructor, context, parserServices) => {
|
|
|
135
144
|
};
|
|
136
145
|
|
|
137
146
|
const SYMBOL_FLAGS = {
|
|
138
|
-
CLASS: 32
|
|
139
|
-
};
|
|
147
|
+
CLASS: 32};
|
|
140
148
|
const SYNTAX_KIND = {
|
|
141
149
|
CLASS_DECLARATION: 263,
|
|
142
|
-
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";
|
|
143
246
|
};
|
|
144
247
|
|
|
145
248
|
const noConstructInInterface = createRule({
|
|
@@ -164,9 +267,9 @@ const noConstructInInterface = createRule({
|
|
|
164
267
|
continue;
|
|
165
268
|
}
|
|
166
269
|
const type = parserServices.getTypeAtLocation(property);
|
|
167
|
-
if (!
|
|
168
|
-
|
|
169
|
-
|
|
270
|
+
if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
170
273
|
context.report({
|
|
171
274
|
node: property,
|
|
172
275
|
messageId: "invalidInterfaceProperty",
|
|
@@ -224,9 +327,7 @@ const validatePublicPropertyOfConstruct = (node, context, parserServices) => {
|
|
|
224
327
|
}
|
|
225
328
|
if (!property.typeAnnotation) continue;
|
|
226
329
|
const type = parserServices.getTypeAtLocation(property);
|
|
227
|
-
if (!
|
|
228
|
-
const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
|
|
229
|
-
if (!isClass) continue;
|
|
330
|
+
if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
|
|
230
331
|
context.report({
|
|
231
332
|
node: property,
|
|
232
333
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
@@ -247,9 +348,7 @@ const validateConstructorParameterProperty = (constructor, context, parserServic
|
|
|
247
348
|
}
|
|
248
349
|
if (!param.parameter.typeAnnotation) continue;
|
|
249
350
|
const type = parserServices.getTypeAtLocation(param);
|
|
250
|
-
if (!
|
|
251
|
-
const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
|
|
252
|
-
if (!isClass) continue;
|
|
351
|
+
if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
|
|
253
352
|
context.report({
|
|
254
353
|
node: param,
|
|
255
354
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
@@ -286,12 +385,6 @@ const getConstructorPropertyNames = (type) => {
|
|
|
286
385
|
if (!constructor?.parameters.length) return [];
|
|
287
386
|
return constructor.parameters.map((param) => param.name.getText());
|
|
288
387
|
};
|
|
289
|
-
const isClassDeclaration = (declaration) => {
|
|
290
|
-
return declaration.kind === SYNTAX_KIND.CLASS_DECLARATION;
|
|
291
|
-
};
|
|
292
|
-
const isConstructorDeclaration = (node) => {
|
|
293
|
-
return node.kind === SYNTAX_KIND.CONSTRUCTOR;
|
|
294
|
-
};
|
|
295
388
|
|
|
296
389
|
const SUFFIX_TYPE = {
|
|
297
390
|
CONSTRUCT: "Construct",
|
|
@@ -1174,7 +1267,8 @@ const requireJSDoc = createRule({
|
|
|
1174
1267
|
return;
|
|
1175
1268
|
}
|
|
1176
1269
|
const classType = parserServices.getTypeAtLocation(classDeclaration);
|
|
1177
|
-
|
|
1270
|
+
const accessibility = node.accessibility ?? "public";
|
|
1271
|
+
if (!isConstructType(classType) || accessibility !== "public") {
|
|
1178
1272
|
return;
|
|
1179
1273
|
}
|
|
1180
1274
|
const sourceCode = context.sourceCode;
|
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.
|
|
6
|
+
var version = "3.4.0";
|
|
7
7
|
|
|
8
8
|
const createRule = ESLintUtils.RuleCreator(
|
|
9
9
|
(name) => `https://eslint-cdk-plugin.dev/rules/${name}`
|
|
@@ -15,7 +15,7 @@ const getConstructor = (node) => {
|
|
|
15
15
|
);
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
|
|
18
|
+
const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage", "CfnOutput"]) => {
|
|
19
19
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
20
20
|
return isTargetSuperClassType(
|
|
21
21
|
type,
|
|
@@ -23,10 +23,19 @@ const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
|
|
|
23
23
|
isConstructOrStackType
|
|
24
24
|
);
|
|
25
25
|
};
|
|
26
|
-
const isConstructType = (type, ignoredClasses = [
|
|
26
|
+
const isConstructType = (type, ignoredClasses = [
|
|
27
|
+
"App",
|
|
28
|
+
"Stage",
|
|
29
|
+
"CfnOutput",
|
|
30
|
+
"Stack"
|
|
31
|
+
]) => {
|
|
27
32
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
28
33
|
return isTargetSuperClassType(type, ["Construct"], isConstructType);
|
|
29
34
|
};
|
|
35
|
+
const isResourceType = (type, ignoredClasses = []) => {
|
|
36
|
+
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
37
|
+
return isTargetSuperClassType(type, ["Resource"], isResourceType);
|
|
38
|
+
};
|
|
30
39
|
const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
|
|
31
40
|
if (!type.symbol) return false;
|
|
32
41
|
if (targetSuperClasses.some((suffix) => type.symbol.name === suffix)) {
|
|
@@ -112,11 +121,105 @@ const validateConstructorProperty = (constructor, context, parserServices) => {
|
|
|
112
121
|
};
|
|
113
122
|
|
|
114
123
|
const SYMBOL_FLAGS = {
|
|
115
|
-
CLASS: 32
|
|
116
|
-
};
|
|
124
|
+
CLASS: 32};
|
|
117
125
|
const SYNTAX_KIND = {
|
|
118
126
|
CLASS_DECLARATION: 263,
|
|
119
|
-
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";
|
|
120
223
|
};
|
|
121
224
|
|
|
122
225
|
const noConstructInInterface = createRule({
|
|
@@ -141,9 +244,9 @@ const noConstructInInterface = createRule({
|
|
|
141
244
|
continue;
|
|
142
245
|
}
|
|
143
246
|
const type = parserServices.getTypeAtLocation(property);
|
|
144
|
-
if (!
|
|
145
|
-
|
|
146
|
-
|
|
247
|
+
if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
147
250
|
context.report({
|
|
148
251
|
node: property,
|
|
149
252
|
messageId: "invalidInterfaceProperty",
|
|
@@ -201,9 +304,7 @@ const validatePublicPropertyOfConstruct = (node, context, parserServices) => {
|
|
|
201
304
|
}
|
|
202
305
|
if (!property.typeAnnotation) continue;
|
|
203
306
|
const type = parserServices.getTypeAtLocation(property);
|
|
204
|
-
if (!
|
|
205
|
-
const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
|
|
206
|
-
if (!isClass) continue;
|
|
307
|
+
if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
|
|
207
308
|
context.report({
|
|
208
309
|
node: property,
|
|
209
310
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
@@ -224,9 +325,7 @@ const validateConstructorParameterProperty = (constructor, context, parserServic
|
|
|
224
325
|
}
|
|
225
326
|
if (!param.parameter.typeAnnotation) continue;
|
|
226
327
|
const type = parserServices.getTypeAtLocation(param);
|
|
227
|
-
if (!
|
|
228
|
-
const isClass = type.symbol.flags === SYMBOL_FLAGS.CLASS;
|
|
229
|
-
if (!isClass) continue;
|
|
328
|
+
if (!isClassType(type) || !isResourceWithReadonlyInterface(type)) continue;
|
|
230
329
|
context.report({
|
|
231
330
|
node: param,
|
|
232
331
|
messageId: "invalidPublicPropertyOfConstruct",
|
|
@@ -263,12 +362,6 @@ const getConstructorPropertyNames = (type) => {
|
|
|
263
362
|
if (!constructor?.parameters.length) return [];
|
|
264
363
|
return constructor.parameters.map((param) => param.name.getText());
|
|
265
364
|
};
|
|
266
|
-
const isClassDeclaration = (declaration) => {
|
|
267
|
-
return declaration.kind === SYNTAX_KIND.CLASS_DECLARATION;
|
|
268
|
-
};
|
|
269
|
-
const isConstructorDeclaration = (node) => {
|
|
270
|
-
return node.kind === SYNTAX_KIND.CONSTRUCTOR;
|
|
271
|
-
};
|
|
272
365
|
|
|
273
366
|
const SUFFIX_TYPE = {
|
|
274
367
|
CONSTRUCT: "Construct",
|
|
@@ -1151,7 +1244,8 @@ const requireJSDoc = createRule({
|
|
|
1151
1244
|
return;
|
|
1152
1245
|
}
|
|
1153
1246
|
const classType = parserServices.getTypeAtLocation(classDeclaration);
|
|
1154
|
-
|
|
1247
|
+
const accessibility = node.accessibility ?? "public";
|
|
1248
|
+
if (!isConstructType(classType) || accessibility !== "public") {
|
|
1155
1249
|
return;
|
|
1156
1250
|
}
|
|
1157
1251
|
const sourceCode = context.sourceCode;
|
package/package.json
CHANGED
|
@@ -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/
|
|
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 {
|
|
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 (!
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|
|
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 (!
|
|
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 (!
|
|
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/
|
|
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/
|
|
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 {
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
@@ -77,7 +77,8 @@ export const requireJSDoc = createRule({
|
|
|
77
77
|
|
|
78
78
|
// NOTE: Check if the class extends Construct and the property is public
|
|
79
79
|
const classType = parserServices.getTypeAtLocation(classDeclaration);
|
|
80
|
-
|
|
80
|
+
const accessibility = node.accessibility ?? "public";
|
|
81
|
+
if (!isConstructType(classType) || accessibility !== "public") {
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -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/
|
|
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 {
|
|
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
|
|
@@ -10,7 +10,7 @@ type SuperClassType = "Construct" | "Stack";
|
|
|
10
10
|
*/
|
|
11
11
|
export const isConstructOrStackType = (
|
|
12
12
|
type: Type,
|
|
13
|
-
ignoredClasses: readonly string[] = ["App", "Stage"] as const
|
|
13
|
+
ignoredClasses: readonly string[] = ["App", "Stage", "CfnOutput"] as const
|
|
14
14
|
): boolean => {
|
|
15
15
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
16
16
|
return isTargetSuperClassType(
|
|
@@ -28,12 +28,25 @@ export const isConstructOrStackType = (
|
|
|
28
28
|
*/
|
|
29
29
|
export const isConstructType = (
|
|
30
30
|
type: Type,
|
|
31
|
-
ignoredClasses: readonly string[] = [
|
|
31
|
+
ignoredClasses: readonly string[] = [
|
|
32
|
+
"App",
|
|
33
|
+
"Stage",
|
|
34
|
+
"CfnOutput",
|
|
35
|
+
"Stack",
|
|
36
|
+
] as const
|
|
32
37
|
): boolean => {
|
|
33
38
|
if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
|
|
34
39
|
return isTargetSuperClassType(type, ["Construct"], isConstructType);
|
|
35
40
|
};
|
|
36
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
|
+
|
|
37
50
|
/**
|
|
38
51
|
* Check if the type extends target super class
|
|
39
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
|
+
};
|