Use same access modifier as the declaring class.

This commit is contained in:
Mohammadreza Taikandi 2021-01-03 15:44:59 +00:00
parent c7d6d3ff27
commit f7ff6e4793
5 changed files with 238 additions and 219 deletions

View File

@ -9,7 +9,7 @@ namespace MapTo.Extensions
internal static T GetBuildGlobalOption<T>(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}";
}
}

View File

@ -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<ClassDeclarationSyntax> 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<TypeOfExpressionSyntax>()
.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<string> GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
{
return sourceTypeSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Select(p => (p.Name, p.Type.ToString()))
.Intersect(classSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName))
.Select(p => (p.Name, p.Type.ToString())))
.Select(p => p.Name)
.ToImmutableArray();
}
}
}

View File

@ -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<string> MappedProperties
);
)
{
internal static (MapModel? model, IEnumerable<Diagnostic> diagnostics) CreateModel(
SemanticModel classSemanticModel,
ClassDeclarationSyntax classSyntax,
SourceGenerationOptions sourceGenerationOptions)
{
var diagnostics = new List<Diagnostic>();
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<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
private static ImmutableArray<string> GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
{
return sourceTypeSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Select(p => (p.Name, p.Type.ToString()))
.Intersect(classSymbol
.GetAllMembersOfType<IPropertySymbol>()
.Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName))
.Select(p => (p.Name, p.Type.ToString())))
.Select(p => p.Name)
.ToImmutableArray();
}
}
}

View File

@ -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

View File

@ -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<string, string> 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<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();
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<string, string>
{
[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<string, string>
{
[$"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);
}
}
}