Add support for c# records.
This commit is contained in:
parent
91f5e9bcf5
commit
02e530156f
|
@ -2,7 +2,7 @@
|
|||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nerdbank.GitVersioning">
|
||||
<Version>3.4.216</Version>
|
||||
<Version>3.4.22</Version>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using MapTo.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace MapTo
|
||||
{
|
||||
internal class ClassMappingContext : MappingContext
|
||||
{
|
||||
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
|
||||
: base(compilation, sourceGenerationOptions, typeSyntax) { }
|
||||
|
||||
protected override ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
|
||||
{
|
||||
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
|
||||
|
||||
return typeSymbol
|
||||
.GetAllMembers(!isInheritFromMappedBaseClass)
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
|
||||
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
|
||||
.Where(mappedProperty => mappedProperty is not null)
|
||||
.ToImmutableArray()!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
// ReSharper disable CheckNamespace
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
// These attributes already shipped with .NET Core 3.1 in System.Runtime
|
||||
#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1
|
||||
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
|
||||
internal sealed class AllowNullAttribute : Attribute { }
|
||||
|
||||
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
|
||||
internal sealed class DisallowNullAttribute : Attribute { }
|
||||
|
||||
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
|
||||
internal sealed class MaybeNullAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input
|
||||
/// argument was not null when the call returns.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
|
||||
internal sealed class NotNullAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter may be null even if the
|
||||
/// corresponding type disallows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
internal sealed class MaybeNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter may be null.
|
||||
/// </param>
|
||||
public MaybeNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter will not be null even if the
|
||||
/// corresponding type allows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
|
||||
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
|
||||
internal sealed class NotNullIfNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the associated parameter name.</summary>
|
||||
/// <param name="parameterName">
|
||||
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
|
||||
/// </param>
|
||||
public NotNullIfNotNullAttribute(string parameterName)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
}
|
||||
|
||||
/// <summary>Gets the associated parameter name.</summary>
|
||||
public string ParameterName { get; }
|
||||
}
|
||||
|
||||
/// <summary>Applied to a method that will never return under any circumstance.</summary>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class DoesNotReturnAttribute : Attribute { }
|
||||
|
||||
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
internal sealed class DoesNotReturnIfAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified parameter value.</summary>
|
||||
/// <param name="parameterValue">
|
||||
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
|
||||
/// the associated parameter matches this value.
|
||||
/// </param>
|
||||
public DoesNotReturnIfAttribute(bool parameterValue)
|
||||
{
|
||||
ParameterValue = parameterValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the condition parameter value.</summary>
|
||||
public bool ParameterValue { get; }
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the method or property will ensure that the listed field and property members have not-null
|
||||
/// values.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
|
||||
internal sealed class MemberNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with a field or property member.</summary>
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(string member)
|
||||
{
|
||||
Members = new[] { member };
|
||||
}
|
||||
|
||||
/// <summary>Initializes the attribute with the list of field and property members.</summary>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(params string[] members)
|
||||
{
|
||||
Members = members;
|
||||
}
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the method or property will ensure that the listed field and property members have not-null
|
||||
/// values when returning with the specified return value condition.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
|
||||
internal sealed class MemberNotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, string member)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = new[] { member };
|
||||
}
|
||||
|
||||
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = members;
|
||||
}
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using MapTo.Extensions;
|
||||
using MapTo.Sources;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
@ -23,11 +24,11 @@ namespace MapTo
|
|||
internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) =>
|
||||
Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types.");
|
||||
|
||||
internal static Diagnostic NoMatchingPropertyTypeFoundError(IPropertySymbol property) =>
|
||||
internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) =>
|
||||
Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"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(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.Type.ToDisplayString()}>'.");
|
||||
internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) =>
|
||||
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'.");
|
||||
|
||||
internal static Diagnostic ConfigurationParseError(string error) =>
|
||||
Create($"{ErrorId}040", Location.None, error);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
@ -48,15 +49,36 @@ namespace MapTo.Extensions
|
|||
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
|
||||
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
|
||||
|
||||
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) =>
|
||||
typeDeclarationSyntax.Ancestors()
|
||||
.OfType<NamespaceDeclarationSyntax>()
|
||||
.FirstOrDefault()
|
||||
?.Name
|
||||
.ToString();
|
||||
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
|
||||
.Ancestors()
|
||||
.OfType<NamespaceDeclarationSyntax>()
|
||||
.FirstOrDefault()
|
||||
?.Name
|
||||
.ToString();
|
||||
|
||||
public static bool HasCompatibleTypes(this Compilation compilation, IPropertySymbol sourceProperty, IPropertySymbol destinationProperty) =>
|
||||
SymbolEqualityComparer.Default.Equals(destinationProperty.Type, sourceProperty.Type) || compilation.HasImplicitConversion(sourceProperty.Type, destinationProperty.Type);
|
||||
public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) =>
|
||||
source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) &&
|
||||
(SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType));
|
||||
|
||||
public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol)
|
||||
{
|
||||
switch (symbol)
|
||||
{
|
||||
case IPropertySymbol propertySymbol:
|
||||
typeSymbol = propertySymbol.Type;
|
||||
return true;
|
||||
|
||||
case IParameterSymbol parameterSymbol:
|
||||
typeSymbol = parameterSymbol.Type;
|
||||
return true;
|
||||
|
||||
default:
|
||||
typeSymbol = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null;
|
||||
|
||||
public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty)
|
||||
{
|
||||
|
@ -91,5 +113,8 @@ namespace MapTo.Extensions
|
|||
SpecialType.System_Double or
|
||||
SpecialType.System_Char or
|
||||
SpecialType.System_Object;
|
||||
|
||||
public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) =>
|
||||
symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null;
|
||||
}
|
||||
}
|
|
@ -47,18 +47,26 @@ namespace MapTo
|
|||
}
|
||||
}
|
||||
|
||||
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateClasses, SourceGenerationOptions options)
|
||||
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateTypes, SourceGenerationOptions options)
|
||||
{
|
||||
foreach (var classSyntax in candidateClasses)
|
||||
foreach (var typeDeclarationSyntax in candidateTypes)
|
||||
{
|
||||
var mappingContext = new MappingContext(compilation, options, classSyntax);
|
||||
var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax);
|
||||
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
|
||||
|
||||
if (mappingContext.Model is not null)
|
||||
|
||||
if (mappingContext.Model is null)
|
||||
{
|
||||
var (source, hintName) = MapClassSource.Generate(mappingContext.Model);
|
||||
context.AddSource(hintName, source);
|
||||
continue;
|
||||
}
|
||||
|
||||
var (source, hintName) = typeDeclarationSyntax switch
|
||||
{
|
||||
ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model),
|
||||
RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
context.AddSource(hintName, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,101 +1,301 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using MapTo.Extensions;
|
||||
using MapTo.Sources;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace MapTo
|
||||
{
|
||||
internal class MappingContext
|
||||
internal abstract class MappingContext
|
||||
{
|
||||
private readonly TypeDeclarationSyntax _typeSyntax;
|
||||
private readonly Compilation _compilation;
|
||||
private readonly List<Diagnostic> _diagnostics;
|
||||
private readonly INamedTypeSymbol _ignorePropertyAttributeTypeSymbol;
|
||||
private readonly INamedTypeSymbol _mapFromAttributeTypeSymbol;
|
||||
private readonly INamedTypeSymbol _mapPropertyAttributeTypeSymbol;
|
||||
private readonly INamedTypeSymbol _mapTypeConverterAttributeTypeSymbol;
|
||||
private readonly INamedTypeSymbol _mappingContextTypeSymbol;
|
||||
private readonly SourceGenerationOptions _sourceGenerationOptions;
|
||||
private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol;
|
||||
private readonly List<string> _usings;
|
||||
|
||||
internal MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
|
||||
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
|
||||
{
|
||||
_diagnostics = new List<Diagnostic>();
|
||||
_usings = new List<string> { "System", Constants.RootNamespace };
|
||||
_sourceGenerationOptions = sourceGenerationOptions;
|
||||
_typeSyntax = typeSyntax;
|
||||
_compilation = compilation;
|
||||
Diagnostics = ImmutableArray<Diagnostic>.Empty;
|
||||
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
|
||||
SourceGenerationOptions = sourceGenerationOptions;
|
||||
TypeSyntax = typeSyntax;
|
||||
Compilation = compilation;
|
||||
|
||||
_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);
|
||||
_mappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
|
||||
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);
|
||||
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
|
||||
|
||||
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
|
||||
|
||||
public MappingModel? Model { get; private set; }
|
||||
|
||||
public IEnumerable<Diagnostic> Diagnostics => _diagnostics;
|
||||
protected Compilation Compilation { get; }
|
||||
|
||||
private void Initialize()
|
||||
protected INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
|
||||
|
||||
protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; }
|
||||
|
||||
protected INamedTypeSymbol MappingContextTypeSymbol { get; }
|
||||
|
||||
protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
|
||||
|
||||
protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
|
||||
|
||||
protected SourceGenerationOptions SourceGenerationOptions { get; }
|
||||
|
||||
protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
|
||||
|
||||
protected TypeDeclarationSyntax TypeSyntax { get; }
|
||||
|
||||
protected ImmutableArray<string> Usings { get; private set; }
|
||||
|
||||
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
|
||||
{
|
||||
var semanticModel = _compilation.GetSemanticModel(_typeSyntax.SyntaxTree);
|
||||
if (ModelExtensions.GetDeclaredSymbol(semanticModel, _typeSyntax) is not INamedTypeSymbol classTypeSymbol)
|
||||
MappingContext context = typeSyntax switch
|
||||
{
|
||||
_diagnostics.Add(DiagnosticsFactory.TypeNotFoundError(_typeSyntax.GetLocation(), _typeSyntax.Identifier.ValueText));
|
||||
return;
|
||||
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
|
||||
RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
context.Model = context.CreateMappingModel();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
protected void AddDiagnostic(Diagnostic diagnostic)
|
||||
{
|
||||
Diagnostics = Diagnostics.Add(diagnostic);
|
||||
}
|
||||
|
||||
protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
|
||||
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace.ToDisplayString());
|
||||
|
||||
protected void AddUsingIfRequired(bool condition, string? ns)
|
||||
{
|
||||
if (condition && ns is not null && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns))
|
||||
{
|
||||
Usings = Usings.Add(ns);
|
||||
}
|
||||
}
|
||||
|
||||
protected IPropertySymbol? FindSourceProperty(IEnumerable<IPropertySymbol> sourceProperties, ISymbol property)
|
||||
{
|
||||
var propertyName = property
|
||||
.GetAttribute(MapPropertyAttributeTypeSymbol)
|
||||
?.NamedArguments
|
||||
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
|
||||
.Value.Value as string ?? property.Name;
|
||||
|
||||
return sourceProperties.SingleOrDefault(p => p.Name == propertyName);
|
||||
}
|
||||
|
||||
protected abstract ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
|
||||
|
||||
protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
|
||||
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
|
||||
|
||||
protected INamedTypeSymbol? GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
|
||||
{
|
||||
if (attributeSyntax is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceTypeSymbol = GetSourceTypeSymbol(_typeSyntax, semanticModel);
|
||||
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
|
||||
var sourceTypeExpressionSyntax = attributeSyntax
|
||||
.DescendantNodes()
|
||||
.OfType<TypeOfExpressionSyntax>()
|
||||
.SingleOrDefault();
|
||||
|
||||
return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
|
||||
}
|
||||
|
||||
protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
|
||||
{
|
||||
return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types
|
||||
.Select(t => semanticModel.GetTypeInfo(t.Type).Type)
|
||||
.Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null);
|
||||
}
|
||||
|
||||
protected virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<IPropertySymbol> sourceProperties, ISymbol property)
|
||||
{
|
||||
var sourceProperty = FindSourceProperty(sourceProperties, property);
|
||||
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string? converterFullyQualifiedName = null;
|
||||
var converterParameters = ImmutableArray<string>.Empty;
|
||||
ITypeSymbol? mappedSourcePropertyType = null;
|
||||
ITypeSymbol? enumerableTypeArgumentType = null;
|
||||
|
||||
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
|
||||
{
|
||||
if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
|
||||
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
AddUsingIfRequired(propertyType);
|
||||
AddUsingIfRequired(sourceTypeSymbol);
|
||||
AddUsingIfRequired(enumerableTypeArgumentType);
|
||||
AddUsingIfRequired(mappedSourcePropertyType);
|
||||
|
||||
return new MappedProperty(
|
||||
property.Name,
|
||||
propertyType.Name,
|
||||
converterFullyQualifiedName,
|
||||
converterParameters.ToImmutableArray(),
|
||||
sourceProperty.Name,
|
||||
mappedSourcePropertyType?.Name,
|
||||
enumerableTypeArgumentType?.Name);
|
||||
}
|
||||
|
||||
protected bool TryGetMapTypeConverter(ISymbol 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)
|
||||
{
|
||||
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
|
||||
return false;
|
||||
}
|
||||
|
||||
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
|
||||
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument)
|
||||
{
|
||||
mappedSourcePropertyType = null;
|
||||
enumerableTypeArgument = null;
|
||||
|
||||
if (!Diagnostics.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!property.TryGetTypeSymbol(out var propertyType))
|
||||
{
|
||||
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
|
||||
return false;
|
||||
}
|
||||
|
||||
var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol);
|
||||
if (mapFromAttribute is null &&
|
||||
propertyType is INamedTypeSymbol namedTypeSymbol &&
|
||||
!propertyType.IsPrimitiveType() &&
|
||||
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
|
||||
{
|
||||
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
|
||||
mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol);
|
||||
}
|
||||
|
||||
mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol;
|
||||
|
||||
if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
|
||||
{
|
||||
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
|
||||
}
|
||||
|
||||
return Diagnostics.IsEmpty();
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
|
||||
{
|
||||
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
|
||||
return converterParameter.IsNull
|
||||
? ImmutableArray<string>.Empty
|
||||
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
|
||||
}
|
||||
|
||||
private MappingModel? CreateMappingModel()
|
||||
{
|
||||
var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree);
|
||||
if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText));
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceTypeSymbol = GetSourceTypeSymbol(TypeSyntax, semanticModel);
|
||||
if (sourceTypeSymbol is null)
|
||||
{
|
||||
_diagnostics.Add(DiagnosticsFactory.MapFromAttributeNotFoundError(_typeSyntax.GetLocation()));
|
||||
return;
|
||||
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
|
||||
return null;
|
||||
}
|
||||
|
||||
var typeIdentifierName = _typeSyntax.GetIdentifierName();
|
||||
var typeIdentifierName = TypeSyntax.GetIdentifierName();
|
||||
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
|
||||
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
|
||||
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
|
||||
|
||||
var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
|
||||
var mappedProperties = GetMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
|
||||
if (!mappedProperties.Any())
|
||||
{
|
||||
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyFoundError(_typeSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
|
||||
return;
|
||||
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
|
||||
return null;
|
||||
}
|
||||
|
||||
AddUsingIfRequired(sourceTypeSymbol);
|
||||
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
|
||||
|
||||
Model = new MappingModel(
|
||||
_sourceGenerationOptions,
|
||||
_typeSyntax.GetNamespace(),
|
||||
_typeSyntax.Modifiers,
|
||||
_typeSyntax.Keyword.Text,
|
||||
return new MappingModel(
|
||||
SourceGenerationOptions,
|
||||
TypeSyntax.GetNamespace(),
|
||||
TypeSyntax.Modifiers,
|
||||
TypeSyntax.Keyword.Text,
|
||||
typeIdentifierName,
|
||||
sourceTypeSymbol.ContainingNamespace.ToString(),
|
||||
sourceTypeIdentifierName,
|
||||
sourceTypeSymbol.ToString(),
|
||||
mappedProperties.ToImmutableArray(),
|
||||
mappedProperties,
|
||||
isTypeInheritFromMappedBaseClass,
|
||||
_usings.ToImmutableArray(),
|
||||
Usings,
|
||||
shouldGenerateSecondaryConstructor);
|
||||
}
|
||||
|
||||
private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty)
|
||||
{
|
||||
if (!property.TryGetTypeSymbol(out var propertyType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return converterTypeSymbol.AllInterfaces
|
||||
.SingleOrDefault(i =>
|
||||
i.TypeArguments.Length == 2 &&
|
||||
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
|
||||
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
|
||||
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
|
||||
}
|
||||
|
||||
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
|
||||
{
|
||||
var constructorSyntax = _typeSyntax.DescendantNodes()
|
||||
var constructorSyntax = TypeSyntax.DescendantNodes()
|
||||
.OfType<ConstructorDeclarationSyntax>()
|
||||
.SingleOrDefault(c =>
|
||||
c.ParameterList.Parameters.Count == 1 &&
|
||||
|
@ -108,186 +308,13 @@ namespace MapTo
|
|||
}
|
||||
|
||||
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
|
||||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, _mappingContextTypeSymbol) ||
|
||||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) ||
|
||||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
|
||||
{
|
||||
_diagnostics.Add(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
||||
AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
|
||||
{
|
||||
return _typeSyntax.BaseList is not null && _typeSyntax.BaseList.Types
|
||||
.Select(t => ModelExtensions.GetTypeInfo(semanticModel, t.Type).Type)
|
||||
.Any(t => t?.GetAttribute(_mapFromAttributeTypeSymbol) != null);
|
||||
}
|
||||
|
||||
private ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isClassInheritFromMappedBaseClass)
|
||||
{
|
||||
var mappedProperties = new List<MappedProperty>();
|
||||
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
|
||||
var classProperties = typeSymbol.GetAllMembers(!isClassInheritFromMappedBaseClass)
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => !p.HasAttribute(_ignorePropertyAttributeTypeSymbol));
|
||||
|
||||
foreach (var property in classProperties)
|
||||
{
|
||||
var sourceProperty = FindSourceProperty(sourceProperties, property);
|
||||
if (sourceProperty is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? converterFullyQualifiedName = null;
|
||||
var converterParameters = ImmutableArray<string>.Empty;
|
||||
ITypeSymbol? mappedSourcePropertyType = null;
|
||||
ITypeSymbol? enumerableTypeArgumentType = null;
|
||||
|
||||
if (!_compilation.HasCompatibleTypes(sourceProperty, property))
|
||||
{
|
||||
if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
|
||||
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
AddUsingIfRequired(property.Type);
|
||||
AddUsingIfRequired(sourceTypeSymbol);
|
||||
AddUsingIfRequired(enumerableTypeArgumentType);
|
||||
AddUsingIfRequired(mappedSourcePropertyType);
|
||||
|
||||
mappedProperties.Add(
|
||||
new MappedProperty(
|
||||
property.Name,
|
||||
property.Type.Name,
|
||||
converterFullyQualifiedName,
|
||||
converterParameters.ToImmutableArray(),
|
||||
sourceProperty.Name,
|
||||
mappedSourcePropertyType?.Name,
|
||||
enumerableTypeArgumentType?.Name));
|
||||
}
|
||||
|
||||
return mappedProperties.ToImmutableArray();
|
||||
}
|
||||
|
||||
private bool TryGetNestedObjectMappings(IPropertySymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument)
|
||||
{
|
||||
mappedSourcePropertyType = null;
|
||||
enumerableTypeArgument = null;
|
||||
|
||||
if (!_diagnostics.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol);
|
||||
if (mapFromAttribute is null &&
|
||||
property.Type is INamedTypeSymbol namedTypeSymbol &&
|
||||
!property.Type.IsPrimitiveType() &&
|
||||
(_compilation.IsGenericEnumerable(property.Type) || property.Type.AllInterfaces.Any(i => _compilation.IsGenericEnumerable(i))))
|
||||
{
|
||||
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
|
||||
mapFromAttribute = enumerableTypeArgument.GetAttribute(_mapFromAttributeTypeSymbol);
|
||||
}
|
||||
|
||||
mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol;
|
||||
|
||||
if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
|
||||
{
|
||||
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
|
||||
}
|
||||
|
||||
return _diagnostics.IsEmpty();
|
||||
}
|
||||
|
||||
private void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
|
||||
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace.ToDisplayString());
|
||||
|
||||
private void AddUsingIfRequired(bool condition, string? ns)
|
||||
{
|
||||
if (condition && ns is not null && ns != _typeSyntax.GetNamespace() && !_usings.Contains(ns))
|
||||
{
|
||||
_usings.Add(ns);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_diagnostics.Add(DiagnosticsFactory.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(_mapPropertyAttributeTypeSymbol)
|
||||
?.NamedArguments
|
||||
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
|
||||
.Value.Value as string ?? property.Name;
|
||||
|
||||
return sourceProperties.SingleOrDefault(p => p.Name == propertyName);
|
||||
}
|
||||
|
||||
private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, IPropertySymbol property, IPropertySymbol sourceProperty)
|
||||
{
|
||||
return converterTypeSymbol.AllInterfaces
|
||||
.SingleOrDefault(i =>
|
||||
i.TypeArguments.Length == 2 &&
|
||||
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, _typeConverterInterfaceTypeSymbol) &&
|
||||
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
|
||||
SymbolEqualityComparer.Default.Equals(property.Type, i.TypeArguments[1]));
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
|
||||
{
|
||||
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
|
||||
return converterParameter.IsNull
|
||||
? ImmutableArray<string>.Empty
|
||||
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
|
||||
}
|
||||
|
||||
private INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
|
||||
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
|
||||
|
||||
private INamedTypeSymbol? GetSourceTypeSymbol(AttributeSyntax? attributeSyntax, SemanticModel? semanticModel = null)
|
||||
{
|
||||
if (attributeSyntax is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
semanticModel ??= _compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
|
||||
var sourceTypeExpressionSyntax = attributeSyntax
|
||||
.DescendantNodes()
|
||||
.OfType<TypeOfExpressionSyntax>()
|
||||
.SingleOrDefault();
|
||||
|
||||
return sourceTypeExpressionSyntax is not null ? ModelExtensions.GetTypeInfo(semanticModel, sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using MapTo.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace MapTo
|
||||
{
|
||||
internal class RecordMappingContext : MappingContext
|
||||
{
|
||||
internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
|
||||
: base(compilation, sourceGenerationOptions, typeSyntax) { }
|
||||
|
||||
protected override ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
|
||||
{
|
||||
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
|
||||
return typeSymbol.GetMembers()
|
||||
.OfType<IMethodSymbol>()
|
||||
.OrderByDescending(s => s.Parameters.Length)
|
||||
.First(s => s.Name == ".ctor")
|
||||
.Parameters
|
||||
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
|
||||
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
|
||||
.Where(mappedProperty => mappedProperty is not null)
|
||||
.ToImmutableArray()!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ namespace MapTo.Sources
|
|||
// End namespace declaration
|
||||
.WriteClosingBracket();
|
||||
|
||||
return new(builder.ToString(), $"{model.TypeIdentifierName}.g.cs");
|
||||
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace MapTo.Sources
|
|||
}
|
||||
|
||||
builder
|
||||
.WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]")
|
||||
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]")
|
||||
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
||||
.WriteOpeningBracket();
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
using System;
|
||||
using MapTo.Extensions;
|
||||
using static MapTo.Sources.Constants;
|
||||
|
||||
namespace MapTo.Sources
|
||||
{
|
||||
internal static class MapRecordSource
|
||||
{
|
||||
internal static SourceCode Generate(MappingModel model)
|
||||
{
|
||||
using var builder = new SourceBuilder()
|
||||
.WriteLine(GeneratedFilesHeader)
|
||||
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
|
||||
|
||||
// Namespace declaration
|
||||
.WriteLine($"namespace {model.Namespace}")
|
||||
.WriteOpeningBracket()
|
||||
.WriteUsings(model.Usings)
|
||||
.WriteLine()
|
||||
|
||||
// Class declaration
|
||||
.WriteLine($"partial record {model.TypeIdentifierName}")
|
||||
.WriteOpeningBracket();
|
||||
|
||||
// Class body
|
||||
if (model.GenerateSecondaryConstructor)
|
||||
{
|
||||
builder
|
||||
.GenerateSecondaryConstructor(model)
|
||||
.WriteLine();
|
||||
}
|
||||
|
||||
builder
|
||||
.GeneratePrivateConstructor(model)
|
||||
.WriteLine()
|
||||
.GenerateFactoryMethod(model)
|
||||
|
||||
// End class declaration
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
|
||||
// Extension class declaration
|
||||
.GenerateSourceTypeExtensionClass(model)
|
||||
|
||||
// End namespace declaration
|
||||
.WriteClosingBracket();
|
||||
|
||||
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
|
||||
|
||||
if (model.Options.GenerateXmlDocument)
|
||||
{
|
||||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
|
||||
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
|
||||
.WriteLine("/// </summary>")
|
||||
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
|
||||
}
|
||||
|
||||
return builder
|
||||
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceTypeIdentifierName} {sourceClassParameterName})")
|
||||
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
|
||||
}
|
||||
|
||||
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
|
||||
const string mappingContextParameterName = "context";
|
||||
|
||||
builder
|
||||
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceTypeIdentifierName} {sourceClassParameterName})")
|
||||
.Indent()
|
||||
.Write(": this(");
|
||||
|
||||
for (var i = 0; i < model.MappedProperties.Length; i++)
|
||||
{
|
||||
var property = model.MappedProperties[i];
|
||||
if (property.TypeConverter is null)
|
||||
{
|
||||
if (property.IsEnumerable)
|
||||
{
|
||||
builder.Write(
|
||||
$"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Write(property.MappedSourcePropertyTypeName is null
|
||||
? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}"
|
||||
: $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var parameters = property.TypeConverterParameters.IsEmpty
|
||||
? "null"
|
||||
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
|
||||
|
||||
builder.Write($"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})");
|
||||
}
|
||||
|
||||
if (i < model.MappedProperties.Length - 1)
|
||||
{
|
||||
builder.Write(", ");
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteLine(")")
|
||||
.Unindent()
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
|
||||
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
|
||||
.WriteLine()
|
||||
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);");
|
||||
|
||||
// End constructor declaration
|
||||
return builder.WriteClosingBracket();
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
|
||||
|
||||
return builder
|
||||
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
||||
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
|
||||
.WriteLine(
|
||||
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceTypeIdentifierName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine(
|
||||
$"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceTypeIdentifierName}, {model.TypeIdentifierName}>({sourceClassParameterName});")
|
||||
.WriteClosingBracket();
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
|
||||
{
|
||||
if (!model.Options.GenerateXmlDocument)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
|
||||
return builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
|
||||
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
|
||||
.WriteLine("/// </summary>")
|
||||
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceTypeIdentifierName}\"/> to use as source.</param>")
|
||||
.WriteLine(
|
||||
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
return builder
|
||||
.WriteLine(
|
||||
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
|
||||
.WriteOpeningBracket()
|
||||
.GenerateSourceTypeExtensionMethod(model)
|
||||
.WriteClosingBracket();
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
|
||||
|
||||
return builder
|
||||
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
||||
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
|
||||
.WriteLine(
|
||||
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceTypeIdentifierName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
|
||||
.WriteClosingBracket();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ namespace MapTo.Sources
|
|||
}
|
||||
|
||||
builder
|
||||
.WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]")
|
||||
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]")
|
||||
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
||||
.WriteOpeningBracket();
|
||||
|
||||
|
|
|
@ -38,6 +38,12 @@ namespace MapTo.Sources
|
|||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder Write(string? value = null)
|
||||
{
|
||||
_indentedWriter.Write(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder WriteLineIf(bool condition, string? value)
|
||||
{
|
||||
if (condition)
|
||||
|
@ -83,6 +89,18 @@ namespace MapTo.Sources
|
|||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder Indent()
|
||||
{
|
||||
_indentedWriter.Indent++;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder Unindent()
|
||||
{
|
||||
_indentedWriter.Indent--;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _writer.ToString();
|
||||
}
|
||||
|
|
|
@ -14,6 +14,6 @@ namespace MapTo.Tests.Infrastructure
|
|||
_backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer);
|
||||
}
|
||||
|
||||
public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) => _backing.TryGetValue(key, out value);
|
||||
public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value);
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ using System;
|
|||
|
||||
namespace MapTo
|
||||
{{
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
|
||||
public sealed class MapPropertyAttribute : Attribute
|
||||
{{
|
||||
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
|
||||
|
|
|
@ -23,7 +23,7 @@ using System;
|
|||
|
||||
namespace MapTo
|
||||
{{
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
public sealed class MapTypeConverterAttribute : Attribute
|
||||
{{
|
||||
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
|
||||
|
@ -60,7 +60,7 @@ using System;
|
|||
|
||||
namespace MapTo
|
||||
{{
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
public sealed class MapTypeConverterAttribute : Attribute
|
||||
{{
|
||||
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MapTo.Tests.Extensions;
|
||||
using MapTo.Tests.Infrastructure;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
@ -18,40 +20,15 @@ namespace MapTo.Tests
|
|||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyMappedClassSource()
|
||||
[Theory]
|
||||
[MemberData(nameof(SecondaryConstructorCheckData))]
|
||||
public void When_SecondaryConstructorExists_Should_NotGenerateOne(string source, LanguageVersion languageVersion)
|
||||
{
|
||||
// Arrange
|
||||
var sources = new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass };
|
||||
source = source.Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
_output.WriteLine(compilation.PrintSyntaxTree());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_SecondaryConstructorExists_Should_NotGenerateOne()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
using MapTo;
|
||||
namespace Test.Data.Models
|
||||
{
|
||||
public class SourceClass { public string Prop1 { get; set; } }
|
||||
|
||||
[MapFrom(typeof(SourceClass))]
|
||||
public partial class DestinationClass
|
||||
{
|
||||
public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
|
||||
public string Prop1 { get; set; }
|
||||
}
|
||||
}
|
||||
".Trim();
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
|
@ -65,11 +42,71 @@ namespace Test.Data.Models
|
|||
.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError()
|
||||
public static IEnumerable<object[]> SecondaryConstructorCheckData => new List<object[]>
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
@"
|
||||
using MapTo;
|
||||
namespace Test.Data.Models
|
||||
{
|
||||
public class SourceClass { public string Prop1 { get; set; } }
|
||||
|
||||
[MapFrom(typeof(SourceClass))]
|
||||
public partial class DestinationClass
|
||||
{
|
||||
public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
|
||||
public string Prop1 { get; set; }
|
||||
}
|
||||
}
|
||||
",
|
||||
LanguageVersion.CSharp7_3
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
@"
|
||||
using MapTo;
|
||||
namespace Test.Data.Models
|
||||
{
|
||||
public record SourceClass(string Prop1);
|
||||
|
||||
[MapFrom(typeof(SourceClass))]
|
||||
public partial record DestinationClass(string Prop1)
|
||||
{
|
||||
public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
|
||||
}
|
||||
}
|
||||
",
|
||||
LanguageVersion.CSharp9
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SecondaryCtorWithoutPrivateCtorData))]
|
||||
public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError(string source, LanguageVersion languageVersion)
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
source = source.Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
|
||||
|
||||
// Assert
|
||||
var constructorSyntax = compilation.SyntaxTrees
|
||||
.First()
|
||||
.GetRoot()
|
||||
.DescendantNodes()
|
||||
.OfType<ConstructorDeclarationSyntax>()
|
||||
.Single();
|
||||
|
||||
diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> SecondaryCtorWithoutPrivateCtorData => new List<object[]>
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
@"
|
||||
using MapTo;
|
||||
namespace Test.Data.Models
|
||||
{
|
||||
|
@ -82,20 +119,27 @@ namespace Test.Data.Models
|
|||
public string Prop1 { get; set; }
|
||||
}
|
||||
}
|
||||
".Trim();
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
",
|
||||
LanguageVersion.CSharp7_3
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
@"
|
||||
using MapTo;
|
||||
namespace Test.Data.Models
|
||||
{
|
||||
public record SourceClass(string Prop1);
|
||||
|
||||
// Assert
|
||||
var constructorSyntax = compilation.SyntaxTrees
|
||||
.First()
|
||||
.GetRoot()
|
||||
.DescendantNodes()
|
||||
.OfType<ConstructorDeclarationSyntax>()
|
||||
.Single();
|
||||
|
||||
diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
||||
}
|
||||
[MapFrom(typeof(SourceClass))]
|
||||
public partial record DestinationClass(string Prop1)
|
||||
{
|
||||
public DestinationClass(SourceClass source) : this(""invalid"") { }
|
||||
}
|
||||
}
|
||||
",
|
||||
LanguageVersion.CSharp9
|
||||
}
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly()
|
||||
|
@ -122,26 +166,67 @@ namespace SaleModel
|
|||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
}
|
||||
|
||||
private static string NestedSourceClass => @"
|
||||
namespace Test.Data.Models
|
||||
[Theory]
|
||||
[MemberData(nameof(VerifyMappedTypesData))]
|
||||
public void VerifyMappedTypes(string[] sources, LanguageVersion languageVersion)
|
||||
{
|
||||
// Arrange
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
_output.WriteLine(compilation.PrintSyntaxTree());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> VerifyMappedTypesData => new List<object[]>
|
||||
{
|
||||
new object[] { new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass }, LanguageVersion.CSharp7_3 },
|
||||
new object[] { new[] { MainSourceRecord, NestedSourceRecord, MainDestinationRecord, NestedDestinationRecord }, LanguageVersion.CSharp9 }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void VerifySelfReferencingRecords()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
namespace Tests.Data.Models
|
||||
{
|
||||
public class Profile
|
||||
{
|
||||
public string FirstName { get; set; }
|
||||
using System.Collections.Generic;
|
||||
|
||||
public record Employee(int Id, string EmployeeCode, Manager Manager);
|
||||
|
||||
public string LastName { get; set; }
|
||||
public record Manager(int Id, string EmployeeCode, Manager Manager, int Level, List<Employee> Employees) : Employee(Id, EmployeeCode, Manager);
|
||||
}
|
||||
|
||||
public string FullName => $""{FirstName} {LastName}"";
|
||||
}
|
||||
namespace Tests.Data.ViewModels
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Tests.Data.Models;
|
||||
using MapTo;
|
||||
|
||||
[MapFrom(typeof(Employee))]
|
||||
public partial record EmployeeViewModel(int Id, string EmployeeCode, ManagerViewModel Manager);
|
||||
|
||||
[MapFrom(typeof(Manager))]
|
||||
public partial record ManagerViewModel(int Id, string EmployeeCode, ManagerViewModel Manager, int Level, List<EmployeeViewModel> Employees) : EmployeeViewModel(Id, EmployeeCode, Manager);
|
||||
}
|
||||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
_output.WriteLine(compilation.PrintSyntaxTree());
|
||||
}
|
||||
|
||||
private static string MainSourceClass => @"
|
||||
using System;
|
||||
|
||||
|
@ -157,24 +242,21 @@ namespace Test.Data.Models
|
|||
}
|
||||
}
|
||||
".Trim();
|
||||
|
||||
private static string NestedDestinationClass => @"
|
||||
using MapTo;
|
||||
using Test.Data.Models;
|
||||
|
||||
namespace Test.ViewModels
|
||||
|
||||
private static string NestedSourceClass => @"
|
||||
namespace Test.Data.Models
|
||||
{
|
||||
[MapFrom(typeof(Profile))]
|
||||
public partial class ProfileViewModel
|
||||
public class Profile
|
||||
{
|
||||
public string FirstName { get; }
|
||||
public string FirstName { get; set; }
|
||||
|
||||
public string LastName { get; }
|
||||
public string LastName { get; set; }
|
||||
|
||||
public string FullName => $""{FirstName} {LastName}"";
|
||||
}
|
||||
}
|
||||
".Trim();
|
||||
|
||||
|
||||
|
||||
private static string MainDestinationClass => @"
|
||||
using System;
|
||||
using MapTo;
|
||||
|
@ -201,5 +283,67 @@ namespace Test.ViewModels
|
|||
}
|
||||
}
|
||||
".Trim();
|
||||
|
||||
private static string NestedDestinationClass => @"
|
||||
using MapTo;
|
||||
using Test.Data.Models;
|
||||
|
||||
namespace Test.ViewModels
|
||||
{
|
||||
[MapFrom(typeof(Profile))]
|
||||
public partial class ProfileViewModel
|
||||
{
|
||||
public string FirstName { get; }
|
||||
|
||||
public string LastName { get; }
|
||||
}
|
||||
}
|
||||
".Trim();
|
||||
|
||||
private static string MainSourceRecord => BuildSourceRecord("public record User(int Id, DateTimeOffset RegisteredAt, Profile Profile);");
|
||||
|
||||
private static string MainDestinationRecord => BuildDestinationRecord(@"
|
||||
[MapFrom(typeof(User))]
|
||||
public partial record UserViewModel(
|
||||
[MapProperty(SourcePropertyName = nameof(User.Id))]
|
||||
[MapTypeConverter(typeof(UserViewModel.IdConverter))]
|
||||
string Key,
|
||||
DateTimeOffset RegisteredAt)
|
||||
{
|
||||
private class IdConverter : ITypeConverter<int, string>
|
||||
{
|
||||
public string Convert(int source, object[] converterParameters) => $""{source:X}"";
|
||||
}
|
||||
}");
|
||||
|
||||
private static string NestedSourceRecord => BuildSourceRecord("public record Profile(string FirstName, string LastName) { public string FullName => $\"{FirstName} {LastName}\"; }");
|
||||
|
||||
private static string NestedDestinationRecord => BuildDestinationRecord("[MapFrom(typeof(Profile))] public partial record ProfileViewModel(string FirstName, string LastName);");
|
||||
|
||||
private static string BuildSourceRecord(string record)
|
||||
{
|
||||
return $@"
|
||||
using System;
|
||||
|
||||
namespace RecordTest.Data.Models
|
||||
{{
|
||||
{record}
|
||||
}}
|
||||
".Trim();
|
||||
}
|
||||
|
||||
private static string BuildDestinationRecord(string record)
|
||||
{
|
||||
return $@"
|
||||
using System;
|
||||
using MapTo;
|
||||
using RecordTest.Data.Models;
|
||||
|
||||
namespace RecordTest.ViewModels
|
||||
{{
|
||||
{record}
|
||||
}}
|
||||
".Trim();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue