diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index 5a753e1..b2b1cf8 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -16,7 +16,6 @@ namespace MapTo private readonly INamedTypeSymbol _mapFromAttributeTypeSymbol; private readonly INamedTypeSymbol _mapPropertyAttributeTypeSymbol; private readonly INamedTypeSymbol _mapTypeConverterAttributeTypeSymbol; - private readonly SemanticModel _semanticModel; private readonly SourceGenerationOptions _sourceGenerationOptions; private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol; @@ -26,7 +25,6 @@ namespace MapTo _sourceGenerationOptions = sourceGenerationOptions; _classSyntax = classSyntax; _compilation = compilation; - _semanticModel = _compilation.GetSemanticModel(_classSyntax.SyntaxTree); _ignorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName); _mapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName); @@ -43,13 +41,14 @@ namespace MapTo private void Initialize() { - if (!(_semanticModel.GetDeclaredSymbol(_classSyntax) is INamedTypeSymbol classTypeSymbol)) + var semanticModel = _compilation.GetSemanticModel(_classSyntax.SyntaxTree); + if (!(semanticModel.GetDeclaredSymbol(_classSyntax) is INamedTypeSymbol classTypeSymbol)) { ReportDiagnostic(DiagnosticProvider.TypeNotFoundError(_classSyntax.GetLocation(), _classSyntax.Identifier.ValueText)); return; } - var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax); + var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax, semanticModel); if (sourceTypeSymbol is null) { ReportDiagnostic(DiagnosticProvider.MapFromAttributeNotFoundError(_classSyntax.GetLocation())); @@ -132,8 +131,7 @@ namespace MapTo return false; } - var nestedAttributeSyntax = nestedSourceMapFromAttribute.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax; - if (nestedAttributeSyntax is null) + if (!(nestedSourceMapFromAttribute.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax nestedAttributeSyntax)) { ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); return false; @@ -212,17 +210,23 @@ namespace MapTo Diagnostics = Diagnostics.Add(diagnostic); } - private INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classDeclarationSyntax) => - GetSourceTypeSymbol(classDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName)); + private INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classDeclarationSyntax, SemanticModel? semanticModel = null) => + GetSourceTypeSymbol(classDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel); - private INamedTypeSymbol? GetSourceTypeSymbol(AttributeSyntax? attributeSyntax) + private INamedTypeSymbol? GetSourceTypeSymbol(AttributeSyntax? attributeSyntax, SemanticModel? semanticModel = null) { + if (attributeSyntax is null) + { + return null; + } + + semanticModel ??= _compilation.GetSemanticModel(attributeSyntax.SyntaxTree); var sourceTypeExpressionSyntax = attributeSyntax - ?.DescendantNodes() + .DescendantNodes() .OfType() .SingleOrDefault(); - return sourceTypeExpressionSyntax is not null ? _semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; + return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; } } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 32bbcc6..f7e2f5b 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -80,7 +80,7 @@ namespace MapTo.Sources ? "null" : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; - builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.Name}, {parameters});"); + builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});"); } } diff --git a/test/MapTo.Tests/MappedClassesTests.cs b/test/MapTo.Tests/MappedClassesTests.cs new file mode 100644 index 0000000..8e215b2 --- /dev/null +++ b/test/MapTo.Tests/MappedClassesTests.cs @@ -0,0 +1,106 @@ +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Xunit; +using Xunit.Abstractions; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MappedClassesTests + { + private readonly ITestOutputHelper _output; + + public MappedClassesTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void VerifyMappedClassSource() + { + // Arrange + var sources = new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass }; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + _output.WriteLine(compilation.PrintSyntaxTree()); + } + + private static string NestedSourceClass => @" +namespace Test.Data.Models +{ + public class Profile + { + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string FullName => $""{FirstName} {LastName}""; + } +} +".Trim(); + + private static string MainSourceClass => @" +using System; + +namespace Test.Data.Models +{ + public class User + { + public int Id { get; set; } + + public DateTimeOffset RegisteredAt { get; set; } + + public Profile Profile { get; set; } + } +} +".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 MainDestinationClass => @" +using System; +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(User))] + public partial class UserViewModel + { + [MapProperty(SourcePropertyName = nameof(User.Id))] + [MapTypeConverter(typeof(IdConverter))] + public string Key { get; } + + public DateTimeOffset RegisteredAt { get; set; } + + // [IgnoreProperty] + public ProfileViewModel Profile { get; set; } + + private class IdConverter : ITypeConverter + { + public string Convert(int source, object[] converterParameters) => $""{source:X}""; + } + } +} +".Trim(); + } +} \ No newline at end of file