Add parameter to ITypeConverter.

This commit is contained in:
Mohammadreza Taikandi 2021-01-23 11:05:58 +00:00
parent 2ef7ded450
commit 13c5279e15
10 changed files with 128 additions and 44 deletions

View File

@ -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);

View File

@ -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()
};
}
}

View File

@ -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())

View File

@ -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<string>();
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();

View File

@ -16,7 +16,7 @@ namespace MapTo
);
}
internal record MappedProperty(string Name, string? TypeConverter);
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
internal record MappingModel (
SourceGenerationOptions Options,

View File

@ -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("/// <summary>")
.WriteLine("/// Converts the value of <paramref name=\"value\"/> object to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// Converts the value of <paramref name=\"source\"/> object to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <param name=\"value\">The object to convert.</param>")
.WriteLine("/// <param name=\"source\">The <see cref=\"TSource\"/> to convert.</param>")
.WriteLine($"/// <param name=\"converterParameters\">The parameter list passed to the <see cref=\"{MapTypeConverterAttributeSource.AttributeClassName}\"/></param>")
.WriteLine("/// <returns><typeparamref name=\"TDestination\"/> object.</returns>");
}
builder
.WriteLine("TDestination Convert(TSource source);")
.WriteLine("TDestination Convert(TSource source, object[] converterParameters);")
.WriteClosingBracket()
.WriteClosingBracket();

View File

@ -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});");
}
}

View File

@ -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("/// <summary>")
.WriteLine($"/// Initializes a new instance of <see cref=\"{AttributeClassName}\"/>.")
.WriteLine("/// </summary>");
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"converter\">The <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.</param>")
.WriteLine("/// <param name=\"converterParameters\">The parameter list to pass to the <paramref name=\"converter\"/> during the type conversion.</param>");
}
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("/// <summary>")
.WriteLine($"/// Gets or sets the <see cref=\"{TypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
.WriteLine($"/// Gets or sets the <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public Type {ConverterPropertyName} {{ get; }}")
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Gets the parameter list to pass to the <see cref=\"{ConverterPropertyName}\"/> during the type conversion.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public object[] {ConverterParametersPropertyName} {{ get; }}")
.WriteClosingBracket()
.WriteClosingBracket();

View File

@ -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<in TSource, out TDestination>
{{
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<string, int>
{
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<string, long>
{
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<long, string>
{
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<PropertyDeclarationSyntax>()
.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);
}

View File

@ -16,7 +16,7 @@ namespace TestConsoleApp.ViewModels
private class LastNameConverter : ITypeConverter<long, string>
{
/// <inheritdoc />
public string Convert(long source) => $"{source} :: With Type Converter";
public string Convert(long source, object[] converterParameters) => $"{source} :: With Type Converter";
}
}
}