Add support for nested object property map.

This commit is contained in:
Mohammadreza Taikandi 2021-02-07 08:42:02 +00:00
parent 8ed331ed95
commit 4693dcfa55
6 changed files with 248 additions and 96 deletions

View File

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

View File

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

View File

@ -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<Diagnostic>.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<Diagnostic> 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<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
private static ImmutableArray<MappedProperty> GetMappedProperties(MappingContext context, ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
private ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
{
var mappedProperties = new List<MappedProperty>();
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
var classProperties = classSymbol.GetAllMembers().OfType<IPropertySymbol>().Where(p => !p.HasAttribute(context.IgnorePropertyAttributeTypeSymbol));
var classProperties = classSymbol.GetAllMembers().OfType<IPropertySymbol>().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<string>();
var converterParameters = ImmutableArray<string>.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<IPropertySymbol> 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<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.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<IPropertySymbol> 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<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? Enumerable.Empty<string>()
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString());
? ImmutableArray<string>.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<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? _semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
}
}

View File

@ -9,9 +9,11 @@ namespace MapTo
internal record MappedProperty(
string Name,
string Type,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName);
string SourcePropertyName,
string? MappedSourcePropertyTypeName);
internal record MappingModel (
SourceGenerationOptions Options,

View File

@ -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
{

View File

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