From f7ff6e47935044d933fe8de462d8e8608454ac02 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Sun, 3 Jan 2021 15:44:59 +0000 Subject: [PATCH] Use same access modifier as the declaring class. --- .../GeneratorExecutionContextExtensions.cs | 4 +- src/MapTo/MapToGenerator.cs | 76 +---- src/MapTo/Models/MapModel.cs | 77 ++++- src/MapTo/SourceBuilder.cs | 7 +- test/MapTo.Tests/Tests.cs | 293 +++++++++--------- 5 files changed, 238 insertions(+), 219 deletions(-) diff --git a/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs b/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs index 2d29e91..ae70b20 100644 --- a/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs +++ b/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs @@ -9,7 +9,7 @@ namespace MapTo.Extensions internal static T GetBuildGlobalOption(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T: notnull { - if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{PropertyNameSuffix}{propertyName}", out var optionValue)) + if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue)) { return defaultValue; } @@ -31,5 +31,7 @@ namespace MapTo.Extensions return defaultValue; } } + + internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}"; } } \ No newline at end of file diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index 8730d5f..e2ce4bc 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -30,87 +30,27 @@ namespace MapTo AddGeneratedMappingsClasses(context, receiver.CandidateClasses, options); } } + + private static void AddAttribute(GeneratorExecutionContext context, (string source, string hintName) attribute) + => context.AddSource(attribute.hintName, attribute.source); private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, IEnumerable candidateClasses, SourceGenerationOptions options) { foreach (var classSyntax in candidateClasses) { - var model = CreateModel(context, classSyntax, options); + var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree); + var (model, diagnostics) = MapModel.CreateModel(classSemanticModel, classSyntax, options); + + diagnostics.ForEach(context.ReportDiagnostic); + if (model is null) { continue; } var (source, hintName) = SourceBuilder.GenerateSource(model); - 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) - { - var sourceTypeExpressionSyntax = classSyntax - .GetAttribute(SourceBuilder.MapFromAttributeName) - ?.DescendantNodes() - .OfType() - .SingleOrDefault(); - - return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; - } - - private static MapModel? CreateModel(GeneratorExecutionContext context, ClassDeclarationSyntax classSyntax, SourceGenerationOptions sourceGenerationOptions) - { - var root = classSyntax.GetCompilationUnit(); - var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree); - - if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol)) - { - context.ReportDiagnostic(Diagnostics.SymbolNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText)); - return null; - } - - var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel); - if (sourceTypeSymbol is null) - { - context.ReportDiagnostic(Diagnostics.MapFromAttributeNotFoundError(classSyntax.GetLocation())); - return null; - } - - 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( - sourceGenerationOptions, - 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, p.Type.ToString())) - .Intersect(classSymbol - .GetAllMembersOfType() - .Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName)) - .Select(p => (p.Name, p.Type.ToString()))) - .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 5d4190e..361db26 100644 --- a/src/MapTo/Models/MapModel.cs +++ b/src/MapTo/Models/MapModel.cs @@ -1,5 +1,9 @@ -using System.Collections.Immutable; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using MapTo.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace MapTo.Models { @@ -12,5 +16,74 @@ namespace MapTo.Models string SourceClassName, string SourceClassFullName, ImmutableArray MappedProperties - ); + ) + { + internal static (MapModel? model, IEnumerable diagnostics) CreateModel( + SemanticModel classSemanticModel, + ClassDeclarationSyntax classSyntax, + SourceGenerationOptions sourceGenerationOptions) + { + var diagnostics = new List(); + var root = classSyntax.GetCompilationUnit(); + + if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol)) + { + diagnostics.Add(Diagnostics.SymbolNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText)); + return (default, diagnostics); + } + + var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel); + if (sourceTypeSymbol is null) + { + diagnostics.Add(Diagnostics.MapFromAttributeNotFoundError(classSyntax.GetLocation())); + return (default, diagnostics); + } + + var className = classSyntax.GetClassName(); + var sourceClassName = sourceTypeSymbol.Name; + + var mappedProperties = GetMappedProperties(classSymbol, sourceTypeSymbol); + if (!mappedProperties.Any()) + { + diagnostics.Add(Diagnostics.NoMatchingPropertyFoundError(classSyntax.GetLocation(), className, sourceClassName)); + return (default, diagnostics); + } + + var model = new MapModel( + sourceGenerationOptions, + root.GetNamespace(), + classSyntax.Modifiers, + className, + sourceTypeSymbol.ContainingNamespace.ToString(), + sourceClassName, + sourceTypeSymbol.ToString(), + mappedProperties); + + return (model, diagnostics); + } + + private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model) + { + var sourceTypeExpressionSyntax = classSyntax + .GetAttribute(SourceBuilder.MapFromAttributeName) + ?.DescendantNodes() + .OfType() + .SingleOrDefault(); + + return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; + } + + private static ImmutableArray GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol) + { + return sourceTypeSymbol + .GetAllMembersOfType() + .Select(p => (p.Name, p.Type.ToString())) + .Intersect(classSymbol + .GetAllMembersOfType() + .Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName)) + .Select(p => (p.Name, p.Type.ToString()))) + .Select(p => p.Name) + .ToImmutableArray(); + } + } } \ No newline at end of file diff --git a/src/MapTo/SourceBuilder.cs b/src/MapTo/SourceBuilder.cs index 3e6198a..647e581 100644 --- a/src/MapTo/SourceBuilder.cs +++ b/src/MapTo/SourceBuilder.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Text; +using System.Text; using MapTo.Extensions; using MapTo.Models; @@ -111,7 +110,7 @@ namespace MapTo // Class declaration .PadLeft(Indent1) - .AppendFormat("{0} class {1}", model.ClassModifiers.ToFullString().Trim(), model.ClassName) + .AppendFormat("partial class {0}", model.ClassName) .AppendOpeningBracket(Indent1) // Class body @@ -126,7 +125,7 @@ namespace MapTo .AppendLine() .AppendLine() .PadLeft(Indent1) - .AppendFormat("{0} static partial class {1}To{2}Extensions", model.ClassModifiers.FirstOrDefault().ToFullString().Trim(), model.SourceClassName, model.ClassName) + .AppendFormat("{0} static partial class {1}To{2}Extensions", model.Options.GeneratedMethodsAccessModifier.ToLowercaseString(), model.SourceClassName, model.ClassName) .AppendOpeningBracket(Indent1) // Extension class body diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index 8f9fd10..004b1bc 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -8,6 +8,7 @@ using MapTo.Tests.Infrastructure; using Microsoft.CodeAnalysis; using Shouldly; using Xunit; +using static MapTo.Extensions.GeneratorExecutionContextExtensions; namespace MapTo.Tests { @@ -19,9 +20,9 @@ namespace MapTo.Tests private static readonly Dictionary DefaultAnalyzerOptions = new() { - ["build_property.MapTo_GenerateXmlDocument"] = "false" + [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" }; - + private static readonly string ExpectedAttribute = $@"{SourceBuilder.GeneratedFilesHeader} using System; @@ -40,20 +41,20 @@ namespace MapTo }}"; private record SourceGeneratorOptions( - bool UseMapToNamespace = false, - string SourceClassNamespace = "Test.Models", - int ClassPropertiesCount = 3, + 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(); - + if (options.UseMapToNamespace) { builder.AppendFormat("using {0};", SourceBuilder.NamespaceName).AppendLine(); @@ -76,7 +77,7 @@ namespace MapTo .AppendLine() .AppendLine(); } - + builder .PadLeft(Indent1) .AppendLine(options.UseMapToNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]") @@ -89,15 +90,15 @@ namespace MapTo .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(Indent1, false) .AppendClosingBracket() .AppendLine() .AppendLine(); - + builder .AppendFormat("namespace {0}", options.SourceClassNamespace) .AppendOpeningBracket() @@ -110,16 +111,40 @@ namespace MapTo .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(Indent1, false) .AppendClosingBracket(); return builder.ToString(); } + [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, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContain(c => c.ToString() == expectedAttribute); + } + [Fact] public void VerifyMapToAttribute() { @@ -134,6 +159,112 @@ namespace MapTo compilation.SyntaxTrees.ShouldContain(c => c.ToString() == ExpectedAttribute); } + [Fact] + public void When_FoundMatchingPropertyNameWithDifferentType_Should_Ignore() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); + + var 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; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + } + + [Fact] + public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + 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 = @" + 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, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + } + + [Fact] + public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier() + { + // Arrange + var source = GetSourceText(); + var configOptions = new Dictionary + { + [GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal", + [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" + }; + + var expectedExtension = @" + internal static partial class BazToFooExtensions + { + internal static Foo ToFoo(this Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + } + }".Trim(); + + var expectedFactory = @" + internal static Foo From(Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + }".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + + var syntaxTree = compilation.SyntaxTrees.Last().ToString(); + syntaxTree.ShouldContain(expectedFactory); + syntaxTree.ShouldContain(expectedExtension); + } + [Fact] public void When_MapToAttributeFound_Should_GenerateTheClass() { @@ -162,7 +293,7 @@ using System; namespace Test { - public partial class Foo + partial class Foo { public Foo(Test.Baz baz) { @@ -179,7 +310,7 @@ namespace Test diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); } - + [Fact] public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError() { @@ -225,7 +356,7 @@ using System; namespace Test { - public partial class Foo + partial class Foo { public Foo(Test.Baz baz) { @@ -289,7 +420,7 @@ namespace Test var source = GetSourceText(); const string expectedResult = @" - public partial class Foo + partial class Foo { public Foo(Test.Models.Baz baz) { @@ -353,131 +484,5 @@ namespace Test diagnostics.ShouldBeSuccessful(); 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, analyzerConfigOptions: DefaultAnalyzerOptions); - - // 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, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithDifferentType_Should_Ignore() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - UseMapToNamespace: true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("public string 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, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); - } - - [Fact] - public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier() - { - // Arrange - var source = GetSourceText(); - var configOptions = new Dictionary - { - [$"build_property.MapTo_{nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier)}"] = "Internal" - }; - - var expectedExtension = @" - internal static Foo ToFoo(this Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - }".Trim(); - - var expectedFactory = @" - internal static Foo From(Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - }".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - - var syntaxTree = compilation.SyntaxTrees.Last().ToString(); - syntaxTree.ShouldContain(expectedFactory); - syntaxTree.ShouldContain(expectedExtension); - } } } \ No newline at end of file