Skip to content

Commit

Permalink
fix: unique method names
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz committed Feb 16, 2022
1 parent 0fe04ec commit 7f50a43
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 12 deletions.
11 changes: 10 additions & 1 deletion src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class DescriptorBuilder
// queue of mappings which don't have the body built yet
private readonly Queue<(TypeMapping, MappingBuilderContext)> _mappingsToBuildBody = new();

private readonly MethodNameBuilder _methodNameBuilder = new();

public DescriptorBuilder(
SourceProductionContext sourceContext,
Compilation compilation,
Expand Down Expand Up @@ -95,6 +97,7 @@ private string BuildName()
: _mapperSymbol.Name + MapperDescriptor.ImplClassNameSuffix;
}


public MapperDescriptor Build()
{
// extract mappings from declarations
Expand All @@ -107,7 +110,7 @@ public MapperDescriptor Build()
this,
userMapping.SourceType,
userMapping.TargetType,
(userMapping as IHasUserSymbolMapping)?.Method);
(userMapping as IUserMapping)?.Method);
_mappingsToBuildBody.Enqueue((userMapping, ctx));
}

Expand Down Expand Up @@ -169,6 +172,11 @@ internal void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? locati

private void BuildMappingBody(MappingBuilderContext ctx, TypeMapping typeMapping)
{
if (typeMapping is not MethodMapping methodMapping)
return;

methodMapping.SetMethodNameIfNeeded(_methodNameBuilder.Build);

switch (typeMapping)
{
case ObjectPropertyMapping mapping:
Expand All @@ -185,6 +193,7 @@ private void AddMapping(TypeMapping mapping)

private void AddUserMapping(TypeMapping mapping)
{
_methodNameBuilder.Add(((IUserMapping)mapping).Method.Name);
if (mapping.CallableByOtherMappings && FindMapping(mapping.SourceType, mapping.TargetType) is null)
{
AddMapping(mapping);
Expand Down
27 changes: 27 additions & 0 deletions src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors;

internal class MethodNameBuilder
{
private const string MethodNamePrefix = "MapTo";
private readonly HashSet<string> _usedNames = new();

internal void Add(string name)
=> _usedNames.Add(name);

internal string Build(MethodMapping mapping)
{
var i = 0;
var prefix = MethodNamePrefix + mapping.TargetType.NonNullable().Name;
var name = prefix;
while (!_usedNames.Add(name))
{
i++;
name = prefix + i;
}

return name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
/// <summary>
/// A user defined / implemented mapping.
/// </summary>
public interface IHasUserSymbolMapping
public interface IUserMapping
{
IMethodSymbol Method { get; }
}
14 changes: 11 additions & 3 deletions src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;

Expand All @@ -13,7 +12,7 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
public abstract class MethodMapping : TypeMapping
{
private const string SourceParamName = "source";
private const string MappingMethodNamePrefix = "MapTo";
private string? _methodName;

protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType)
{
Expand All @@ -23,7 +22,11 @@ protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(s

protected bool Override { get; set; }

protected virtual string MethodName => MappingMethodNamePrefix + TargetType.NonNullable().Name;
protected string MethodName
{
get => _methodName ?? throw new InvalidOperationException();
set => _methodName = value;
}

public override ExpressionSyntax Build(ExpressionSyntax source)
=> Invocation(MethodName, source);
Expand All @@ -43,6 +46,11 @@ public MethodDeclarationSyntax BuildMethod()

public abstract IEnumerable<StatementSyntax> BuildBody(ExpressionSyntax source);

internal void SetMethodNameIfNeeded(Func<MethodMapping, string> methodNameBuilder)
{
_methodName ??= methodNameBuilder(this);
}

protected virtual ITypeSymbol? ReturnType => TargetType;

protected virtual IEnumerable<ParameterSyntax> BuildParameters()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
/// Represents a mapping method declared but not implemented by the user which reuses an existing target object instance.
/// Is implicitly an <see cref="ObjectPropertyMapping"/>, since no other mappings work with an existing target object instance.
/// </summary>
public class UserDefinedExistingInstanceMethodMapping : ObjectPropertyMapping, IHasUserSymbolMapping
public class UserDefinedExistingInstanceMethodMapping : ObjectPropertyMapping, IUserMapping
{
public UserDefinedExistingInstanceMethodMapping(
IMethodSymbol method,
Expand All @@ -20,14 +20,13 @@ public UserDefinedExistingInstanceMethodMapping(
Override = isAbstractMapperDefinition;
Accessibility = Accessibility.Public;
Method = method;
MethodName = method.Name;
}

public IMethodSymbol Method { get; }

private IParameterSymbol TargetParameter => Method.Parameters[1];

protected override string MethodName => Method.Name;

public override bool CallableByOtherMappings => false;

public override ExpressionSyntax Build(ExpressionSyntax source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
/// <summary>
/// Represents a mapping method declared but not implemented by the user which results in a new target object instance.
/// </summary>
public class UserDefinedNewInstanceMethodMapping : MethodMapping, IHasUserSymbolMapping
public class UserDefinedNewInstanceMethodMapping : MethodMapping, IUserMapping
{
private const string NoMappingComment = "// Could not generate mapping";

Expand All @@ -18,14 +18,13 @@ public UserDefinedNewInstanceMethodMapping(IMethodSymbol method, bool isAbstract
Override = isAbstractMapperDefinition;
Accessibility = Accessibility.Public;
Method = method;
MethodName = method.Name;
}

public IMethodSymbol Method { get; }

public TypeMapping? DelegateMapping { get; set; }

protected override string MethodName => Method.Name;

public override IEnumerable<StatementSyntax> BuildBody(ExpressionSyntax source)
{
if (DelegateMapping == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
/// <summary>
/// Represents a mapping method on the mapper which is implemented by the user.
/// </summary>
public class UserImplementedMethodMapping : TypeMapping, IHasUserSymbolMapping
public class UserImplementedMethodMapping : TypeMapping, IUserMapping
{
public UserImplementedMethodMapping(IMethodSymbol method)
: base(method.Parameters.Single().Type, method.ReturnType)
Expand Down
1 change: 1 addition & 0 deletions test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@
</PackageReference>
<PackageReference Include="Meziantou.Xunit.ParallelTestFramework" Version="1.0.0"/>
<PackageReference Include="Verify.XUnit" Version="16.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
</Project>
48 changes: 48 additions & 0 deletions test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Moq;
using Riok.Mapperly.Descriptors;
using Riok.Mapperly.Descriptors.TypeMappings;

namespace Riok.Mapperly.Tests.Descriptors;

public class MethodNameBuilderTest
{
[Fact]
public void ShouldGenerateUniqueMethodNames()
{
var builder = new MethodNameBuilder();
builder.Add("MapToA");
builder.Build(NewMethodMappingMock("A"))
.Should()
.BeEquivalentTo("MapToA1");
builder.Build(NewMethodMappingMock("A"))
.Should()
.BeEquivalentTo("MapToA2");
builder.Build(NewMethodMappingMock("B"))
.Should()
.BeEquivalentTo("MapToB");
builder.Build(NewMethodMappingMock("B"))
.Should()
.BeEquivalentTo("MapToB1");
}

private MethodMapping NewMethodMappingMock(string targetTypeName)
{
var targetTypeMock = new Mock<ITypeSymbol>();
targetTypeMock.Setup(x => x.Name).Returns(targetTypeName);
targetTypeMock.Setup(x => x.NullableAnnotation).Returns(NullableAnnotation.NotAnnotated);

return new MockedMethodMapping(targetTypeMock.Object);
}

private class MockedMethodMapping : MethodMapping
{
public MockedMethodMapping(ITypeSymbol t) : base(t, t)
{
}

public override IEnumerable<StatementSyntax> BuildBody(ExpressionSyntax source)
=> Enumerable.Empty<StatementSyntax>();
}
}
15 changes: 15 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@ public void WithMultipleUserDefinedMethodDifferentConfigShouldWork()
return target;".ReplaceLineEndings());
}

[Fact]
public void WithSameNamesShouldGenerateUniqueMethodNames()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"B MapToB(A source);",
TestSourceBuilderOptions.WithDeepCloning,
"class A { public B? Value { get; set; } }",
"class B { public B? Value { get; set; } }");

TestHelper.GenerateMapperMethodBodies(source)
.Select(x => x.Name)
.Should()
.BeEquivalentTo("MapToB", "MapToB2");
}

[Fact]
public Task WithInvalidSignatureShouldDiagnostic()
{
Expand Down

0 comments on commit 7f50a43

Please sign in to comment.