diff --git a/src/Common/ITypeSymbolExtensions.cs b/src/Common/ITypeSymbolExtensions.cs index d5b87c1..ac0c43f 100644 --- a/src/Common/ITypeSymbolExtensions.cs +++ b/src/Common/ITypeSymbolExtensions.cs @@ -130,6 +130,25 @@ typeSymbol is }, }; + public static bool IsNotNullAttribute([NotNullWhen(returnValue: true)] this INamedTypeSymbol? typeSymbol) => + typeSymbol is + { + Name: "NotNullAttribute", + ContainingNamespace: + { + Name: "Shared", + ContainingNamespace: + { + Name: "Validations", + ContainingNamespace: + { + Name: "Immediate", + ContainingNamespace.IsGlobalNamespace: true, + }, + }, + }, + }; + public static bool IsValidationResult(this INamedTypeSymbol? typeSymbol) => typeSymbol is { diff --git a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Shipped.md b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Shipped.md index c46d5de..f73f144 100644 --- a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Shipped.md @@ -3,4 +3,29 @@ ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- +--------|----------|----------|------- +IV0001 | ImmediateValidations | Error | ValidateMethodAnalyzer +IV0002 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0003 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0004 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0005 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0009 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0010 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0011 | ImmediateValidations | Warning | AssemblyBehaviorAnalyzer +IV0012 | ImmediateValidations | Error | ValidateClassAnalyzer +IV0013 | ImmediateValidations | Warning | ValidateClassAnalyzer +IV0014 | ImmediateValidations | Warning | ValidateClassAnalyzer +IV0015 | ImmediateValidations | Warning | ValidateClassAnalyzer +IV0016 | ImmediateValidations | Warning | ValidateClassAnalyzer +IV0017 | ImmediateValidations | Error | ValidateClassAnalyzer + +## Release 1.1 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +IV0018 | ImmediateValidations | Warning | ValidateClassAnalyzer diff --git a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md index 18f5889..34c8af7 100644 --- a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md @@ -1,21 +1 @@ ### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- -IV0001 | ImmediateValidations | Error | ValidateMethodAnalyzer -IV0002 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0003 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0004 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0005 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0009 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0010 | ImmediateValidations | Error | ValidatorClassAnalyzer -IV0011 | ImmediateValidations | Warning | AssemblyBehaviorAnalyzer -IV0012 | ImmediateValidations | Error | ValidateClassAnalyzer -IV0013 | ImmediateValidations | Warning | ValidateClassAnalyzer -IV0014 | ImmediateValidations | Warning | ValidateClassAnalyzer -IV0015 | ImmediateValidations | Warning | ValidateClassAnalyzer -IV0016 | ImmediateValidations | Warning | ValidateClassAnalyzer -IV0017 | ImmediateValidations | Error | ValidateClassAnalyzer diff --git a/src/Immediate.Validations.Analyzers/DiagnosticIds.cs b/src/Immediate.Validations.Analyzers/DiagnosticIds.cs index 154980b..4fc93fc 100644 --- a/src/Immediate.Validations.Analyzers/DiagnosticIds.cs +++ b/src/Immediate.Validations.Analyzers/DiagnosticIds.cs @@ -22,4 +22,5 @@ public static class DiagnosticIds public const string IV0015ValidateParameterIncompatibleType = "IV0015"; public const string IV0016ValidateParameterPropertyIncompatibleType = "IV0016"; public const string IV0017ValidateParameterNameofInvalid = "IV0017"; + public const string IV0018ValidateNotNullWhenInvalid = "IV0018"; } diff --git a/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs b/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs index 7b24fb2..87f7484 100644 --- a/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs +++ b/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs @@ -76,6 +76,17 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer description: "Invalid nameof will generate incorrect code." ); + public static readonly DiagnosticDescriptor ValidateNotNullWhenInvalid = + new( + id: DiagnosticIds.IV0018ValidateNotNullWhenInvalid, + title: "`[NotNull]` applied to not-null property", + messageFormat: "`[NotNull]` is applied to property `{0}`, but type `{1}` cannot be null", + category: "ImmediateValidations", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Invalid `[NotNull]` will generate incorrect code." + ); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( [ @@ -85,6 +96,7 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer ValidateParameterIncompatibleType, ValidateParameterPropertyIncompatibleType, ValidateParameterNameofInvalid, + ValidateNotNullWhenInvalid, ]); public override void Initialize(AnalysisContext context) @@ -168,7 +180,7 @@ or IMethodSymbol { var status = ValidateAttribute(context.Compilation, property.Type, attribute.AttributeClass!, token); - if (status.Report) + if (status.IncompatibleType) { context.ReportDiagnostic( Diagnostic.Create( @@ -179,6 +191,17 @@ or IMethodSymbol ) ); } + else if (status.InvalidNotNull) + { + context.ReportDiagnostic( + Diagnostic.Create( + ValidateNotNullWhenInvalid, + attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation(), + property.Name, + property.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + ) + ); + } else if (status.ValidateArguments) { ValidateArguments( @@ -195,7 +218,8 @@ or IMethodSymbol private sealed record AttributeValidationStatus { - public required bool Report { get; init; } + public bool IncompatibleType { get; init; } + public bool InvalidNotNull { get; init; } public bool ValidateArguments { get; init; } public ImmutableArray ValidateParameterSymbols { get; init; } } @@ -210,7 +234,7 @@ CancellationToken token token.ThrowIfCancellationRequested(); if (!attributeSymbol.ImplementsValidatorAttribute()) - return new() { Report = false }; + return new(); token.ThrowIfCancellationRequested(); @@ -225,16 +249,23 @@ CancellationToken token ) { // covered by other analyzers - return new() { Report = false }; + return new(); } + token.ThrowIfCancellationRequested(); + if (targetParameterType is ITypeParameterSymbol tps) { + if (attributeSymbol.IsNotNullAttribute() + && propertyType is { IsReferenceType: false, OriginalDefinition.SpecialType: not SpecialType.System_Nullable_T }) + { + return new() { InvalidNotNull = true }; + } + if (tps.SatisfiesConstraints(propertyType, compilation)) { return new() { - Report = false, ValidateArguments = true, ValidateParameterSymbols = validateMethod.Parameters, }; @@ -255,13 +286,14 @@ CancellationToken token { return new() { - Report = false, ValidateArguments = true, ValidateParameterSymbols = validateMethod.Parameters, }; } } + token.ThrowIfCancellationRequested(); + return propertyType switch { IArrayTypeSymbol ats => @@ -285,7 +317,7 @@ CancellationToken token token ), - _ => new() { Report = true, }, + _ => new() { IncompatibleType = true, }, }; } diff --git a/src/Immediate.Validations.CodeFixes/AddAdditionalValidationsCodeRefactoringProvider.cs b/src/Immediate.Validations.CodeFixes/AddAdditionalValidationsCodeRefactoringProvider.cs index 81e8893..e53b13e 100644 --- a/src/Immediate.Validations.CodeFixes/AddAdditionalValidationsCodeRefactoringProvider.cs +++ b/src/Immediate.Validations.CodeFixes/AddAdditionalValidationsCodeRefactoringProvider.cs @@ -59,13 +59,13 @@ TypeDeclarationSyntax typeDeclarationSyntax ])) .WithParameterList( ParameterList( - SeparatedList(new ParameterSyntax[] - { + SeparatedList( + [ Parameter(Identifier("errors")) .WithType(IdentifierName("ValidationResult")), Parameter(Identifier("target")) .WithType(IdentifierName(typeDeclarationSyntax.Identifier)), - }))) + ]))) .WithBody( Block()) .WithAdditionalAnnotations(Formatter.Annotation); diff --git a/src/Immediate.Validations.Generators/Models.cs b/src/Immediate.Validations.Generators/Models.cs index cb377c8..e353112 100644 --- a/src/Immediate.Validations.Generators/Models.cs +++ b/src/Immediate.Validations.Generators/Models.cs @@ -25,6 +25,7 @@ public sealed record ValidationTargetProperty public required string TypeFullName { get; init; } public required bool IsReferenceType { get; init; } public required bool IsNullable { get; init; } + public required bool ValidateNotNull { get; init; } public required bool IsValidationProperty { get; init; } public required string? ValidationTypeFullName { get; init; } public required ValidationTargetProperty? CollectionPropertyDetails { get; init; } diff --git a/src/Immediate.Validations.Generators/Templates/Validations.sbntxt b/src/Immediate.Validations.Generators/Templates/Validations.sbntxt index 61afe9a..f88f97b 100644 --- a/src/Immediate.Validations.Generators/Templates/Validations.sbntxt +++ b/src/Immediate.Validations.Generators/Templates/Validations.sbntxt @@ -119,7 +119,7 @@ partial {{ class.type }} {{ class.name }} {{~ if p.is_reference_type || p.is_nullable ~}} if (target is not { } t) { - {{~ if !p.is_nullable ~}} + {{~ if p.validate_not_null ~}} errors.Add( $"{{ get_prop_name(p.property_name, depth) }}", $"'{{ get_prop_name(p.name, depth) }}' must not be null." diff --git a/src/Immediate.Validations.Generators/ValidateTargetTransformer.cs b/src/Immediate.Validations.Generators/ValidateTargetTransformer.cs index 9282473..b2c646c 100644 --- a/src/Immediate.Validations.Generators/ValidateTargetTransformer.cs +++ b/src/Immediate.Validations.Generators/ValidateTargetTransformer.cs @@ -190,7 +190,9 @@ ImmutableArray attributes || attributes.Any(a => a.AttributeClass.IsAllowNullAttribute())) : propertyType.IsNullableType(); - var baseType = !isReferenceType && isNullable + var validateNotNull = !isNullable || attributes.Any(a => a.AttributeClass.IsNotNullAttribute()); + + var baseType = propertyType.IsNullableType() ? ((INamedTypeSymbol)propertyType).TypeArguments[0] : propertyType; @@ -261,7 +263,8 @@ ImmutableArray attributes }; if ( - (isNullable || !isReferenceType) + (!validateNotNull + || (!isReferenceType && !propertyType.IsNullableType())) && !isValidationProperty && collectionPropertyDetails is null && validations is [] @@ -277,6 +280,7 @@ ImmutableArray attributes TypeFullName = propertyType.ToDisplayString(s_fullyQualifiedPlusNullable), IsReferenceType = isReferenceType, IsNullable = isNullable, + ValidateNotNull = validateNotNull, IsValidationProperty = isValidationProperty, ValidationTypeFullName = isValidationProperty @@ -303,7 +307,7 @@ AttributeData attribute _token.ThrowIfCancellationRequested(); var @class = attribute.AttributeClass; - if (!@class.ImplementsValidatorAttribute()) + if (!@class.ImplementsValidatorAttribute() || @class.IsNotNullAttribute()) return null; _token.ThrowIfCancellationRequested(); diff --git a/src/Immediate.Validations.Shared/Validators/NotNullAttribute.cs b/src/Immediate.Validations.Shared/Validators/NotNullAttribute.cs index 712dcbd..dbd4880 100644 --- a/src/Immediate.Validations.Shared/Validators/NotNullAttribute.cs +++ b/src/Immediate.Validations.Shared/Validators/NotNullAttribute.cs @@ -17,8 +17,8 @@ public sealed class NotNullAttribute : ValidatorAttribute /// /// if the property is valid; otherwise. /// - public static bool ValidateProperty(T value) - where T : class => value is not null; + public static bool ValidateProperty(T value) => + value is not null; /// /// The default message template when the property is invalid. diff --git a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs index c810e5f..c6bb123 100644 --- a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs +++ b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs @@ -106,11 +106,20 @@ await AnalyzerTestHelpers.CreateAnalyzerTest( """ using System.Collections.Generic; using Immediate.Validations.Shared; + + public sealed class NotNullClassAttribute : ValidatorAttribute + { + public static bool ValidateProperty(T value) + where T : class => + value is not null; + + public static string DefaultMessage => ValidationConfiguration.Localizer[nameof(NotNullAttribute)].Value; + } [Validate] public sealed partial record Target : IValidationTarget { - [{|IV0014:NotNull|}] + [{|IV0014:NotNullClass|}] public required int Id { get; init; } public static ValidationResult Validate(Target target) => []; @@ -940,4 +949,64 @@ public partial interface IInterface : IBaseInterface, IValidationTarget + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using System; + using System.Collections.Generic; + using Immediate.Validations.Shared; + + [Validate] + public sealed partial record Target : IValidationTarget + { + [{|IV0018:NotNull|}] + public required int IntProperty { get; init; } + + public static ValidationResult Validate(Target target) => []; + public static ValidationResult Validate(Target target, ValidationResult errors) => []; + } + """ + ).RunAsync(); + + [Fact] + public async Task NotNullOnNullableIntShouldNotWarn() => + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using System; + using System.Collections.Generic; + using Immediate.Validations.Shared; + + [Validate] + public sealed partial record Target : IValidationTarget + { + [NotNull] + public required int? IntProperty { get; init; } + + public static ValidationResult Validate(Target target) => []; + public static ValidationResult Validate(Target target, ValidationResult errors) => []; + } + """ + ).RunAsync(); + + [Fact] + public async Task NotNullOnStringShouldNotWarn() => + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using System; + using System.Collections.Generic; + using Immediate.Validations.Shared; + + [Validate] + public sealed partial record Target : IValidationTarget + { + [NotNull] + public required string StringProperty { get; init; } + + public static ValidationResult Validate(Target target) => []; + public static ValidationResult Validate(Target target, ValidationResult errors) => []; + } + """ + ).RunAsync(); } diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullAsCustomValidationOnGenericType#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullAsCustomValidationOnGenericType#IV...ValidateClass.g.verified.cs index d98a39d..efc2abb 100644 --- a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullAsCustomValidationOnGenericType#IV...ValidateClass.g.verified.cs +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullAsCustomValidationOnGenericType#IV...ValidateClass.g.verified.cs @@ -46,29 +46,16 @@ private static void __ValidateStringProperty( if (target is not { } t) { + errors.Add( + $"StringProperty", + $"'String Property' must not be null." + ); return; } - { - if (!global::Immediate.Validations.Shared.NotNullAttribute.ValidateProperty( - t - ) - ) - { - errors.Add( - $"StringProperty", - global::Immediate.Validations.Shared.NotNullAttribute.DefaultMessage, - new() - { - ["PropertyName"] = $"String Property", - ["PropertyValue"] = t, - } - ); - } - } } } diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnInt#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnInt#IV...ValidateClass.g.verified.cs new file mode 100644 index 0000000..0350da4 --- /dev/null +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnInt#IV...ValidateClass.g.verified.cs @@ -0,0 +1,42 @@ +//HintName: IV...ValidateClass.g.cs +using System.Collections.Generic; +using Immediate.Validations.Shared; + +#nullable enable +#pragma warning disable CS1591 + + +partial class ValidateClass +{ + static ValidationResult IValidationTarget.Validate(ValidateClass? target) => + Validate(target, []); + + static ValidationResult IValidationTarget.Validate(ValidateClass? target, ValidationResult errors) => + Validate(target, errors); + + public static ValidationResult Validate(ValidateClass? target) => + Validate(target, []); + + public static ValidationResult Validate(ValidateClass? target, ValidationResult errors) + { + if (target is not { } t) + { + return new() + { + { ".self", "`target` must not be `null`." }, + }; + } + + if (!errors.VisitType(typeof(ValidateClass))) + return errors; + + + + + return errors; + } + + + +} + diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnNullableInt#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnNullableInt#IV...ValidateClass.g.verified.cs new file mode 100644 index 0000000..ec22dea --- /dev/null +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnNullableInt#IV...ValidateClass.g.verified.cs @@ -0,0 +1,62 @@ +//HintName: IV...ValidateClass.g.cs +using System.Collections.Generic; +using Immediate.Validations.Shared; + +#nullable enable +#pragma warning disable CS1591 + + +partial class ValidateClass +{ + static ValidationResult IValidationTarget.Validate(ValidateClass? target) => + Validate(target, []); + + static ValidationResult IValidationTarget.Validate(ValidateClass? target, ValidationResult errors) => + Validate(target, errors); + + public static ValidationResult Validate(ValidateClass? target) => + Validate(target, []); + + public static ValidationResult Validate(ValidateClass? target, ValidationResult errors) + { + if (target is not { } t) + { + return new() + { + { ".self", "`target` must not be `null`." }, + }; + } + + if (!errors.VisitType(typeof(ValidateClass))) + return errors; + + + __ValidateIntProperty(errors, t, t.IntProperty); + + + return errors; + } + + + + private static void __ValidateIntProperty( + ValidationResult errors, ValidateClass instance, int? target + ) + { + + if (target is not { } t) + { + errors.Add( + $"IntProperty", + $"'Int Property' must not be null." + ); + + return; + } + + + + } + +} + diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnString#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnString#IV...ValidateClass.g.verified.cs new file mode 100644 index 0000000..efc2abb --- /dev/null +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.NotNullOnString#IV...ValidateClass.g.verified.cs @@ -0,0 +1,62 @@ +//HintName: IV...ValidateClass.g.cs +using System.Collections.Generic; +using Immediate.Validations.Shared; + +#nullable enable +#pragma warning disable CS1591 + + +partial class ValidateClass +{ + static ValidationResult IValidationTarget.Validate(ValidateClass? target) => + Validate(target, []); + + static ValidationResult IValidationTarget.Validate(ValidateClass? target, ValidationResult errors) => + Validate(target, errors); + + public static ValidationResult Validate(ValidateClass? target) => + Validate(target, []); + + public static ValidationResult Validate(ValidateClass? target, ValidationResult errors) + { + if (target is not { } t) + { + return new() + { + { ".self", "`target` must not be `null`." }, + }; + } + + if (!errors.VisitType(typeof(ValidateClass))) + return errors; + + + __ValidateStringProperty(errors, t, t.StringProperty); + + + return errors; + } + + + + private static void __ValidateStringProperty( + ValidationResult errors, ValidateClass instance, string? target + ) + { + + if (target is not { } t) + { + errors.Add( + $"StringProperty", + $"'String Property' must not be null." + ); + + return; + } + + + + } + +} + diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs index 638da4c..7693084 100644 --- a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs @@ -58,6 +58,90 @@ public partial class ValidateClass : IValidationTarget _ = await Verify(result); } + [Fact] + public async Task NotNullOnInt() + { + var result = GeneratorTestHelper.RunGenerator( + """ + #nullable enable + + using Immediate.Validations.Shared; + + [Validate] + public partial class ValidateClass : IValidationTarget + { + [NotNull] + public required int IntProperty { get; init; } + } + """ + ); + + Assert.Equal( + [ + @"Immediate.Validations.Generators/Immediate.Validations.Generators.ImmediateValidationsGenerator/IV...ValidateClass.g.cs", + ], + result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/')) + ); + + _ = await Verify(result); + } + + [Fact] + public async Task NotNullOnNullableInt() + { + var result = GeneratorTestHelper.RunGenerator( + """ + #nullable enable + + using Immediate.Validations.Shared; + + [Validate] + public partial class ValidateClass : IValidationTarget + { + [NotNull] + public required int? IntProperty { get; init; } + } + """ + ); + + Assert.Equal( + [ + @"Immediate.Validations.Generators/Immediate.Validations.Generators.ImmediateValidationsGenerator/IV...ValidateClass.g.cs", + ], + result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/')) + ); + + _ = await Verify(result); + } + + [Fact] + public async Task NotNullOnString() + { + var result = GeneratorTestHelper.RunGenerator( + """ + #nullable enable + + using Immediate.Validations.Shared; + + [Validate] + public partial class ValidateClass : IValidationTarget + { + [NotNull] + public required string? StringProperty { get; init; } + } + """ + ); + + Assert.Equal( + [ + @"Immediate.Validations.Generators/Immediate.Validations.Generators.ImmediateValidationsGenerator/IV...ValidateClass.g.cs", + ], + result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/')) + ); + + _ = await Verify(result); + } + [Fact] public async Task CustomValidationOnProperType() { @@ -199,10 +283,19 @@ public async Task NotNullAsCustomValidationOnInvalidGenericType() using Immediate.Validations.Shared; + public sealed class NotNullClassAttribute : ValidatorAttribute + { + public static bool ValidateProperty(T value) + where T : class => + value is not null; + + public static string DefaultMessage => ValidationConfiguration.Localizer[nameof(NotNullAttribute)].Value; + } + [Validate] public partial class ValidateClass : IValidationTarget { - [NotNull] + [NotNullClass] public required int? IntProperty { get; init; } } """ diff --git a/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj b/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj index 19afda7..90dcec9 100644 --- a/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj +++ b/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/Immediate.Validations.Tests/Utility.cs b/tests/Immediate.Validations.Tests/Utility.cs index 0d519e2..d6b0063 100644 --- a/tests/Immediate.Validations.Tests/Utility.cs +++ b/tests/Immediate.Validations.Tests/Utility.cs @@ -8,5 +8,6 @@ public static MetadataReference[] GetMetadataReferences() => [ MetadataReference.CreateFromFile("./Immediate.Handlers.Shared.dll"), MetadataReference.CreateFromFile("./Immediate.Validations.Shared.dll"), + MetadataReference.CreateFromFile("./Microsoft.Extensions.Localization.Abstractions.dll"), ]; }