Prevent generating the secondary constructor if one is available in the destination class.

This commit is contained in:
Mohammadreza Taikandi 2021-04-17 08:14:39 +01:00
parent 2f8a897676
commit 9e662c7e90
6 changed files with 134 additions and 29 deletions

View File

@ -1,5 +1,4 @@
using System.Collections.Immutable; using System.Linq;
using System.Linq;
using MapTo.Sources; using MapTo.Sources;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -37,6 +36,6 @@ namespace MapTo
Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter."); Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter.");
private static Diagnostic Create(string id, Location? location, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) => private static Diagnostic Create(string id, Location? location, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
Diagnostic.Create(new DiagnosticDescriptor(id, null, message, UsageCategory, severity, true), location ?? Location.None); Diagnostic.Create(new DiagnosticDescriptor(id, string.Empty, message, UsageCategory, severity, true), location ?? Location.None);
} }
} }

View File

@ -4,6 +4,7 @@ using System.Linq;
using MapTo.Extensions; using MapTo.Extensions;
using MapTo.Sources; using MapTo.Sources;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo namespace MapTo
@ -17,6 +18,7 @@ namespace MapTo
private readonly INamedTypeSymbol _mapFromAttributeTypeSymbol; private readonly INamedTypeSymbol _mapFromAttributeTypeSymbol;
private readonly INamedTypeSymbol _mapPropertyAttributeTypeSymbol; private readonly INamedTypeSymbol _mapPropertyAttributeTypeSymbol;
private readonly INamedTypeSymbol _mapTypeConverterAttributeTypeSymbol; private readonly INamedTypeSymbol _mapTypeConverterAttributeTypeSymbol;
private readonly INamedTypeSymbol _mappingContextTypeSymbol;
private readonly SourceGenerationOptions _sourceGenerationOptions; private readonly SourceGenerationOptions _sourceGenerationOptions;
private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol; private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol;
private readonly List<string> _usings; private readonly List<string> _usings;
@ -34,6 +36,7 @@ namespace MapTo
_typeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName); _typeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
_mapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName); _mapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
_mapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName); _mapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
_mappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
@ -47,27 +50,28 @@ namespace MapTo
private void Initialize() private void Initialize()
{ {
var semanticModel = _compilation.GetSemanticModel(_classSyntax.SyntaxTree); var semanticModel = _compilation.GetSemanticModel(_classSyntax.SyntaxTree);
if (!(semanticModel.GetDeclaredSymbol(_classSyntax) is INamedTypeSymbol classTypeSymbol)) if (!(ModelExtensions.GetDeclaredSymbol(semanticModel, _classSyntax) is INamedTypeSymbol classTypeSymbol))
{ {
_diagnostics.Add(DiagnosticProvider.TypeNotFoundError(_classSyntax.GetLocation(), _classSyntax.Identifier.ValueText)); _diagnostics.Add(DiagnosticsFactory.TypeNotFoundError(_classSyntax.GetLocation(), _classSyntax.Identifier.ValueText));
return; return;
} }
var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax, semanticModel); var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax, semanticModel);
if (sourceTypeSymbol is null) if (sourceTypeSymbol is null)
{ {
_diagnostics.Add(DiagnosticProvider.MapFromAttributeNotFoundError(_classSyntax.GetLocation())); _diagnostics.Add(DiagnosticsFactory.MapFromAttributeNotFoundError(_classSyntax.GetLocation()));
return; return;
} }
var className = _classSyntax.GetClassName(); var className = _classSyntax.GetClassName();
var sourceClassName = sourceTypeSymbol.Name; var sourceClassName = sourceTypeSymbol.Name;
var isClassInheritFromMappedBaseClass = IsClassInheritFromMappedBaseClass(semanticModel); var isClassInheritFromMappedBaseClass = IsClassInheritFromMappedBaseClass(semanticModel);
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isClassInheritFromMappedBaseClass); var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isClassInheritFromMappedBaseClass);
if (!mappedProperties.Any()) if (!mappedProperties.Any())
{ {
_diagnostics.Add(DiagnosticProvider.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol)); _diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
return; return;
} }
@ -84,13 +88,38 @@ namespace MapTo
sourceTypeSymbol.ToString(), sourceTypeSymbol.ToString(),
mappedProperties.ToImmutableArray(), mappedProperties.ToImmutableArray(),
isClassInheritFromMappedBaseClass, isClassInheritFromMappedBaseClass,
_usings.ToImmutableArray()); _usings.ToImmutableArray(),
shouldGenerateSecondaryConstructor);
}
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = _classSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.SingleOrDefault(c =>
c.ParameterList.Parameters.Count == 1 &&
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol));
if (constructorSyntax is null)
{
// Secondary constructor is not defined.
return true;
}
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, _mappingContextTypeSymbol) ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
{
_diagnostics.Add(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
return false;
} }
private bool IsClassInheritFromMappedBaseClass(SemanticModel semanticModel) private bool IsClassInheritFromMappedBaseClass(SemanticModel semanticModel)
{ {
return _classSyntax.BaseList is not null && _classSyntax.BaseList.Types return _classSyntax.BaseList is not null && _classSyntax.BaseList.Types
.Select(t => semanticModel.GetTypeInfo(t.Type).Type) .Select(t => ModelExtensions.GetTypeInfo(semanticModel, t.Type).Type)
.Any(t => t?.GetAttribute(_mapFromAttributeTypeSymbol) != null); .Any(t => t?.GetAttribute(_mapFromAttributeTypeSymbol) != null);
} }
@ -154,8 +183,8 @@ namespace MapTo
} }
var mapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol); var mapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol);
if (mapFromAttribute is null && if (mapFromAttribute is null &&
property.Type is INamedTypeSymbol namedTypeSymbol && property.Type is INamedTypeSymbol namedTypeSymbol &&
!property.Type.IsPrimitiveType() && !property.Type.IsPrimitiveType() &&
(_compilation.IsGenericEnumerable(property.Type) || property.Type.AllInterfaces.Any(i => _compilation.IsGenericEnumerable(i)))) (_compilation.IsGenericEnumerable(property.Type) || property.Type.AllInterfaces.Any(i => _compilation.IsGenericEnumerable(i))))
{ {
@ -167,7 +196,7 @@ namespace MapTo
if (mappedSourcePropertyType is null && enumerableTypeArgument is null) if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
{ {
_diagnostics.Add(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property)); _diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
} }
return _diagnostics.IsEmpty(); return _diagnostics.IsEmpty();
@ -203,7 +232,7 @@ namespace MapTo
var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty); var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null) if (baseInterface is null)
{ {
_diagnostics.Add(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty)); _diagnostics.Add(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
return false; return false;
} }
@ -257,7 +286,7 @@ namespace MapTo
.OfType<TypeOfExpressionSyntax>() .OfType<TypeOfExpressionSyntax>()
.SingleOrDefault(); .SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; return sourceTypeExpressionSyntax is not null ? ModelExtensions.GetTypeInfo(semanticModel, sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
} }
} }
} }

View File

@ -29,7 +29,8 @@ namespace MapTo
string SourceClassFullName, string SourceClassFullName,
ImmutableArray<MappedProperty> MappedProperties, ImmutableArray<MappedProperty> MappedProperties,
bool HasMappedBaseClass, bool HasMappedBaseClass,
ImmutableArray<string> Usings ImmutableArray<string> Usings,
bool GenerateSecondaryConstructor
); );
internal record SourceGenerationOptions( internal record SourceGenerationOptions(

View File

@ -20,24 +20,30 @@ namespace MapTo.Sources
// Class declaration // Class declaration
.WriteLine($"partial class {model.ClassName}") .WriteLine($"partial class {model.ClassName}")
.WriteOpeningBracket() .WriteOpeningBracket();
// Class body // Class body
.GenerateSecondaryConstructor(model) if (model.GenerateSecondaryConstructor)
.WriteLine() {
.GeneratePrivateConstructor(model) builder
.WriteLine() .GenerateSecondaryConstructor(model)
.GenerateFactoryMethod(model) .WriteLine();
}
// End class declaration builder
.WriteClosingBracket() .GeneratePrivateConstructor(model)
.WriteLine() .WriteLine()
.GenerateFactoryMethod(model)
// Extension class declaration // End class declaration
.GenerateSourceTypeExtensionClass(model) .WriteClosingBracket()
.WriteLine()
// End namespace declaration // Extension class declaration
.WriteClosingBracket(); .GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.ClassName}.g.cs"); return new(builder.ToString(), $"{model.ClassName}.g.cs");
} }

View File

@ -7,6 +7,9 @@ namespace MapTo.Tests.Extensions
{ {
internal static class RoslynExtensions internal static class RoslynExtensions
{ {
internal static SyntaxTree GetGeneratedSyntaxTree(this Compilation compilation, string className) =>
compilation.SyntaxTrees.SingleOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs"));
internal static string PrintSyntaxTree(this Compilation compilation) internal static string PrintSyntaxTree(this Compilation compilation)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();

View File

@ -1,5 +1,8 @@
using MapTo.Tests.Extensions; using System.Linq;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure; using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using static MapTo.Tests.Common; using static MapTo.Tests.Common;
@ -29,6 +32,70 @@ namespace MapTo.Tests
_output.WriteLine(compilation.PrintSyntaxTree()); _output.WriteLine(compilation.PrintSyntaxTree());
} }
[Fact]
public void When_SecondaryConstructorExists_Should_NotGenerateOne()
{
// Arrange
var source = @"
using MapTo;
namespace Test.Data.Models
{
public class SourceClass { public string Prop1 { get; set; } }
[MapFrom(typeof(SourceClass))]
public partial class DestinationClass
{
public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
public string Prop1 { get; set; }
}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation
.GetGeneratedSyntaxTree("DestinationClass")
.GetRoot()
.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.Count()
.ShouldBe(1);
}
[Fact]
public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError()
{
// Arrange
var source = @"
using MapTo;
namespace Test.Data.Models
{
public class SourceClass { public string Prop1 { get; set; } }
[MapFrom(typeof(SourceClass))]
public partial class DestinationClass
{
public DestinationClass(SourceClass source) { }
public string Prop1 { get; set; }
}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var constructorSyntax = compilation.SyntaxTrees
.First()
.GetRoot()
.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.Single();
diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
private static string NestedSourceClass => @" private static string NestedSourceClass => @"
namespace Test.Data.Models namespace Test.Data.Models
{ {