diff --git a/src/MapTo/DiagnosticProvider.cs b/src/MapTo/DiagnosticProvider.cs index fafc7a2..7085bcd 100644 --- a/src/MapTo/DiagnosticProvider.cs +++ b/src/MapTo/DiagnosticProvider.cs @@ -23,7 +23,7 @@ namespace MapTo Create($"{ErrorId}030", location, "Type Mismatch", $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types."); internal static Diagnostic NoMatchingPropertyTypeFoundError(IPropertySymbol property) => - 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 '{RootNamespace}.{MapPropertyAttributeSource.AttributeName}Attribute' to provide a type converter or ignore the property using '{RootNamespace}.{IgnorePropertyAttributeSource.AttributeName}Attribute'."); + 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 '{RootNamespace}.{IgnorePropertyAttributeSource.AttributeName}Attribute'."); 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()}>'."); diff --git a/src/MapTo/Extensions/RoslynExtensions.cs b/src/MapTo/Extensions/RoslynExtensions.cs index 6702667..a32fb80 100644 --- a/src/MapTo/Extensions/RoslynExtensions.cs +++ b/src/MapTo/Extensions/RoslynExtensions.cs @@ -42,6 +42,9 @@ namespace MapTo.Extensions public static IEnumerable GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) => symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true); + public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => + symbol.GetAttributes(attributeSymbol).FirstOrDefault(); + public static string? GetNamespace(this ClassDeclarationSyntax classDeclarationSyntax) { return classDeclarationSyntax.Ancestors() diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index ac4c187..97b3ee0 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -25,7 +25,7 @@ namespace MapTo .AddSource(ref context, MapFromAttributeSource.Generate(options)) .AddSource(ref context, IgnorePropertyAttributeSource.Generate(options)) .AddSource(ref context, TypeConverterSource.Generate(options)) - .AddSource(ref context, MapPropertyAttributeSource.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 53ff45c..2b5419f 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -21,14 +21,14 @@ namespace MapTo IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(IgnorePropertyAttributeSource.FullyQualifiedName) ?? throw new TypeLoadException($"Unable to find '{IgnorePropertyAttributeSource.FullyQualifiedName}' type."); - MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapPropertyAttributeSource.FullyQualifiedName) - ?? throw new TypeLoadException($"Unable to find '{MapPropertyAttributeSource.FullyQualifiedName}' type."); + 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."); } - public INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; } + public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; } public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; } @@ -90,7 +90,7 @@ namespace MapTo ?.DescendantNodes() .OfType() .SingleOrDefault(); - + return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; } @@ -114,37 +114,28 @@ namespace MapTo } string? converterFullyQualifiedName = null; - if (!SymbolEqualityComparer.Default.Equals(property.Type, sourceProperty.Type)) + if (!SymbolEqualityComparer.Default.Equals(property.Type, sourceProperty.Type) && !context.Compilation.HasImplicitConversion(sourceProperty.Type, property.Type)) { - var conversionClassification = context.Compilation.ClassifyCommonConversion(sourceProperty.Type, property.Type); - if (!conversionClassification.Exists || !conversionClassification.IsImplicit) + var converterTypeSymbol = property.GetAttribute(context.MapTypeConverterAttributeTypeSymbol)?.ConstructorArguments.First().Value as INamedTypeSymbol; + if (converterTypeSymbol is null) { - var mapPropertyAttribute = property.GetAttributes(context.MapPropertyAttributeTypeSymbol) - .FirstOrDefault(a => a.NamedArguments.Any(na => na.Key == MapPropertyAttributeSource.ConverterPropertyName)); - - var converterTypeSymbol = mapPropertyAttribute?.NamedArguments - .SingleOrDefault(na => na.Key == MapPropertyAttributeSource.ConverterPropertyName).Value.Value as INamedTypeSymbol; - - if (mapPropertyAttribute is null || converterTypeSymbol is null) - { - context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); - continue; - } - - var baseInterface = converterTypeSymbol.AllInterfaces - .SingleOrDefault(i => SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, context.TypeConverterInterfaceTypeSymbol) && - i.TypeArguments.Length == 2 && - SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && - SymbolEqualityComparer.Default.Equals(property.Type, i.TypeArguments[1])); - - if (baseInterface is null) - { - context.ReportDiagnostic(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty)); - continue; - } - - converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); + context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); + continue; } + + var baseInterface = converterTypeSymbol.AllInterfaces + .SingleOrDefault(i => SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, context.TypeConverterInterfaceTypeSymbol) && + i.TypeArguments.Length == 2 && + SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && + SymbolEqualityComparer.Default.Equals(property.Type, i.TypeArguments[1])); + + if (baseInterface is null) + { + context.ReportDiagnostic(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty)); + continue; + } + + converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); } mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName)); diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index 8c2efdf..49b7c75 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -16,7 +16,7 @@ namespace MapTo ); } - internal record MappedProperty(string Name, string? ConverterFullyQualifiedName); + internal record MappedProperty(string Name, string? TypeConverter); internal record MappingModel ( SourceGenerationOptions Options, diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index ef7ad34..614b655 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -66,9 +66,9 @@ namespace MapTo.Sources foreach (var property in model.MappedProperties) { - if (property.ConverterFullyQualifiedName is not null) + if (property.TypeConverter is not null) { - builder.WriteLine($"{property.Name} = new {property.ConverterFullyQualifiedName}().Convert({sourceClassParameterName}.{property.Name});"); + builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.Name});"); } else { diff --git a/src/MapTo/Sources/MapPropertyAttributeSource.cs b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs similarity index 70% rename from src/MapTo/Sources/MapPropertyAttributeSource.cs rename to src/MapTo/Sources/MapTypeConverterAttributeSource.cs index 5116344..7bac936 100644 --- a/src/MapTo/Sources/MapPropertyAttributeSource.cs +++ b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs @@ -3,10 +3,11 @@ using static MapTo.Sources.Constants; namespace MapTo.Sources { - internal static class MapPropertyAttributeSource + internal static class MapTypeConverterAttributeSource { - internal const string AttributeName = "MapProperty"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeName + "Attribute"; + internal const string AttributeName = "MapTypeConverter"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string ConverterPropertyName = "Converter"; internal static SourceCode Generate(SourceGenerationOptions options) @@ -28,19 +29,22 @@ namespace MapTo.Sources builder .WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]") - .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") + .WriteLine($"public sealed class {AttributeClassName} : Attribute") .WriteOpeningBracket(); if (options.GenerateXmlDocument) { builder .WriteLine("/// ") - .WriteLine("/// Initializes a new instance of .") + .WriteLine($"/// Initializes a new instance of .") .WriteLine("/// "); } builder - .WriteLine($"public {AttributeName}Attribute() {{ }}") + .WriteLine($"public {AttributeClassName}(Type converter)") + .WriteOpeningBracket() + .WriteLine($"{ConverterPropertyName} = converter;") + .WriteClosingBracket() .WriteLine(); if (options.GenerateXmlDocument) @@ -52,11 +56,11 @@ namespace MapTo.Sources } builder - .WriteLine($"public Type {ConverterPropertyName} {{ get; set; }}") + .WriteLine($"public Type {ConverterPropertyName} {{ get; }}") .WriteClosingBracket() .WriteClosingBracket(); - return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); } } } \ No newline at end of file diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index e742ecf..9e6ccbb 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -517,7 +517,7 @@ namespace MapTo } [Fact] - public void VerifyMapPropertyAttribute() + public void VerifyMapTypeConverterAttribute() { // Arrange const string source = ""; @@ -528,11 +528,14 @@ using System; namespace MapTo {{ [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public sealed class MapPropertyAttribute : Attribute + public sealed class MapTypeConverterAttribute : Attribute {{ - public MapPropertyAttribute() {{ }} + public MapTypeConverterAttribute(Type converter) + {{ + Converter = converter; + }} - public Type Converter {{ get; set; }} + public Type Converter {{ get; }} }} }} ".Trim(); @@ -542,7 +545,7 @@ namespace MapTo // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface); + compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); } [Fact] @@ -591,8 +594,7 @@ namespace MapTo builder .PadLeft(Indent2).AppendLine("[IgnoreProperty]") .PadLeft(Indent2).AppendLine("public long IgnoreMe { get; set; }") - .PadLeft(Indent2).AppendLine("[MapProperty]") - .PadLeft(Indent2).AppendLine("[MapProperty(Converter = typeof(Prop4Converter))]") + .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter))]") .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); }, SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"))); @@ -626,7 +628,7 @@ namespace Test PropertyBuilder: builder => { builder - .PadLeft(Indent2).AppendLine("[MapProperty(Converter = typeof(Prop4Converter))]") + .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter))]") .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); }, SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }")));