From 4a3d5ac174455f305fb654c9e86d6a9668200f1e Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Sun, 19 Nov 2023 12:00:52 +0000 Subject: [PATCH] feat: add `MapDerivedType` for existing target type mapping --- .../DerivedTypeMappingBuilder.cs | 75 +++++ .../ExistingTargetMappingBuilder.cs | 1 + .../DerivedExistingTargetTypeMapping.cs | 83 +++++ .../Mappings/TypeMappingBuildContext.cs | 8 + .../Syntax/SyntaxFactoryHelper.Pattern.cs | 7 + .../Emit/Syntax/SyntaxFactoryHelper.Switch.cs | 17 + .../Mapping/DerivedExistingTargetTypeTest.cs | 294 ++++++++++++++++++ 7 files changed, 485 insertions(+) create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeMapping.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs index 1ebf6798c89..e3db731e784 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; @@ -19,6 +20,15 @@ public static class DerivedTypeMappingBuilder : new DerivedTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings); } + public static IExistingTargetMapping? TryBuildExistingTargetMapping(MappingBuilderContext ctx) + { + var derivedTypeMappings = TryBuildExistingTargetContainedMappings(ctx); + if (derivedTypeMappings == null) + return null; + + return new DerivedExistingTargetTypeMapping(ctx.Source, ctx.Target, derivedTypeMappings); + } + public static IReadOnlyCollection? TryBuildContainedMappings( MappingBuilderContext ctx, bool duplicatedSourceTypesAllowed = false @@ -29,6 +39,16 @@ public static class DerivedTypeMappingBuilder : BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, duplicatedSourceTypesAllowed); } + public static IReadOnlyCollection? TryBuildExistingTargetContainedMappings( + MappingBuilderContext ctx, + bool duplicatedSourceTypesAllowed = false + ) + { + return ctx.Configuration.DerivedTypes.Count == 0 + ? null + : BuildExistingTargetContainedMappings(ctx, ctx.Configuration.DerivedTypes, duplicatedSourceTypesAllowed); + } + private static IReadOnlyCollection BuildContainedMappings( MappingBuilderContext ctx, IReadOnlyCollection configs, @@ -83,4 +103,59 @@ bool duplicatedSourceTypesAllowed return derivedTypeMappings; } + + private static IReadOnlyCollection BuildExistingTargetContainedMappings( + MappingBuilderContext ctx, + IReadOnlyCollection configs, + bool duplicatedSourceTypesAllowed + ) + { + var derivedTypeMappingSourceTypes = new HashSet(SymbolEqualityComparer.Default); + var derivedTypeMappings = new List(configs.Count); + Func isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter + ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t, ctx.Source.NullableAnnotation) + : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source); + Func isAssignableToTarget = ctx.Target is ITypeParameterSymbol targetTypeParameter + ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(targetTypeParameter, t, ctx.Target.NullableAnnotation) + : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Target); + + foreach (var config in configs) + { + // set types non-nullable as they can never be null when type-switching. + var sourceType = config.SourceType.NonNullable(); + if (!duplicatedSourceTypesAllowed && !derivedTypeMappingSourceTypes.Add(sourceType)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeDuplicated, sourceType); + continue; + } + + if (!isAssignableToSource(sourceType)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, sourceType, ctx.Source); + continue; + } + + var targetType = config.TargetType.NonNullable(); + if (!isAssignableToTarget(targetType)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target); + continue; + } + + var mapping = ctx.FindOrBuildExistingTargetMapping( + sourceType, + targetType, + MappingBuildingOptions.KeepUserSymbol | MappingBuildingOptions.MarkAsReusable | MappingBuildingOptions.ClearDerivedTypes + ); + if (mapping == null) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.CouldNotCreateMapping, sourceType, targetType); + continue; + } + + derivedTypeMappings.Add(mapping); + } + + return derivedTypeMappings; + } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs index 64a82a16813..8984d009851 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs @@ -10,6 +10,7 @@ public class ExistingTargetMappingBuilder private static readonly IReadOnlyCollection _builders = new BuildExistingTargetMapping[] { NullableMappingBuilder.TryBuildExistingTargetMapping, + DerivedTypeMappingBuilder.TryBuildExistingTargetMapping, DictionaryMappingBuilder.TryBuildExistingTargetMapping, SpanMappingBuilder.TryBuildExistingTargetMapping, MemoryMappingBuilder.TryBuildExistingTargetMapping, diff --git a/src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeMapping.cs new file mode 100644 index 00000000000..041f902157a --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeMapping.cs @@ -0,0 +1,83 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; +using Riok.Mapperly.Emit.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.Mappings; + +/// +/// A derived type mapping maps one base type or interface to another +/// by implementing a if with instance checks over known types and performs the provided mapping for each type. +/// +public class DerivedExistingTargetTypeMapping : ExistingTargetMapping +{ + private const string SourceName = "source"; + private const string TargetName = "target"; + private const string GetTypeMethodName = nameof(GetType); + private readonly IReadOnlyCollection _existingTargetTypeMappings; + + public DerivedExistingTargetTypeMapping( + ITypeSymbol sourceType, + ITypeSymbol targetType, + IReadOnlyCollection existingTargetTypeMappings + ) + : base(sourceType, targetType) + { + _existingTargetTypeMappings = existingTargetTypeMappings; + } + + public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax target) + { + var sourceExpression = TupleExpression(CommaSeparatedList(Argument(ctx.Source), Argument(target))); + var sections = _existingTargetTypeMappings.Select(x => BuildSwitchSection(ctx, x)).Append(BuildDefaultSwitchSection(ctx)); + + yield return ctx.SyntaxFactory.SwitchStatement(sourceExpression, List(sections)).AddLeadingLineFeed(ctx.SyntaxFactory.Indentation); + } + + private SwitchSectionSyntax BuildSwitchSection(TypeMappingBuildContext ctx, IExistingTargetMapping mapping) + { + // MapToB(source, target); + var (sectionCtx, sourceVariableName) = ctx.WithNewScopedSource(SourceName); + var targetVariableName = sectionCtx.NameBuilder.New(TargetName); + sectionCtx = sectionCtx.AddIndentation(); + + var positionalTypeMatch = PositionalPatternClause( + CommaSeparatedList( + Subpattern(DeclarationPattern(mapping.SourceType, sourceVariableName)), + Subpattern(DeclarationPattern(mapping.TargetType, targetVariableName)) + ) + ); + + var pattern = RecursivePattern().WithPositionalPatternClause(positionalTypeMatch); + var caseLabel = CasePatternSwitchLabel(pattern).AddLeadingLineFeed(sectionCtx.SyntaxFactory.Indentation); + + var target = IdentifierName(targetVariableName); + + var statementContext = sectionCtx.AddIndentation(); + var statements = List( + mapping.Build(statementContext, target).Append(BreakStatement().AddLeadingLineFeed(statementContext.SyntaxFactory.Indentation)) + ); + var section = SwitchSection().WithLabels(SingletonList(caseLabel)).WithStatements(statements); + + return section; + } + + private SwitchSectionSyntax BuildDefaultSwitchSection(TypeMappingBuildContext ctx) + { + // default: + var defaultCaseLabel = DefaultSwitchLabel().AddLeadingLineFeed(ctx.SyntaxFactory.Indentation + 1); + + // throw new ArgumentException(msg, nameof(ctx.Source)), + var sourceType = Invocation(MemberAccess(ctx.Source, GetTypeMethodName)); + var throwExpression = ThrowArgumentExpression( + InterpolatedString($"Cannot map {sourceType} to {TargetType.ToDisplayString()} as there is no known derived type mapping"), + ctx.Source + ) + .AddLeadingLineFeed(ctx.SyntaxFactory.Indentation + 2); + + var stat = new StatementSyntax[] { ExpressionStatement(throwExpression) }; + return SwitchSection().WithLabels(SingletonList(defaultCaseLabel)).WithStatements(List(stat)); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs b/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs index afadf1584dd..670ead1729e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs @@ -63,6 +63,14 @@ SyntaxFactoryHelper syntaxFactory return (ctx, scopedSourceName); } + public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(string sourceName) + { + var scopedNameBuilder = NameBuilder.NewScope(); + var scopedSourceName = scopedNameBuilder.New(sourceName); + var ctx = new TypeMappingBuildContext(IdentifierName(scopedSourceName), ReferenceHandler, scopedNameBuilder, SyntaxFactory); + return (ctx, scopedSourceName); + } + public (TypeMappingBuildContext Context, string SourceName) WithNewSource(string sourceName = DefaultSourceName) { var scopedSourceName = NameBuilder.New(sourceName); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs index 4bad9249ca4..1322a1880bb 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Helpers; @@ -16,6 +17,12 @@ public static PatternSyntax OrPattern(IEnumerable values) => public static IsPatternExpressionSyntax IsPattern(ExpressionSyntax expression, PatternSyntax pattern) => IsPatternExpression(expression, SpacedToken(SyntaxKind.IsKeyword), pattern); + public static DeclarationPatternSyntax DeclarationPattern(ITypeSymbol type, string designation) => + SyntaxFactory.DeclarationPattern( + FullyQualifiedIdentifier(type).AddTrailingSpace(), + SingleVariableDesignation(Identifier(designation)) + ); + private static BinaryPatternSyntax BinaryPattern(SyntaxKind kind, PatternSyntax left, PatternSyntax right) { var binaryPattern = SyntaxFactory.BinaryPattern(kind, left, right); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs index dfcc32c4331..8059bfab165 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs @@ -23,4 +23,21 @@ public static SwitchExpressionArmSyntax SwitchArm(PatternSyntax pattern, Express } public static WhenClauseSyntax SwitchWhen(ExpressionSyntax condition) => WhenClause(SpacedToken(SyntaxKind.WhenKeyword), condition); + + public SwitchStatementSyntax SwitchStatement(ExpressionSyntax governingExpression, IEnumerable sections) + { + return SyntaxFactory.SwitchStatement( + default, + TrailingSpacedToken(SyntaxKind.SwitchKeyword), + Token(SyntaxKind.None), + governingExpression, + Token(SyntaxKind.None), + LeadingLineFeedToken(SyntaxKind.OpenBraceToken), + List(sections), + LeadingLineFeedToken(SyntaxKind.CloseBraceToken) + ); + } + + public static CasePatternSwitchLabelSyntax CasePatternSwitchLabel(PatternSyntax pattern) => + SyntaxFactory.CasePatternSwitchLabel(TrailingSpacedToken(SyntaxKind.CaseKeyword), pattern, null, Token(SyntaxKind.ColonToken)); } diff --git a/test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs b/test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs new file mode 100644 index 00000000000..9d28e626f19 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs @@ -0,0 +1,294 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class DerivedExistingTargetTypeTest +{ + [Fact] + public Task WithAbstractBaseClassShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "abstract class A { public string BaseValue { get; set; } }", + "abstract class B { public string BaseValue { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value1 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value1 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithAbstractBaseClassAndNonGenericInterfaceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType(typeof(ASubType1), typeof(BSubType1))] + [MapDerivedType(typeof(ASubType2), typeof(BSubType2))] + public partial void Map(A src, B trg); + """, + "abstract class A { public string BaseValue { get; set; } }", + "abstract class B { public string BaseValue { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value2 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfaceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfaceSourceNullableShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A? src, B trg); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfaceSourceAndTargetNullableShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A? src, B? trg); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithObjectShouldWork() + { + var source = TestSourceBuilder.MapperWithBody( + """ + [MapDerivedType] + [MapDerivedType] + [MapDerivedType, IEnumerable>] + public partial Map(object src, B trg); + """ + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void NotAssignableTargetTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "interface A {}", + "interface B {}", + "class AImpl1 : A { }", + "class BImpl1 { }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, + "Derived target type BImpl1 is not assignable to return type B" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public Task WithEnumerableOfInterfaceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial IEnumerable Map(IEnumerable source); + + [MapDerivedType] + [MapDerivedType] + private partial B Map(A src); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfacePropertyShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial B Map(A source); + + [MapDerivedType] + [MapDerivedType] + private partial BIntf Map(AIntf src); + """, + "class A { public AIntf Value { get; set; } }", + "class B { public BIntf Value { get; set; } }", + "interface AIntf { string BaseValue { get; set; } }", + "interface BIntf { string BaseValue { get; set; }}", + "class AImpl1 : AIntf { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : AIntf { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : BIntf { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : BIntf { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithBaseTypeConfigShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + [MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)] + public partial B Map(A src); + """, + "abstract class A { public string BaseValueA { get; set; } }", + "abstract class B { public string BaseValueB { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value2 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithBaseTypeConfigAndSeparateMethodShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + [MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)] + public partial B Map(A src); + + [MapperIgnoreSource(nameof(A.BaseValueA)] + [MapperIgnoreTarget(nameof(B.BaseValueB)] + public partial BSubType1 Map(ASubType1 src); + """, + "abstract class A { public string BaseValueA { get; set; } }", + "abstract class B { public string BaseValueB { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value2 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void NotAssignableSourceTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + public partial B Map(A src); + """, + "interface A {}", + "interface B {}", + "class AImpl1 { }", + "class BImpl1 : B { }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, + "Derived source type AImpl1 is not assignable to parameter type A" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void DuplicatedSourceTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial B Map(A src); + """, + "interface A {}", + "interface B {}", + "class AImpl1 : A { }", + "class BImpl1 : B { }", + "class BImpl2 : B { }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.DerivedSourceTypeDuplicated, + "Derived source type AImpl1 is specified multiple times, a source type may only be specified once" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void NotMappableShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBody( + """ + [MapDerivedType] + public partial object Map(object src); + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.CouldNotCreateMapping, + "Could not create mapping from System.Version to int. Consider implementing the mapping manually." + ) + .HaveAssertedAllDiagnostics(); + } +}