Prevent generating the secondary constructor if one is available in the destination class.
This commit is contained in:
parent
2f8a897676
commit
9e662c7e90
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue