From 6ad7ea83f9ea98be12677c792ce0e3c26c58b9f3 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Sat, 13 Feb 2021 11:27:32 +0000 Subject: [PATCH] Fix base constructor call issue. --- src/MapTo/Extensions/RoslynExtensions.cs | 6 +- src/MapTo/MapTo.csproj | 4 - src/MapTo/MappingContext.cs | 39 ++++++---- src/MapTo/Models.cs | 3 +- src/MapTo/Sources/MapClassSource.cs | 4 +- test/MapTo.Tests/Common.cs | 75 ++++++++++++++++++- .../Extensions/ShouldlyExtensions.cs | 11 ++- .../IgnorePropertyAttributeTests.cs | 2 +- test/MapTo.Tests/MapPropertyTests.cs | 2 +- test/MapTo.Tests/MapToTests.cs | 62 ++++++++++++++- test/MapTo.Tests/MapTypeConverterTests.cs | 6 +- test/TestConsoleApp/Data/Models/Employee.cs | 15 ++++ test/TestConsoleApp/Data/Models/Manager.cs | 13 ++++ test/TestConsoleApp/Program.cs | 39 ++++++++++ .../ViewModels/EmployeeViewModel.cs | 15 ++++ .../ViewModels/ManagerViewModel.cs | 15 ++++ 16 files changed, 277 insertions(+), 34 deletions(-) create mode 100644 test/TestConsoleApp/Data/Models/Employee.cs create mode 100644 test/TestConsoleApp/Data/Models/Manager.cs create mode 100644 test/TestConsoleApp/ViewModels/EmployeeViewModel.cs create mode 100644 test/TestConsoleApp/ViewModels/ManagerViewModel.cs diff --git a/src/MapTo/Extensions/RoslynExtensions.cs b/src/MapTo/Extensions/RoslynExtensions.cs index c8a3660..e74f288 100644 --- a/src/MapTo/Extensions/RoslynExtensions.cs +++ b/src/MapTo/Extensions/RoslynExtensions.cs @@ -20,9 +20,11 @@ namespace MapTo.Extensions } } - public static IEnumerable GetAllMembers(this ITypeSymbol type) + public static IEnumerable GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true) { - return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()); + return includeBaseTypeMembers + ? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers()) + : type.GetMembers(); } public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType().Single(); diff --git a/src/MapTo/MapTo.csproj b/src/MapTo/MapTo.csproj index b5cacc8..c229ebf 100644 --- a/src/MapTo/MapTo.csproj +++ b/src/MapTo/MapTo.csproj @@ -20,10 +20,6 @@ MapTo - - bin\Debug\MapTo.xml - - bin\Release\MapTo.xml diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index b2b1cf8..12759f1 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -57,8 +57,9 @@ namespace MapTo var className = _classSyntax.GetClassName(); var sourceClassName = sourceTypeSymbol.Name; + var isClassInheritFromMappedBaseClass = IsClassInheritFromMappedBaseClass(semanticModel); - var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol); + var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isClassInheritFromMappedBaseClass); if (!mappedProperties.Any()) { ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol)); @@ -73,14 +74,24 @@ namespace MapTo sourceTypeSymbol.ContainingNamespace.ToString(), sourceClassName, sourceTypeSymbol.ToString(), - mappedProperties.ToImmutableArray()); + mappedProperties.ToImmutableArray(), + isClassInheritFromMappedBaseClass); } - private ImmutableArray GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) + private bool IsClassInheritFromMappedBaseClass(SemanticModel semanticModel) + { + return _classSyntax.BaseList is not null && _classSyntax.BaseList.Types + .Select(t => semanticModel.GetTypeInfo(t.Type).Type) + .Any(t => t?.GetAttribute(_mapFromAttributeTypeSymbol) != null); + } + + private ImmutableArray GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol, bool isClassInheritFromMappedBaseClass) { var mappedProperties = new List(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - var classProperties = classSymbol.GetAllMembers().OfType().Where(p => !p.HasAttribute(_ignorePropertyAttributeTypeSymbol)); + var classProperties = classSymbol.GetAllMembers(!isClassInheritFromMappedBaseClass) + .OfType() + .Where(p => !p.HasAttribute(_ignorePropertyAttributeTypeSymbol)); foreach (var property in classProperties) { @@ -96,7 +107,7 @@ namespace MapTo if (!_compilation.HasCompatibleTypes(sourceProperty, property)) { - if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && + if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && !TryGetNestedObjectMappings(property, out mappedSourcePropertyType)) { continue; @@ -104,11 +115,11 @@ namespace MapTo } mappedProperties.Add(new MappedProperty( - property.Name, + property.Name, property.Type.Name, - converterFullyQualifiedName, - converterParameters.ToImmutableArray(), - sourceProperty.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + sourceProperty.Name, mappedSourcePropertyType)); } @@ -118,12 +129,12 @@ namespace MapTo private bool TryGetNestedObjectMappings(IPropertySymbol property, out string? mappedSourcePropertyType) { mappedSourcePropertyType = null; - + if (!Diagnostics.IsEmpty) { return false; } - + var nestedSourceMapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol); if (nestedSourceMapFromAttribute is null) { @@ -152,7 +163,7 @@ namespace MapTo { converterFullyQualifiedName = null; converterParameters = ImmutableArray.Empty; - + if (!Diagnostics.IsEmpty) { return false; @@ -204,7 +215,7 @@ namespace MapTo ? ImmutableArray.Empty : converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray(); } - + private void ReportDiagnostic(Diagnostic diagnostic) { Diagnostics = Diagnostics.Add(diagnostic); @@ -219,7 +230,7 @@ namespace MapTo { return null; } - + semanticModel ??= _compilation.GetSemanticModel(attributeSyntax.SyntaxTree); var sourceTypeExpressionSyntax = attributeSyntax .DescendantNodes() diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index 5594c00..d30f547 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -23,7 +23,8 @@ namespace MapTo string SourceNamespace, string SourceClassName, string SourceClassFullName, - ImmutableArray MappedProperties + ImmutableArray MappedProperties, + bool HasMappedBaseClass ); internal record SourceGenerationOptions( diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 5b01509..72dbb9e 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -61,8 +61,10 @@ namespace MapTo.Sources .WriteLine($"/// {sourceClassParameterName} is null"); } + var baseConstructor = model.HasMappedBaseClass ? $" : base({sourceClassParameterName})" : string.Empty; + builder - .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.ClassName}({model.SourceClassFullName} {sourceClassParameterName})") + .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.ClassName}({model.SourceClassFullName} {sourceClassParameterName}){baseConstructor}") .WriteOpeningBracket() .WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));") .WriteLine(); diff --git a/test/MapTo.Tests/Common.cs b/test/MapTo.Tests/Common.cs index 42a25c6..dba0663 100644 --- a/test/MapTo.Tests/Common.cs +++ b/test/MapTo.Tests/Common.cs @@ -34,6 +34,8 @@ namespace MapTo.Tests builder.WriteLine("//"); builder.WriteLine(); + options.Usings?.ForEach(s => builder.WriteLine($"using {s};")); + if (options.UseMapToNamespace) { builder.WriteLine($"using {Constants.RootNamespace};"); @@ -94,6 +96,76 @@ namespace MapTo.Tests return builder.ToString(); } + internal static string[] GetEmployeeManagerSourceText(Func employeeClassSource = null, Func managerClassSource = null, Func employeeViewModelSource = null, Func managerViewModelSource = null) + { + return new[] + { + employeeClassSource?.Invoke() ?? @" +using System; +using System.Collections.Generic; +using System.Text; + +namespace Test.Data.Models +{ + public class Employee + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public Manager Manager { get; set; } + } +}".Trim(), + managerClassSource?.Invoke() ?? @"using System; +using System.Collections.Generic; +using System.Text; + +namespace Test.Data.Models +{ + public class Manager: Employee + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +} +".Trim(), + employeeViewModelSource?.Invoke() ?? @" +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(Employee))] + public partial class EmployeeViewModel + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public ManagerViewModel Manager { get; set; } + } +} +".Trim(), + managerViewModelSource?.Invoke() ?? @" +using System; +using System.Collections.Generic; +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(Manager))] + public partial class ManagerViewModel : EmployeeViewModel + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +}".Trim() + }; + } + internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo") { return syntaxTree.GetRoot() @@ -120,6 +192,7 @@ namespace MapTo.Tests int ClassPropertiesCount = 3, int SourceClassPropertiesCount = 3, Action PropertyBuilder = null, - Action SourcePropertyBuilder = null); + Action SourcePropertyBuilder = null, + IEnumerable Usings = null); } } \ No newline at end of file diff --git a/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs b/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs index bf8114e..36b9580 100644 --- a/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs +++ b/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs @@ -21,10 +21,17 @@ namespace MapTo.Tests.Extensions syntax.ShouldBe(expectedSource, customMessage); } - internal static void ShouldBeSuccessful(this IEnumerable diagnostics, Compilation compilation = null) + internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string customMessage = null) + { + var syntax = syntaxTree.ToString(); + syntax.ShouldNotBeNullOrWhiteSpace(); + syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); + } + + internal static void ShouldBeSuccessful(this IEnumerable diagnostics, Compilation compilation = null, IEnumerable ignoreDiagnosticsIds = null) { var actual = diagnostics - .Where(d => !d.Id.StartsWith("MT") && (d.Severity == DiagnosticSeverity.Warning || d.Severity == DiagnosticSeverity.Error)) + .Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity == DiagnosticSeverity.Warning || d.Severity == DiagnosticSeverity.Error)) .Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray(); if (!actual.Any()) diff --git a/test/MapTo.Tests/IgnorePropertyAttributeTests.cs b/test/MapTo.Tests/IgnorePropertyAttributeTests.cs index cf83a18..95fa946 100644 --- a/test/MapTo.Tests/IgnorePropertyAttributeTests.cs +++ b/test/MapTo.Tests/IgnorePropertyAttributeTests.cs @@ -67,7 +67,7 @@ namespace MapTo // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); } } } \ No newline at end of file diff --git a/test/MapTo.Tests/MapPropertyTests.cs b/test/MapTo.Tests/MapPropertyTests.cs index 015181f..66f238c 100644 --- a/test/MapTo.Tests/MapPropertyTests.cs +++ b/test/MapTo.Tests/MapPropertyTests.cs @@ -76,7 +76,7 @@ namespace MapTo // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); } } } \ No newline at end of file diff --git a/test/MapTo.Tests/MapToTests.cs b/test/MapTo.Tests/MapToTests.cs index e75385c..5602ee3 100644 --- a/test/MapTo.Tests/MapToTests.cs +++ b/test/MapTo.Tests/MapToTests.cs @@ -286,7 +286,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); } [Fact] @@ -307,7 +307,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); } [Fact] @@ -331,7 +331,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); } [Fact] @@ -372,7 +372,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ToArray()[^2].ToString().ShouldContain(expectedResult); + compilation.SyntaxTrees.ToArray()[^2].ShouldContainPartialSource(expectedResult); } [Fact] @@ -429,5 +429,59 @@ namespace Test var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); diagnostics.ShouldBeUnsuccessful(expectedError); } + + [Fact] + public void When_SourceTypeEnumerableProperties_Should_CreateConstructorAndAssignSrcToDest() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + Usings: new[] { "System.Collections.Generic"}, + PropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"), + SourcePropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"))); + + const string 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; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); + } + + [Fact] + public void When_DestinationTypeHasBaseClass_Should_CallBaseConstructor() + { + // Arrange + var sources = GetEmployeeManagerSourceText(); + + const string expectedResult = @" +public ManagerViewModel(Test.Data.Models.Manager manager) : base(manager) +{ + if (manager == null) throw new ArgumentNullException(nameof(manager)); + + Level = manager.Level; +} +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } } } \ No newline at end of file diff --git a/test/MapTo.Tests/MapTypeConverterTests.cs b/test/MapTo.Tests/MapTypeConverterTests.cs index 53312e6..83bfc8e 100644 --- a/test/MapTo.Tests/MapTypeConverterTests.cs +++ b/test/MapTo.Tests/MapTypeConverterTests.cs @@ -168,7 +168,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); } [Fact] @@ -204,7 +204,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); } [Fact] @@ -235,7 +235,7 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); } [Fact] diff --git a/test/TestConsoleApp/Data/Models/Employee.cs b/test/TestConsoleApp/Data/Models/Employee.cs new file mode 100644 index 0000000..9ab3564 --- /dev/null +++ b/test/TestConsoleApp/Data/Models/Employee.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TestConsoleApp.Data.Models +{ + public class Employee + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public Manager Manager { get; set; } + } +} diff --git a/test/TestConsoleApp/Data/Models/Manager.cs b/test/TestConsoleApp/Data/Models/Manager.cs new file mode 100644 index 0000000..e41364d --- /dev/null +++ b/test/TestConsoleApp/Data/Models/Manager.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TestConsoleApp.Data.Models +{ + public class Manager: Employee + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +} diff --git a/test/TestConsoleApp/Program.cs b/test/TestConsoleApp/Program.cs index e5ac5f9..c73e36e 100644 --- a/test/TestConsoleApp/Program.cs +++ b/test/TestConsoleApp/Program.cs @@ -7,6 +7,45 @@ namespace TestConsoleApp internal class Program { private static void Main(string[] args) + { + //UserTest(); + var manager1 = new Manager + { + Id = 1, + EmployeeCode = "M001", + Level = 100 + }; + + var manager2 = new Manager + { + Id = 2, + EmployeeCode = "M002", + Level = 100, + Manager = manager1 + }; + + var employee1 = new Employee + { + Id = 101, + EmployeeCode = "E101", + Manager = manager1 + }; + + var employee2 = new Employee + { + Id = 102, + EmployeeCode = "E102", + Manager = manager2 + }; + + manager1.Employees = new[] { employee1, manager2 }; + manager2.Employees = new[] { employee2 }; + + var manager1ViewModel = manager1.ToManagerViewModel(); + int a = 0; + } + + private static void UserTest() { var user = new User { diff --git a/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs b/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs new file mode 100644 index 0000000..7baa3ea --- /dev/null +++ b/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs @@ -0,0 +1,15 @@ +using MapTo; +using TestConsoleApp.Data.Models; + +namespace TestConsoleApp.ViewModels +{ + [MapFrom(typeof(Employee))] + public partial class EmployeeViewModel + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public ManagerViewModel Manager { get; set; } + } +} diff --git a/test/TestConsoleApp/ViewModels/ManagerViewModel.cs b/test/TestConsoleApp/ViewModels/ManagerViewModel.cs new file mode 100644 index 0000000..e2c5518 --- /dev/null +++ b/test/TestConsoleApp/ViewModels/ManagerViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using MapTo; +using TestConsoleApp.Data.Models; + +namespace TestConsoleApp.ViewModels +{ + [MapFrom(typeof(Manager))] + public partial class ManagerViewModel : EmployeeViewModel + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +} \ No newline at end of file