From dd9254ab7468332183f8cf0e817a44bc996a5225 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Thu, 21 Jan 2021 08:19:17 +0000 Subject: [PATCH] Initial implementation of TypeConverter. --- src/MapTo/{Models => }/AccessModifier.cs | 2 +- src/MapTo/DiagnosticProvider.cs | 37 ++++ src/MapTo/Diagnostics.cs | 28 --- src/MapTo/Extensions/EnumerableExtensions.cs | 5 +- .../GeneratorExecutionContextExtensions.cs | 19 +- src/MapTo/Extensions/RoslynExtensions.cs | 14 +- src/MapTo/MapToGenerator.cs | 29 ++- src/MapTo/MappingContext.cs | 156 +++++++++++++++ src/MapTo/Models.cs | 31 +++ src/MapTo/Models/MapModel.cs | 90 --------- src/MapTo/Models/SourceGenerationOptions.cs | 17 -- .../Sources/IgnorePropertyAttributeSource.cs | 4 +- src/MapTo/Sources/MapClassSource.cs | 24 ++- src/MapTo/Sources/MapFromAttributeSource.cs | 3 +- .../Sources/MapPropertyAttributeSource.cs | 16 +- src/MapTo/Sources/TypeConverterSource.cs | 8 +- .../Extensions/ShouldlyExtensions.cs | 30 ++- .../Infrastructure/CSharpGenerator.cs | 26 ++- test/MapTo.Tests/Tests.cs | 184 +++++++++++++++--- test/TestConsoleApp/Data/Models/User.cs | 2 + test/TestConsoleApp/Program.cs | 5 +- test/TestConsoleApp/ViewModels/User.cs | 2 - .../ViewModels/UserViewModel.cs | 6 +- 23 files changed, 510 insertions(+), 228 deletions(-) rename src/MapTo/{Models => }/AccessModifier.cs (78%) create mode 100644 src/MapTo/DiagnosticProvider.cs delete mode 100644 src/MapTo/Diagnostics.cs create mode 100644 src/MapTo/MappingContext.cs create mode 100644 src/MapTo/Models.cs delete mode 100644 src/MapTo/Models/MapModel.cs delete mode 100644 src/MapTo/Models/SourceGenerationOptions.cs diff --git a/src/MapTo/Models/AccessModifier.cs b/src/MapTo/AccessModifier.cs similarity index 78% rename from src/MapTo/Models/AccessModifier.cs rename to src/MapTo/AccessModifier.cs index 8788916..64f557b 100644 --- a/src/MapTo/Models/AccessModifier.cs +++ b/src/MapTo/AccessModifier.cs @@ -1,4 +1,4 @@ -namespace MapTo.Models +namespace MapTo { internal enum AccessModifier { diff --git a/src/MapTo/DiagnosticProvider.cs b/src/MapTo/DiagnosticProvider.cs new file mode 100644 index 0000000..fafc7a2 --- /dev/null +++ b/src/MapTo/DiagnosticProvider.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/MapTo/Diagnostics.cs b/src/MapTo/Diagnostics.cs deleted file mode 100644 index 1f602c5..0000000 --- a/src/MapTo/Diagnostics.cs +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/MapTo/Extensions/EnumerableExtensions.cs b/src/MapTo/Extensions/EnumerableExtensions.cs index 21d0887..7823e58 100644 --- a/src/MapTo/Extensions/EnumerableExtensions.cs +++ b/src/MapTo/Extensions/EnumerableExtensions.cs @@ -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(this IEnumerable enumerable) => !enumerable.Any(); } } \ No newline at end of file diff --git a/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs b/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs index e03c45f..7f3e4cb 100644 --- a/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs +++ b/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs @@ -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(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T: notnull + internal static T GetBuildGlobalOption(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)); + } } } \ No newline at end of file diff --git a/src/MapTo/Extensions/RoslynExtensions.cs b/src/MapTo/Extensions/RoslynExtensions.cs index d73b318..6702667 100644 --- a/src/MapTo/Extensions/RoslynExtensions.cs +++ b/src/MapTo/Extensions/RoslynExtensions.cs @@ -23,8 +23,6 @@ namespace MapTo.Extensions return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()); } - public static IEnumerable GetAllMembersOfType(this ITypeSymbol type) where T : ISymbol => type.GetAllMembers().OfType(); - public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType().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 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() .FirstOrDefault() ?.Name .ToString(); + } } } \ No newline at end of file diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index e0a18ed..ac4c187 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -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 candidateClasses, SourceGenerationOptions options) + private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable 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); } } } diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs new file mode 100644 index 0000000..53ff45c --- /dev/null +++ b/src/MapTo/MappingContext.cs @@ -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.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 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() + .SingleOrDefault(); + + return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; + } + + private static ImmutableArray GetMappedProperties(MappingContext context, ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) + { + var mappedProperties = new List(); + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + var classProperties = classSymbol.GetAllMembers().OfType().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(); + } + } +} \ No newline at end of file diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs new file mode 100644 index 0000000..8c2efdf --- /dev/null +++ b/src/MapTo/Models.cs @@ -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(nameof(ConstructorAccessModifier)), + context.GetBuildGlobalOption(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 MappedProperties + ); +} \ No newline at end of file diff --git a/src/MapTo/Models/MapModel.cs b/src/MapTo/Models/MapModel.cs deleted file mode 100644 index 3520518..0000000 --- a/src/MapTo/Models/MapModel.cs +++ /dev/null @@ -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 MappedProperties - ) - { - internal static (MapModel? model, IEnumerable diagnostics) CreateModel( - SemanticModel classSemanticModel, - ClassDeclarationSyntax classSyntax, - SourceGenerationOptions sourceGenerationOptions) - { - var diagnostics = new List(); - 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() - .SingleOrDefault(); - - return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; - } - - private static ImmutableArray GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) - { - return sourceTypeSymbol - .GetAllMembersOfType() - .Select(p => (p.Name, p.Type.ToString())) - .Intersect(classSymbol - .GetAllMembersOfType() - .Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != IgnorePropertyAttributeSource.AttributeName)) - .Select(p => (p.Name, p.Type.ToString()))) - .Select(p => p.Name) - .ToImmutableArray(); - } - } -} \ No newline at end of file diff --git a/src/MapTo/Models/SourceGenerationOptions.cs b/src/MapTo/Models/SourceGenerationOptions.cs deleted file mode 100644 index d65c6a2..0000000 --- a/src/MapTo/Models/SourceGenerationOptions.cs +++ /dev/null @@ -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(nameof(ConstructorAccessModifier)), - context.GetBuildGlobalOption(nameof(GeneratedMethodsAccessModifier)), - context.GetBuildGlobalOption(nameof(GenerateXmlDocument), defaultValue: true) - ); - } -} \ No newline at end of file diff --git a/src/MapTo/Sources/IgnorePropertyAttributeSource.cs b/src/MapTo/Sources/IgnorePropertyAttributeSource.cs index bb0a0bf..18eb5ed 100644 --- a/src/MapTo/Sources/IgnorePropertyAttributeSource.cs +++ b/src/MapTo/Sources/IgnorePropertyAttributeSource.cs @@ -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) { diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 2952117..ef7ad34 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -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($"/// A new instance of -or- null if is null."); } - 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(); diff --git a/src/MapTo/Sources/MapFromAttributeSource.cs b/src/MapTo/Sources/MapFromAttributeSource.cs index 8b404fd..c483391 100644 --- a/src/MapTo/Sources/MapFromAttributeSource.cs +++ b/src/MapTo/Sources/MapFromAttributeSource.cs @@ -1,5 +1,4 @@ -using MapTo.Models; -using static MapTo.Sources.Constants; +using static MapTo.Sources.Constants; namespace MapTo.Sources { diff --git a/src/MapTo/Sources/MapPropertyAttributeSource.cs b/src/MapTo/Sources/MapPropertyAttributeSource.cs index 7e389ef..5116344 100644 --- a/src/MapTo/Sources/MapPropertyAttributeSource.cs +++ b/src/MapTo/Sources/MapPropertyAttributeSource.cs @@ -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("/// ") .WriteLine("/// Initializes a new instance of .") - .WriteLine("/// ") - .WriteLine("/// The to convert the value of the annotated property."); + .WriteLine("/// "); } builder - .WriteLine($"public {AttributeName}Attribute(Type converter = null)") - .WriteOpeningBracket() - .WriteLine("Converter = converter;") - .WriteClosingBracket() + .WriteLine($"public {AttributeName}Attribute() {{ }}") .WriteLine(); if (options.GenerateXmlDocument) { builder .WriteLine("/// ") - .WriteLine("/// Gets the to convert the value of the annotated property.") + .WriteLine("/// Gets or sets the to be used to convert the source type.") .WriteLine("/// "); } builder - .WriteLine("public Type Converter { get; }") + .WriteLine($"public Type {ConverterPropertyName} {{ get; set; }}") .WriteClosingBracket() .WriteClosingBracket(); diff --git a/src/MapTo/Sources/TypeConverterSource.cs b/src/MapTo/Sources/TypeConverterSource.cs index ee751de..ad86bf6 100644 --- a/src/MapTo/Sources/TypeConverterSource.cs +++ b/src/MapTo/Sources/TypeConverterSource.cs @@ -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()}>"; } } \ No newline at end of file diff --git a/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs b/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs index c8f669f..03df91d 100644 --- a/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs +++ b/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs @@ -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 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 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); + } + } } } \ No newline at end of file diff --git a/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs b/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs index 12bd416..8b390e2 100644 --- a/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs +++ b/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs @@ -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 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 diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary 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); } } diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index 2b9cce9..e742ecf 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -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 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 + { + 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 + { + 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() + .Single(c => c.Identifier.ValueText == targetClass) + .DescendantNodes() + .OfType() + .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); + } } } \ No newline at end of file diff --git a/test/TestConsoleApp/Data/Models/User.cs b/test/TestConsoleApp/Data/Models/User.cs index f9cdc05..541c315 100644 --- a/test/TestConsoleApp/Data/Models/User.cs +++ b/test/TestConsoleApp/Data/Models/User.cs @@ -9,5 +9,7 @@ public string LastName { get; set; } public string FullName => $"{FirstName} {LastName}"; + + public long Key { get; } } } \ No newline at end of file diff --git a/test/TestConsoleApp/Program.cs b/test/TestConsoleApp/Program.cs index 2126761..be96136 100644 --- a/test/TestConsoleApp/Program.cs +++ b/test/TestConsoleApp/Program.cs @@ -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()); \ No newline at end of file +var userViewModel2 = new Data.User().ToUserViewModel(); \ No newline at end of file diff --git a/test/TestConsoleApp/ViewModels/User.cs b/test/TestConsoleApp/ViewModels/User.cs index 27c4c80..bf5410c 100644 --- a/test/TestConsoleApp/ViewModels/User.cs +++ b/test/TestConsoleApp/ViewModels/User.cs @@ -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; } diff --git a/test/TestConsoleApp/ViewModels/UserViewModel.cs b/test/TestConsoleApp/ViewModels/UserViewModel.cs index 68d36ac..6da6987 100644 --- a/test/TestConsoleApp/ViewModels/UserViewModel.cs +++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs @@ -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 + private class LastNameConverter : ITypeConverter { /// - public string Convert(int source) => $"{source} :: With Type Converter"; + public string Convert(long source) => $"{source} :: With Type Converter"; } } } \ No newline at end of file