Add support for c# records.

This commit is contained in:
Mohammadreza Taikandi 2021-06-30 19:16:28 +01:00
parent 91f5e9bcf5
commit 02e530156f
17 changed files with 955 additions and 319 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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