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 Microsoft.CodeAnalysis;
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.");
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.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
@ -17,6 +18,7 @@ namespace MapTo
private readonly INamedTypeSymbol _mapFromAttributeTypeSymbol;
private readonly INamedTypeSymbol _mapPropertyAttributeTypeSymbol;
private readonly INamedTypeSymbol _mapTypeConverterAttributeTypeSymbol;
private readonly INamedTypeSymbol _mappingContextTypeSymbol;
private readonly SourceGenerationOptions _sourceGenerationOptions;
private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol;
private readonly List<string> _usings;
@ -34,6 +36,7 @@ namespace MapTo
_typeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
_mapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
_mapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
_mappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
@ -47,27 +50,28 @@ namespace MapTo
private void Initialize()
{
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;
}
var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax, semanticModel);
if (sourceTypeSymbol is null)
{
_diagnostics.Add(DiagnosticProvider.MapFromAttributeNotFoundError(_classSyntax.GetLocation()));
_diagnostics.Add(DiagnosticsFactory.MapFromAttributeNotFoundError(_classSyntax.GetLocation()));
return;
}
var className = _classSyntax.GetClassName();
var sourceClassName = sourceTypeSymbol.Name;
var isClassInheritFromMappedBaseClass = IsClassInheritFromMappedBaseClass(semanticModel);
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isClassInheritFromMappedBaseClass);
if (!mappedProperties.Any())
{
_diagnostics.Add(DiagnosticProvider.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
return;
}
@ -84,13 +88,38 @@ namespace MapTo
sourceTypeSymbol.ToString(),
mappedProperties.ToImmutableArray(),
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)
{
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);
}
@ -154,8 +183,8 @@ namespace MapTo
}
var mapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol);
if (mapFromAttribute is null &&
property.Type is INamedTypeSymbol namedTypeSymbol &&
if (mapFromAttribute is null &&
property.Type is INamedTypeSymbol namedTypeSymbol &&
!property.Type.IsPrimitiveType() &&
(_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)
{
_diagnostics.Add(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property));
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
}
return _diagnostics.IsEmpty();
@ -203,7 +232,7 @@ namespace MapTo
var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
_diagnostics.Add(DiagnosticProvider.InvalidTypeConverterGenericTypesError(property, sourceProperty));
_diagnostics.Add(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
return false;
}
@ -257,7 +286,7 @@ namespace MapTo
.OfType<TypeOfExpressionSyntax>()
.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,
ImmutableArray<MappedProperty> MappedProperties,
bool HasMappedBaseClass,
ImmutableArray<string> Usings
ImmutableArray<string> Usings,
bool GenerateSecondaryConstructor
);
internal record SourceGenerationOptions(

View File

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

View File

@ -7,6 +7,9 @@ namespace MapTo.Tests.Extensions
{
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)
{
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 Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using static MapTo.Tests.Common;
@ -29,6 +32,70 @@ namespace MapTo.Tests
_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 => @"
namespace Test.Data.Models
{