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">
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Nerdbank.GitVersioning">
|
<PackageReference Include="Nerdbank.GitVersioning">
|
||||||
<Version>3.4.216</Version>
|
<Version>3.4.22</Version>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</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 System.Linq;
|
||||||
|
using MapTo.Extensions;
|
||||||
using MapTo.Sources;
|
using MapTo.Sources;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
@ -23,11 +24,11 @@ namespace MapTo
|
||||||
internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) =>
|
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.");
|
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}'.");
|
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) =>
|
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.Type.ToDisplayString()}>'.");
|
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) =>
|
internal static Diagnostic ConfigurationParseError(string error) =>
|
||||||
Create($"{ErrorId}040", Location.None, error);
|
Create($"{ErrorId}040", Location.None, error);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
@ -48,15 +49,36 @@ namespace MapTo.Extensions
|
||||||
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
|
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
|
||||||
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
|
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
|
||||||
|
|
||||||
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) =>
|
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
|
||||||
typeDeclarationSyntax.Ancestors()
|
.Ancestors()
|
||||||
.OfType<NamespaceDeclarationSyntax>()
|
.OfType<NamespaceDeclarationSyntax>()
|
||||||
.FirstOrDefault()
|
.FirstOrDefault()
|
||||||
?.Name
|
?.Name
|
||||||
.ToString();
|
.ToString();
|
||||||
|
|
||||||
public static bool HasCompatibleTypes(this Compilation compilation, IPropertySymbol sourceProperty, IPropertySymbol destinationProperty) =>
|
public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) =>
|
||||||
SymbolEqualityComparer.Default.Equals(destinationProperty.Type, sourceProperty.Type) || compilation.HasImplicitConversion(sourceProperty.Type, destinationProperty.Type);
|
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)
|
public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty)
|
||||||
{
|
{
|
||||||
|
@ -91,5 +113,8 @@ namespace MapTo.Extensions
|
||||||
SpecialType.System_Double or
|
SpecialType.System_Double or
|
||||||
SpecialType.System_Char or
|
SpecialType.System_Char or
|
||||||
SpecialType.System_Object;
|
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);
|
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
|
||||||
|
|
||||||
if (mappingContext.Model is not null)
|
if (mappingContext.Model is null)
|
||||||
{
|
{
|
||||||
var (source, hintName) = MapClassSource.Generate(mappingContext.Model);
|
continue;
|
||||||
context.AddSource(hintName, source);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MapTo.Extensions;
|
using MapTo.Extensions;
|
||||||
using MapTo.Sources;
|
using MapTo.Sources;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|
||||||
namespace MapTo
|
namespace MapTo
|
||||||
{
|
{
|
||||||
internal class MappingContext
|
internal abstract class MappingContext
|
||||||
{
|
{
|
||||||
private readonly TypeDeclarationSyntax _typeSyntax;
|
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, 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)
|
|
||||||
{
|
{
|
||||||
_diagnostics = new List<Diagnostic>();
|
Diagnostics = ImmutableArray<Diagnostic>.Empty;
|
||||||
_usings = new List<string> { "System", Constants.RootNamespace };
|
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
|
||||||
_sourceGenerationOptions = sourceGenerationOptions;
|
SourceGenerationOptions = sourceGenerationOptions;
|
||||||
_typeSyntax = typeSyntax;
|
TypeSyntax = typeSyntax;
|
||||||
_compilation = compilation;
|
Compilation = compilation;
|
||||||
|
|
||||||
_ignorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName);
|
IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName);
|
||||||
_mapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName);
|
MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName);
|
||||||
_typeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
|
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
|
||||||
_mapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
|
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
|
||||||
_mapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
|
MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
|
||||||
_mappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
|
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
|
||||||
|
|
||||||
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
|
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
|
||||||
|
|
||||||
Initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
|
||||||
|
|
||||||
public MappingModel? Model { 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);
|
MappingContext context = typeSyntax switch
|
||||||
if (ModelExtensions.GetDeclaredSymbol(semanticModel, _typeSyntax) is not INamedTypeSymbol classTypeSymbol)
|
|
||||||
{
|
{
|
||||||
_diagnostics.Add(DiagnosticsFactory.TypeNotFoundError(_typeSyntax.GetLocation(), _typeSyntax.Identifier.ValueText));
|
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
|
||||||
return;
|
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)
|
if (sourceTypeSymbol is null)
|
||||||
{
|
{
|
||||||
_diagnostics.Add(DiagnosticsFactory.MapFromAttributeNotFoundError(_typeSyntax.GetLocation()));
|
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeIdentifierName = _typeSyntax.GetIdentifierName();
|
var typeIdentifierName = TypeSyntax.GetIdentifierName();
|
||||||
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
|
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
|
||||||
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
|
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
|
||||||
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
|
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
|
||||||
|
|
||||||
var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
|
var mappedProperties = GetMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
|
||||||
if (!mappedProperties.Any())
|
if (!mappedProperties.Any())
|
||||||
{
|
{
|
||||||
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyFoundError(_typeSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
|
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddUsingIfRequired(sourceTypeSymbol);
|
AddUsingIfRequired(sourceTypeSymbol);
|
||||||
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
|
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
|
||||||
|
|
||||||
Model = new MappingModel(
|
return new MappingModel(
|
||||||
_sourceGenerationOptions,
|
SourceGenerationOptions,
|
||||||
_typeSyntax.GetNamespace(),
|
TypeSyntax.GetNamespace(),
|
||||||
_typeSyntax.Modifiers,
|
TypeSyntax.Modifiers,
|
||||||
_typeSyntax.Keyword.Text,
|
TypeSyntax.Keyword.Text,
|
||||||
typeIdentifierName,
|
typeIdentifierName,
|
||||||
sourceTypeSymbol.ContainingNamespace.ToString(),
|
sourceTypeSymbol.ContainingNamespace.ToString(),
|
||||||
sourceTypeIdentifierName,
|
sourceTypeIdentifierName,
|
||||||
sourceTypeSymbol.ToString(),
|
sourceTypeSymbol.ToString(),
|
||||||
mappedProperties.ToImmutableArray(),
|
mappedProperties,
|
||||||
isTypeInheritFromMappedBaseClass,
|
isTypeInheritFromMappedBaseClass,
|
||||||
_usings.ToImmutableArray(),
|
Usings,
|
||||||
shouldGenerateSecondaryConstructor);
|
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)
|
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
|
||||||
{
|
{
|
||||||
var constructorSyntax = _typeSyntax.DescendantNodes()
|
var constructorSyntax = TypeSyntax.DescendantNodes()
|
||||||
.OfType<ConstructorDeclarationSyntax>()
|
.OfType<ConstructorDeclarationSyntax>()
|
||||||
.SingleOrDefault(c =>
|
.SingleOrDefault(c =>
|
||||||
c.ParameterList.Parameters.Count == 1 &&
|
c.ParameterList.Parameters.Count == 1 &&
|
||||||
|
@ -108,186 +308,13 @@ namespace MapTo
|
||||||
}
|
}
|
||||||
|
|
||||||
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
|
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))
|
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
|
||||||
{
|
{
|
||||||
_diagnostics.Add(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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
|
// End namespace declaration
|
||||||
.WriteClosingBracket();
|
.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)
|
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
|
||||||
|
|
|
@ -36,7 +36,7 @@ namespace MapTo.Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]")
|
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]")
|
||||||
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
||||||
.WriteOpeningBracket();
|
.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
|
builder
|
||||||
.WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]")
|
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]")
|
||||||
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
||||||
.WriteOpeningBracket();
|
.WriteOpeningBracket();
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,12 @@ namespace MapTo.Sources
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SourceBuilder Write(string? value = null)
|
||||||
|
{
|
||||||
|
_indentedWriter.Write(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SourceBuilder WriteLineIf(bool condition, string? value)
|
public SourceBuilder WriteLineIf(bool condition, string? value)
|
||||||
{
|
{
|
||||||
if (condition)
|
if (condition)
|
||||||
|
@ -83,6 +89,18 @@ namespace MapTo.Sources
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SourceBuilder Indent()
|
||||||
|
{
|
||||||
|
_indentedWriter.Indent++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceBuilder Unindent()
|
||||||
|
{
|
||||||
|
_indentedWriter.Indent--;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => _writer.ToString();
|
public override string ToString() => _writer.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,6 @@ namespace MapTo.Tests.Infrastructure
|
||||||
_backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer);
|
_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
|
namespace MapTo
|
||||||
{{
|
{{
|
||||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
|
||||||
public sealed class MapPropertyAttribute : Attribute
|
public sealed class MapPropertyAttribute : Attribute
|
||||||
{{
|
{{
|
||||||
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
|
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
|
||||||
|
|
|
@ -23,7 +23,7 @@ using System;
|
||||||
|
|
||||||
namespace MapTo
|
namespace MapTo
|
||||||
{{
|
{{
|
||||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
|
||||||
public sealed class MapTypeConverterAttribute : Attribute
|
public sealed class MapTypeConverterAttribute : Attribute
|
||||||
{{
|
{{
|
||||||
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
|
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
|
||||||
|
@ -60,7 +60,7 @@ using System;
|
||||||
|
|
||||||
namespace MapTo
|
namespace MapTo
|
||||||
{{
|
{{
|
||||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
|
||||||
public sealed class MapTypeConverterAttribute : Attribute
|
public sealed class MapTypeConverterAttribute : Attribute
|
||||||
{{
|
{{
|
||||||
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
|
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.Extensions;
|
||||||
using MapTo.Tests.Infrastructure;
|
using MapTo.Tests.Infrastructure;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -18,40 +20,15 @@ namespace MapTo.Tests
|
||||||
_output = output;
|
_output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void VerifyMappedClassSource()
|
[MemberData(nameof(SecondaryConstructorCheckData))]
|
||||||
|
public void When_SecondaryConstructorExists_Should_NotGenerateOne(string source, LanguageVersion languageVersion)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var sources = new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass };
|
source = source.Trim();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
|
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
diagnostics.ShouldBeSuccessful();
|
diagnostics.ShouldBeSuccessful();
|
||||||
|
@ -65,11 +42,71 @@ namespace Test.Data.Models
|
||||||
.ShouldBe(1);
|
.ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
public static IEnumerable<object[]> SecondaryConstructorCheckData => new List<object[]>
|
||||||
public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError()
|
{
|
||||||
|
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
|
// 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;
|
using MapTo;
|
||||||
namespace Test.Data.Models
|
namespace Test.Data.Models
|
||||||
{
|
{
|
||||||
|
@ -82,20 +119,27 @@ namespace Test.Data.Models
|
||||||
public string Prop1 { get; set; }
|
public string Prop1 { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
".Trim();
|
",
|
||||||
// Act
|
LanguageVersion.CSharp7_3
|
||||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
},
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
@"
|
||||||
|
using MapTo;
|
||||||
|
namespace Test.Data.Models
|
||||||
|
{
|
||||||
|
public record SourceClass(string Prop1);
|
||||||
|
|
||||||
// Assert
|
[MapFrom(typeof(SourceClass))]
|
||||||
var constructorSyntax = compilation.SyntaxTrees
|
public partial record DestinationClass(string Prop1)
|
||||||
.First()
|
{
|
||||||
.GetRoot()
|
public DestinationClass(SourceClass source) : this(""invalid"") { }
|
||||||
.DescendantNodes()
|
}
|
||||||
.OfType<ConstructorDeclarationSyntax>()
|
}
|
||||||
.Single();
|
",
|
||||||
|
LanguageVersion.CSharp9
|
||||||
diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly()
|
public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly()
|
||||||
|
@ -122,26 +166,67 @@ namespace SaleModel
|
||||||
".Trim();
|
".Trim();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
diagnostics.ShouldBeSuccessful();
|
diagnostics.ShouldBeSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NestedSourceClass => @"
|
[Theory]
|
||||||
namespace Test.Data.Models
|
[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
|
using System.Collections.Generic;
|
||||||
{
|
|
||||||
public string FirstName { get; set; }
|
|
||||||
|
|
||||||
public string LastName { get; set; }
|
public record Employee(int Id, string EmployeeCode, Manager Manager);
|
||||||
|
|
||||||
public string FullName => $""{FirstName} {LastName}"";
|
public record Manager(int Id, string EmployeeCode, Manager Manager, int Level, List<Employee> Employees) : Employee(Id, EmployeeCode, Manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
".Trim();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
diagnostics.ShouldBeSuccessful();
|
||||||
|
_output.WriteLine(compilation.PrintSyntaxTree());
|
||||||
|
}
|
||||||
|
|
||||||
private static string MainSourceClass => @"
|
private static string MainSourceClass => @"
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
@ -158,23 +243,20 @@ namespace Test.Data.Models
|
||||||
}
|
}
|
||||||
".Trim();
|
".Trim();
|
||||||
|
|
||||||
private static string NestedDestinationClass => @"
|
private static string NestedSourceClass => @"
|
||||||
using MapTo;
|
namespace Test.Data.Models
|
||||||
using Test.Data.Models;
|
|
||||||
|
|
||||||
namespace Test.ViewModels
|
|
||||||
{
|
{
|
||||||
[MapFrom(typeof(Profile))]
|
public class Profile
|
||||||
public partial class ProfileViewModel
|
|
||||||
{
|
{
|
||||||
public string FirstName { get; }
|
public string FirstName { get; set; }
|
||||||
|
|
||||||
public string LastName { get; }
|
public string LastName { get; set; }
|
||||||
|
|
||||||
|
public string FullName => $""{FirstName} {LastName}"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
".Trim();
|
".Trim();
|
||||||
|
|
||||||
|
|
||||||
private static string MainDestinationClass => @"
|
private static string MainDestinationClass => @"
|
||||||
using System;
|
using System;
|
||||||
using MapTo;
|
using MapTo;
|
||||||
|
@ -201,5 +283,67 @@ namespace Test.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
".Trim();
|
".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