Add attribute to exclude a property from mapping.

This commit is contained in:
Mohammadreza Taikandi 2020-12-24 10:05:29 +00:00
parent f559661357
commit c2088c540e
5 changed files with 210 additions and 72 deletions

View File

@ -20,8 +20,9 @@ namespace MapTo
/// <inheritdoc /> /// <inheritdoc />
public void Execute(GeneratorExecutionContext context) public void Execute(GeneratorExecutionContext context)
{ {
AddMapFromAttribute(context); AddAttribute(context, SourceBuilder.GenerateMapFromAttribute());
AddAttribute(context, SourceBuilder.GenerateIgnorePropertyAttribute());
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any()) if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
{ {
AddGeneratedMappingsClasses(context, receiver.CandidateClasses); AddGeneratedMappingsClasses(context, receiver.CandidateClasses);
@ -44,11 +45,8 @@ namespace MapTo
} }
} }
private static void AddMapFromAttribute(GeneratorExecutionContext context) private static void AddAttribute(GeneratorExecutionContext context, (string source, string hintName) attribute)
{ => context.AddSource(attribute.hintName, attribute.source);
var (source, hintName) = SourceBuilder.GenerateMapFromAttribute();
context.AddSource(hintName, source);
}
private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model) private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model)
{ {
@ -104,7 +102,10 @@ namespace MapTo
return sourceTypeSymbol return sourceTypeSymbol
.GetAllMembersOfType<IPropertySymbol>() .GetAllMembersOfType<IPropertySymbol>()
.Select(p => p.Name) .Select(p => p.Name)
.Intersect(classSymbol.GetAllMembersOfType<IPropertySymbol>().Select(p => p.Name)) .Intersect(classSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName))
.Select(p => p.Name))
.ToImmutableArray(); .ToImmutableArray();
} }
} }

View File

@ -1,10 +1,7 @@
using System; using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using MapTo.Extensions; using MapTo.Extensions;
using MapTo.Models; using MapTo.Models;
using Microsoft.CodeAnalysis;
namespace MapTo namespace MapTo
{ {
@ -12,33 +9,49 @@ namespace MapTo
{ {
internal const string NamespaceName = "MapTo"; internal const string NamespaceName = "MapTo";
internal const string MapFromAttributeName = "MapFrom"; internal const string MapFromAttributeName = "MapFrom";
private const int Indent1 = 4; //" "; internal const string IgnorePropertyAttributeName = "IgnoreProperty";
private const int Indent2 = Indent1 * 2; // " "; internal const string GeneratedFilesHeader = "// <auto-generated />";
private const int Indent3 = Indent1 * 3; // " ";
private const int Indent1 = 4;
private const int Indent2 = Indent1 * 2;
private const int Indent3 = Indent1 * 3;
internal static (string source, string hintName) GenerateMapFromAttribute() internal static (string source, string hintName) GenerateMapFromAttribute()
{ {
const string source = @" var source = $@"{GeneratedFilesHeader}
using System; using System;
namespace MapTo namespace MapTo
{ {{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class MapFromAttribute : Attribute public sealed class {MapFromAttributeName}Attribute : Attribute
{ {{
public MapFromAttribute(Type sourceType) public {MapFromAttributeName}Attribute(Type sourceType)
{ {{
SourceType = sourceType; SourceType = sourceType;
} }}
public Type SourceType { get; } public Type SourceType {{ get; }}
} }}
} }}";
";
return (source, $"{MapFromAttributeName}Attribute.g.cs"); 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) internal static (string source, string hintName) GenerateSource(MapModel model)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
@ -141,7 +154,6 @@ namespace MapTo
} }
private static StringBuilder AppendFileHeader(this StringBuilder builder) => private static StringBuilder AppendFileHeader(this StringBuilder builder) =>
builder builder.AppendLine(GeneratedFilesHeader);
.AppendLine("// <auto-generated />");
} }
} }

View File

@ -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
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
}

View File

@ -1,5 +1,7 @@
using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using MapTo.Extensions;
using MapToTests; using MapToTests;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Shouldly; using Shouldly;
@ -10,6 +12,10 @@ namespace MapTo.Tests
{ {
public class 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) public Tests(ITestOutputHelper output)
{ {
_output = output; _output = output;
@ -17,54 +23,100 @@ namespace MapTo.Tests
private readonly ITestOutputHelper _output; private readonly ITestOutputHelper _output;
private const string ExpectedAttribute = @" private static readonly string ExpectedAttribute = $@"{SourceBuilder.GeneratedFilesHeader}
using System; using System;
namespace MapTo namespace MapTo
{ {{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class MapFromAttribute : Attribute public sealed class MapFromAttribute : Attribute
{ {{
public MapFromAttribute(Type sourceType) public MapFromAttribute(Type sourceType)
{ {{
SourceType = 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<StringBuilder> PropertyBuilder = null,
Action<StringBuilder> 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(); var builder = new StringBuilder();
builder.AppendLine($@"
{(includeAttributeNamespace ? string.Empty : "using MapTo;")} if (options.UseMapToNamespace)
namespace Test {
{{ builder.AppendFormat("using {0};", SourceBuilder.NamespaceName).AppendLine();
{(sourceClassNamespace != "Test" && !includeAttributeNamespace ? $"using {sourceClassNamespace};" : string.Empty)} }
{(includeAttributeNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]")} builder
public partial class Foo .AppendFormat("using {0};", options.SourceClassNamespace)
{{ .AppendLine()
public int Prop1 {{ get; set; }} .AppendLine();
public int Prop2 {{ get; }}
public int Prop3 {{ get; }}
}}
}}
");
builder.AppendLine($@" builder
namespace {sourceClassNamespace} .AppendFormat("namespace {0}", ns)
{{ .AppendOpeningBracket();
public class Baz
{{ if (hasDifferentSourceNamespace && options.UseMapToNamespace)
public int Prop1 {{ get; set; }} {
public int Prop2 {{ get; }} builder
public int Prop3 {{ get; set; }} .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(); return builder.ToString();
} }
@ -126,7 +178,6 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
} }
@ -190,7 +241,6 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
} }
@ -205,15 +255,18 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(s => s.ToString() == ExpectedAttribute); compilation.SyntaxTrees
compilation.SyntaxTrees.Select(s => s.ToString()).Where(s => s != string.Empty && s != ExpectedAttribute).ShouldBeEmpty(); .Select(s => s.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s.ToString()))
.All(s => s.Contains(": Attribute"))
.ShouldBeTrue();
} }
[Fact] [Fact]
public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings()
{ {
// Arrange // Arrange
var source = GetSourceText(sourceClassNamespace: "Bazaar"); var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar"));
const string expectedResult = @" const string expectedResult = @"
// <auto-generated /> // <auto-generated />
@ -227,7 +280,6 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
} }
@ -255,7 +307,6 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
} }
@ -277,7 +328,6 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
} }
@ -302,8 +352,66 @@ namespace Test
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); 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);
}
} }
} }

View File

@ -7,6 +7,7 @@ namespace TestConsoleApp.ViewModels
{ {
public string FirstName { get; } public string FirstName { get; }
// [IgnoreProerty]
public string LastName { get; } public string LastName { get; }
} }
} }