From c2088c540ef994fbfa7629348110c18646b81a03 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Thu, 24 Dec 2020 10:05:29 +0000 Subject: [PATCH] Add attribute to exclude a property from mapping. --- src/MapTo/MapToGenerator.cs | 17 +- src/MapTo/SourceBuilder.cs | 52 +++-- .../CompilerServices/IsExternalInit.cs | 16 ++ test/MapTo.Tests/Tests.cs | 196 ++++++++++++++---- .../ViewModels/UserViewModel.cs | 1 + 5 files changed, 210 insertions(+), 72 deletions(-) create mode 100644 test/MapTo.Tests/CompilerServices/IsExternalInit.cs diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index 56a2358..1d9c71a 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -20,8 +20,9 @@ namespace MapTo /// public void Execute(GeneratorExecutionContext context) { - AddMapFromAttribute(context); - + AddAttribute(context, SourceBuilder.GenerateMapFromAttribute()); + AddAttribute(context, SourceBuilder.GenerateIgnorePropertyAttribute()); + if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any()) { AddGeneratedMappingsClasses(context, receiver.CandidateClasses); @@ -44,11 +45,8 @@ namespace MapTo } } - private static void AddMapFromAttribute(GeneratorExecutionContext context) - { - var (source, hintName) = SourceBuilder.GenerateMapFromAttribute(); - context.AddSource(hintName, source); - } + private static void AddAttribute(GeneratorExecutionContext context, (string source, string hintName) attribute) + => context.AddSource(attribute.hintName, attribute.source); private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model) { @@ -104,7 +102,10 @@ namespace MapTo return sourceTypeSymbol .GetAllMembersOfType() .Select(p => p.Name) - .Intersect(classSymbol.GetAllMembersOfType().Select(p => p.Name)) + .Intersect(classSymbol + .GetAllMembersOfType() + .Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName)) + .Select(p => p.Name)) .ToImmutableArray(); } } diff --git a/src/MapTo/SourceBuilder.cs b/src/MapTo/SourceBuilder.cs index fcefe5a..8c3f4df 100644 --- a/src/MapTo/SourceBuilder.cs +++ b/src/MapTo/SourceBuilder.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Text; using MapTo.Extensions; using MapTo.Models; -using Microsoft.CodeAnalysis; namespace MapTo { @@ -12,33 +9,49 @@ namespace MapTo { internal const string NamespaceName = "MapTo"; internal const string MapFromAttributeName = "MapFrom"; - private const int Indent1 = 4; //" "; - private const int Indent2 = Indent1 * 2; // " "; - private const int Indent3 = Indent1 * 3; // " "; + internal const string IgnorePropertyAttributeName = "IgnoreProperty"; + internal const string GeneratedFilesHeader = "// "; + + private const int Indent1 = 4; + private const int Indent2 = Indent1 * 2; + private const int Indent3 = Indent1 * 3; internal static (string source, string hintName) GenerateMapFromAttribute() { - const string source = @" + var source = $@"{GeneratedFilesHeader} using System; namespace MapTo -{ +{{ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class MapFromAttribute : Attribute - { - public MapFromAttribute(Type sourceType) - { + public sealed class {MapFromAttributeName}Attribute : Attribute + {{ + public {MapFromAttributeName}Attribute(Type sourceType) + {{ SourceType = sourceType; - } + }} - public Type SourceType { get; } - } -} -"; + public Type SourceType {{ get; }} + }} +}}"; return (source, $"{MapFromAttributeName}Attribute.g.cs"); } + internal static (string source, string hintName) GenerateIgnorePropertyAttribute() + { + var source = $@"{GeneratedFilesHeader} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class {IgnorePropertyAttributeName}Attribute : Attribute {{ }} +}}"; + + return (source, $"{IgnorePropertyAttributeName}Attribute.g.cs"); + } + internal static (string source, string hintName) GenerateSource(MapModel model) { var builder = new StringBuilder(); @@ -141,7 +154,6 @@ namespace MapTo } private static StringBuilder AppendFileHeader(this StringBuilder builder) => - builder - .AppendLine("// "); + builder.AppendLine(GeneratedFilesHeader); } } \ No newline at end of file diff --git a/test/MapTo.Tests/CompilerServices/IsExternalInit.cs b/test/MapTo.Tests/CompilerServices/IsExternalInit.cs new file mode 100644 index 0000000..e750e2f --- /dev/null +++ b/test/MapTo.Tests/CompilerServices/IsExternalInit.cs @@ -0,0 +1,16 @@ +// ReSharper disable UnusedType.Global +// ReSharper disable CheckNamespace +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index aa5423e..0f4499a 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -1,5 +1,7 @@ +using System; using System.Linq; using System.Text; +using MapTo.Extensions; using MapToTests; using Microsoft.CodeAnalysis; using Shouldly; @@ -10,6 +12,10 @@ namespace MapTo.Tests { public class Tests { + private const int Indent1 = 4; + private const int Indent2 = Indent1 * 2; + private const int Indent3 = Indent1 * 3; + public Tests(ITestOutputHelper output) { _output = output; @@ -17,54 +23,100 @@ namespace MapTo.Tests private readonly ITestOutputHelper _output; - private const string ExpectedAttribute = @" + private static readonly string ExpectedAttribute = $@"{SourceBuilder.GeneratedFilesHeader} using System; namespace MapTo -{ +{{ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class MapFromAttribute : Attribute - { + {{ public MapFromAttribute(Type sourceType) - { + {{ SourceType = sourceType; - } + }} - public Type SourceType { get; } - } -} -"; + public Type SourceType {{ get; }} + }} +}}"; - private static string GetSourceText(bool includeAttributeNamespace = false, string sourceClassNamespace = "Test.Models") + private record SourceGeneratorOptions( + bool UseMapToNamespace = false, + string SourceClassNamespace = "Test.Models", + int ClassPropertiesCount = 3, + int SourceClassPropertiesCount = 3, + Action PropertyBuilder = null, + Action SourcePropertyBuilder = null); + + private static string GetSourceText(SourceGeneratorOptions options = null) { + const string ns = "Test"; + options ??= new SourceGeneratorOptions(); + var hasDifferentSourceNamespace = options.SourceClassNamespace != ns; var builder = new StringBuilder(); - builder.AppendLine($@" -{(includeAttributeNamespace ? string.Empty : "using MapTo;")} -namespace Test -{{ - {(sourceClassNamespace != "Test" && !includeAttributeNamespace ? $"using {sourceClassNamespace};" : string.Empty)} + + if (options.UseMapToNamespace) + { + builder.AppendFormat("using {0};", SourceBuilder.NamespaceName).AppendLine(); + } - {(includeAttributeNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]")} - public partial class Foo - {{ - public int Prop1 {{ get; set; }} - public int Prop2 {{ get; }} - public int Prop3 {{ get; }} - }} -}} -"); + builder + .AppendFormat("using {0};", options.SourceClassNamespace) + .AppendLine() + .AppendLine(); - builder.AppendLine($@" -namespace {sourceClassNamespace} -{{ - public class Baz - {{ - public int Prop1 {{ get; set; }} - public int Prop2 {{ get; }} - public int Prop3 {{ get; set; }} - }} -}} -"); + builder + .AppendFormat("namespace {0}", ns) + .AppendOpeningBracket(); + + if (hasDifferentSourceNamespace && options.UseMapToNamespace) + { + builder + .PadLeft(Indent1) + .AppendFormat("using {0};", options.SourceClassNamespace) + .AppendLine() + .AppendLine(); + } + + builder + .PadLeft(Indent1) + .AppendLine(options.UseMapToNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]") + .PadLeft(Indent1).Append("public partial class Foo") + .AppendOpeningBracket(Indent1); + + for (var i = 1; i <= options.ClassPropertiesCount; i++) + { + builder + .PadLeft(Indent2) + .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); + } + + options.PropertyBuilder?.Invoke(builder); + + builder + .AppendClosingBracket(Indent1, padNewLine: false) + .AppendClosingBracket() + .AppendLine() + .AppendLine(); + + builder + .AppendFormat("namespace {0}", options.SourceClassNamespace) + .AppendOpeningBracket() + .PadLeft(Indent1).Append("public class Baz") + .AppendOpeningBracket(Indent1); + + for (var i = 1; i <= options.SourceClassPropertiesCount; i++) + { + builder + .PadLeft(Indent2) + .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); + } + + options.SourcePropertyBuilder?.Invoke(builder); + + builder + .AppendClosingBracket(Indent1, padNewLine: false) + .AppendClosingBracket(); return builder.ToString(); } @@ -126,7 +178,6 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); } @@ -190,7 +241,6 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); } @@ -205,15 +255,18 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContain(s => s.ToString() == ExpectedAttribute); - compilation.SyntaxTrees.Select(s => s.ToString()).Where(s => s != string.Empty && s != ExpectedAttribute).ShouldBeEmpty(); + compilation.SyntaxTrees + .Select(s => s.ToString()) + .Where(s => !string.IsNullOrWhiteSpace(s.ToString())) + .All(s => s.Contains(": Attribute")) + .ShouldBeTrue(); } [Fact] public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() { // Arrange - var source = GetSourceText(sourceClassNamespace: "Bazaar"); + var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar")); const string expectedResult = @" // @@ -227,7 +280,6 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); } @@ -255,7 +307,6 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); } @@ -277,7 +328,6 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); } @@ -302,8 +352,66 @@ namespace Test // Assert diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); } + + [Fact] + public void VerifyIgnorePropertyAttribute() + { + // Arrange + const string source = ""; + var expectedAttribute = $@" +{SourceBuilder.GeneratedFilesHeader} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class IgnorePropertyAttribute : Attribute {{ }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContain(c => c.ToString() == expectedAttribute); + } + + [Fact] + public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + UseMapToNamespace: true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[IgnoreProperty]") + .PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + public 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); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + } } } \ No newline at end of file diff --git a/test/TestConsoleApp/ViewModels/UserViewModel.cs b/test/TestConsoleApp/ViewModels/UserViewModel.cs index 0b56b07..404c759 100644 --- a/test/TestConsoleApp/ViewModels/UserViewModel.cs +++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs @@ -7,6 +7,7 @@ namespace TestConsoleApp.ViewModels { public string FirstName { get; } + // [IgnoreProerty] public string LastName { get; } } } \ No newline at end of file