From 13c5279e150d714b33d86a8f176570a6d85a9236 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Sat, 23 Jan 2021 11:05:58 +0000 Subject: [PATCH] Add parameter to ITypeConverter. --- src/MapTo/DiagnosticProvider.cs | 2 +- src/MapTo/Extensions/StringExtensions.cs | 12 ++- src/MapTo/MapToGenerator.cs | 2 +- src/MapTo/MappingContext.cs | 22 ++++- src/MapTo/Models.cs | 2 +- ...erterSource.cs => ITypeConverterSource.cs} | 13 +-- src/MapTo/Sources/MapClassSource.cs | 10 ++- .../MapTypeConverterAttributeSource.cs | 22 ++++- test/MapTo.Tests/Tests.cs | 85 +++++++++++++------ .../ViewModels/UserViewModel.cs | 2 +- 10 files changed, 128 insertions(+), 44 deletions(-) rename src/MapTo/Sources/{TypeConverterSource.cs => ITypeConverterSource.cs} (75%) diff --git a/src/MapTo/DiagnosticProvider.cs b/src/MapTo/DiagnosticProvider.cs index 65e6281..a43506b 100644 --- a/src/MapTo/DiagnosticProvider.cs +++ b/src/MapTo/DiagnosticProvider.cs @@ -26,7 +26,7 @@ namespace MapTo Create($"{ErrorId}031", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnorePropertyAttributeSource.FullyQualifiedName}'."); internal static Diagnostic InvalidTypeConverterGenericTypesError(IPropertySymbol property, IPropertySymbol sourceProperty) => - Create($"{ErrorId}032", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{TypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.Type.ToDisplayString()}>'."); + Create($"{ErrorId}032", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.Type.ToDisplayString()}>'."); internal static Diagnostic ConfigurationParseError(string error) => Create($"{ErrorId}040", Location.None, "Incorrect Configuration", error); diff --git a/src/MapTo/Extensions/StringExtensions.cs b/src/MapTo/Extensions/StringExtensions.cs index ee786cb..8e1437a 100644 --- a/src/MapTo/Extensions/StringExtensions.cs +++ b/src/MapTo/Extensions/StringExtensions.cs @@ -1,7 +1,17 @@ -namespace MapTo.Extensions +using System.Threading.Tasks; + +namespace MapTo.Extensions { internal static class StringExtensions { public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}"; + + public static string ToSourceCodeString(this object? value) => value switch + { + null => "null", + string strValue => $"\"{strValue}\"", + char charValue => $"'{charValue}'", + _ => value.ToString() + }; } } \ No newline at end of file diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index 97b3ee0..6f7a62c 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -24,7 +24,7 @@ namespace MapTo var compilation = context.Compilation .AddSource(ref context, MapFromAttributeSource.Generate(options)) .AddSource(ref context, IgnorePropertyAttributeSource.Generate(options)) - .AddSource(ref context, TypeConverterSource.Generate(options)) + .AddSource(ref context, ITypeConverterSource.Generate(options)) .AddSource(ref context, MapTypeConverterAttributeSource.Generate(options)); if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any()) diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index 2b5419f..bc78691 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -24,8 +24,8 @@ namespace MapTo MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapTypeConverterAttributeSource.FullyQualifiedName) ?? throw new TypeLoadException($"Unable to find '{MapTypeConverterAttributeSource.FullyQualifiedName}' type."); - TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(TypeConverterSource.FullyQualifiedName) - ?? throw new TypeLoadException($"Unable to find '{TypeConverterSource.FullyQualifiedName}' type."); + TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName) + ?? throw new TypeLoadException($"Unable to find '{ITypeConverterSource.FullyQualifiedName}' type."); } public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; } @@ -114,9 +114,17 @@ namespace MapTo } string? converterFullyQualifiedName = null; + var converterParameters = new List(); if (!SymbolEqualityComparer.Default.Equals(property.Type, sourceProperty.Type) && !context.Compilation.HasImplicitConversion(sourceProperty.Type, property.Type)) { - var converterTypeSymbol = property.GetAttribute(context.MapTypeConverterAttributeTypeSymbol)?.ConstructorArguments.First().Value as INamedTypeSymbol; + var typeConverterAttribute = property.GetAttribute(context.MapTypeConverterAttributeTypeSymbol); + if (typeConverterAttribute is null) + { + context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); + continue; + } + + var converterTypeSymbol = typeConverterAttribute.ConstructorArguments.First().Value as INamedTypeSymbol; if (converterTypeSymbol is null) { context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); @@ -136,9 +144,15 @@ namespace MapTo } converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); + + var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault(); + if (!converterParameter.IsNull ) + { + converterParameters.AddRange(converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString())); + } } - mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName)); + mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray())); } return mappedProperties.ToImmutableArray(); diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index 49b7c75..8442b32 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -16,7 +16,7 @@ namespace MapTo ); } - internal record MappedProperty(string Name, string? TypeConverter); + internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray TypeConverterParameters); internal record MappingModel ( SourceGenerationOptions Options, diff --git a/src/MapTo/Sources/TypeConverterSource.cs b/src/MapTo/Sources/ITypeConverterSource.cs similarity index 75% rename from src/MapTo/Sources/TypeConverterSource.cs rename to src/MapTo/Sources/ITypeConverterSource.cs index ad86bf6..819e523 100644 --- a/src/MapTo/Sources/TypeConverterSource.cs +++ b/src/MapTo/Sources/ITypeConverterSource.cs @@ -1,9 +1,11 @@ -using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using static MapTo.Sources.Constants; namespace MapTo.Sources { - internal class TypeConverterSource + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class ITypeConverterSource { internal const string InterfaceName = "ITypeConverter"; internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2"; @@ -34,14 +36,15 @@ namespace MapTo.Sources { builder .WriteLine("/// ") - .WriteLine("/// Converts the value of object to .") + .WriteLine("/// Converts the value of object to .") .WriteLine("/// ") - .WriteLine("/// The object to convert.") + .WriteLine("/// The to convert.") + .WriteLine($"/// The parameter list passed to the ") .WriteLine("/// object."); } builder - .WriteLine("TDestination Convert(TSource source);") + .WriteLine("TDestination Convert(TSource source, object[] converterParameters);") .WriteClosingBracket() .WriteClosingBracket(); diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 614b655..b6e036d 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -66,13 +66,17 @@ namespace MapTo.Sources foreach (var property in model.MappedProperties) { - if (property.TypeConverter is not null) + if (property.TypeConverter is null) { - builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.Name});"); + builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};"); } else { - builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};"); + var parameters = property.TypeConverterParameters.IsEmpty + ? "null" + : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; + + builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.Name}, {parameters});"); } } diff --git a/src/MapTo/Sources/MapTypeConverterAttributeSource.cs b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs index 648d29b..663e549 100644 --- a/src/MapTo/Sources/MapTypeConverterAttributeSource.cs +++ b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs @@ -9,6 +9,7 @@ namespace MapTo.Sources internal const string AttributeClassName = AttributeName + "Attribute"; internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string ConverterPropertyName = "Converter"; + internal const string ConverterParametersPropertyName = "ConverterParameters"; internal static SourceCode Generate(SourceGenerationOptions options) { @@ -37,13 +38,16 @@ namespace MapTo.Sources builder .WriteLine("/// ") .WriteLine($"/// Initializes a new instance of .") - .WriteLine("/// "); + .WriteLine("/// ") + .WriteLine($"/// The to be used to convert the source type.") + .WriteLine("/// The parameter list to pass to the during the type conversion."); } builder - .WriteLine($"public {AttributeClassName}(Type converter)") + .WriteLine($"public {AttributeClassName}(Type converter, object[] converterParameters = null)") .WriteOpeningBracket() .WriteLine($"{ConverterPropertyName} = converter;") + .WriteLine($"{ConverterParametersPropertyName} = converterParameters;") .WriteClosingBracket() .WriteLine(); @@ -51,12 +55,24 @@ namespace MapTo.Sources { builder .WriteLine("/// ") - .WriteLine($"/// Gets or sets the to be used to convert the source type.") + .WriteLine($"/// Gets or sets the to be used to convert the source type.") .WriteLine("/// "); } builder .WriteLine($"public Type {ConverterPropertyName} {{ get; }}") + .WriteLine(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine($"/// Gets the parameter list to pass to the during the type conversion.") + .WriteLine("/// "); + } + + builder + .WriteLine($"public object[] {ConverterParametersPropertyName} {{ get; }}") .WriteClosingBracket() .WriteClosingBracket(); diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index 24f581f..bc20428 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Text; using MapTo.Extensions; @@ -10,7 +9,6 @@ using MapTo.Tests.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; using Shouldly; using Xunit; using static MapTo.Extensions.GeneratorExecutionContextExtensions; @@ -65,7 +63,7 @@ namespace MapTo builder.AppendLine("// Test source code."); builder.AppendLine("//"); builder.AppendLine(); - + if (options.UseMapToNamespace) { builder.AppendFormat("using {0};", Constants.RootNamespace).AppendLine(); @@ -91,7 +89,7 @@ namespace MapTo builder .PadLeft(Indent1) - .AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]": "[MapTo.MapFrom(typeof(Baz))]") + .AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]") .PadLeft(Indent1).Append("public partial class Foo") .AppendOpeningBracket(Indent1); @@ -182,10 +180,10 @@ namespace MapTo .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); }, SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); - + // Act var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - + // Assert var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation)); @@ -221,7 +219,7 @@ namespace MapTo // Act var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - + // Assert diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); @@ -332,7 +330,7 @@ namespace Test // Assert var fooType = compilation.GetTypeByMetadataName("Test.Foo"); fooType.ShouldNotBeNull(); - + var bazType = compilation.GetTypeByMetadataName("Test.Baz"); bazType.ShouldNotBeNull(); @@ -384,7 +382,7 @@ namespace Test { // Arrange const string source = ""; - var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, TypeConverterSource.InterfaceName }; + var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, ITypeConverterSource.InterfaceName }; // Act var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); @@ -490,7 +488,7 @@ namespace Test diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); } - + [Fact] public void VerifyTypeConverterInterface() { @@ -503,7 +501,7 @@ namespace MapTo {{ public interface ITypeConverter {{ - TDestination Convert(TSource source); + TDestination Convert(TSource source, object[] converterParameters); }} }} ".Trim(); @@ -513,9 +511,9 @@ namespace MapTo // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(TypeConverterSource.InterfaceName, expectedInterface); + compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); } - + [Fact] public void VerifyMapTypeConverterAttribute() { @@ -530,12 +528,15 @@ namespace MapTo [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public sealed class MapTypeConverterAttribute : Attribute {{ - public MapTypeConverterAttribute(Type converter) + public MapTypeConverterAttribute(Type converter, object[] converterParameters = null) {{ Converter = converter; + ConverterParameters = converterParameters; }} public Type Converter {{ get; }} + + public object[] ConverterParameters {{ get; }} }} }} ".Trim(); @@ -547,7 +548,7 @@ namespace MapTo diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); } - + [Fact] public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() { @@ -582,7 +583,7 @@ namespace MapTo diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); } - + [Fact] public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError() { @@ -606,19 +607,19 @@ namespace Test public class Prop4Converter: ITypeConverter { - public int Convert(string source) => int.Parse(source); + public int Convert(string source, object[] converterParameters) => int.Parse(source); } } "; // Act var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - + // Assert var expectedError = DiagnosticProvider.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz")); diagnostics.ShouldBeUnsuccessful(expectedError); } - + [Fact] public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties() { @@ -640,16 +641,52 @@ namespace Test public class Prop4Converter: ITypeConverter { - public long Convert(string source) => long.Parse(source); + public long Convert(string source, object[] converterParameters) => long.Parse(source); } } "; - const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4);"; + const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);"; // Act var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]") + .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string); + } +} +"; + + const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + // Assert diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); @@ -665,12 +702,12 @@ namespace Test .OfType() .Single(p => p.Identifier.ValueText == targetPropertyName); } - + private static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo") { var syntaxTree = compilation.SyntaxTrees.First(); var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass); - + var semanticModel = compilation.GetSemanticModel(syntaxTree); return semanticModel.GetDeclaredSymbol(propSyntax); } diff --git a/test/TestConsoleApp/ViewModels/UserViewModel.cs b/test/TestConsoleApp/ViewModels/UserViewModel.cs index 0a75f43..bca6bc9 100644 --- a/test/TestConsoleApp/ViewModels/UserViewModel.cs +++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs @@ -16,7 +16,7 @@ namespace TestConsoleApp.ViewModels private class LastNameConverter : ITypeConverter { /// - public string Convert(long source) => $"{source} :: With Type Converter"; + public string Convert(long source, object[] converterParameters) => $"{source} :: With Type Converter"; } } } \ No newline at end of file