From da6fb3e589380ecc2508535d9b8ee982db7add9e Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Tue, 22 Dec 2020 17:35:20 +0000 Subject: [PATCH] Report diagnostics error when no matching property found. --- src/MapTo/Diagnostics.cs | 7 +++-- src/MapTo/MapTo.csproj | 6 ++++ src/MapTo/MapToGenerator.cs | 59 +++++++++++++++++++++++++----------- src/MapTo/Models/MapModel.cs | 27 +++-------------- src/MapTo/SourceBuilder.cs | 26 +++++----------- test/MapTo.Tests/Tests.cs | 55 ++++++++++++++++++++++++--------- 6 files changed, 105 insertions(+), 75 deletions(-) diff --git a/src/MapTo/Diagnostics.cs b/src/MapTo/Diagnostics.cs index ae2eb9f..f796d1a 100644 --- a/src/MapTo/Diagnostics.cs +++ b/src/MapTo/Diagnostics.cs @@ -6,15 +6,18 @@ namespace MapTo { private const string UsageCategory = "Usage"; - internal static Diagnostic SymbolNotFound(Location location, string syntaxName) => + internal static Diagnostic SymbolNotFoundError(Location location, string syntaxName) => Create("MT0001", "Symbol not found.", $"Unable to find any symbols for {syntaxName}", location); - internal static Diagnostic MapFromAttributeNotFound(Location location) => + internal static Diagnostic MapFromAttributeNotFoundError(Location location) => Create("MT0002", "Attribute Not Available", $"Unable to find {SourceBuilder.MapFromAttributeName} type.", location); internal static Diagnostic ClassMappingsGenerated(Location location, string typeName) => Create("MT1001", "Mapped Type", $"Generated mappings for {typeName}", location, DiagnosticSeverity.Info); + internal static Diagnostic NoMatchingPropertyFoundError(Location location, string className, string sourceTypeName) => + Create("MT2001", "Property Not Found", $"No matching properties found between '{className}' and '{sourceTypeName}' types.", location); + private static Diagnostic Create(string id, string title, string message, Location location, DiagnosticSeverity severity = DiagnosticSeverity.Error) => Diagnostic.Create(new DiagnosticDescriptor(id, title, message, UsageCategory, severity, true), location); } diff --git a/src/MapTo/MapTo.csproj b/src/MapTo/MapTo.csproj index 93b8b26..e842703 100644 --- a/src/MapTo/MapTo.csproj +++ b/src/MapTo/MapTo.csproj @@ -20,6 +20,12 @@ MapTo + + + <_Parameter1>$(AssemblyName).Tests + + + all diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index e3b28a6..56a2358 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using MapTo.Extensions; using MapTo.Models; @@ -31,19 +32,11 @@ namespace MapTo { foreach (var classSyntax in candidateClasses) { - var root = classSyntax.GetCompilationUnit(); - var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree); - var classSymbol = classSemanticModel.GetDeclaredSymbol(classSyntax) as INamedTypeSymbol; - var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel); - - var (isValid, diagnostics) = Verify(root, classSyntax, classSemanticModel, classSymbol, sourceTypeSymbol); - if (!isValid) + var model = CreateModel(context, classSyntax); + if (model is null) { - diagnostics.ForEach(context.ReportDiagnostic); continue; } - - var model = new MapModel(root, classSyntax, classSymbol!, sourceTypeSymbol!); var (source, hintName) = SourceBuilder.GenerateSource(model); context.AddSource(hintName, source); @@ -68,21 +61,51 @@ namespace MapTo return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; } - private static (bool isValid, IEnumerable diagnostics) Verify(CompilationUnitSyntax root, ClassDeclarationSyntax classSyntax, SemanticModel classSemanticModel, INamedTypeSymbol? classSymbol, INamedTypeSymbol? sourceTypeSymbol) + private static MapModel? CreateModel(GeneratorExecutionContext context, ClassDeclarationSyntax classSyntax) { - var diagnostics = new List(); - - if (classSymbol is null) + var root = classSyntax.GetCompilationUnit(); + var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree); + + if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol)) { - diagnostics.Add(Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText)); + context.ReportDiagnostic(Diagnostics.SymbolNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText)); + return null; } - + + var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel); if (sourceTypeSymbol is null) { - diagnostics.Add(Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText)); + context.ReportDiagnostic(Diagnostics.MapFromAttributeNotFoundError(classSyntax.GetLocation())); + return null; } - return (!diagnostics.Any(), diagnostics); + var className = classSyntax.GetClassName(); + var sourceClassName = sourceTypeSymbol.Name; + + var mappedProperties = GetMappedProperties(classSymbol, sourceTypeSymbol); + if (!mappedProperties.Any()) + { + context.ReportDiagnostic(Diagnostics.NoMatchingPropertyFoundError(classSyntax.GetLocation(), className, sourceClassName)); + return null; + } + + return new MapModel( + root.GetNamespace(), + classSyntax.Modifiers, + className, + sourceTypeSymbol.ContainingNamespace.ToString(), + sourceClassName, + sourceTypeSymbol.ToString(), + mappedProperties); + } + + private static ImmutableArray GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) + { + return sourceTypeSymbol + .GetAllMembersOfType() + .Select(p => p.Name) + .Intersect(classSymbol.GetAllMembersOfType().Select(p => p.Name)) + .ToImmutableArray(); } } } \ No newline at end of file diff --git a/src/MapTo/Models/MapModel.cs b/src/MapTo/Models/MapModel.cs index 05ca354..610882d 100644 --- a/src/MapTo/Models/MapModel.cs +++ b/src/MapTo/Models/MapModel.cs @@ -1,57 +1,40 @@ -using System.Collections.Generic; -using MapTo.Extensions; +using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace MapTo.Models { public class MapModel { - private MapModel( + internal MapModel( string? ns, SyntaxTokenList classModifiers, string className, - IEnumerable properties, string sourceNamespace, string sourceClassName, string sourceClassFullName, - IEnumerable sourceTypeProperties) + ImmutableArray mappedProperties) { Namespace = ns; ClassModifiers = classModifiers; ClassName = className; - Properties = properties; SourceNamespace = sourceNamespace; SourceClassName = sourceClassName; SourceClassFullName = sourceClassFullName; - SourceTypeProperties = sourceTypeProperties; + MappedProperties = mappedProperties; } - internal MapModel(CompilationUnitSyntax root, ClassDeclarationSyntax classSyntax, ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) - : this( - root.GetNamespace(), - classSyntax.Modifiers, - classSyntax.GetClassName(), - classSymbol.GetAllMembersOfType(), - sourceTypeSymbol.ContainingNamespace.ToString(), - sourceTypeSymbol.Name, - sourceTypeSymbol.ToString(), - sourceTypeSymbol.GetAllMembersOfType()) { } - public string? Namespace { get; } public SyntaxTokenList ClassModifiers { get; } public string ClassName { get; } - public IEnumerable Properties { get; } - public string SourceNamespace { get; } public string SourceClassName { get; } public string SourceClassFullName { get; } - public IEnumerable SourceTypeProperties { get; } + public ImmutableArray MappedProperties { get; } } } \ No newline at end of file diff --git a/src/MapTo/SourceBuilder.cs b/src/MapTo/SourceBuilder.cs index 2b32327..fcefe5a 100644 --- a/src/MapTo/SourceBuilder.cs +++ b/src/MapTo/SourceBuilder.cs @@ -57,7 +57,7 @@ namespace MapTo .AppendOpeningBracket(Indent1) // Class body - .GenerateConstructor(model, out var mappedProperties) + .GenerateConstructor(model) .AppendLine() .GenerateFactoryMethod(model) @@ -89,7 +89,7 @@ namespace MapTo return builder.AppendLine(); } - private static StringBuilder GenerateConstructor(this StringBuilder builder, MapModel model, out List mappedProperties) + private static StringBuilder GenerateConstructor(this StringBuilder builder, MapModel model) { var sourceClassParameterName = model.SourceClassName.ToCamelCase(); @@ -98,25 +98,15 @@ namespace MapTo .AppendFormat("public {0}({1} {2})", model.ClassName, model.SourceClassFullName, sourceClassParameterName) .AppendOpeningBracket(Indent2) .PadLeft(Indent3) - .AppendFormat("if ({0} == null) throw new ArgumentNullException(nameof({0}));", sourceClassParameterName) + .AppendFormat("if ({0} == null) throw new ArgumentNullException(nameof({0}));", sourceClassParameterName).AppendLine() .AppendLine(); - mappedProperties = new List(); - - if (model.SourceTypeProperties.Any()) + foreach (var property in model.MappedProperties) { - builder.AppendLine(); - - foreach (var propertySymbol in model.SourceTypeProperties) - { - if (model.Properties.Any(p => p.Name == propertySymbol.Name)) - { - mappedProperties.Add(propertySymbol); - builder - .PadLeft(Indent3) - .AppendFormat("{0} = {1}.{2};{3}", propertySymbol.Name, sourceClassParameterName, propertySymbol.Name, Environment.NewLine); - } - } + builder + .PadLeft(Indent3) + .AppendFormat("{0} = {1}.{2};", property, sourceClassParameterName, property) + .AppendLine(); } // End constructor declaration diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index be2e825..aa5423e 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -1,10 +1,12 @@ using System.Linq; using System.Text; +using MapToTests; +using Microsoft.CodeAnalysis; using Shouldly; using Xunit; using Xunit.Abstractions; -namespace MapToTests +namespace MapTo.Tests { public class Tests { @@ -93,14 +95,12 @@ namespace Test [MapFrom(typeof(Baz))] public partial class Foo { - + public int Prop1 { get; set; } } public class Baz { public int Prop1 { get; set; } - public int Prop2 { get; } - public int Prop3 { get; set; } } } "; @@ -116,6 +116,8 @@ namespace Test public Foo(Test.Baz baz) { if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; } "; @@ -127,6 +129,31 @@ namespace Test compilation.SyntaxTrees.Count().ShouldBe(3); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); } + + [Fact] + public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError() + { + // Arrange + var expectedDiagnostic = Diagnostics.NoMatchingPropertyFoundError(Location.None, "Foo", "Baz"); + const string source = @" +using MapTo; + +namespace Test +{ + [MapFrom(typeof(Baz))] + public partial class Foo { } + + public class Baz { public int Prop1 { get; set; } } +} +"; + + // Act + var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id); + error.ShouldNotBeNull(); + } [Fact] public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass() @@ -136,15 +163,9 @@ namespace Test namespace Test { [MapTo.MapFrom(typeof(Baz))] - public partial class Foo - { - - } + public partial class Foo { public int Prop1 { get; set; } } - public class Baz - { - - } + public class Baz { public int Prop1 { get; set; } } } "; @@ -159,6 +180,8 @@ namespace Test public Foo(Test.Baz baz) { if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; } "; @@ -187,7 +210,7 @@ namespace Test } [Fact] - public void When_SourceTypeHasDifferentNamespace_Should_AddToUsings() + public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() { // Arrange var source = GetSourceText(sourceClassNamespace: "Bazaar"); @@ -195,7 +218,8 @@ namespace Test const string expectedResult = @" // using System; -using Bazaar; + +namespace Test "; // Act @@ -219,6 +243,7 @@ using Bazaar; public Foo(Test.Models.Baz baz) { if (baz == null) throw new ArgumentNullException(nameof(baz)); + Prop1 = baz.Prop1; Prop2 = baz.Prop2; Prop3 = baz.Prop3; @@ -263,7 +288,7 @@ using Bazaar; var source = GetSourceText(); const string expectedResult = @" - public static partial class BazExtensions + public static partial class BazToFooExtensions { public static Foo ToFoo(this Test.Models.Baz baz) {