eslint-plugin-class-validator-type-match 1.4.0 → 1.5.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
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" | "invalidEachOption" | "missingTypeDecorator" | "tupleValidationWarning", [], 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" | "multiTypeUnionWarning" | "mixedComplexityUnionWarning" | "pickOmitWarning", [], 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' | 'invalidEachOption' | 'missingTypeDecorator' | 'tupleValidationWarning';
|
|
2
|
+
type MessageIds = 'mismatch' | 'nestedArrayMismatch' | 'missingValidateNested' | 'enumMismatch' | 'typeMismatch' | 'missingEachOption' | 'unnecessaryValidateNested' | 'invalidEachOption' | 'missingTypeDecorator' | 'tupleValidationWarning' | 'multiTypeUnionWarning' | 'mixedComplexityUnionWarning' | 'pickOmitWarning';
|
|
3
3
|
/**
|
|
4
4
|
* ESLint rule to ensure class-validator decorators match TypeScript type annotations.
|
|
5
5
|
*
|
|
@@ -29,6 +29,9 @@ type MessageIds = 'mismatch' | 'nestedArrayMismatch' | 'missingValidateNested' |
|
|
|
29
29
|
* - Missing @Type decorator when @ValidateNested is present
|
|
30
30
|
* - Type aliases (via TypeScript type checker)
|
|
31
31
|
* - Record<K, V> utility types with primitive values
|
|
32
|
+
* - Utility types: Partial<T>, Required<T>, Pick<T, K>, Omit<T, K>, ReadonlyArray<T>, NonNullable<T>, Extract<T, U>, Exclude<T, U>
|
|
33
|
+
* - Multi-type unions with explicit warnings for complex scenarios
|
|
34
|
+
* - Mixed complexity unions (primitive | complex types)
|
|
32
35
|
*/
|
|
33
36
|
declare const _default: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener>;
|
|
34
37
|
export default _default;
|
|
@@ -309,10 +309,10 @@ function getArrayElementTypeNode(typeAnnotation) {
|
|
|
309
309
|
if (unwrapped.type === 'TSArrayType') {
|
|
310
310
|
return unwrapped.elementType;
|
|
311
311
|
}
|
|
312
|
-
// Handle Array<Type> syntax
|
|
312
|
+
// Handle Array<Type> and ReadonlyArray<Type> syntax
|
|
313
313
|
if (unwrapped.type === 'TSTypeReference' &&
|
|
314
314
|
unwrapped.typeName.type === 'Identifier' &&
|
|
315
|
-
unwrapped.typeName.name === 'Array' &&
|
|
315
|
+
(unwrapped.typeName.name === 'Array' || unwrapped.typeName.name === 'ReadonlyArray') &&
|
|
316
316
|
unwrapped.typeArguments?.params[0]) {
|
|
317
317
|
return unwrapped.typeArguments.params[0];
|
|
318
318
|
}
|
|
@@ -372,6 +372,100 @@ function isRecordWithPrimitiveValue(typeNode, checker, esTreeNodeMap) {
|
|
|
372
372
|
// If value type is a primitive, Record is not complex
|
|
373
373
|
return valueTypeStr === 'string' || valueTypeStr === 'number' || valueTypeStr === 'boolean';
|
|
374
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Gets the underlying type from utility types like Partial, Required, Pick, Omit, etc.
|
|
377
|
+
* Returns null if not a supported utility type or if type arguments are missing.
|
|
378
|
+
*/
|
|
379
|
+
function getUtilityTypeArgument(typeNode) {
|
|
380
|
+
const unwrapped = unwrapReadonlyOperator(typeNode);
|
|
381
|
+
if (unwrapped.type !== 'TSTypeReference') {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const typeName = getTypeReferenceName(unwrapped.typeName);
|
|
385
|
+
const supportedUtilityTypes = [
|
|
386
|
+
'Partial',
|
|
387
|
+
'Required',
|
|
388
|
+
'Pick',
|
|
389
|
+
'Omit',
|
|
390
|
+
'Readonly',
|
|
391
|
+
'NonNullable',
|
|
392
|
+
'Extract',
|
|
393
|
+
'Exclude',
|
|
394
|
+
'ReadonlyArray',
|
|
395
|
+
];
|
|
396
|
+
if (!supportedUtilityTypes.includes(typeName)) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
// All these utility types have at least one type argument
|
|
400
|
+
if (!unwrapped.typeArguments || unwrapped.typeArguments.params.length === 0) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
utilityType: typeName,
|
|
405
|
+
typeArgument: unwrapped.typeArguments.params[0],
|
|
406
|
+
allTypeArguments: unwrapped.typeArguments.params,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Unwraps utility types to get the base type reference for @Type decorator suggestions.
|
|
411
|
+
* For Pick<User, 'name'> -> returns User
|
|
412
|
+
* For Partial<Address> -> returns Address
|
|
413
|
+
*/
|
|
414
|
+
function unwrapUtilityTypeForClassName(typeNode) {
|
|
415
|
+
let current = typeNode;
|
|
416
|
+
// Keep unwrapping utility types until we hit the base type
|
|
417
|
+
while (true) {
|
|
418
|
+
const utilityInfo = getUtilityTypeArgument(current);
|
|
419
|
+
if (!utilityInfo) {
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
// Use the first argument, assuming it's the source type for most utility types
|
|
423
|
+
current = utilityInfo.typeArgument;
|
|
424
|
+
}
|
|
425
|
+
return current;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Analyzes a union type to determine its composition.
|
|
429
|
+
* Returns information about what types are present in the union.
|
|
430
|
+
* Uses isComplexType to accurately classify each union member.
|
|
431
|
+
*/
|
|
432
|
+
function analyzeUnionType(typeNode, checker = null, esTreeNodeMap = null) {
|
|
433
|
+
const result = {
|
|
434
|
+
isNullable: false,
|
|
435
|
+
hasMultiplePrimitives: false,
|
|
436
|
+
hasMultipleComplexTypes: false,
|
|
437
|
+
hasMixedComplexity: false,
|
|
438
|
+
nonNullTypes: [],
|
|
439
|
+
primitiveTypes: [],
|
|
440
|
+
complexTypes: [],
|
|
441
|
+
};
|
|
442
|
+
if (typeNode.type !== 'TSUnionType') {
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
for (const type of typeNode.types) {
|
|
446
|
+
if (type.type === 'TSNullKeyword' || type.type === 'TSUndefinedKeyword') {
|
|
447
|
+
result.isNullable = true;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
result.nonNullTypes.push(type);
|
|
451
|
+
// Use isComplexType for accurate classification
|
|
452
|
+
const typeIsComplex = isComplexType(type, checker, esTreeNodeMap);
|
|
453
|
+
if (typeIsComplex) {
|
|
454
|
+
result.complexTypes.push(type);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
// It's a simple/primitive type
|
|
458
|
+
const typeStr = getTypeString(type, checker, esTreeNodeMap);
|
|
459
|
+
if (typeStr) {
|
|
460
|
+
result.primitiveTypes.push(typeStr);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
result.hasMultiplePrimitives = result.primitiveTypes.length > 1;
|
|
465
|
+
result.hasMultipleComplexTypes = result.complexTypes.length > 1;
|
|
466
|
+
result.hasMixedComplexity = result.primitiveTypes.length > 0 && result.complexTypes.length > 0;
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
375
469
|
/**
|
|
376
470
|
* Determines if a type requires @ValidateNested decorator for proper validation.
|
|
377
471
|
* Complex types include objects, class instances, type literals, and intersections.
|
|
@@ -391,14 +485,9 @@ function isComplexType(typeNode, checker = null, esTreeNodeMap = null) {
|
|
|
391
485
|
}
|
|
392
486
|
// Union types are complex if any non-null/undefined member is complex
|
|
393
487
|
if (unwrapped.type === 'TSUnionType') {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
// Recursively check if this union member is complex
|
|
400
|
-
return isComplexType(type, checker, esTreeNodeMap);
|
|
401
|
-
});
|
|
488
|
+
const unionAnalysis = analyzeUnionType(unwrapped, checker, esTreeNodeMap);
|
|
489
|
+
// If there are any complex types in the union, it's complex
|
|
490
|
+
return unionAnalysis.complexTypes.length > 0;
|
|
402
491
|
}
|
|
403
492
|
// Intersection types: check if any member is a primitive (branded types)
|
|
404
493
|
if (unwrapped.type === 'TSIntersectionType') {
|
|
@@ -431,6 +520,41 @@ function isComplexType(typeNode, checker = null, esTreeNodeMap = null) {
|
|
|
431
520
|
if (nonValidatableTypes.includes(typeName)) {
|
|
432
521
|
return false;
|
|
433
522
|
}
|
|
523
|
+
// Check for utility types - delegate to the underlying type
|
|
524
|
+
const utilityTypeInfo = getUtilityTypeArgument(unwrapped);
|
|
525
|
+
if (utilityTypeInfo) {
|
|
526
|
+
// ReadonlyArray<T> behaves like Array<T>
|
|
527
|
+
if (utilityTypeInfo.utilityType === 'ReadonlyArray') {
|
|
528
|
+
return isComplexType(utilityTypeInfo.typeArgument, checker, esTreeNodeMap);
|
|
529
|
+
}
|
|
530
|
+
// Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>
|
|
531
|
+
// For complexity checking, we need to use the type checker if available
|
|
532
|
+
if (['Partial', 'Required', 'Readonly', 'Pick', 'Omit'].includes(utilityTypeInfo.utilityType)) {
|
|
533
|
+
// Try to resolve with type checker first for Pick/Omit
|
|
534
|
+
if (checker &&
|
|
535
|
+
esTreeNodeMap &&
|
|
536
|
+
(utilityTypeInfo.utilityType === 'Pick' || utilityTypeInfo.utilityType === 'Omit')) {
|
|
537
|
+
const resolved = resolveTypeWithChecker(unwrapped, checker, esTreeNodeMap);
|
|
538
|
+
if (resolved) {
|
|
539
|
+
// If type checker resolved it to a primitive, it's not complex
|
|
540
|
+
return resolved !== 'string' && resolved !== 'number' && resolved !== 'boolean' && resolved !== 'Date';
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Fall back to checking if the underlying type is complex
|
|
544
|
+
// Note: Pick/Omit always create object types, but if we can't resolve with type checker,
|
|
545
|
+
// we check the source type. This may produce false positives for Pick<Complex, 'primitiveField'>
|
|
546
|
+
// but it's safer than false negatives.
|
|
547
|
+
return isComplexType(utilityTypeInfo.typeArgument, checker, esTreeNodeMap);
|
|
548
|
+
}
|
|
549
|
+
// NonNullable<T> - check if the underlying type is complex
|
|
550
|
+
if (utilityTypeInfo.utilityType === 'NonNullable') {
|
|
551
|
+
return isComplexType(utilityTypeInfo.typeArgument, checker, esTreeNodeMap);
|
|
552
|
+
}
|
|
553
|
+
// Extract<T, U> and Exclude<T, U> - check the first type argument
|
|
554
|
+
if (utilityTypeInfo.utilityType === 'Extract' || utilityTypeInfo.utilityType === 'Exclude') {
|
|
555
|
+
return isComplexType(utilityTypeInfo.typeArgument, checker, esTreeNodeMap);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
434
558
|
// Record<K, V> with primitive V is not complex
|
|
435
559
|
if (isRecordWithPrimitiveValue(unwrapped, checker, esTreeNodeMap)) {
|
|
436
560
|
return false;
|
|
@@ -458,7 +582,7 @@ function isArrayType(typeAnnotation) {
|
|
|
458
582
|
}
|
|
459
583
|
if (unwrapped.type === 'TSTypeReference' &&
|
|
460
584
|
unwrapped.typeName.type === 'Identifier' &&
|
|
461
|
-
unwrapped.typeName.name === 'Array') {
|
|
585
|
+
(unwrapped.typeName.name === 'Array' || unwrapped.typeName.name === 'ReadonlyArray')) {
|
|
462
586
|
return true;
|
|
463
587
|
}
|
|
464
588
|
if (unwrapped.type === 'TSTupleType') {
|
|
@@ -580,7 +704,7 @@ function hasValidateNestedEachOption(decorator) {
|
|
|
580
704
|
}
|
|
581
705
|
/**
|
|
582
706
|
* Validates if a decorator matches the TypeScript type annotation.
|
|
583
|
-
* Handles special cases like @IsEnum validation and
|
|
707
|
+
* Handles special cases like @IsEnum validation, nullable unions, and utility types.
|
|
584
708
|
*/
|
|
585
709
|
function checkTypeMatch(decorator, typeAnnotation, actualType, checker, esTreeNodeMap) {
|
|
586
710
|
const expectedTypes = decoratorTypeMap[decorator];
|
|
@@ -601,6 +725,15 @@ function checkTypeMatch(decorator, typeAnnotation, actualType, checker, esTreeNo
|
|
|
601
725
|
}
|
|
602
726
|
return false;
|
|
603
727
|
}
|
|
728
|
+
// For utility types, unwrap to the underlying type
|
|
729
|
+
const utilityTypeInfo = getUtilityTypeArgument(typeAnnotation);
|
|
730
|
+
if (utilityTypeInfo && utilityTypeInfo.utilityType !== 'ReadonlyArray') {
|
|
731
|
+
// Recursively check the underlying type
|
|
732
|
+
const underlyingType = getTypeString(utilityTypeInfo.typeArgument, checker, esTreeNodeMap);
|
|
733
|
+
if (underlyingType) {
|
|
734
|
+
return checkTypeMatch(decorator, utilityTypeInfo.typeArgument, underlyingType, checker, esTreeNodeMap);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
604
737
|
// For nullable unions (T | null | undefined), validate against the base type
|
|
605
738
|
const nullableCheck = isNullableUnion(typeAnnotation);
|
|
606
739
|
if (nullableCheck.isNullable && nullableCheck.baseType) {
|
|
@@ -653,13 +786,16 @@ function checkTypeMatch(decorator, typeAnnotation, actualType, checker, esTreeNo
|
|
|
653
786
|
* - Missing @Type decorator when @ValidateNested is present
|
|
654
787
|
* - Type aliases (via TypeScript type checker)
|
|
655
788
|
* - Record<K, V> utility types with primitive values
|
|
789
|
+
* - Utility types: Partial<T>, Required<T>, Pick<T, K>, Omit<T, K>, ReadonlyArray<T>, NonNullable<T>, Extract<T, U>, Exclude<T, U>
|
|
790
|
+
* - Multi-type unions with explicit warnings for complex scenarios
|
|
791
|
+
* - Mixed complexity unions (primitive | complex types)
|
|
656
792
|
*/
|
|
657
793
|
exports.default = createRule({
|
|
658
794
|
name: 'decorator-type-match',
|
|
659
795
|
meta: {
|
|
660
796
|
type: 'problem',
|
|
661
797
|
docs: {
|
|
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
|
|
798
|
+
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, Record utility types, and other utility types (Partial, Required, Pick, Omit, ReadonlyArray, NonNullable, Extract, Exclude). Provides explicit warnings for multi-type unions.',
|
|
663
799
|
},
|
|
664
800
|
messages: {
|
|
665
801
|
mismatch: 'Decorator @{{decorator}} does not match type annotation {{actualType}}. Expected: {{expectedTypes}}',
|
|
@@ -672,6 +808,9 @@ exports.default = createRule({
|
|
|
672
808
|
invalidEachOption: 'Decorator @{{decorator}} has { each: true } option but property type is not an array. Remove { each: true } or change type to an array.',
|
|
673
809
|
missingTypeDecorator: 'Complex type {{actualType}} with @ValidateNested() requires @Type(() => {{className}}) decorator for proper transformation.',
|
|
674
810
|
tupleValidationWarning: 'Tuple type contains complex elements. Consider using a regular array with @ValidateNested({ each: true }) or validate elements individually.',
|
|
811
|
+
multiTypeUnionWarning: 'Union type contains multiple complex types ({{types}}). Discriminated unions require custom validation logic - consider splitting into separate properties or using a custom validator.',
|
|
812
|
+
mixedComplexityUnionWarning: 'Union type mixes simple and complex types ({{primitives}} | {{complexTypes}}). This requires careful validation - simple types need type validators while complex types need @ValidateNested(). Consider using discriminated unions or custom validators.',
|
|
813
|
+
pickOmitWarning: 'Type {{utilityType}}<{{baseType}}, ...> may be picking/omitting primitive fields. If the resulting type is primitive, use the appropriate primitive decorator instead of @ValidateNested(). If complex, @ValidateNested() is correct.',
|
|
675
814
|
},
|
|
676
815
|
schema: [],
|
|
677
816
|
},
|
|
@@ -831,6 +970,38 @@ exports.default = createRule({
|
|
|
831
970
|
});
|
|
832
971
|
}
|
|
833
972
|
}
|
|
973
|
+
// Special handling for multi-type unions - only warn for truly problematic cases
|
|
974
|
+
if (typeAnnotation.type === 'TSUnionType' && !isUnionOfLiterals(typeAnnotation)) {
|
|
975
|
+
const unionAnalysis = analyzeUnionType(typeAnnotation, checker, esTreeNodeMap);
|
|
976
|
+
// Only warn about unions with multiple complex types (genuinely hard to validate)
|
|
977
|
+
// Don't warn about multi-primitive unions (string | number) as they're common and not necessarily wrong
|
|
978
|
+
if (unionAnalysis.hasMultipleComplexTypes) {
|
|
979
|
+
const complexTypeNames = unionAnalysis.complexTypes
|
|
980
|
+
.map((t) => getTypeString(t, checker, esTreeNodeMap) || 'unknown')
|
|
981
|
+
.join(' | ');
|
|
982
|
+
context.report({
|
|
983
|
+
node,
|
|
984
|
+
messageId: 'multiTypeUnionWarning',
|
|
985
|
+
data: {
|
|
986
|
+
types: complexTypeNames,
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
// Warn about unions mixing primitives and complex types (requires different decorators)
|
|
991
|
+
if (unionAnalysis.hasMixedComplexity) {
|
|
992
|
+
const complexTypeNames = unionAnalysis.complexTypes
|
|
993
|
+
.map((t) => getTypeString(t, checker, esTreeNodeMap) || 'unknown')
|
|
994
|
+
.join(' | ');
|
|
995
|
+
context.report({
|
|
996
|
+
node,
|
|
997
|
+
messageId: 'mixedComplexityUnionWarning',
|
|
998
|
+
data: {
|
|
999
|
+
primitives: unionAnalysis.primitiveTypes.join(' | '),
|
|
1000
|
+
complexTypes: complexTypeNames,
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
834
1005
|
// Validate arrays of complex types have proper nested validation
|
|
835
1006
|
if (isArrayType(typeAnnotation) && !isTupleType(typeAnnotation)) {
|
|
836
1007
|
const elementTypeNode = getArrayElementTypeNode(typeAnnotation);
|
|
@@ -876,13 +1047,16 @@ exports.default = createRule({
|
|
|
876
1047
|
if (nullableCheck.isNullable && nullableCheck.baseType) {
|
|
877
1048
|
elementTypeToCheck = nullableCheck.baseType;
|
|
878
1049
|
}
|
|
879
|
-
|
|
880
|
-
|
|
1050
|
+
// Unwrap utility types to get the base class name
|
|
1051
|
+
const unwrappedType = unwrapUtilityTypeForClassName(elementTypeToCheck);
|
|
1052
|
+
if (unwrappedType.type === 'TSTypeReference') {
|
|
1053
|
+
const className = getTypeReferenceName(unwrappedType.typeName);
|
|
1054
|
+
const displayType = getTypeString(elementTypeNode, checker, esTreeNodeMap);
|
|
881
1055
|
context.report({
|
|
882
1056
|
node,
|
|
883
1057
|
messageId: 'missingTypeDecorator',
|
|
884
1058
|
data: {
|
|
885
|
-
actualType: `${
|
|
1059
|
+
actualType: `${displayType}[]`,
|
|
886
1060
|
className,
|
|
887
1061
|
},
|
|
888
1062
|
});
|
|
@@ -970,13 +1144,32 @@ exports.default = createRule({
|
|
|
970
1144
|
!hasTypedDecorator) {
|
|
971
1145
|
// Complex types should have @ValidateNested()
|
|
972
1146
|
if (!hasValidateNested && decorators.length > 0) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1147
|
+
// Check if it's a Pick/Omit type that might be a false positive
|
|
1148
|
+
const utilityInfo = getUtilityTypeArgument(typeAnnotation);
|
|
1149
|
+
if (utilityInfo && (utilityInfo.utilityType === 'Pick' || utilityInfo.utilityType === 'Omit')) {
|
|
1150
|
+
// Provide a more helpful message for Pick/Omit
|
|
1151
|
+
const baseTypeName = utilityInfo.typeArgument.type === 'TSTypeReference'
|
|
1152
|
+
? getTypeReferenceName(utilityInfo.typeArgument.typeName)
|
|
1153
|
+
: 'unknown';
|
|
1154
|
+
context.report({
|
|
1155
|
+
node,
|
|
1156
|
+
messageId: 'pickOmitWarning',
|
|
1157
|
+
data: {
|
|
1158
|
+
utilityType: utilityInfo.utilityType,
|
|
1159
|
+
baseType: baseTypeName,
|
|
1160
|
+
},
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
// Standard complex type warning
|
|
1165
|
+
context.report({
|
|
1166
|
+
node,
|
|
1167
|
+
messageId: 'missingValidateNested',
|
|
1168
|
+
data: {
|
|
1169
|
+
actualType,
|
|
1170
|
+
},
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
980
1173
|
}
|
|
981
1174
|
}
|
|
982
1175
|
// Check if complex non-array types with @ValidateNested also have @Type
|
|
@@ -990,13 +1183,16 @@ exports.default = createRule({
|
|
|
990
1183
|
if (nullableCheck.isNullable && nullableCheck.baseType) {
|
|
991
1184
|
typeToCheck = nullableCheck.baseType;
|
|
992
1185
|
}
|
|
993
|
-
|
|
994
|
-
|
|
1186
|
+
// Unwrap utility types to get the base class name
|
|
1187
|
+
const unwrappedType = unwrapUtilityTypeForClassName(typeToCheck);
|
|
1188
|
+
if (unwrappedType.type === 'TSTypeReference') {
|
|
1189
|
+
const className = getTypeReferenceName(unwrappedType.typeName);
|
|
1190
|
+
const displayType = getTypeString(typeToCheck, checker, esTreeNodeMap);
|
|
995
1191
|
context.report({
|
|
996
1192
|
node,
|
|
997
1193
|
messageId: 'missingTypeDecorator',
|
|
998
1194
|
data: {
|
|
999
|
-
actualType: className,
|
|
1195
|
+
actualType: displayType || className,
|
|
1000
1196
|
className,
|
|
1001
1197
|
},
|
|
1002
1198
|
});
|