eslint-plugin-class-validator-type-match 1.3.0 → 1.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.d.ts +1 -1
- package/dist/rules/decorator-type-match.d.ts +10 -152
- package/dist/rules/decorator-type-match.js +349 -248
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
2
|
rules: {
|
|
3
|
-
'decorator-type-match': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"mismatch" | "nestedArrayMismatch" | "missingValidateNested" | "enumMismatch" | "typeMismatch" | "missingEachOption" | "unnecessaryValidateNested", [], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
|
|
3
|
+
'decorator-type-match': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"mismatch" | "nestedArrayMismatch" | "missingValidateNested" | "enumMismatch" | "typeMismatch" | "missingEachOption" | "unnecessaryValidateNested" | "invalidEachOption" | "missingTypeDecorator" | "tupleValidationWarning", [], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
|
|
4
4
|
'optional-decorator-match': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingOptionalDecorator" | "missingOptionalSyntax" | "conflictingDefiniteAssignment" | "undefinedUnionWithoutDecorator" | "undefinedUnionWithoutOptional" | "nullUnionIncorrect" | "redundantUndefinedInType", [{
|
|
5
5
|
strictNullChecks?: boolean;
|
|
6
6
|
checkDefaultValues?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageIds = 'mismatch' | 'nestedArrayMismatch' | 'missingValidateNested' | 'enumMismatch' | 'typeMismatch' | 'missingEachOption' | 'unnecessaryValidateNested';
|
|
2
|
+
type MessageIds = 'mismatch' | 'nestedArrayMismatch' | 'missingValidateNested' | 'enumMismatch' | 'typeMismatch' | 'missingEachOption' | 'unnecessaryValidateNested' | 'invalidEachOption' | 'missingTypeDecorator' | 'tupleValidationWarning';
|
|
3
3
|
/**
|
|
4
4
|
* ESLint rule to ensure class-validator decorators match TypeScript type annotations.
|
|
5
5
|
*
|
|
@@ -10,167 +10,25 @@ type MessageIds = 'mismatch' | 'nestedArrayMismatch' | 'missingValidateNested' |
|
|
|
10
10
|
* - Arrays of objects requiring @ValidateNested({ each: true })
|
|
11
11
|
* - Nested objects requiring @ValidateNested()
|
|
12
12
|
* - Nullable complex types (Address | null, Address | undefined)
|
|
13
|
-
* -
|
|
13
|
+
* - Nested arrays (Address[][])
|
|
14
14
|
* - Type literals
|
|
15
15
|
* - class-transformer decorators
|
|
16
16
|
* - Enum types (both TypeScript enums and union types)
|
|
17
17
|
* - Literal types (e.g., "admin", 25)
|
|
18
18
|
* - Intersection types (e.g., Profile & Settings)
|
|
19
|
+
* - Branded types (string & { __brand: 'UserId' })
|
|
19
20
|
* - @Type(() => ClassName) decorator matching
|
|
20
21
|
* - Readonly arrays (readonly T[])
|
|
21
22
|
* - Tuple types ([T, U])
|
|
22
23
|
* - Nullable unions (T | null | undefined)
|
|
23
24
|
* - Unnecessary @ValidateNested on primitive arrays
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* // ✅ Good - types match
|
|
34
|
-
* class User {
|
|
35
|
-
* @IsString()
|
|
36
|
-
* name!: string;
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* // ❌ Bad - @Type doesn't match TypeScript type
|
|
41
|
-
* class User {
|
|
42
|
-
* @ValidateNested()
|
|
43
|
-
* @Type(() => Address)
|
|
44
|
-
* address: string;
|
|
45
|
-
* }
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* // ✅ Good - @Type matches TypeScript type
|
|
49
|
-
* class User {
|
|
50
|
-
* @ValidateNested()
|
|
51
|
-
* @Type(() => Address)
|
|
52
|
-
* address: Address;
|
|
53
|
-
* }
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* // ❌ Bad - array of objects without @ValidateNested
|
|
57
|
-
* class User {
|
|
58
|
-
* @IsArray()
|
|
59
|
-
* addresses!: Address[];
|
|
60
|
-
* }
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* // ✅ Good - array of objects with @ValidateNested
|
|
64
|
-
* class User {
|
|
65
|
-
* @IsArray()
|
|
66
|
-
* @ValidateNested({ each: true })
|
|
67
|
-
* @Type(() => Address)
|
|
68
|
-
* addresses!: Address[];
|
|
69
|
-
* }
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* // ✅ Good - array of primitives without @ValidateNested
|
|
73
|
-
* class User {
|
|
74
|
-
* @IsArray()
|
|
75
|
-
* @IsString({ each: true })
|
|
76
|
-
* tags!: string[];
|
|
77
|
-
* }
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* // ❌ Bad - array of primitives with unnecessary @ValidateNested
|
|
81
|
-
* class User {
|
|
82
|
-
* @IsArray()
|
|
83
|
-
* @ValidateNested({ each: true })
|
|
84
|
-
* @IsString({ each: true })
|
|
85
|
-
* tags!: string[];
|
|
86
|
-
* }
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* // ❌ Bad - complex type without @ValidateNested
|
|
90
|
-
* class User {
|
|
91
|
-
* @IsDefined()
|
|
92
|
-
* profile!: Profile;
|
|
93
|
-
* }
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* // ✅ Good - complex type with @ValidateNested
|
|
97
|
-
* class User {
|
|
98
|
-
* @ValidateNested()
|
|
99
|
-
* @Type(() => Profile)
|
|
100
|
-
* profile!: Profile;
|
|
101
|
-
* }
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* // ❌ Bad - intersection type without @ValidateNested
|
|
105
|
-
* class User {
|
|
106
|
-
* @IsDefined()
|
|
107
|
-
* data!: Profile & Settings;
|
|
108
|
-
* }
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* // ✅ Good - intersection type with @ValidateNested
|
|
112
|
-
* class User {
|
|
113
|
-
* @ValidateNested()
|
|
114
|
-
* data!: Profile & Settings;
|
|
115
|
-
* }
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* // ✅ Good - literal type
|
|
119
|
-
* class User {
|
|
120
|
-
* @IsString()
|
|
121
|
-
* role!: "admin";
|
|
122
|
-
* }
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* // ❌ Bad - @IsEnum with wrong enum type
|
|
126
|
-
* class User {
|
|
127
|
-
* @IsEnum(UserRole)
|
|
128
|
-
* status!: UserStatus;
|
|
129
|
-
* }
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* // ✅ Good - @IsEnum matches enum type
|
|
133
|
-
* class User {
|
|
134
|
-
* @IsEnum(UserStatus)
|
|
135
|
-
* status!: UserStatus;
|
|
136
|
-
* }
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* // ✅ Good - union type with @IsEnum
|
|
140
|
-
* class User {
|
|
141
|
-
* @IsEnum({ ACTIVE: 'active', INACTIVE: 'inactive' })
|
|
142
|
-
* status!: 'active' | 'inactive';
|
|
143
|
-
* }
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* // ❌ Bad - enum type used with wrong decorator
|
|
147
|
-
* class User {
|
|
148
|
-
* @IsString()
|
|
149
|
-
* status!: UserStatus; // Should use @IsEnum, will report type mismatch
|
|
150
|
-
* }
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* // ✅ Good - nullable union with @IsOptional
|
|
154
|
-
* class User {
|
|
155
|
-
* @IsOptional()
|
|
156
|
-
* @IsString()
|
|
157
|
-
* name!: string | null;
|
|
158
|
-
* }
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* // ✅ Good - readonly array
|
|
162
|
-
* class User {
|
|
163
|
-
* @IsArray()
|
|
164
|
-
* @ValidateNested({ each: true })
|
|
165
|
-
* addresses!: readonly Address[];
|
|
166
|
-
* }
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* // ✅ Good - tuple type
|
|
170
|
-
* class User {
|
|
171
|
-
* @IsArray()
|
|
172
|
-
* coords!: [number, number];
|
|
173
|
-
* }
|
|
25
|
+
* - Decorators with { each: true } option for array element validation
|
|
26
|
+
* - Template literal types (`user-${string}`)
|
|
27
|
+
* - Namespace/qualified type references (MyNamespace.MyEnum)
|
|
28
|
+
* - Invalid { each: true } on non-array types
|
|
29
|
+
* - Missing @Type decorator when @ValidateNested is present
|
|
30
|
+
* - Type aliases (via TypeScript type checker)
|
|
31
|
+
* - Record<K, V> utility types with primitive values
|
|
174
32
|
*/
|
|
175
33
|
declare const _default: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener>;
|
|
176
34
|
export default _default;
|
|
@@ -27,43 +27,6 @@ const TYPE_AGNOSTIC_DECORATORS = new Set([
|
|
|
27
27
|
/**
|
|
28
28
|
* Mapping of class-validator decorators to their expected TypeScript types.
|
|
29
29
|
* Only includes decorators that enforce specific types.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* // ✅ Good - nullable complex type with @ValidateNested
|
|
33
|
-
* class User {
|
|
34
|
-
* @IsOptional()
|
|
35
|
-
* @ValidateNested()
|
|
36
|
-
* @Type(() => Address)
|
|
37
|
-
* address?: Address | null;
|
|
38
|
-
* }
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* // ❌ Bad - nullable complex type without @ValidateNested
|
|
42
|
-
* class User {
|
|
43
|
-
* @IsOptional()
|
|
44
|
-
* @Type(() => Address)
|
|
45
|
-
* address?: Address | null;
|
|
46
|
-
* }
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* // ✅ Good - array of nullable complex types
|
|
50
|
-
* class User {
|
|
51
|
-
* @IsArray()
|
|
52
|
-
* @ValidateNested({ each: true })
|
|
53
|
-
* @Type(() => Address)
|
|
54
|
-
* addresses!: (Address | null)[];
|
|
55
|
-
* }
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* // ❌ Bad - array of nullable complex types without @ValidateNested
|
|
59
|
-
* class User {
|
|
60
|
-
* @IsArray()
|
|
61
|
-
* @Type(() => Address)
|
|
62
|
-
* addresses!: (Address | null)[];
|
|
63
|
-
* }
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* IsString: ["string"] - expects string type
|
|
67
30
|
*/
|
|
68
31
|
const decoratorTypeMap = {
|
|
69
32
|
// String validators
|
|
@@ -216,12 +179,83 @@ function isNullableUnion(typeNode) {
|
|
|
216
179
|
}
|
|
217
180
|
return { isNullable: false, baseType: null };
|
|
218
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Extracts the name from a TSQualifiedName or Identifier
|
|
184
|
+
*/
|
|
185
|
+
function getTypeReferenceName(typeName) {
|
|
186
|
+
if (typeName.type === 'Identifier') {
|
|
187
|
+
return typeName.name;
|
|
188
|
+
}
|
|
189
|
+
// Handle TSQualifiedName (e.g., MyNamespace.MyEnum)
|
|
190
|
+
if (typeName.type === 'TSQualifiedName') {
|
|
191
|
+
const parts = [];
|
|
192
|
+
let current = typeName;
|
|
193
|
+
while (current.type === 'TSQualifiedName') {
|
|
194
|
+
if (current.right.type === 'Identifier') {
|
|
195
|
+
parts.unshift(current.right.name);
|
|
196
|
+
}
|
|
197
|
+
current = current.left;
|
|
198
|
+
}
|
|
199
|
+
if (current.type === 'Identifier') {
|
|
200
|
+
parts.unshift(current.name);
|
|
201
|
+
}
|
|
202
|
+
return parts.join('.');
|
|
203
|
+
}
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Uses TypeScript's type checker to resolve the actual type, handling type aliases.
|
|
208
|
+
* Returns the resolved type string, or null if it cannot be determined.
|
|
209
|
+
*/
|
|
210
|
+
function resolveTypeWithChecker(typeNode, checker, esTreeNodeMap) {
|
|
211
|
+
if (!checker || !esTreeNodeMap) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const tsNode = esTreeNodeMap.get(typeNode);
|
|
215
|
+
if (!tsNode) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
220
|
+
// Check for primitive types using TypeScript's type flags
|
|
221
|
+
if (type.flags & (1 << 2))
|
|
222
|
+
return 'string'; // ts.TypeFlags.String
|
|
223
|
+
if (type.flags & (1 << 3))
|
|
224
|
+
return 'number'; // ts.TypeFlags.Number
|
|
225
|
+
if (type.flags & (1 << 4))
|
|
226
|
+
return 'boolean'; // ts.TypeFlags.Boolean
|
|
227
|
+
// Check if it's an array type
|
|
228
|
+
if (checker.isArrayType(type)) {
|
|
229
|
+
return 'array';
|
|
230
|
+
}
|
|
231
|
+
// Check if it's a tuple type
|
|
232
|
+
// TypeScript's ObjectType has objectFlags property, but it's not exposed in the public types
|
|
233
|
+
if ('objectFlags' in type && type.objectFlags & 8) {
|
|
234
|
+
// ts.ObjectFlags.Tuple = 8
|
|
235
|
+
return 'array';
|
|
236
|
+
}
|
|
237
|
+
// For other types, fall back to AST analysis
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
219
244
|
/**
|
|
220
245
|
* Converts a TypeScript type node to its string representation for validation purposes.
|
|
221
246
|
* Handles primitives, arrays, tuples, type references, literals, unions, and intersections.
|
|
222
247
|
* Returns null for types that cannot be validated.
|
|
248
|
+
*
|
|
249
|
+
* Can optionally use TypeScript's type checker to resolve type aliases.
|
|
223
250
|
*/
|
|
224
|
-
function getTypeString(typeNode) {
|
|
251
|
+
function getTypeString(typeNode, checker = null, esTreeNodeMap = null) {
|
|
252
|
+
// Try to resolve with type checker first (handles type aliases)
|
|
253
|
+
if (checker && esTreeNodeMap) {
|
|
254
|
+
const resolved = resolveTypeWithChecker(typeNode, checker, esTreeNodeMap);
|
|
255
|
+
if (resolved) {
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
225
259
|
const unwrapped = unwrapReadonlyOperator(typeNode);
|
|
226
260
|
switch (unwrapped.type) {
|
|
227
261
|
case 'TSStringKeyword':
|
|
@@ -234,11 +268,10 @@ function getTypeString(typeNode) {
|
|
|
234
268
|
return 'array';
|
|
235
269
|
case 'TSTupleType':
|
|
236
270
|
return 'array';
|
|
271
|
+
case 'TSTemplateLiteralType':
|
|
272
|
+
return 'string';
|
|
237
273
|
case 'TSTypeReference':
|
|
238
|
-
|
|
239
|
-
return unwrapped.typeName.name;
|
|
240
|
-
}
|
|
241
|
-
break;
|
|
274
|
+
return getTypeReferenceName(unwrapped.typeName);
|
|
242
275
|
case 'TSTypeLiteral':
|
|
243
276
|
return 'object';
|
|
244
277
|
case 'TSUnionType':
|
|
@@ -254,6 +287,13 @@ function getTypeString(typeNode) {
|
|
|
254
287
|
}
|
|
255
288
|
return 'literal';
|
|
256
289
|
case 'TSIntersectionType':
|
|
290
|
+
// Check if intersection contains a primitive type (branded types)
|
|
291
|
+
for (const type of unwrapped.types) {
|
|
292
|
+
const typeStr = getTypeString(type, checker, esTreeNodeMap);
|
|
293
|
+
if (typeStr === 'string' || typeStr === 'number' || typeStr === 'boolean') {
|
|
294
|
+
return typeStr;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
257
297
|
return 'intersection';
|
|
258
298
|
}
|
|
259
299
|
return null;
|
|
@@ -261,7 +301,7 @@ function getTypeString(typeNode) {
|
|
|
261
301
|
/**
|
|
262
302
|
* Extracts the element type from an array type annotation.
|
|
263
303
|
* Supports T[] and Array<T> syntax.
|
|
264
|
-
*
|
|
304
|
+
* For tuple types, returns null since they require per-element validation.
|
|
265
305
|
*/
|
|
266
306
|
function getArrayElementTypeNode(typeAnnotation) {
|
|
267
307
|
const unwrapped = unwrapReadonlyOperator(typeAnnotation);
|
|
@@ -276,12 +316,28 @@ function getArrayElementTypeNode(typeAnnotation) {
|
|
|
276
316
|
unwrapped.typeArguments?.params[0]) {
|
|
277
317
|
return unwrapped.typeArguments.params[0];
|
|
278
318
|
}
|
|
279
|
-
// Tuples
|
|
319
|
+
// Tuples require special handling
|
|
280
320
|
if (unwrapped.type === 'TSTupleType') {
|
|
281
321
|
return null;
|
|
282
322
|
}
|
|
283
323
|
return null;
|
|
284
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Gets all element types from a tuple type.
|
|
327
|
+
*/
|
|
328
|
+
function getTupleElementTypes(typeAnnotation) {
|
|
329
|
+
const unwrapped = unwrapReadonlyOperator(typeAnnotation);
|
|
330
|
+
if (unwrapped.type === 'TSTupleType') {
|
|
331
|
+
return unwrapped.elementTypes.map((element) => {
|
|
332
|
+
// Handle named tuple elements: [name: Type]
|
|
333
|
+
if (element.type === 'TSNamedTupleMember') {
|
|
334
|
+
return element.elementType;
|
|
335
|
+
}
|
|
336
|
+
return element;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
285
341
|
/**
|
|
286
342
|
* Checks if a type is a union of literal types (e.g., 'active' | 'inactive').
|
|
287
343
|
* These are typically used for enum-like type definitions.
|
|
@@ -292,13 +348,38 @@ function isUnionOfLiterals(typeNode) {
|
|
|
292
348
|
}
|
|
293
349
|
return false;
|
|
294
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Checks if a type reference is a Record utility type with a primitive value type.
|
|
353
|
+
* Record<string, number> = not complex
|
|
354
|
+
* Record<string, Address> = complex
|
|
355
|
+
*/
|
|
356
|
+
function isRecordWithPrimitiveValue(typeNode, checker, esTreeNodeMap) {
|
|
357
|
+
const unwrapped = unwrapReadonlyOperator(typeNode);
|
|
358
|
+
if (unwrapped.type !== 'TSTypeReference') {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
const typeName = getTypeReferenceName(unwrapped.typeName);
|
|
362
|
+
if (typeName !== 'Record') {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
// Check if it has type arguments
|
|
366
|
+
if (!unwrapped.typeArguments || unwrapped.typeArguments.params.length < 2) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
// Get the value type (second type argument)
|
|
370
|
+
const valueType = unwrapped.typeArguments.params[1];
|
|
371
|
+
const valueTypeStr = getTypeString(valueType, checker, esTreeNodeMap);
|
|
372
|
+
// If value type is a primitive, Record is not complex
|
|
373
|
+
return valueTypeStr === 'string' || valueTypeStr === 'number' || valueTypeStr === 'boolean';
|
|
374
|
+
}
|
|
295
375
|
/**
|
|
296
376
|
* Determines if a type requires @ValidateNested decorator for proper validation.
|
|
297
377
|
* Complex types include objects, class instances, type literals, and intersections.
|
|
298
|
-
*
|
|
378
|
+
* Arrays of complex types are also considered complex.
|
|
379
|
+
* Built-in types with complex generic parameters are checked selectively.
|
|
299
380
|
* Union types are complex if any non-null/undefined member is complex.
|
|
300
381
|
*/
|
|
301
|
-
function isComplexType(typeNode) {
|
|
382
|
+
function isComplexType(typeNode, checker = null, esTreeNodeMap = null) {
|
|
302
383
|
const unwrapped = unwrapReadonlyOperator(typeNode);
|
|
303
384
|
// Type literals are always complex
|
|
304
385
|
if (unwrapped.type === 'TSTypeLiteral') {
|
|
@@ -309,7 +390,6 @@ function isComplexType(typeNode) {
|
|
|
309
390
|
return false;
|
|
310
391
|
}
|
|
311
392
|
// Union types are complex if any non-null/undefined member is complex
|
|
312
|
-
// This handles cases like: Address | null, Address | undefined, Address | Profile
|
|
313
393
|
if (unwrapped.type === 'TSUnionType') {
|
|
314
394
|
return unwrapped.types.some((type) => {
|
|
315
395
|
// Skip null and undefined - they don't affect complexity
|
|
@@ -317,29 +397,53 @@ function isComplexType(typeNode) {
|
|
|
317
397
|
return false;
|
|
318
398
|
}
|
|
319
399
|
// Recursively check if this union member is complex
|
|
320
|
-
return isComplexType(type);
|
|
400
|
+
return isComplexType(type, checker, esTreeNodeMap);
|
|
321
401
|
});
|
|
322
402
|
}
|
|
323
|
-
// Intersection types
|
|
403
|
+
// Intersection types: check if any member is a primitive (branded types)
|
|
324
404
|
if (unwrapped.type === 'TSIntersectionType') {
|
|
405
|
+
// If intersection contains a primitive, treat as primitive (branded type)
|
|
406
|
+
for (const type of unwrapped.types) {
|
|
407
|
+
const typeStr = getTypeString(type, checker, esTreeNodeMap);
|
|
408
|
+
if (typeStr === 'string' || typeStr === 'number' || typeStr === 'boolean') {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Otherwise, it's a complex intersection
|
|
325
413
|
return true;
|
|
326
414
|
}
|
|
415
|
+
// Arrays are complex if their elements are complex
|
|
416
|
+
if (unwrapped.type === 'TSArrayType') {
|
|
417
|
+
return isComplexType(unwrapped.elementType, checker, esTreeNodeMap);
|
|
418
|
+
}
|
|
419
|
+
// Tuples are complex if any element is complex
|
|
420
|
+
if (unwrapped.type === 'TSTupleType') {
|
|
421
|
+
const elements = getTupleElementTypes(unwrapped);
|
|
422
|
+
return elements.some((element) => isComplexType(element, checker, esTreeNodeMap));
|
|
423
|
+
}
|
|
327
424
|
// Check for type references (class names, interfaces, etc.)
|
|
328
|
-
if (unwrapped.type === 'TSTypeReference'
|
|
329
|
-
const typeName = unwrapped.typeName
|
|
425
|
+
if (unwrapped.type === 'TSTypeReference') {
|
|
426
|
+
const typeName = getTypeReferenceName(unwrapped.typeName);
|
|
330
427
|
// Common built-in types that don't require @ValidateNested
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
for (const param of unwrapped.typeArguments.params) {
|
|
336
|
-
if (isComplexType(param)) {
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
428
|
+
const builtInTypes = ['String', 'Number', 'Boolean', 'Date'];
|
|
429
|
+
// Types that can't be validated even with generic parameters
|
|
430
|
+
const nonValidatableTypes = ['Promise', 'Map', 'Set'];
|
|
431
|
+
if (nonValidatableTypes.includes(typeName)) {
|
|
340
432
|
return false;
|
|
341
433
|
}
|
|
342
|
-
|
|
434
|
+
// Record<K, V> with primitive V is not complex
|
|
435
|
+
if (isRecordWithPrimitiveValue(unwrapped, checker, esTreeNodeMap)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
// Array<T> needs to check the element type
|
|
439
|
+
if (typeName === 'Array' && unwrapped.typeArguments?.params[0]) {
|
|
440
|
+
return isComplexType(unwrapped.typeArguments.params[0], checker, esTreeNodeMap);
|
|
441
|
+
}
|
|
442
|
+
// Other built-in types are not complex
|
|
443
|
+
if (builtInTypes.includes(typeName)) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
343
447
|
}
|
|
344
448
|
return false;
|
|
345
449
|
}
|
|
@@ -362,6 +466,13 @@ function isArrayType(typeAnnotation) {
|
|
|
362
466
|
}
|
|
363
467
|
return false;
|
|
364
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Checks if a type is a tuple type.
|
|
471
|
+
*/
|
|
472
|
+
function isTupleType(typeAnnotation) {
|
|
473
|
+
const unwrapped = unwrapReadonlyOperator(typeAnnotation);
|
|
474
|
+
return unwrapped.type === 'TSTupleType';
|
|
475
|
+
}
|
|
365
476
|
/**
|
|
366
477
|
* Checks if a type is a union of literals, used for enum-like type definitions.
|
|
367
478
|
*/
|
|
@@ -422,35 +533,56 @@ function getTypeDecoratorClassName(decorator) {
|
|
|
422
533
|
return null;
|
|
423
534
|
}
|
|
424
535
|
/**
|
|
425
|
-
* Checks if
|
|
536
|
+
* Checks if a decorator includes the { each: true } option.
|
|
426
537
|
* Returns false for decorators without parentheses or without the each option.
|
|
538
|
+
* This works for any decorator, not just @ValidateNested.
|
|
539
|
+
*
|
|
540
|
+
* Handles both single and two-parameter decorator signatures:
|
|
541
|
+
* - @IsString({ each: true })
|
|
542
|
+
* - @IsNumber({}, { each: true })
|
|
543
|
+
* - @Min(0, { each: true })
|
|
427
544
|
*/
|
|
428
|
-
function
|
|
429
|
-
// If decorator is used without parentheses (e.g., @
|
|
545
|
+
function hasEachOption(decorator) {
|
|
546
|
+
// If decorator is used without parentheses (e.g., @IsString), it has no options
|
|
430
547
|
if (decorator.expression.type === 'Identifier') {
|
|
431
548
|
return false;
|
|
432
549
|
}
|
|
433
|
-
if (decorator.expression.type === 'CallExpression'
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
550
|
+
if (decorator.expression.type === 'CallExpression') {
|
|
551
|
+
const args = decorator.expression.arguments;
|
|
552
|
+
// Check both first and second arguments for { each: true }
|
|
553
|
+
// Some decorators have validation options as first param: @IsString({ each: true })
|
|
554
|
+
// Others have it as second param: @Min(0, { each: true })
|
|
555
|
+
for (let i = 0; i < Math.min(args.length, 2); i++) {
|
|
556
|
+
const arg = args[i];
|
|
557
|
+
if (arg.type === 'ObjectExpression') {
|
|
558
|
+
const hasEach = arg.properties.some((prop) => {
|
|
559
|
+
if (prop.type === 'Property' &&
|
|
560
|
+
prop.key.type === 'Identifier' &&
|
|
561
|
+
prop.key.name === 'each' &&
|
|
562
|
+
prop.value.type === 'Literal') {
|
|
563
|
+
return prop.value.value === true;
|
|
564
|
+
}
|
|
565
|
+
return false;
|
|
566
|
+
});
|
|
567
|
+
if (hasEach)
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
445
570
|
}
|
|
446
571
|
}
|
|
447
572
|
return false;
|
|
448
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Checks if @ValidateNested decorator includes the { each: true } option.
|
|
576
|
+
* Returns false for decorators without parentheses or without the each option.
|
|
577
|
+
*/
|
|
578
|
+
function hasValidateNestedEachOption(decorator) {
|
|
579
|
+
return hasEachOption(decorator);
|
|
580
|
+
}
|
|
449
581
|
/**
|
|
450
582
|
* Validates if a decorator matches the TypeScript type annotation.
|
|
451
583
|
* Handles special cases like @IsEnum validation and nullable unions.
|
|
452
584
|
*/
|
|
453
|
-
function checkTypeMatch(decorator, typeAnnotation, actualType) {
|
|
585
|
+
function checkTypeMatch(decorator, typeAnnotation, actualType, checker, esTreeNodeMap) {
|
|
454
586
|
const expectedTypes = decoratorTypeMap[decorator];
|
|
455
587
|
// Skip decorators not in our map
|
|
456
588
|
if (!expectedTypes || expectedTypes.length === 0) {
|
|
@@ -464,7 +596,7 @@ function checkTypeMatch(decorator, typeAnnotation, actualType) {
|
|
|
464
596
|
if (isUnionEnumType(typeAnnotation)) {
|
|
465
597
|
return expectedTypes.includes('union-literal');
|
|
466
598
|
}
|
|
467
|
-
if (typeAnnotation.type === 'TSTypeReference'
|
|
599
|
+
if (typeAnnotation.type === 'TSTypeReference') {
|
|
468
600
|
return expectedTypes.includes('type-reference');
|
|
469
601
|
}
|
|
470
602
|
return false;
|
|
@@ -472,7 +604,7 @@ function checkTypeMatch(decorator, typeAnnotation, actualType) {
|
|
|
472
604
|
// For nullable unions (T | null | undefined), validate against the base type
|
|
473
605
|
const nullableCheck = isNullableUnion(typeAnnotation);
|
|
474
606
|
if (nullableCheck.isNullable && nullableCheck.baseType) {
|
|
475
|
-
const baseTypeString = getTypeString(nullableCheck.baseType);
|
|
607
|
+
const baseTypeString = getTypeString(nullableCheck.baseType, checker, esTreeNodeMap);
|
|
476
608
|
if (baseTypeString) {
|
|
477
609
|
return expectedTypes.some((expected) => {
|
|
478
610
|
if (expected === 'array' && baseTypeString === 'Array')
|
|
@@ -502,174 +634,32 @@ function checkTypeMatch(decorator, typeAnnotation, actualType) {
|
|
|
502
634
|
* - Arrays of objects requiring @ValidateNested({ each: true })
|
|
503
635
|
* - Nested objects requiring @ValidateNested()
|
|
504
636
|
* - Nullable complex types (Address | null, Address | undefined)
|
|
505
|
-
* -
|
|
637
|
+
* - Nested arrays (Address[][])
|
|
506
638
|
* - Type literals
|
|
507
639
|
* - class-transformer decorators
|
|
508
640
|
* - Enum types (both TypeScript enums and union types)
|
|
509
641
|
* - Literal types (e.g., "admin", 25)
|
|
510
642
|
* - Intersection types (e.g., Profile & Settings)
|
|
643
|
+
* - Branded types (string & { __brand: 'UserId' })
|
|
511
644
|
* - @Type(() => ClassName) decorator matching
|
|
512
645
|
* - Readonly arrays (readonly T[])
|
|
513
646
|
* - Tuple types ([T, U])
|
|
514
647
|
* - Nullable unions (T | null | undefined)
|
|
515
648
|
* - Unnecessary @ValidateNested on primitive arrays
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
* @example
|
|
525
|
-
* // ✅ Good - types match
|
|
526
|
-
* class User {
|
|
527
|
-
* @IsString()
|
|
528
|
-
* name!: string;
|
|
529
|
-
* }
|
|
530
|
-
*
|
|
531
|
-
* @example
|
|
532
|
-
* // ❌ Bad - @Type doesn't match TypeScript type
|
|
533
|
-
* class User {
|
|
534
|
-
* @ValidateNested()
|
|
535
|
-
* @Type(() => Address)
|
|
536
|
-
* address: string;
|
|
537
|
-
* }
|
|
538
|
-
*
|
|
539
|
-
* @example
|
|
540
|
-
* // ✅ Good - @Type matches TypeScript type
|
|
541
|
-
* class User {
|
|
542
|
-
* @ValidateNested()
|
|
543
|
-
* @Type(() => Address)
|
|
544
|
-
* address: Address;
|
|
545
|
-
* }
|
|
546
|
-
*
|
|
547
|
-
* @example
|
|
548
|
-
* // ❌ Bad - array of objects without @ValidateNested
|
|
549
|
-
* class User {
|
|
550
|
-
* @IsArray()
|
|
551
|
-
* addresses!: Address[];
|
|
552
|
-
* }
|
|
553
|
-
*
|
|
554
|
-
* @example
|
|
555
|
-
* // ✅ Good - array of objects with @ValidateNested
|
|
556
|
-
* class User {
|
|
557
|
-
* @IsArray()
|
|
558
|
-
* @ValidateNested({ each: true })
|
|
559
|
-
* @Type(() => Address)
|
|
560
|
-
* addresses!: Address[];
|
|
561
|
-
* }
|
|
562
|
-
*
|
|
563
|
-
* @example
|
|
564
|
-
* // ✅ Good - array of primitives without @ValidateNested
|
|
565
|
-
* class User {
|
|
566
|
-
* @IsArray()
|
|
567
|
-
* @IsString({ each: true })
|
|
568
|
-
* tags!: string[];
|
|
569
|
-
* }
|
|
570
|
-
*
|
|
571
|
-
* @example
|
|
572
|
-
* // ❌ Bad - array of primitives with unnecessary @ValidateNested
|
|
573
|
-
* class User {
|
|
574
|
-
* @IsArray()
|
|
575
|
-
* @ValidateNested({ each: true })
|
|
576
|
-
* @IsString({ each: true })
|
|
577
|
-
* tags!: string[];
|
|
578
|
-
* }
|
|
579
|
-
*
|
|
580
|
-
* @example
|
|
581
|
-
* // ❌ Bad - complex type without @ValidateNested
|
|
582
|
-
* class User {
|
|
583
|
-
* @IsDefined()
|
|
584
|
-
* profile!: Profile;
|
|
585
|
-
* }
|
|
586
|
-
*
|
|
587
|
-
* @example
|
|
588
|
-
* // ✅ Good - complex type with @ValidateNested
|
|
589
|
-
* class User {
|
|
590
|
-
* @ValidateNested()
|
|
591
|
-
* @Type(() => Profile)
|
|
592
|
-
* profile!: Profile;
|
|
593
|
-
* }
|
|
594
|
-
*
|
|
595
|
-
* @example
|
|
596
|
-
* // ❌ Bad - intersection type without @ValidateNested
|
|
597
|
-
* class User {
|
|
598
|
-
* @IsDefined()
|
|
599
|
-
* data!: Profile & Settings;
|
|
600
|
-
* }
|
|
601
|
-
*
|
|
602
|
-
* @example
|
|
603
|
-
* // ✅ Good - intersection type with @ValidateNested
|
|
604
|
-
* class User {
|
|
605
|
-
* @ValidateNested()
|
|
606
|
-
* data!: Profile & Settings;
|
|
607
|
-
* }
|
|
608
|
-
*
|
|
609
|
-
* @example
|
|
610
|
-
* // ✅ Good - literal type
|
|
611
|
-
* class User {
|
|
612
|
-
* @IsString()
|
|
613
|
-
* role!: "admin";
|
|
614
|
-
* }
|
|
615
|
-
*
|
|
616
|
-
* @example
|
|
617
|
-
* // ❌ Bad - @IsEnum with wrong enum type
|
|
618
|
-
* class User {
|
|
619
|
-
* @IsEnum(UserRole)
|
|
620
|
-
* status!: UserStatus;
|
|
621
|
-
* }
|
|
622
|
-
*
|
|
623
|
-
* @example
|
|
624
|
-
* // ✅ Good - @IsEnum matches enum type
|
|
625
|
-
* class User {
|
|
626
|
-
* @IsEnum(UserStatus)
|
|
627
|
-
* status!: UserStatus;
|
|
628
|
-
* }
|
|
629
|
-
*
|
|
630
|
-
* @example
|
|
631
|
-
* // ✅ Good - union type with @IsEnum
|
|
632
|
-
* class User {
|
|
633
|
-
* @IsEnum({ ACTIVE: 'active', INACTIVE: 'inactive' })
|
|
634
|
-
* status!: 'active' | 'inactive';
|
|
635
|
-
* }
|
|
636
|
-
*
|
|
637
|
-
* @example
|
|
638
|
-
* // ❌ Bad - enum type used with wrong decorator
|
|
639
|
-
* class User {
|
|
640
|
-
* @IsString()
|
|
641
|
-
* status!: UserStatus; // Should use @IsEnum, will report type mismatch
|
|
642
|
-
* }
|
|
643
|
-
*
|
|
644
|
-
* @example
|
|
645
|
-
* // ✅ Good - nullable union with @IsOptional
|
|
646
|
-
* class User {
|
|
647
|
-
* @IsOptional()
|
|
648
|
-
* @IsString()
|
|
649
|
-
* name!: string | null;
|
|
650
|
-
* }
|
|
651
|
-
*
|
|
652
|
-
* @example
|
|
653
|
-
* // ✅ Good - readonly array
|
|
654
|
-
* class User {
|
|
655
|
-
* @IsArray()
|
|
656
|
-
* @ValidateNested({ each: true })
|
|
657
|
-
* addresses!: readonly Address[];
|
|
658
|
-
* }
|
|
659
|
-
*
|
|
660
|
-
* @example
|
|
661
|
-
* // ✅ Good - tuple type
|
|
662
|
-
* class User {
|
|
663
|
-
* @IsArray()
|
|
664
|
-
* coords!: [number, number];
|
|
665
|
-
* }
|
|
649
|
+
* - Decorators with { each: true } option for array element validation
|
|
650
|
+
* - Template literal types (`user-${string}`)
|
|
651
|
+
* - Namespace/qualified type references (MyNamespace.MyEnum)
|
|
652
|
+
* - Invalid { each: true } on non-array types
|
|
653
|
+
* - Missing @Type decorator when @ValidateNested is present
|
|
654
|
+
* - Type aliases (via TypeScript type checker)
|
|
655
|
+
* - Record<K, V> utility types with primitive values
|
|
666
656
|
*/
|
|
667
657
|
exports.default = createRule({
|
|
668
658
|
name: 'decorator-type-match',
|
|
669
659
|
meta: {
|
|
670
660
|
type: 'problem',
|
|
671
661
|
docs: {
|
|
672
|
-
description: 'Ensure class-validator decorators match TypeScript type annotations, including arrays of objects, nested objects, enum types, literal types, intersection types, readonly arrays, tuple types, nullable unions,
|
|
662
|
+
description: 'Ensure class-validator decorators match TypeScript type annotations, including arrays of objects, nested objects, enum types, literal types, intersection types, readonly arrays, tuple types, nullable unions, @Type decorator matching, { each: true } option handling, template literals, namespace references, type aliases, branded types, and Record utility types',
|
|
673
663
|
},
|
|
674
664
|
messages: {
|
|
675
665
|
mismatch: 'Decorator @{{decorator}} does not match type annotation {{actualType}}. Expected: {{expectedTypes}}',
|
|
@@ -679,11 +669,27 @@ exports.default = createRule({
|
|
|
679
669
|
typeMismatch: '@Type(() => {{typeDecoratorClass}}) does not match type annotation {{actualType}}.',
|
|
680
670
|
missingEachOption: 'Array of complex types requires @ValidateNested({ each: true }), but only @ValidateNested() was found.',
|
|
681
671
|
unnecessaryValidateNested: 'Array of primitive type {{elementType}} does not need @ValidateNested(). Remove @ValidateNested() or use @{{decorator}}({ each: true }) instead.',
|
|
672
|
+
invalidEachOption: 'Decorator @{{decorator}} has { each: true } option but property type is not an array. Remove { each: true } or change type to an array.',
|
|
673
|
+
missingTypeDecorator: 'Complex type {{actualType}} with @ValidateNested() requires @Type(() => {{className}}) decorator for proper transformation.',
|
|
674
|
+
tupleValidationWarning: 'Tuple type contains complex elements. Consider using a regular array with @ValidateNested({ each: true }) or validate elements individually.',
|
|
682
675
|
},
|
|
683
676
|
schema: [],
|
|
684
677
|
},
|
|
685
678
|
defaultOptions: [],
|
|
686
679
|
create(context) {
|
|
680
|
+
// Get TypeScript type checker if available
|
|
681
|
+
let checker = null;
|
|
682
|
+
let esTreeNodeMap = null;
|
|
683
|
+
try {
|
|
684
|
+
const parserServices = context.parserServices;
|
|
685
|
+
if (parserServices?.program && parserServices?.esTreeNodeToTSNodeMap) {
|
|
686
|
+
checker = parserServices.program.getTypeChecker();
|
|
687
|
+
esTreeNodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
// Type checker not available, continue with AST-only analysis
|
|
692
|
+
}
|
|
687
693
|
return {
|
|
688
694
|
/**
|
|
689
695
|
* Analyzes class property definitions to validate decorator and type annotation matching
|
|
@@ -716,28 +722,31 @@ exports.default = createRule({
|
|
|
716
722
|
/**
|
|
717
723
|
* Determine the actual TypeScript type from the annotation.
|
|
718
724
|
* Supports primitive types, arrays, type references, literals, and intersections.
|
|
725
|
+
* Uses TypeScript's type checker when available to resolve type aliases.
|
|
719
726
|
*/
|
|
720
|
-
actualType = getTypeString(typeAnnotation);
|
|
727
|
+
actualType = getTypeString(typeAnnotation, checker, esTreeNodeMap);
|
|
721
728
|
// Skip if we couldn't determine the type
|
|
722
729
|
if (!actualType)
|
|
723
730
|
return;
|
|
724
731
|
const hasValidateNested = decorators.includes('ValidateNested');
|
|
725
732
|
const hasIsEnum = decorators.includes('IsEnum');
|
|
733
|
+
const hasTypeDecorator = decorators.includes('Type');
|
|
726
734
|
// Validate @IsEnum argument matches the type annotation for enum type references
|
|
727
|
-
if (hasIsEnum && typeAnnotation.type === 'TSTypeReference'
|
|
735
|
+
if (hasIsEnum && typeAnnotation.type === 'TSTypeReference') {
|
|
728
736
|
const isEnumDecorator = node.decorators?.find((d) => d.expression.type === 'CallExpression' &&
|
|
729
737
|
d.expression.callee.type === 'Identifier' &&
|
|
730
738
|
d.expression.callee.name === 'IsEnum');
|
|
731
739
|
if (isEnumDecorator) {
|
|
732
740
|
const enumArg = getIsEnumArgument(isEnumDecorator);
|
|
741
|
+
const typeName = getTypeReferenceName(typeAnnotation.typeName);
|
|
733
742
|
// For TypeScript enum references, the argument should match the type
|
|
734
|
-
if (enumArg && enumArg !==
|
|
743
|
+
if (enumArg && enumArg !== typeName) {
|
|
735
744
|
context.report({
|
|
736
745
|
node,
|
|
737
746
|
messageId: 'enumMismatch',
|
|
738
747
|
data: {
|
|
739
748
|
enumArg,
|
|
740
|
-
actualType:
|
|
749
|
+
actualType: typeName,
|
|
741
750
|
},
|
|
742
751
|
});
|
|
743
752
|
}
|
|
@@ -751,8 +760,8 @@ exports.default = createRule({
|
|
|
751
760
|
const typeClassName = getTypeDecoratorClassName(typeDecorator);
|
|
752
761
|
// For non-array types, check if @Type matches the TypeScript type
|
|
753
762
|
if (typeClassName && !isArrayType(typeAnnotation)) {
|
|
754
|
-
if (typeAnnotation.type === 'TSTypeReference'
|
|
755
|
-
const tsTypeName = typeAnnotation.typeName
|
|
763
|
+
if (typeAnnotation.type === 'TSTypeReference') {
|
|
764
|
+
const tsTypeName = getTypeReferenceName(typeAnnotation.typeName);
|
|
756
765
|
if (typeClassName !== tsTypeName) {
|
|
757
766
|
context.report({
|
|
758
767
|
node,
|
|
@@ -764,7 +773,7 @@ exports.default = createRule({
|
|
|
764
773
|
});
|
|
765
774
|
}
|
|
766
775
|
}
|
|
767
|
-
else
|
|
776
|
+
else {
|
|
768
777
|
// @Type is used with a primitive type
|
|
769
778
|
context.report({
|
|
770
779
|
node,
|
|
@@ -780,8 +789,8 @@ exports.default = createRule({
|
|
|
780
789
|
if (typeClassName && isArrayType(typeAnnotation)) {
|
|
781
790
|
const elementTypeNode = getArrayElementTypeNode(typeAnnotation);
|
|
782
791
|
if (elementTypeNode) {
|
|
783
|
-
if (elementTypeNode.type === 'TSTypeReference'
|
|
784
|
-
const elementTypeName = elementTypeNode.typeName
|
|
792
|
+
if (elementTypeNode.type === 'TSTypeReference') {
|
|
793
|
+
const elementTypeName = getTypeReferenceName(elementTypeNode.typeName);
|
|
785
794
|
if (typeClassName !== elementTypeName) {
|
|
786
795
|
context.report({
|
|
787
796
|
node,
|
|
@@ -795,7 +804,7 @@ exports.default = createRule({
|
|
|
795
804
|
}
|
|
796
805
|
else {
|
|
797
806
|
// @Type is used with an array of primitives
|
|
798
|
-
const elementType = getTypeString(elementTypeNode);
|
|
807
|
+
const elementType = getTypeString(elementTypeNode, checker, esTreeNodeMap);
|
|
799
808
|
if (elementType) {
|
|
800
809
|
context.report({
|
|
801
810
|
node,
|
|
@@ -810,13 +819,24 @@ exports.default = createRule({
|
|
|
810
819
|
}
|
|
811
820
|
}
|
|
812
821
|
}
|
|
822
|
+
// Special handling for tuple types
|
|
823
|
+
if (isTupleType(typeAnnotation)) {
|
|
824
|
+
const elements = getTupleElementTypes(typeAnnotation);
|
|
825
|
+
const hasComplexElements = elements.some((element) => isComplexType(element, checker, esTreeNodeMap));
|
|
826
|
+
if (hasComplexElements) {
|
|
827
|
+
context.report({
|
|
828
|
+
node,
|
|
829
|
+
messageId: 'tupleValidationWarning',
|
|
830
|
+
data: {},
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
813
834
|
// Validate arrays of complex types have proper nested validation
|
|
814
|
-
if (isArrayType(typeAnnotation)) {
|
|
835
|
+
if (isArrayType(typeAnnotation) && !isTupleType(typeAnnotation)) {
|
|
815
836
|
const elementTypeNode = getArrayElementTypeNode(typeAnnotation);
|
|
816
|
-
// Tuples return null for elementTypeNode, skip nested validation check
|
|
817
837
|
if (elementTypeNode) {
|
|
818
|
-
const elementTypeName = getTypeString(elementTypeNode);
|
|
819
|
-
const isElementComplex = isComplexType(elementTypeNode);
|
|
838
|
+
const elementTypeName = getTypeString(elementTypeNode, checker, esTreeNodeMap);
|
|
839
|
+
const isElementComplex = isComplexType(elementTypeNode, checker, esTreeNodeMap);
|
|
820
840
|
if (isElementComplex && elementTypeName) {
|
|
821
841
|
// Complex element type - requires @ValidateNested({ each: true })
|
|
822
842
|
if (!hasValidateNested) {
|
|
@@ -848,6 +868,26 @@ exports.default = createRule({
|
|
|
848
868
|
data: {},
|
|
849
869
|
});
|
|
850
870
|
}
|
|
871
|
+
// Check if @Type decorator is present for complex element types
|
|
872
|
+
if (!hasTypeDecorator) {
|
|
873
|
+
let elementTypeToCheck = elementTypeNode;
|
|
874
|
+
// Handle nullable element types: (Address | null)[]
|
|
875
|
+
const nullableCheck = isNullableUnion(elementTypeNode);
|
|
876
|
+
if (nullableCheck.isNullable && nullableCheck.baseType) {
|
|
877
|
+
elementTypeToCheck = nullableCheck.baseType;
|
|
878
|
+
}
|
|
879
|
+
if (elementTypeToCheck.type === 'TSTypeReference') {
|
|
880
|
+
const className = getTypeReferenceName(elementTypeToCheck.typeName);
|
|
881
|
+
context.report({
|
|
882
|
+
node,
|
|
883
|
+
messageId: 'missingTypeDecorator',
|
|
884
|
+
data: {
|
|
885
|
+
actualType: `${className}[]`,
|
|
886
|
+
className,
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
851
891
|
}
|
|
852
892
|
}
|
|
853
893
|
else if (!isElementComplex && hasValidateNested && elementTypeName) {
|
|
@@ -872,7 +912,43 @@ exports.default = createRule({
|
|
|
872
912
|
// Skip type-agnostic decorators
|
|
873
913
|
if (TYPE_AGNOSTIC_DECORATORS.has(decorator))
|
|
874
914
|
continue;
|
|
875
|
-
|
|
915
|
+
// Get the decorator node to check for { each: true }
|
|
916
|
+
const decoratorNode = node.decorators?.find((d) => {
|
|
917
|
+
if (d.expression.type === 'CallExpression' && d.expression.callee.type === 'Identifier') {
|
|
918
|
+
return d.expression.callee.name === decorator;
|
|
919
|
+
}
|
|
920
|
+
if (d.expression.type === 'Identifier') {
|
|
921
|
+
return d.expression.name === decorator;
|
|
922
|
+
}
|
|
923
|
+
return false;
|
|
924
|
+
});
|
|
925
|
+
// Check if this decorator has { each: true } option
|
|
926
|
+
const hasEach = decoratorNode ? hasEachOption(decoratorNode) : false;
|
|
927
|
+
// Validate { each: true } is only used with arrays
|
|
928
|
+
if (hasEach && !isArrayType(typeAnnotation)) {
|
|
929
|
+
context.report({
|
|
930
|
+
node,
|
|
931
|
+
messageId: 'invalidEachOption',
|
|
932
|
+
data: {
|
|
933
|
+
decorator,
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
let typeToCheck = actualType;
|
|
939
|
+
let typeNodeToCheck = typeAnnotation;
|
|
940
|
+
// If decorator has { each: true }, validate against array element type
|
|
941
|
+
if (hasEach && isArrayType(typeAnnotation)) {
|
|
942
|
+
const elementTypeNode = getArrayElementTypeNode(typeAnnotation);
|
|
943
|
+
if (elementTypeNode) {
|
|
944
|
+
const elementType = getTypeString(elementTypeNode, checker, esTreeNodeMap);
|
|
945
|
+
if (elementType) {
|
|
946
|
+
typeToCheck = elementType;
|
|
947
|
+
typeNodeToCheck = elementTypeNode;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const matches = checkTypeMatch(decorator, typeNodeToCheck, typeToCheck, checker, esTreeNodeMap);
|
|
876
952
|
// Report mismatch
|
|
877
953
|
if (!matches) {
|
|
878
954
|
const expectedTypes = decoratorTypeMap[decorator];
|
|
@@ -881,7 +957,7 @@ exports.default = createRule({
|
|
|
881
957
|
messageId: 'mismatch',
|
|
882
958
|
data: {
|
|
883
959
|
decorator,
|
|
884
|
-
actualType,
|
|
960
|
+
actualType: typeToCheck,
|
|
885
961
|
expectedTypes: expectedTypes?.join(' or ') || 'unknown',
|
|
886
962
|
},
|
|
887
963
|
});
|
|
@@ -889,7 +965,9 @@ exports.default = createRule({
|
|
|
889
965
|
}
|
|
890
966
|
// Validate complex object types have @ValidateNested decorator
|
|
891
967
|
const hasTypedDecorator = decorators.some((d) => !TYPE_AGNOSTIC_DECORATORS.has(d) && decoratorTypeMap[d]);
|
|
892
|
-
if (!isArrayType(typeAnnotation) &&
|
|
968
|
+
if (!isArrayType(typeAnnotation) &&
|
|
969
|
+
isComplexType(typeAnnotation, checker, esTreeNodeMap) &&
|
|
970
|
+
!hasTypedDecorator) {
|
|
893
971
|
// Complex types should have @ValidateNested()
|
|
894
972
|
if (!hasValidateNested && decorators.length > 0) {
|
|
895
973
|
context.report({
|
|
@@ -901,6 +979,29 @@ exports.default = createRule({
|
|
|
901
979
|
});
|
|
902
980
|
}
|
|
903
981
|
}
|
|
982
|
+
// Check if complex non-array types with @ValidateNested also have @Type
|
|
983
|
+
if (!isArrayType(typeAnnotation) &&
|
|
984
|
+
isComplexType(typeAnnotation, checker, esTreeNodeMap) &&
|
|
985
|
+
hasValidateNested &&
|
|
986
|
+
!hasTypeDecorator) {
|
|
987
|
+
let typeToCheck = typeAnnotation;
|
|
988
|
+
// Handle nullable complex types: Address | null | undefined
|
|
989
|
+
const nullableCheck = isNullableUnion(typeAnnotation);
|
|
990
|
+
if (nullableCheck.isNullable && nullableCheck.baseType) {
|
|
991
|
+
typeToCheck = nullableCheck.baseType;
|
|
992
|
+
}
|
|
993
|
+
if (typeToCheck.type === 'TSTypeReference') {
|
|
994
|
+
const className = getTypeReferenceName(typeToCheck.typeName);
|
|
995
|
+
context.report({
|
|
996
|
+
node,
|
|
997
|
+
messageId: 'missingTypeDecorator',
|
|
998
|
+
data: {
|
|
999
|
+
actualType: className,
|
|
1000
|
+
className,
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
904
1005
|
},
|
|
905
1006
|
};
|
|
906
1007
|
},
|