Put source builders in different files and use IndentedTextWriter instead of StringBuilder.

This commit is contained in:
Mohammadreza Taikandi 2021-01-04 10:42:18 +00:00
parent f7ff6e4793
commit 8a3bbc095e
11 changed files with 313 additions and 241 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
namespace MapTo
{
@ -13,7 +14,7 @@ namespace MapTo
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 {SourceBuilder.MapFromAttributeName} type.", 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);

View File

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using MapTo.Models;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -22,8 +22,8 @@ namespace MapTo
{
var options = SourceGenerationOptions.From(context);
AddAttribute(context, SourceBuilder.GenerateMapFromAttribute(options));
AddAttribute(context, SourceBuilder.GenerateIgnorePropertyAttribute(options));
AddAttribute(context, MapFromAttributeSource.Generate(options));
AddAttribute(context, IgnorePropertyAttributeSource.Generate(options));
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
{
@ -48,7 +48,7 @@ namespace MapTo
continue;
}
var (source, hintName) = SourceBuilder.GenerateSource(model);
var (source, hintName) = MapClassSource.Generate(model);
context.AddSource(hintName, source);
}
}

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -20,12 +21,12 @@ namespace MapTo
var attributeSyntax = attributes
.SelectMany(a => a.Attributes)
.SingleOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: SourceBuilder.MapFromAttributeName } } // For: [MapFrom]
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
or
QualifiedNameSyntax // For: [MapTo.MapFrom]
{
Left: IdentifierNameSyntax { Identifier: { ValueText: SourceBuilder.NamespaceName } },
Right: IdentifierNameSyntax { Identifier: { ValueText: SourceBuilder.MapFromAttributeName } }
Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } },
Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } }
}
);

View File

@ -2,6 +2,7 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -65,7 +66,7 @@ namespace MapTo.Models
private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model)
{
var sourceTypeExpressionSyntax = classSyntax
.GetAttribute(SourceBuilder.MapFromAttributeName)
.GetAttribute(MapFromAttributeSource.AttributeName)
?.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
@ -80,7 +81,7 @@ namespace MapTo.Models
.Select(p => (p.Name, p.Type.ToString()))
.Intersect(classSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName))
.Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != IgnorePropertyAttributeSource.AttributeName))
.Select(p => (p.Name, p.Type.ToString())))
.Select(p => p.Name)
.ToImmutableArray();

View File

@ -1,225 +0,0 @@
using System.Text;
using MapTo.Extensions;
using MapTo.Models;
namespace MapTo
{
internal static class SourceBuilder
{
internal const string NamespaceName = "MapTo";
internal const string MapFromAttributeName = "MapFrom";
internal const string IgnorePropertyAttributeName = "IgnoreProperty";
internal const string GeneratedFilesHeader = "// <auto-generated />";
private const int Indent1 = 4;
private const int Indent2 = Indent1 * 2;
private const int Indent3 = Indent1 * 3;
internal static (string source, string hintName) GenerateMapFromAttribute(SourceGenerationOptions options)
{
var builder = new StringBuilder();
builder
.AppendFileHeader()
.AppendLine("using System;")
.AppendLine()
.AppendFormat("namespace {0}", NamespaceName)
.AppendOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.PadLeft(Indent1).AppendLine("/// <summary>")
.PadLeft(Indent1).AppendLine("/// Specifies that the annotated class can be mapped from the provided <see cref=\"SourceType\"/>.")
.PadLeft(Indent1).AppendLine("/// </summary>");
}
builder
.PadLeft(Indent1).AppendLine("[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]")
.PadLeft(Indent1).AppendFormat("public sealed class {0}Attribute : Attribute", MapFromAttributeName)
.AppendOpeningBracket(Indent1);
if (options.GenerateXmlDocument)
{
builder
.PadLeft(Indent2).AppendLine("/// <summary>")
.PadLeft(Indent2).AppendFormat("/// Initializes a new instance of the <see cref=\"{0}Attribute\"/> class with the specified <paramref name=\"sourceType\"/>.", MapFromAttributeName).AppendLine()
.PadLeft(Indent2).AppendLine("/// </summary>");
}
builder
.PadLeft(Indent2).AppendFormat("public {0}Attribute(Type sourceType)", MapFromAttributeName)
.AppendOpeningBracket(Indent2)
.PadLeft(Indent3).AppendLine("SourceType = sourceType;")
.AppendClosingBracket(Indent2, false)
.AppendLine()
.AppendLine();
if (options.GenerateXmlDocument)
{
builder
.PadLeft(Indent2).AppendLine("/// <summary>")
.PadLeft(Indent2).AppendLine("/// Gets the type of the class that the annotated class should be able to map from.")
.PadLeft(Indent2).AppendLine("/// </summary>");
}
builder
.PadLeft(Indent2).AppendLine("public Type SourceType { get; }")
.AppendClosingBracket(Indent1, false)
.AppendClosingBracket();
return (builder.ToString(), $"{MapFromAttributeName}Attribute.g.cs");
}
internal static (string source, string hintName) GenerateIgnorePropertyAttribute(SourceGenerationOptions options)
{
var builder = new StringBuilder();
builder
.AppendFileHeader()
.AppendLine("using System;")
.AppendLine()
.AppendFormat("namespace {0}", NamespaceName)
.AppendOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.PadLeft(Indent1).AppendLine("/// <summary>")
.PadLeft(Indent1).AppendLine("/// Specified that the annotated property should not be included in the generated mappings.")
.PadLeft(Indent1).AppendLine("/// </summary>");
}
builder
.PadLeft(Indent1).AppendLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
.PadLeft(Indent1).AppendFormat("public sealed class {0}Attribute : Attribute {{ }}", IgnorePropertyAttributeName)
.AppendClosingBracket();
return (builder.ToString(), $"{IgnorePropertyAttributeName}Attribute.g.cs");
}
internal static (string source, string hintName) GenerateSource(MapModel model)
{
var builder = new StringBuilder();
builder
.AppendFileHeader()
.GenerateUsings(model)
// Namespace declaration
.AppendFormat("namespace {0}", model.Namespace)
.AppendOpeningBracket()
// Class declaration
.PadLeft(Indent1)
.AppendFormat("partial class {0}", model.ClassName)
.AppendOpeningBracket(Indent1)
// Class body
.GenerateConstructor(model)
.AppendLine()
.GenerateFactoryMethod(model)
// End class declaration
.AppendClosingBracket(Indent1)
// Extensions Class declaration
.AppendLine()
.AppendLine()
.PadLeft(Indent1)
.AppendFormat("{0} static partial class {1}To{2}Extensions", model.Options.GeneratedMethodsAccessModifier.ToLowercaseString(), model.SourceClassName, model.ClassName)
.AppendOpeningBracket(Indent1)
// Extension class body
.GenerateSourceTypeExtensionMethod(model)
// End extensions class declaration
.AppendClosingBracket(Indent1)
// End namespace declaration
.AppendClosingBracket();
return (builder.ToString(), $"{model.ClassName}.g.cs");
}
private static StringBuilder GenerateUsings(this StringBuilder builder, MapModel model)
{
builder.AppendLine("using System;");
return builder.AppendLine();
}
private static StringBuilder GenerateConstructor(this StringBuilder builder, MapModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.PadLeft(Indent2).AppendLine("/// <summary>")
.PadLeft(Indent2).AppendFormat("/// Initializes a new instance of the <see cref=\"{0}\"/> class", model.ClassName).AppendLine()
.PadLeft(Indent2).AppendFormat("/// using the property values from the specified <paramref name=\"{0}\"/>.", sourceClassParameterName).AppendLine()
.PadLeft(Indent2).AppendLine("/// </summary>")
.PadLeft(Indent2).AppendFormat("/// <exception cref=\"ArgumentNullException\">{0} is null</exception>", sourceClassParameterName).AppendLine();
}
builder
.PadLeft(Indent2).AppendFormat("{0} {1}({2} {3})", model.Options.ConstructorAccessModifier.ToLowercaseString(), model.ClassName, model.SourceClassFullName, sourceClassParameterName)
.AppendOpeningBracket(Indent2)
.PadLeft(Indent3).AppendFormat("if ({0} == null) throw new ArgumentNullException(nameof({0}));", sourceClassParameterName).AppendLine()
.AppendLine();
foreach (var property in model.MappedProperties)
{
builder
.PadLeft(Indent3)
.AppendFormat("{0} = {1}.{2};", property, sourceClassParameterName, property)
.AppendLine();
}
// End constructor declaration
return builder.AppendClosingBracket(Indent2, false);
}
private static StringBuilder GenerateFactoryMethod(this StringBuilder builder, MapModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
return builder
.AppendLine()
.AppendConvertorMethodsXmlDocs(model, sourceClassParameterName)
.PadLeft(Indent2).AppendFormat("{0} static {1} From({2} {3})", model.Options.GeneratedMethodsAccessModifier.ToLowercaseString(), model.ClassName, model.SourceClassFullName, sourceClassParameterName)
.AppendOpeningBracket(Indent2)
.PadLeft(Indent3).AppendFormat("return {0} == null ? null : new {1}({0});", sourceClassParameterName, model.ClassName)
.AppendClosingBracket(Indent2);
}
private static StringBuilder GenerateSourceTypeExtensionMethod(this StringBuilder builder, MapModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
return builder
.AppendConvertorMethodsXmlDocs(model, sourceClassParameterName)
.PadLeft(Indent2).AppendFormat("{0} static {1} To{1}(this {2} {3})", model.Options.GeneratedMethodsAccessModifier.ToLowercaseString(), model.ClassName, model.SourceClassFullName, sourceClassParameterName)
.AppendOpeningBracket(Indent2)
.PadLeft(Indent3).AppendFormat("return {0} == null ? null : new {1}({0});", sourceClassParameterName, model.ClassName)
.AppendClosingBracket(Indent2);
}
private static StringBuilder AppendFileHeader(this StringBuilder builder) =>
builder.AppendLine(GeneratedFilesHeader);
private static StringBuilder AppendConvertorMethodsXmlDocs(this StringBuilder builder, MapModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.PadLeft(Indent2).AppendLine("/// <summary>")
.PadLeft(Indent2).AppendFormat("/// Creates a new instance of <see cref=\"{0}\"/> and sets its participating properties", model.ClassName).AppendLine()
.PadLeft(Indent2).AppendFormat("/// using the property values from <paramref name=\"{0}\"/>.", sourceClassParameterName).AppendLine()
.PadLeft(Indent2).AppendLine("/// </summary>")
.PadLeft(Indent2).AppendFormat("/// <param name=\"{0}\">Instance of <see cref=\"{1}\"/> to use as source.</param>", sourceClassParameterName, model.SourceClassName).AppendLine()
.PadLeft(Indent2).AppendFormat("/// <returns>A new instance of <see cred=\"{0}\"/> -or- <c>null</c> if <paramref name=\"{1}\"/> is <c>null</c>.</returns>", model.ClassName, sourceClassParameterName).AppendLine();
}
}
}

View File

@ -0,0 +1,8 @@
namespace MapTo.Sources
{
internal class Constants
{
internal const string RootNamespace = "MapTo";
internal const string GeneratedFilesHeader = "// <auto-generated />";
}
}

View File

@ -0,0 +1,35 @@
using MapTo.Models;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class IgnorePropertyAttributeSource
{
internal const string AttributeName = "IgnoreProperty";
internal static (string source, string hintName) Generate(SourceGenerationOptions options)
{
var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specified that the annotated property should not be included in the generated mappings.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute {{ }}")
.WriteClosingBracket();
return (builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -0,0 +1,126 @@
using MapTo.Extensions;
using MapTo.Models;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapClassSource
{
internal static (string source, string hintName) Generate(MapModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteUsings(model)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial class {model.ClassName}")
.WriteOpeningBracket()
// Class body
.GenerateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
// End class declaration
.WriteClosingBracket()
.WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return (builder.ToString(), $"{model.ClassName}.g.cs");
}
private static SourceBuilder WriteUsings(this SourceBuilder builder, MapModel model)
{
return builder
.WriteLine("using System;");
}
private static SourceBuilder GenerateConstructor(this SourceBuilder builder, MapModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.ClassName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
builder
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.ClassName}({model.SourceClassFullName} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine();
foreach (var property in model.MappedProperties)
{
builder.WriteLine($"{property} = {sourceClassParameterName}.{property};");
}
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MapModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
.WriteClosingBracket();
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MapModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.ClassName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">Instance of <see cref=\"{model.SourceClassName}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.ClassName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MapModel model)
{
return builder
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceClassName}To{model.ClassName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MapModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
.WriteClosingBracket();
}
}
}

View File

@ -0,0 +1,63 @@
using MapTo.Models;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapFromAttributeSource
{
internal const string AttributeName = "MapFrom";
internal static (string source, string hintName) Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated class can be mapped from the provided <see cref=\"SourceType\"/>.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{AttributeName}Attribute\"/> class with the specified <paramref name=\"sourceType\"/>.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public {AttributeName}Attribute(Type sourceType)")
.WriteOpeningBracket()
.WriteLine("SourceType = sourceType;")
.WriteClosingBracket()
.WriteLine();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets the type of the class that the annotated class should be able to map from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("public Type SourceType { get; }")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return (builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.CodeDom.Compiler;
using System.IO;
namespace MapTo.Sources
{
public sealed class SourceBuilder : IDisposable
{
private readonly StringWriter _writer;
private readonly IndentedTextWriter _indentedWriter;
public SourceBuilder()
{
_writer = new StringWriter();
_indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4));
}
/// <inheritdoc />
public void Dispose()
{
_writer.Dispose();
_indentedWriter.Dispose();
}
public SourceBuilder WriteLine(string value)
{
_indentedWriter.WriteLine(value);
return this;
}
public SourceBuilder WriteLine()
{
_indentedWriter.WriteLineNoTabs(string.Empty);
return this;
}
public SourceBuilder WriteOpeningBracket()
{
_indentedWriter.WriteLine("{");
_indentedWriter.Indent++;
return this;
}
public SourceBuilder WriteClosingBracket()
{
_indentedWriter.Indent--;
_indentedWriter.WriteLine("}");
return this;
}
/// <inheritdoc />
public override string ToString() => _writer.ToString();
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using MapTo.Extensions;
using MapTo.Models;
using MapTo.Sources;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Shouldly;
@ -23,7 +24,7 @@ namespace MapTo.Tests
[GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
};
private static readonly string ExpectedAttribute = $@"{SourceBuilder.GeneratedFilesHeader}
private static readonly string ExpectedAttribute = $@"{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
@ -57,7 +58,7 @@ namespace MapTo
if (options.UseMapToNamespace)
{
builder.AppendFormat("using {0};", SourceBuilder.NamespaceName).AppendLine();
builder.AppendFormat("using {0};", Constants.RootNamespace).AppendLine();
}
builder
@ -127,7 +128,7 @@ namespace MapTo
// Arrange
const string source = "";
var expectedAttribute = $@"
{SourceBuilder.GeneratedFilesHeader}
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
@ -142,7 +143,9 @@ namespace MapTo
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(c => c.ToString() == expectedAttribute);
var attributeSyntax = compilation.SyntaxTrees.Select(s => s.ToString().Trim()).SingleOrDefault(s => s.Contains(IgnorePropertyAttributeSource.AttributeName));
attributeSyntax.ShouldNotBeNullOrWhiteSpace();
attributeSyntax.ShouldBe(expectedAttribute);
}
[Fact]
@ -156,7 +159,10 @@ namespace MapTo
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(c => c.ToString() == ExpectedAttribute);
var attributeSyntax = compilation.SyntaxTrees.Select(s => s.ToString().Trim()).SingleOrDefault(s => s.Contains(MapFromAttributeSource.AttributeName));
attributeSyntax.ShouldNotBeNullOrWhiteSpace();
attributeSyntax.ShouldBe(ExpectedAttribute);
}
[Fact]