From 4693dcfa552fe163999b91d403e092eb83a88758 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Sun, 7 Feb 2021 08:42:02 +0000 Subject: [PATCH] Add support for nested object property map. --- src/MapTo/Extensions/RoslynExtensions.cs | 7 +- src/MapTo/MapToGenerator.cs | 2 +- src/MapTo/MappingContext.cs | 231 ++++++++++++++--------- src/MapTo/Models.cs | 4 +- src/MapTo/Sources/MapClassSource.cs | 4 +- test/MapTo.Tests/MapToTests.cs | 96 ++++++++++ 6 files changed, 248 insertions(+), 96 deletions(-) diff --git a/src/MapTo/Extensions/RoslynExtensions.cs b/src/MapTo/Extensions/RoslynExtensions.cs index a43fd86..c8a3660 100644 --- a/src/MapTo/Extensions/RoslynExtensions.cs +++ b/src/MapTo/Extensions/RoslynExtensions.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using MapTo.Sources; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -63,5 +65,8 @@ namespace MapTo.Extensions p.NullableAnnotation == NullableAnnotation.Annotated && targetProperty.NullableAnnotation == NullableAnnotation.Annotated)); } + + public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) => + compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type."); } } \ No newline at end of file diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index 9fd9688..6ac4970 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -41,7 +41,7 @@ namespace MapTo { foreach (var classSyntax in candidateClasses) { - var mappingContext = MappingContext.Create(compilation, classSyntax, options); + var mappingContext = new MappingContext(compilation, options, classSyntax); mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); if (mappingContext.Model is not null) diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index c10e984..5a753e1 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using MapTo.Extensions; @@ -11,148 +10,178 @@ namespace MapTo { internal class MappingContext { - private MappingContext(Compilation compilation) + private readonly ClassDeclarationSyntax _classSyntax; + private readonly Compilation _compilation; + private readonly INamedTypeSymbol _ignorePropertyAttributeTypeSymbol; + private readonly INamedTypeSymbol _mapFromAttributeTypeSymbol; + private readonly INamedTypeSymbol _mapPropertyAttributeTypeSymbol; + private readonly INamedTypeSymbol _mapTypeConverterAttributeTypeSymbol; + private readonly SemanticModel _semanticModel; + private readonly SourceGenerationOptions _sourceGenerationOptions; + private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol; + + internal MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, ClassDeclarationSyntax classSyntax) { Diagnostics = ImmutableArray.Empty; - Compilation = compilation; + _sourceGenerationOptions = sourceGenerationOptions; + _classSyntax = classSyntax; + _compilation = compilation; + _semanticModel = _compilation.GetSemanticModel(_classSyntax.SyntaxTree); - IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(IgnorePropertyAttributeSource.FullyQualifiedName) - ?? throw new TypeLoadException($"Unable to find '{IgnorePropertyAttributeSource.FullyQualifiedName}' type."); + _ignorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName); + _mapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName); + _typeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName); + _mapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName); + _mapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName); - MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapTypeConverterAttributeSource.FullyQualifiedName) - ?? throw new TypeLoadException($"Unable to find '{MapTypeConverterAttributeSource.FullyQualifiedName}' type."); - - TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName) - ?? throw new TypeLoadException($"Unable to find '{ITypeConverterSource.FullyQualifiedName}' type."); - - MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapPropertyAttributeSource.FullyQualifiedName) - ?? throw new TypeLoadException($"Unable to find '{MapPropertyAttributeSource.FullyQualifiedName}' type."); + Initialize(); } - private Compilation Compilation { get; } - public MappingModel? Model { get; private set; } public ImmutableArray Diagnostics { get; private set; } - public INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; } - - public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; } - - public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; } - - public INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; } - - internal static MappingContext Create(Compilation compilation, ClassDeclarationSyntax classSyntax, SourceGenerationOptions sourceGenerationOptions) + private void Initialize() { - var context = new MappingContext(compilation); - var root = classSyntax.GetCompilationUnit(); - - var semanticModel = compilation.GetSemanticModel(classSyntax.SyntaxTree); - if (!(semanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classTypeSymbol)) + if (!(_semanticModel.GetDeclaredSymbol(_classSyntax) is INamedTypeSymbol classTypeSymbol)) { - return context.ReportDiagnostic(DiagnosticProvider.TypeNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText)); + ReportDiagnostic(DiagnosticProvider.TypeNotFoundError(_classSyntax.GetLocation(), _classSyntax.Identifier.ValueText)); + return; } - var sourceTypeSymbol = GetSourceTypeSymbol(semanticModel, classSyntax); + var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax); if (sourceTypeSymbol is null) { - return context.ReportDiagnostic(DiagnosticProvider.MapFromAttributeNotFoundError(classSyntax.GetLocation())); + ReportDiagnostic(DiagnosticProvider.MapFromAttributeNotFoundError(_classSyntax.GetLocation())); + return; } - var className = classSyntax.GetClassName(); + var className = _classSyntax.GetClassName(); var sourceClassName = sourceTypeSymbol.Name; - var mappedProperties = GetMappedProperties(context, classTypeSymbol, sourceTypeSymbol); + var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol); if (!mappedProperties.Any()) { - return context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyFoundError(classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol)); + ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol)); + return; } - context.Model = new MappingModel( - sourceGenerationOptions, - classSyntax.GetNamespace(), - classSyntax.Modifiers, + Model = new MappingModel( + _sourceGenerationOptions, + _classSyntax.GetNamespace(), + _classSyntax.Modifiers, className, sourceTypeSymbol.ContainingNamespace.ToString(), sourceClassName, sourceTypeSymbol.ToString(), mappedProperties.ToImmutableArray()); - - return context; } - private MappingContext ReportDiagnostic(Diagnostic diagnostic) - { - Diagnostics = Diagnostics.Add(diagnostic); - return this; - } - - private static INamedTypeSymbol? GetSourceTypeSymbol(SemanticModel semanticModel, ClassDeclarationSyntax classSyntax) - { - var sourceTypeExpressionSyntax = classSyntax - .GetAttribute(MapFromAttributeSource.AttributeName) - ?.DescendantNodes() - .OfType() - .SingleOrDefault(); - - return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; - } - - private static ImmutableArray GetMappedProperties(MappingContext context, ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) + private ImmutableArray GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) { var mappedProperties = new List(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - var classProperties = classSymbol.GetAllMembers().OfType().Where(p => !p.HasAttribute(context.IgnorePropertyAttributeTypeSymbol)); + var classProperties = classSymbol.GetAllMembers().OfType().Where(p => !p.HasAttribute(_ignorePropertyAttributeTypeSymbol)); foreach (var property in classProperties) { - var sourceProperty = FindSourceProperty(context, sourceProperties, property); + var sourceProperty = FindSourceProperty(sourceProperties, property); if (sourceProperty is null) { continue; } string? converterFullyQualifiedName = null; - var converterParameters = new List(); + var converterParameters = ImmutableArray.Empty; + string? mappedSourcePropertyType = null; - if (!context.Compilation.HasCompatibleTypes(sourceProperty, property)) + if (!_compilation.HasCompatibleTypes(sourceProperty, property)) { - var typeConverterAttribute = property.GetAttribute(context.MapTypeConverterAttributeTypeSymbol); - if (typeConverterAttribute is null) + if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && + !TryGetNestedObjectMappings(property, out mappedSourcePropertyType)) { - context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); continue; } - - var converterTypeSymbol = typeConverterAttribute.ConstructorArguments.First().Value as INamedTypeSymbol; - if (converterTypeSymbol is null) - { - context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); - continue; - } - - var baseInterface = GetTypeConverterBaseInterface(context, converterTypeSymbol, property, sourceProperty); - if (baseInterface is null) - { - context.ReportDiagnostic(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty)); - continue; - } - - converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); - converterParameters.AddRange(GetTypeConverterParameters(typeConverterAttribute)); } - mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray(), sourceProperty.Name)); + mappedProperties.Add(new MappedProperty( + property.Name, + property.Type.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + sourceProperty.Name, + mappedSourcePropertyType)); } return mappedProperties.ToImmutableArray(); } - private static IPropertySymbol? FindSourceProperty(MappingContext context, IEnumerable sourceProperties, IPropertySymbol property) + private bool TryGetNestedObjectMappings(IPropertySymbol property, out string? mappedSourcePropertyType) + { + mappedSourcePropertyType = null; + + if (!Diagnostics.IsEmpty) + { + return false; + } + + var nestedSourceMapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol); + if (nestedSourceMapFromAttribute is null) + { + ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); + return false; + } + + var nestedAttributeSyntax = nestedSourceMapFromAttribute.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax; + if (nestedAttributeSyntax is null) + { + ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); + return false; + } + + var nestedSourceTypeSymbol = GetSourceTypeSymbol(nestedAttributeSyntax); + if (nestedSourceTypeSymbol is null) + { + ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); + return false; + } + + mappedSourcePropertyType = nestedSourceTypeSymbol.Name; + return true; + } + + private bool TryGetMapTypeConverter(IPropertySymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName, out ImmutableArray converterParameters) + { + converterFullyQualifiedName = null; + converterParameters = ImmutableArray.Empty; + + if (!Diagnostics.IsEmpty) + { + return false; + } + + var typeConverterAttribute = property.GetAttribute(_mapTypeConverterAttributeTypeSymbol); + if (!(typeConverterAttribute?.ConstructorArguments.First().Value is INamedTypeSymbol converterTypeSymbol)) + { + return false; + } + + var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty); + if (baseInterface is null) + { + ReportDiagnostic(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty)); + return false; + } + + converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); + converterParameters = GetTypeConverterParameters(typeConverterAttribute); + return true; + } + + private IPropertySymbol? FindSourceProperty(IEnumerable sourceProperties, IPropertySymbol property) { var propertyName = property - .GetAttribute(context.MapPropertyAttributeTypeSymbol) + .GetAttribute(_mapPropertyAttributeTypeSymbol) ?.NamedArguments .SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName) .Value.Value as string ?? property.Name; @@ -160,22 +189,40 @@ namespace MapTo return sourceProperties.SingleOrDefault(p => p.Name == propertyName); } - private static INamedTypeSymbol? GetTypeConverterBaseInterface(MappingContext context, ITypeSymbol converterTypeSymbol, IPropertySymbol property, IPropertySymbol sourceProperty) + private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, IPropertySymbol property, IPropertySymbol sourceProperty) { return converterTypeSymbol.AllInterfaces .SingleOrDefault(i => i.TypeArguments.Length == 2 && - SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, context.TypeConverterInterfaceTypeSymbol) && + SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, _typeConverterInterfaceTypeSymbol) && SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && SymbolEqualityComparer.Default.Equals(property.Type, i.TypeArguments[1])); } - private static IEnumerable GetTypeConverterParameters(AttributeData typeConverterAttribute) + private static ImmutableArray GetTypeConverterParameters(AttributeData typeConverterAttribute) { var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault(); return converterParameter.IsNull - ? Enumerable.Empty() - : converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()); + ? ImmutableArray.Empty + : converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray(); + } + + private void ReportDiagnostic(Diagnostic diagnostic) + { + Diagnostics = Diagnostics.Add(diagnostic); + } + + private INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classDeclarationSyntax) => + GetSourceTypeSymbol(classDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName)); + + private INamedTypeSymbol? GetSourceTypeSymbol(AttributeSyntax? attributeSyntax) + { + var sourceTypeExpressionSyntax = attributeSyntax + ?.DescendantNodes() + .OfType() + .SingleOrDefault(); + + return sourceTypeExpressionSyntax is not null ? _semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; } } } \ No newline at end of file diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index 9fe9f83..5594c00 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -9,9 +9,11 @@ namespace MapTo internal record MappedProperty( string Name, + string Type, string? TypeConverter, ImmutableArray TypeConverterParameters, - string SourcePropertyName); + string SourcePropertyName, + string? MappedSourcePropertyTypeName); internal record MappingModel ( SourceGenerationOptions Options, diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 943fab0..32bbcc6 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -70,7 +70,9 @@ namespace MapTo.Sources { if (property.TypeConverter is null) { - builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"); + builder.WriteLine(property.MappedSourcePropertyTypeName is null + ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" + : $"{property.Name} = new {property.Type}({sourceClassParameterName}.{property.SourcePropertyName});"); } else { diff --git a/test/MapTo.Tests/MapToTests.cs b/test/MapTo.Tests/MapToTests.cs index 29d5094..92a835e 100644 --- a/test/MapTo.Tests/MapToTests.cs +++ b/test/MapTo.Tests/MapToTests.cs @@ -333,5 +333,101 @@ namespace Test diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); } + + [Fact] + public void When_HasNestedObjectPropertyTypeHasMapFromAttribute_Should_UseContinueToMap() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + SourceClassNamespace: "Test", + PropertyBuilder: b => b.WriteLine("public B InnerProp1 { get; }"), + SourcePropertyBuilder: b => b.WriteLine("public A InnerProp1 { get; }"))); + + source += @" +namespace Test +{ + public class A { public int Prop1 { get; } } + + [MapTo.MapFrom(typeof(A))] + public partial class B { public int Prop1 { get; }} +} +".Trim(); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + InnerProp1 = new B(baz.InnerProp1); + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ToArray()[^2].ToString().ShouldContain(expectedResult); + } + + [Fact] + public void When_HasNestedObjectPropertyTypeDoesNotHaveMapFromAttribute_Should_ReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + SourceClassNamespace: "Test", + PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"), + SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }"))); + + source += @" +namespace Test +{ + public class FooInner1 { public int Prop1 { get; } } + + public partial class BazInner1 { public int Prop1 { get; }} +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); + diagnostics.ShouldBeUnsuccessful(expectedError); + } + + [Fact] + public void When_HasNestedObjectPropertyTypeHasMapFromAttributeToDifferentType_Should_ReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + SourceClassNamespace: "Test", + PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"), + SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }"))); + + source += @" +namespace Test +{ + public class FooInner1 { public int Prop1 { get; } } + + public class FooInner2 { public int Prop1 { get; } } + + [MapTo.MapFrom(typeof(FooInner2))] + public partial class BazInner1 { public int Prop1 { get; }} +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); + diagnostics.ShouldBeUnsuccessful(expectedError); + } } } \ No newline at end of file