From 615c52ab0e4ceb0ac2c2fdecd5d179da996fc701 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Thu, 30 May 2024 14:18:21 -0500 Subject: [PATCH] Add support for static fields (#46) --- .../ValidateClassAnalyzer.cs | 51 +++++++++++--- ...ImmediateValidationsGenerator.Transform.cs | 7 +- .../ValidateClassAnalyzerTests.cs | 56 ++++++++++++--- ...eArgument#IV...ValidateClass.g.verified.cs | 68 +++++++++++++++++++ ...eArgument#IV...ValidateClass.g.verified.cs | 68 +++++++++++++++++++ .../GeneratorTests/ValidatorArgumentTests.cs | 53 +++++++++++++++ 6 files changed, 280 insertions(+), 23 deletions(-) create mode 100644 tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.FieldAttributeArgument#IV...ValidateClass.g.verified.cs create mode 100644 tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.StaticFieldAttributeArgument#IV...ValidateClass.g.verified.cs diff --git a/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs b/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs index 5422fa4..3e82930 100644 --- a/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs +++ b/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs @@ -150,7 +150,7 @@ private static void AnalyzeSymbol(SyntaxNodeAnalysisContext context) var members = symbol .GetAllMembers() .Where(m => - m is IPropertySymbol + m is IPropertySymbol or IFieldSymbol or IMethodSymbol { Parameters: [], @@ -410,13 +410,6 @@ List members return; var validateParameter = validateParameterSymbols.First(p => p.Name == parameter.Name); - var targetType = validateParameter.Type; - - if (validateParameter.IsParams && targetType is IArrayTypeSymbol { ElementType: { } et }) - targetType = et; - - if (targetType is ITypeParameterSymbol) - targetType = typeArgumentType; if (syntax.Expression.IsNameOfExpression(out var propertyName)) { @@ -439,12 +432,13 @@ List members var memberType = member switch { - IPropertySymbol { Type: { } t } => t, IMethodSymbol { ReturnType: { } t } => t, + IPropertySymbol { Type: { } t } => t, + IFieldSymbol { Type: { } t } => t, _ => throw new InvalidOperationException(), }; - if (!context.Compilation.ClassifyConversion(memberType, targetType).IsValidConversion()) + if (!ValidateArgumentType(context.Compilation, validateParameter, memberType, typeArgumentType, out var targetType)) { context.ReportDiagnostic( Diagnostic.Create( @@ -462,7 +456,7 @@ List members if (context.SemanticModel.GetOperation(syntax.Expression)?.Type is not ITypeSymbol argumentType) return; - if (!context.Compilation.ClassifyConversion(argumentType, targetType).IsValidConversion()) + if (!ValidateArgumentType(context.Compilation, validateParameter, argumentType, typeArgumentType, out var targetType)) { context.ReportDiagnostic( Diagnostic.Create( @@ -475,6 +469,41 @@ List members } } } + + private static bool ValidateArgumentType( + Compilation compilation, + IParameterSymbol parameter, + ITypeSymbol argumentType, + ITypeSymbol typeArgumentType, + out ITypeSymbol targetType + ) + { + targetType = parameter.Type; + var buildArrayTypeSymbol = false; + + if (parameter.IsParams + && targetType is IArrayTypeSymbol { ElementType: { } paramElementType }) + { + targetType = paramElementType; + + if (argumentType is IArrayTypeSymbol { ElementType: { } argumentElementType }) + { + argumentType = argumentElementType; + buildArrayTypeSymbol = true; + } + } + + if (targetType is ITypeParameterSymbol) + targetType = typeArgumentType; + + if (compilation.ClassifyConversion(argumentType, targetType).IsValidConversion()) + return true; + + if (buildArrayTypeSymbol) + targetType = compilation.CreateArrayTypeSymbol(targetType); + + return false; + } } file static class Extensions diff --git a/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs b/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs index d514e20..5c49455 100644 --- a/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs +++ b/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs @@ -102,7 +102,7 @@ CancellationToken token var members = symbol .GetAllMembers() .Where(m => - m is IPropertySymbol + m is IPropertySymbol or IFieldSymbol or IMethodSymbol { Parameters: [], @@ -537,9 +537,8 @@ AttributeArgumentSyntax attributeArgumentSyntax { IMethodSymbol { IsStatic: true } => $"{name}()", IMethodSymbol => $"instance.{name}()", - IPropertySymbol { IsStatic: true } => $"{name}", - IPropertySymbol => $"instance.{name}", - _ => "", + { IsStatic: true } => $"{name}", + _ => $"instance.{name}", }; } diff --git a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs index 783dde2..039155f 100644 --- a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs +++ b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs @@ -535,7 +535,7 @@ params string[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -567,7 +567,7 @@ params string[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -599,7 +599,7 @@ params T[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -631,7 +631,7 @@ params T[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -663,7 +663,7 @@ params string[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -695,7 +695,7 @@ params string[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -727,7 +727,7 @@ params T[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -759,7 +759,7 @@ params T[] first ? default : (true, $"Value '{target}' is not equal to '{first[0]}'"); } - + [Validate] public sealed partial record Target : IValidationTarget { @@ -772,6 +772,46 @@ public sealed partial record Target : IValidationTarget """ ).RunAsync(); + [Fact] + public async Task ValidValidatorTypeShouldNotWarn16() => + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using System.Collections.Generic; + using Immediate.Validations.Shared; + + [Validate] + public sealed partial record Target : IValidationTarget + { + [OneOf(nameof(Values))] + public required string Id { get; init; } + + private static readonly string[] Values = ["123", "456", "789"]; + + public static List Validate(Target target) => []; + } + """ + ).RunAsync(); + + [Fact] + public async Task InvalidValidatorTypeShouldWarn16() => + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using System.Collections.Generic; + using Immediate.Validations.Shared; + + [Validate] + public sealed partial record Target : IValidationTarget + { + [OneOf({|IV0016:nameof(Values)|})] + public required int Id { get; init; } + + private static readonly string[] Values = ["123", "456", "789"]; + + public static List Validate(Target target) => []; + } + """ + ).RunAsync(); + [Fact] public async Task InvalidNameofShouldWarn() => await AnalyzerTestHelpers.CreateAnalyzerTest( diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.FieldAttributeArgument#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.FieldAttributeArgument#IV...ValidateClass.g.verified.cs new file mode 100644 index 0000000..80823f3 --- /dev/null +++ b/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.FieldAttributeArgument#IV...ValidateClass.g.verified.cs @@ -0,0 +1,68 @@ +//HintName: IV...ValidateClass.g.cs +using System.Collections.Generic; +using Immediate.Validations.Shared; + +#nullable enable +#pragma warning disable CS1591 + + +partial class ValidateClass +{ + static List IValidationTarget.Validate(ValidateClass? target) => + Validate(target); + + public static List Validate(ValidateClass? target) + { + if (target is not { } t) + { + return + [ + new() + { + PropertyName = ".self", + ErrorMessage = "`target` must not be `null`.", + }, + ]; + } + + var errors = new List(); + + + __ValidateStringProperty(errors, t, t.StringProperty); + + + return errors; + } + + + + private static void __ValidateStringProperty( + List errors, ValidateClass instance, string target + ) + { + + if (target is not { } t) + { + errors.Add(new() + { + PropertyName = $"StringProperty", + ErrorMessage = "Property must not be `null`.", + }); + + return; + } + + + + errors.Add( + global::Immediate.Validations.Shared.NotEqualAttribute.ValidateProperty( + t + , operand: instance._argumentValue + ), + $"StringProperty", + null + ); + } + +} + diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.StaticFieldAttributeArgument#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.StaticFieldAttributeArgument#IV...ValidateClass.g.verified.cs new file mode 100644 index 0000000..cb6b475 --- /dev/null +++ b/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.StaticFieldAttributeArgument#IV...ValidateClass.g.verified.cs @@ -0,0 +1,68 @@ +//HintName: IV...ValidateClass.g.cs +using System.Collections.Generic; +using Immediate.Validations.Shared; + +#nullable enable +#pragma warning disable CS1591 + + +partial class ValidateClass +{ + static List IValidationTarget.Validate(ValidateClass? target) => + Validate(target); + + public static List Validate(ValidateClass? target) + { + if (target is not { } t) + { + return + [ + new() + { + PropertyName = ".self", + ErrorMessage = "`target` must not be `null`.", + }, + ]; + } + + var errors = new List(); + + + __ValidateStringProperty(errors, t, t.StringProperty); + + + return errors; + } + + + + private static void __ValidateStringProperty( + List errors, ValidateClass instance, string target + ) + { + + if (target is not { } t) + { + errors.Add(new() + { + PropertyName = $"StringProperty", + ErrorMessage = "Property must not be `null`.", + }); + + return; + } + + + + errors.Add( + global::Immediate.Validations.Shared.NotEqualAttribute.ValidateProperty( + t + , operand: _argumentValue + ), + $"StringProperty", + null + ); + } + +} + diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.cs b/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.cs index 0881d1f..def257b 100644 --- a/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.cs +++ b/tests/Immediate.Validations.Tests/GeneratorTests/ValidatorArgumentTests.cs @@ -287,4 +287,57 @@ public partial class ValidateClass _ = await Verify(result); } + [Fact] + public async Task FieldAttributeArgument() + { + var driver = GeneratorTestHelper.GetDriver( + """ + #nullable enable + + using Immediate.Validations.Shared; + + [Validate] + public partial class ValidateClass + { + [NotEqual(nameof(_argumentValue))] + public string StringProperty { get; init; } + + private readonly string _argumentValue = "Hello World"; + } + """); + + var result = driver.GetRunResult(); + + Assert.Empty(result.Diagnostics); + _ = Assert.Single(result.GeneratedTrees); + + _ = await Verify(result); + } + + [Fact] + public async Task StaticFieldAttributeArgument() + { + var driver = GeneratorTestHelper.GetDriver( + """ + #nullable enable + + using Immediate.Validations.Shared; + + [Validate] + public partial class ValidateClass + { + [NotEqual(nameof(_argumentValue))] + public string StringProperty { get; init; } + + private static readonly string _argumentValue = "Hello World"; + } + """); + + var result = driver.GetRunResult(); + + Assert.Empty(result.Diagnostics); + _ = Assert.Single(result.GeneratedTrees); + + _ = await Verify(result); + } }