diff --git a/src/MapTo/DiagnosticsFactory.cs b/src/MapTo/DiagnosticsFactory.cs index 1c0c56f..d5f73a3 100644 --- a/src/MapTo/DiagnosticsFactory.cs +++ b/src/MapTo/DiagnosticsFactory.cs @@ -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); } } \ No newline at end of file diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index 4ca5a24..b65b769 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -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 _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() + .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() .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; } } } \ No newline at end of file diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index d23d71e..6769e67 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -29,7 +29,8 @@ namespace MapTo string SourceClassFullName, ImmutableArray MappedProperties, bool HasMappedBaseClass, - ImmutableArray Usings + ImmutableArray Usings, + bool GenerateSecondaryConstructor ); internal record SourceGenerationOptions( diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 72c435e..58ed9a1 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -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"); } diff --git a/test/MapTo.Tests/Extensions/RoslynExtensions.cs b/test/MapTo.Tests/Extensions/RoslynExtensions.cs index 1c4cc91..98d5b70 100644 --- a/test/MapTo.Tests/Extensions/RoslynExtensions.cs +++ b/test/MapTo.Tests/Extensions/RoslynExtensions.cs @@ -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(); diff --git a/test/MapTo.Tests/MappedClassesTests.cs b/test/MapTo.Tests/MappedClassesTests.cs index 8e215b2..a425c54 100644 --- a/test/MapTo.Tests/MappedClassesTests.cs +++ b/test/MapTo.Tests/MappedClassesTests.cs @@ -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() + .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() + .Single(); + + diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax)); + } + private static string NestedSourceClass => @" namespace Test.Data.Models {