Initial implementation of TypeConverter.

This commit is contained in:
Mohammadreza Taikandi 2021-01-21 08:19:17 +00:00
parent ec21abcf0e
commit dd9254ab74
23 changed files with 510 additions and 228 deletions

View File

@ -1,4 +1,4 @@
namespace MapTo.Models
namespace MapTo
{
internal enum AccessModifier
{

View File

@ -0,0 +1,37 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using static MapTo.Sources.Constants;
namespace MapTo
{
internal static class DiagnosticProvider
{
private const string UsageCategory = "Usage";
private const string ErrorId = "MT0";
private const string InfoId = "MT1";
private const string WarningId = "MT2";
internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) =>
Create($"{ErrorId}010", location, "Type not found.", $"Unable to find '{syntaxName}' type.");
internal static Diagnostic MapFromAttributeNotFoundError(Location location) =>
Create($"{ErrorId}020", location, "Attribute Not Available", $"Unable to find {MapFromAttributeSource.AttributeName} type.");
internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) =>
Create($"{ErrorId}030", location, "Type Mismatch", $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types.");
internal static Diagnostic NoMatchingPropertyTypeFoundError(IPropertySymbol property) =>
Create($"{ErrorId}031", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{RootNamespace}.{MapPropertyAttributeSource.AttributeName}Attribute' to provide a type converter or ignore the property using '{RootNamespace}.{IgnorePropertyAttributeSource.AttributeName}Attribute'.");
internal static Diagnostic InvalidTypeConverterGenericTypesError(IPropertySymbol property, IPropertySymbol sourceProperty) =>
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{TypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.Type.ToDisplayString()}>'.");
internal static Diagnostic ConfigurationParseError(string error) =>
Create($"{ErrorId}040", Location.None, "Incorrect Configuration", error);
private static Diagnostic Create(string id, Location? location, string title, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
Diagnostic.Create(new DiagnosticDescriptor(id, title, message, UsageCategory, severity, true), location ?? Location.None);
}
}

View File

@ -1,28 +0,0 @@
using MapTo.Sources;
using Microsoft.CodeAnalysis;
namespace MapTo
{
internal static class Diagnostics
{
private const string UsageCategory = "Usage";
private const string ErrorId = "MT0";
private const string InfoId = "MT1";
private const string WarningId = "MT2";
internal static Diagnostic SymbolNotFoundError(Location location, string syntaxName) =>
Create($"{ErrorId}001", "Symbol not found.", $"Unable to find any symbols for {syntaxName}", location);
internal static Diagnostic MapFromAttributeNotFoundError(Location location) =>
Create($"{ErrorId}002", "Attribute Not Available", $"Unable to find {MapFromAttributeSource.AttributeName} type.", location);
internal static Diagnostic NoMatchingPropertyFoundError(Location location, string className, string sourceTypeName) =>
Create($"{ErrorId}003", "Property Not Found", $"No matching properties found between '{className}' and '{sourceTypeName}' types.", location);
internal static Diagnostic ConfigurationParseError(string error) =>
Create($"{ErrorId}004", "Incorrect Configuration", error, Location.None);
private static Diagnostic Create(string id, string title, string message, Location location, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
Diagnostic.Create(new DiagnosticDescriptor(id, title, message, UsageCategory, severity, true), location);
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MapTo.Extensions
{
@ -11,6 +12,8 @@ namespace MapTo.Extensions
{
action(item);
}
}
}
internal static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable.Any();
}
}

View File

@ -1,6 +1,9 @@
using System;
using System.Text;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
namespace MapTo.Extensions
{
@ -8,7 +11,7 @@ namespace MapTo.Extensions
{
private const string PropertyNameSuffix = "MapTo_";
internal static T GetBuildGlobalOption<T>(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T: notnull
internal static T GetBuildGlobalOption<T>(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull
{
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue))
{
@ -28,14 +31,22 @@ namespace MapTo.Extensions
}
catch (Exception)
{
context.ReportDiagnostic(Diagnostics.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property."));
context.ReportDiagnostic(DiagnosticProvider.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property."));
return defaultValue;
}
}
internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}";
internal static void AddSource(this GeneratorExecutionContext context, SourceCode sourceCode) =>
context.AddSource(sourceCode.HintName, sourceCode.Text);
internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode)
{
var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8);
context.AddSource(sourceCode.HintName, sourceText);
// NB: https://github.com/dotnet/roslyn/issues/49753
// To be replaced after above issue is resolved.
var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options;
return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options));
}
}
}

View File

@ -23,8 +23,6 @@ namespace MapTo.Extensions
return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
}
public static IEnumerable<T> GetAllMembersOfType<T>(this ITypeSymbol type) where T : ISymbol => type.GetAllMembers().OfType<T>();
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
public static string GetClassName(this ClassDeclarationSyntax classSyntax) => classSyntax.Identifier.Text;
@ -38,11 +36,19 @@ namespace MapTo.Extensions
((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName);
}
public static string? GetNamespace(this CompilationUnitSyntax root) =>
root.ChildNodes()
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static string? GetNamespace(this ClassDeclarationSyntax classDeclarationSyntax)
{
return classDeclarationSyntax.Ancestors()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()
?.Name
.ToString();
}
}
}

View File

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using MapTo.Models;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -21,30 +20,30 @@ namespace MapTo
public void Execute(GeneratorExecutionContext context)
{
var options = SourceGenerationOptions.From(context);
context.AddSource(MapFromAttributeSource.Generate(options));
context.AddSource(IgnorePropertyAttributeSource.Generate(options));
context.AddSource(TypeConverterSource.Generate(options));
context.AddSource(MapPropertyAttributeSource.Generate(options));
var compilation = context.Compilation
.AddSource(ref context, MapFromAttributeSource.Generate(options))
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
.AddSource(ref context, TypeConverterSource.Generate(options))
.AddSource(ref context, MapPropertyAttributeSource.Generate(options));
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
{
AddGeneratedMappingsClasses(context, receiver.CandidateClasses, options);
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateClasses, options);
}
}
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, IEnumerable<ClassDeclarationSyntax> candidateClasses, SourceGenerationOptions options)
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<ClassDeclarationSyntax> candidateClasses, SourceGenerationOptions options)
{
foreach (var classSyntax in candidateClasses)
{
var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree);
var (model, diagnostics) = MapModel.CreateModel(classSemanticModel, classSyntax, options);
var mappingContext = MappingContext.Create(compilation, classSyntax, options);
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
diagnostics.ForEach(context.ReportDiagnostic);
if (model is not null)
if (mappingContext.Model is not null)
{
context.AddSource(MapClassSource.Generate(model));
var (source, hintName) = MapClassSource.Generate(mappingContext.Model);
context.AddSource(hintName, source);
}
}
}

156
src/MapTo/MappingContext.cs Normal file
View File

@ -0,0 +1,156 @@
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.Syntax;
namespace MapTo
{
internal class MappingContext
{
private Compilation Compilation { get; }
private MappingContext(Compilation compilation)
{
Diagnostics = ImmutableArray<Diagnostic>.Empty;
Compilation = compilation;
IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(IgnorePropertyAttributeSource.FullyQualifiedName)
?? throw new TypeLoadException($"Unable to find '{IgnorePropertyAttributeSource.FullyQualifiedName}' type.");
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapPropertyAttributeSource.FullyQualifiedName)
?? throw new TypeLoadException($"Unable to find '{MapPropertyAttributeSource.FullyQualifiedName}' type.");
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(TypeConverterSource.FullyQualifiedName)
?? throw new TypeLoadException($"Unable to find '{TypeConverterSource.FullyQualifiedName}' type.");
}
public INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
public MappingModel? Model { get; private set; }
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
private MappingContext ReportDiagnostic(Diagnostic diagnostic)
{
Diagnostics = Diagnostics.Add(diagnostic);
return this;
}
internal static MappingContext Create(Compilation compilation, ClassDeclarationSyntax classSyntax, SourceGenerationOptions sourceGenerationOptions)
{
var context = new MappingContext(compilation);
var root = classSyntax.GetCompilationUnit();
var semanticModel = compilation.GetSemanticModel(classSyntax.SyntaxTree);
if (!(semanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classTypeSymbol))
{
return context.ReportDiagnostic(DiagnosticProvider.TypeNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
}
var sourceTypeSymbol = GetSourceTypeSymbol(semanticModel, classSyntax);
if (sourceTypeSymbol is null)
{
return context.ReportDiagnostic(DiagnosticProvider.MapFromAttributeNotFoundError(classSyntax.GetLocation()));
}
var className = classSyntax.GetClassName();
var sourceClassName = sourceTypeSymbol.Name;
var mappedProperties = GetMappedProperties(context, classTypeSymbol, sourceTypeSymbol);
if (!mappedProperties.Any())
{
return context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyFoundError(classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
}
context.Model = new MappingModel(
sourceGenerationOptions,
classSyntax.GetNamespace(),
classSyntax.Modifiers,
className,
sourceTypeSymbol.ContainingNamespace.ToString(),
sourceClassName,
sourceTypeSymbol.ToString(),
mappedProperties.ToImmutableArray());
return context;
}
private static INamedTypeSymbol? GetSourceTypeSymbol(SemanticModel semanticModel, ClassDeclarationSyntax classSyntax)
{
var sourceTypeExpressionSyntax = classSyntax
.GetAttribute(MapFromAttributeSource.AttributeName)
?.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
private static ImmutableArray<MappedProperty> GetMappedProperties(MappingContext context, ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
{
var mappedProperties = new List<MappedProperty>();
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
var classProperties = classSymbol.GetAllMembers().OfType<IPropertySymbol>().Where(p => !p.HasAttribute(context.IgnorePropertyAttributeTypeSymbol));
foreach (var property in classProperties)
{
var sourceProperty = sourceProperties.SingleOrDefault(p =>
p.Name == property.Name &&
(p.NullableAnnotation != NullableAnnotation.Annotated ||
p.NullableAnnotation == NullableAnnotation.Annotated &&
property.NullableAnnotation == NullableAnnotation.Annotated));
if (sourceProperty is null)
{
continue;
}
string? converterFullyQualifiedName = null;
if (!SymbolEqualityComparer.Default.Equals(property.Type, sourceProperty.Type))
{
var conversionClassification = context.Compilation.ClassifyCommonConversion(sourceProperty.Type, property.Type);
if (!conversionClassification.Exists || !conversionClassification.IsImplicit)
{
var mapPropertyAttribute = property.GetAttributes(context.MapPropertyAttributeTypeSymbol)
.FirstOrDefault(a => a.NamedArguments.Any(na => na.Key == MapPropertyAttributeSource.ConverterPropertyName));
var converterTypeSymbol = mapPropertyAttribute?.NamedArguments
.SingleOrDefault(na => na.Key == MapPropertyAttributeSource.ConverterPropertyName).Value.Value as INamedTypeSymbol;
if (mapPropertyAttribute is null || converterTypeSymbol is null)
{
context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property));
continue;
}
var baseInterface = converterTypeSymbol.AllInterfaces
.SingleOrDefault(i => SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, context.TypeConverterInterfaceTypeSymbol) &&
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(property.Type, i.TypeArguments[1]));
if (baseInterface is null)
{
context.ReportDiagnostic(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty));
continue;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
}
}
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName));
}
return mappedProperties.ToImmutableArray();
}
}
}

31
src/MapTo/Models.cs Normal file
View File

@ -0,0 +1,31 @@
using System.Collections.Immutable;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
namespace MapTo
{
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context) => new(
context.GetBuildGlobalOption<AccessModifier>(nameof(ConstructorAccessModifier)),
context.GetBuildGlobalOption<AccessModifier>(nameof(GeneratedMethodsAccessModifier)),
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true)
);
}
internal record MappedProperty(string Name, string? ConverterFullyQualifiedName);
internal record MappingModel (
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList ClassModifiers,
string ClassName,
string SourceNamespace,
string SourceClassName,
string SourceClassFullName,
ImmutableArray<MappedProperty> MappedProperties
);
}

View File

@ -1,90 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo.Models
{
internal record MapModel (
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList ClassModifiers,
string ClassName,
string SourceNamespace,
string SourceClassName,
string SourceClassFullName,
ImmutableArray<string> MappedProperties
)
{
internal static (MapModel? model, IEnumerable<Diagnostic> diagnostics) CreateModel(
SemanticModel classSemanticModel,
ClassDeclarationSyntax classSyntax,
SourceGenerationOptions sourceGenerationOptions)
{
var diagnostics = new List<Diagnostic>();
var root = classSyntax.GetCompilationUnit();
if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol))
{
diagnostics.Add(Diagnostics.SymbolNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
return (default, diagnostics);
}
var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel);
if (sourceTypeSymbol is null)
{
diagnostics.Add(Diagnostics.MapFromAttributeNotFoundError(classSyntax.GetLocation()));
return (default, diagnostics);
}
var className = classSyntax.GetClassName();
var sourceClassName = sourceTypeSymbol.Name;
var mappedProperties = GetMappedProperties(classSymbol, sourceTypeSymbol);
if (!mappedProperties.Any())
{
diagnostics.Add(Diagnostics.NoMatchingPropertyFoundError(classSyntax.GetLocation(), className, sourceClassName));
return (default, diagnostics);
}
var model = new MapModel(
sourceGenerationOptions,
root.GetNamespace(),
classSyntax.Modifiers,
className,
sourceTypeSymbol.ContainingNamespace.ToString(),
sourceClassName,
sourceTypeSymbol.ToString(),
mappedProperties);
return (model, diagnostics);
}
private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model)
{
var sourceTypeExpressionSyntax = classSyntax
.GetAttribute(MapFromAttributeSource.AttributeName)
?.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
private static ImmutableArray<string> GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
{
return sourceTypeSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Select(p => (p.Name, p.Type.ToString()))
.Intersect(classSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != IgnorePropertyAttributeSource.AttributeName))
.Select(p => (p.Name, p.Type.ToString())))
.Select(p => p.Name)
.ToImmutableArray();
}
}
}

View File

@ -1,17 +0,0 @@
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
namespace MapTo.Models
{
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context) => new(
context.GetBuildGlobalOption<AccessModifier>(nameof(ConstructorAccessModifier)),
context.GetBuildGlobalOption<AccessModifier>(nameof(GeneratedMethodsAccessModifier)),
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), defaultValue: true)
);
}
}

View File

@ -1,11 +1,11 @@
using MapTo.Models;
using static MapTo.Sources.Constants;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class IgnorePropertyAttributeSource
{
internal const string AttributeName = "IgnoreProperty";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeName + "Attribute";
internal static SourceCode Generate(SourceGenerationOptions options)
{

View File

@ -1,12 +1,11 @@
using MapTo.Extensions;
using MapTo.Models;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapClassSource
{
internal static SourceCode Generate(MapModel model)
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
@ -39,13 +38,13 @@ namespace MapTo.Sources
return new(builder.ToString(), $"{model.ClassName}.g.cs");
}
private static SourceBuilder WriteUsings(this SourceBuilder builder, MapModel model)
private static SourceBuilder WriteUsings(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine("using System;");
}
private static SourceBuilder GenerateConstructor(this SourceBuilder builder, MapModel model)
private static SourceBuilder GenerateConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
@ -67,14 +66,21 @@ namespace MapTo.Sources
foreach (var property in model.MappedProperties)
{
builder.WriteLine($"{property} = {sourceClassParameterName}.{property};");
if (property.ConverterFullyQualifiedName is not null)
{
builder.WriteLine($"{property.Name} = new {property.ConverterFullyQualifiedName}().Convert({sourceClassParameterName}.{property.Name});");
}
else
{
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};");
}
}
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MapModel model)
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
@ -86,7 +92,7 @@ namespace MapTo.Sources
.WriteClosingBracket();
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MapModel model, string sourceClassParameterName)
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
@ -102,7 +108,7 @@ namespace MapTo.Sources
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.ClassName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MapModel model)
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceClassName}To{model.ClassName}Extensions")
@ -111,7 +117,7 @@ namespace MapTo.Sources
.WriteClosingBracket();
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MapModel model)
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();

View File

@ -1,5 +1,4 @@
using MapTo.Models;
using static MapTo.Sources.Constants;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{

View File

@ -1,4 +1,4 @@
using MapTo.Models;
using System;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
@ -6,6 +6,8 @@ namespace MapTo.Sources
internal static class MapPropertyAttributeSource
{
internal const string AttributeName = "MapProperty";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeName + "Attribute";
internal const string ConverterPropertyName = "Converter";
internal static SourceCode Generate(SourceGenerationOptions options)
{
@ -34,27 +36,23 @@ namespace MapTo.Sources
builder
.WriteLine("/// <summary>")
.WriteLine("/// Initializes a new instance of <see cref=\"MapPropertyAttribute\"/>.")
.WriteLine("/// </summary>")
.WriteLine("/// <param name=\"converter\">The <see cref=\"ITypeConverter{TSource,TDestination}\" /> to convert the value of the annotated property.</param>");
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public {AttributeName}Attribute(Type converter = null)")
.WriteOpeningBracket()
.WriteLine("Converter = converter;")
.WriteClosingBracket()
.WriteLine($"public {AttributeName}Attribute() {{ }}")
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets the <see cref=\"ITypeConverter{TSource,TDestination}\" /> to convert the value of the annotated property.")
.WriteLine("/// Gets or sets the <see cref=\"ITypeConverter{TSource,TDestination}\" /> to be used to convert the source type.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("public Type Converter { get; }")
.WriteLine($"public Type {ConverterPropertyName} {{ get; set; }}")
.WriteClosingBracket()
.WriteClosingBracket();

View File

@ -1,4 +1,4 @@
using MapTo.Models;
using Microsoft.CodeAnalysis;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
@ -6,7 +6,8 @@ namespace MapTo.Sources
internal class TypeConverterSource
{
internal const string InterfaceName = "ITypeConverter";
internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
@ -46,5 +47,8 @@ namespace MapTo.Sources
return new(builder.ToString(), $"{InterfaceName}.g.cs");
}
internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) =>
$"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>";
}
}

View File

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Shouldly;
using Xunit;
namespace MapTo.Tests.Extensions
{
@ -16,5 +19,30 @@ namespace MapTo.Tests.Extensions
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldBe(expectedSource, customMessage);
}
internal static void ShouldBeSuccessful(this IEnumerable<Diagnostic> diagnostics, DiagnosticSeverity severity = DiagnosticSeverity.Warning)
{
var actual = diagnostics.Where(d => d.Severity >= severity).Select(c => $"{c.Severity}: {c.Location.GetLineSpan().StartLinePosition} - {c.GetMessage()}").ToArray();
Assert.False(actual.Any(), $"Failed: {Environment.NewLine}{string.Join(Environment.NewLine, actual.Select(c => $"- {c}"))}");
}
internal static void ShouldBeUnsuccessful(this ImmutableArray<Diagnostic> diagnostics, Diagnostic expectedError)
{
var actualDiagnostics = diagnostics.SingleOrDefault(d => d.Id == expectedError.Id);
var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics });
compilationDiagnostics.ShouldBeSuccessful();
Assert.NotNull(actualDiagnostics);
Assert.Equal(expectedError.Id, actualDiagnostics.Id);
Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics.Descriptor.Id);
Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics.Descriptor.Description);
Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics.Descriptor.Title);
if (expectedError.Location != Location.None)
{
Assert.Equal(expectedError.Location, actualDiagnostics.Location);
}
}
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Tests.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
@ -10,11 +11,6 @@ namespace MapTo.Tests.Infrastructure
{
internal static class CSharpGenerator
{
internal static void ShouldBeSuccessful(this ImmutableArray<Diagnostic> diagnostics)
{
Assert.False(diagnostics.Any(d => d.Severity >= DiagnosticSeverity.Warning), $"Failed: {Environment.NewLine}{string.Join($"{Environment.NewLine}- ", diagnostics.Select(c => c.GetMessage()))}");
}
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary<string, string> analyzerConfigOptions = null)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
@ -23,13 +19,12 @@ namespace MapTo.Tests.Infrastructure
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList();
var compilation = CSharpCompilation.Create("foo", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var compilation = CSharpCompilation.Create($"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
if (assertCompilation)
{
// NB: fail tests when the injected program isn't valid _before_ running generators
var compileDiagnostics = compilation.GetDiagnostics();
Assert.False(compileDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), $"Failed: {Environment.NewLine}{string.Join($"{Environment.NewLine}- ", compileDiagnostics.Select(c => c.GetMessage()))}");
compilation.GetDiagnostics().ShouldBeSuccessful();
}
var driver = CSharpGeneratorDriver.Create(
@ -38,6 +33,21 @@ namespace MapTo.Tests.Infrastructure
);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
var diagnostics = outputCompilation.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Warning)
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan().StartLinePosition} - {c.GetMessage()} [in \"{c.Location.SourceTree?.FilePath}\"]").ToArray();
if (diagnostics.Any())
{
Assert.False(diagnostics.Any(), $@"Failed:
{string.Join(Environment.NewLine, diagnostics.Select(c => $"- {c}"))}
Generated Sources:
{string.Join(Environment.NewLine, outputCompilation.SyntaxTrees.Reverse().Select(s => $"----------------------------------------{Environment.NewLine}File Path: \"{s.FilePath}\"{Environment.NewLine}{s}"))}
");
}
return (outputCompilation, generateDiagnostics);
}
}

View File

@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using MapTo.Extensions;
using MapTo.Models;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Shouldly;
using Xunit;
using static MapTo.Extensions.GeneratorExecutionContextExtensions;
@ -19,6 +22,7 @@ namespace MapTo.Tests
private const int Indent1 = 4;
private const int Indent2 = Indent1 * 2;
private const int Indent3 = Indent1 * 3;
private static readonly Location IgnoreLocation = Location.None;
private static readonly Dictionary<string, string> DefaultAnalyzerOptions = new()
{
@ -57,6 +61,11 @@ namespace MapTo
var hasDifferentSourceNamespace = options.SourceClassNamespace != ns;
var builder = new StringBuilder();
builder.AppendLine("//");
builder.AppendLine("// Test source code.");
builder.AppendLine("//");
builder.AppendLine();
if (options.UseMapToNamespace)
{
builder.AppendFormat("using {0};", Constants.RootNamespace).AppendLine();
@ -82,7 +91,7 @@ namespace MapTo
builder
.PadLeft(Indent1)
.AppendLine(options.UseMapToNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]")
.AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]": "[MapTo.MapFrom(typeof(Baz))]")
.PadLeft(Indent1).Append("public partial class Foo")
.AppendOpeningBracket(Indent1);
@ -162,7 +171,7 @@ namespace MapTo
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentType_Should_Ignore()
public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
@ -173,26 +182,14 @@ namespace MapTo
.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
{
if (baz == null) throw new ArgumentNullException(nameof(baz));
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation));
diagnostics.ShouldBeUnsuccessful(expectedError);
}
[Fact]
@ -224,7 +221,7 @@ namespace MapTo
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
@ -317,7 +314,6 @@ namespace Test
public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError()
{
// Arrange
var expectedDiagnostic = Diagnostics.NoMatchingPropertyFoundError(Location.None, "Foo", "Baz");
const string source = @"
using MapTo;
@ -331,9 +327,16 @@ namespace Test
";
// Act
var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
var fooType = compilation.GetTypeByMetadataName("Test.Foo");
fooType.ShouldNotBeNull();
var bazType = compilation.GetTypeByMetadataName("Test.Baz");
bazType.ShouldNotBeNull();
var expectedDiagnostic = DiagnosticProvider.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType);
var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id);
error.ShouldNotBeNull();
}
@ -527,12 +530,9 @@ namespace MapTo
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute
{{
public MapPropertyAttribute(Type converter = null)
{{
Converter = converter;
}}
public MapPropertyAttribute() {{ }}
public Type Converter {{ get; }}
public Type Converter {{ get; set; }}
}}
}}
".Trim();
@ -544,5 +544,133 @@ namespace MapTo
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
{
if (baz == null) throw new ArgumentNullException(nameof(baz));
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop4;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
}
[Fact]
public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.PadLeft(Indent2).AppendLine("[IgnoreProperty]")
.PadLeft(Indent2).AppendLine("public long IgnoreMe { get; set; }")
.PadLeft(Indent2).AppendLine("[MapProperty]")
.PadLeft(Indent2).AppendLine("[MapProperty(Converter = typeof(Prop4Converter))]")
.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, int>
{
public int Convert(string source) => int.Parse(source);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticProvider.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz"));
diagnostics.ShouldBeUnsuccessful(expectedError);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.PadLeft(Indent2).AppendLine("[MapProperty(Converter = typeof(Prop4Converter))]")
.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, long>
{
public long Convert(string source) => long.Parse(source);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4);";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax);
}
private static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
{
return syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Single(c => c.Identifier.ValueText == targetClass)
.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Single(p => p.Identifier.ValueText == targetPropertyName);
}
private static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo")
{
var syntaxTree = compilation.SyntaxTrees.First();
var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
return semanticModel.GetDeclaredSymbol(propSyntax);
}
}
}

View File

@ -9,5 +9,7 @@
public string LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
public long Key { get; }
}
}

View File

@ -1,5 +1,6 @@
using VM = TestConsoleApp.ViewModels;
using TestConsoleApp.ViewModels;
using VM = TestConsoleApp.ViewModels;
using Data = TestConsoleApp.Data.Models;
var userViewModel = VM.User.From(new Data.User());
var userViewModel2 = VM.UserViewModel.From(new Data.User());
var userViewModel2 = new Data.User().ToUserViewModel();

View File

@ -7,8 +7,6 @@ namespace TestConsoleApp.ViewModels
[MapFrom(typeof(Data.Models.User))]
public partial class User
{
public int Id { get; }
public string FirstName { get; }
public string LastName { get; }

View File

@ -10,13 +10,13 @@ namespace TestConsoleApp.ViewModels
[IgnoreProperty]
public string LastName { get; }
[MapProperty(converter: typeof(LastNameConverter))]
[MapProperty(Converter = typeof(LastNameConverter))]
public string Key { get; }
private class LastNameConverter : ITypeConverter<int, string>
private class LastNameConverter : ITypeConverter<long, string>
{
/// <inheritdoc />
public string Convert(int source) => $"{source} :: With Type Converter";
public string Convert(long source) => $"{source} :: With Type Converter";
}
}
}