Skip to content

Commit

Permalink
support capacity settable members (#1526)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Oct 11, 2024
1 parent 26edf6f commit 2c3982e
Show file tree
Hide file tree
Showing 19 changed files with 249 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

internal class CapacityMemberSetter(IMappableMember targetCapacityMember, IMemberSetter setter) : ICapacityMemberSetter
{
public bool SupportsCoalesceAssignment => setter.SupportsCoalesceAssignment;

public IMappableMember TargetCapacity => targetCapacityMember;

public ExpressionSyntax BuildAssignment(
ExpressionSyntax? baseAccess,
ExpressionSyntax valueToAssign,
bool coalesceAssignment = false
) => setter.BuildAssignment(baseAccess, valueToAssign, coalesceAssignment);

public static ICapacityMemberSetter Build(MappingBuilderContext ctx, IMappableMember member) =>
new CapacityMemberSetter(member, member.BuildSetter(ctx.UnsafeAccessorContext));
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
using Microsoft.CodeAnalysis;

namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

/// <summary>
/// Generates an <see cref="EnsureCapacityInfo"/> of types <see cref="EnsureCapacityNonEnumerated"/> or <see cref="EnsureCapacityMember"/> depending on type information.
/// Generates an <see cref="ICapacitySetter"/> of types <see cref="NonEnumeratedCapacitySetter"/> or <see cref="SimpleCapacitySetter"/> depending on type information.
/// </summary>
public static class EnsureCapacityBuilder
public static class CapacitySetterBuilder
{
private const string EnsureCapacityName = "EnsureCapacity";
private const string CapacityMemberName = "Capacity";
private const string TryGetNonEnumeratedCountMethodName = "TryGetNonEnumeratedCount";

public static EnsureCapacityInfo? TryBuildEnsureCapacity(
public static ICapacitySetter? TryBuildCapacitySetter(
MappingBuilderContext ctx,
CollectionInfos collectionInfos,
bool includeTargetCount
)
{
var source = collectionInfos.Source;
var target = collectionInfos.Target;
var capacityMethod = ctx
.SymbolAccessor.GetAllMethods(target.Type, EnsureCapacityName)
.FirstOrDefault(x => x.Parameters is [{ Type.SpecialType: SpecialType.System_Int32 }] && !x.IsStatic);

// if EnsureCapacity is not available then return null
if (capacityMethod == null)
var capacitySetter = BuildCapacitySetter(ctx, target);
if (capacitySetter == null)
return null;

var targetCount = includeTargetCount ? target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext) : null;
Expand All @@ -32,7 +28,7 @@ bool includeTargetCount
if (source.CountIsKnown)
{
var sourceCount = source.CountMember.BuildGetter(ctx.UnsafeAccessorContext);
return new EnsureCapacityMember(targetCount, sourceCount);
return new SimpleCapacitySetter(capacitySetter, targetCount, sourceCount);
}

var nonEnumeratedCountMethod = ctx
Expand All @@ -49,6 +45,21 @@ bool includeTargetCount
return null;

// if source does not have a count use GetNonEnumeratedCount, calling EnsureCapacity if count is available
return new EnsureCapacityNonEnumerated(targetCount, nonEnumeratedCountMethod);
return new NonEnumeratedCapacitySetter(capacitySetter, targetCount, nonEnumeratedCountMethod);
}

private static ICapacityMemberSetter? BuildCapacitySetter(MappingBuilderContext ctx, CollectionInfo target)
{
var ensureCapacityMethod = ctx
.SymbolAccessor.GetAllMethods(target.Type, EnsureCapacityMethodSetter.EnsureCapacityMethodName)
.FirstOrDefault(x => x.Parameters is [{ Type.SpecialType: SpecialType.System_Int32 }] && !x.IsStatic);
if (ensureCapacityMethod != null)
return EnsureCapacityMethodSetter.Instance;

var member = ctx.SymbolAccessor.GetMappableMember(target.Type, CapacityMemberName);
if (member is { CanSetDirectly: true, IsInitOnly: false, Type.SpecialType: SpecialType.System_Int32 })
return CapacityMemberSetter.Build(ctx, member);

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Symbols.Members;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

/// <summary>
/// Ensures the capacity of a collection by calling `EnsureCapacity(int)`
/// </summary>
internal class EnsureCapacityMethodSetter : ICapacityMemberSetter
{
public static readonly EnsureCapacityMethodSetter Instance = new();

public const string EnsureCapacityMethodName = "EnsureCapacity";

private EnsureCapacityMethodSetter() { }

public bool SupportsCoalesceAssignment => false;

public IMappableMember? TargetCapacity => null;

public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false)
{
if (baseAccess == null)
throw new ArgumentNullException(nameof(baseAccess));

return InvocationWithoutIndention(MemberAccess(baseAccess, EnsureCapacityMethodName), valueToAssign);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

/// <summary>
/// Sets the capacity of a collection to the provided count.
/// </summary>
public interface ICapacityMemberSetter : IMemberSetter
{
IMappableMember? TargetCapacity { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

/// <summary>
/// Sets the capacity of a collection to the calculated count.
/// </summary>
public interface ICapacitySetter
{
IMappableMember? CapacityTargetMember { get; }

StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

/// <summary>
/// Represents a call to EnsureCapacity on a collection where there is an attempt
/// to get the number of elements in the source collection without enumeration,
/// to get the number of elements in the source collection without enumeration,
/// calling EnsureCapacity if it is available.
/// </summary>
/// <remarks>
Expand All @@ -18,26 +18,29 @@ namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
/// target.EnsureCapacity(sourceCount + target.Count);
/// </code>
/// </remarks>
public class EnsureCapacityNonEnumerated(IMemberGetter? targetAccessor, IMethodSymbol getNonEnumeratedMethod) : EnsureCapacityInfo
public class NonEnumeratedCapacitySetter(
ICapacityMemberSetter capacitySetter,
IMemberGetter? targetAccessor,
IMethodSymbol getNonEnumeratedMethod
) : ICapacitySetter
{
private const string SourceCountVariableName = "sourceCount";

public override StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
var targetCount = targetAccessor?.BuildAccess(target);
public IMappableMember? CapacityTargetMember => capacitySetter.TargetCapacity;

public StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
var sourceCountName = ctx.NameBuilder.New(SourceCountVariableName);
ExpressionSyntax count = IdentifierName(sourceCountName);
if (targetAccessor != null)
{
count = Add(count, targetAccessor.BuildAccess(target));
}

var enumerableArgument = Argument(ctx.Source);
var outVarArgument = OutVarArgument(sourceCountName);

var getNonEnumeratedInvocation = ctx.SyntaxFactory.StaticInvocation(getNonEnumeratedMethod, enumerableArgument, outVarArgument);
var ensureCapacity = EnsureCapacityStatement(
ctx.SyntaxFactory.AddIndentation(),
target,
IdentifierName(sourceCountName),
targetCount
);
return ctx.SyntaxFactory.If(getNonEnumeratedInvocation, ensureCapacity);
var setCapacity = ctx.SyntaxFactory.AddIndentation().ExpressionStatement(capacitySetter.BuildAssignment(target, count));
return ctx.SyntaxFactory.If(getNonEnumeratedInvocation, setCapacity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Symbols.Members;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Enumerables.Capacity;

/// <summary>
/// Represents setting the capacity on a collection where both the source and targets counts are accessible.
/// </summary>
/// <remarks>
/// <code>
/// target.EnsureCapacity(source.Length + target.Count);
/// // or
/// target.Capacity = source.Length + target.Count;
/// </code>
/// </remarks>
public class SimpleCapacitySetter(ICapacityMemberSetter capacitySetter, IMemberGetter? targetAccessor, IMemberGetter sourceAccessor)
: ICapacitySetter
{
public IMappableMember? CapacityTargetMember => capacitySetter.TargetCapacity;

public StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
var count = sourceAccessor.BuildAccess(ctx.Source);
if (targetAccessor != null)
{
count = Add(count, targetAccessor.BuildAccess(target));
}

return ctx.SyntaxFactory.ExpressionStatement(capacitySetter.BuildAssignment(target, count));
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
using Riok.Mapperly.Descriptors.Enumerables.Capacity;
using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
Expand All @@ -10,6 +10,13 @@ internal static class EnumerableMappingBodyBuilder
{
private const string SystemNamespaceName = "System";

private static readonly IReadOnlyCollection<string> _sourceCountAlias =
[
nameof(Array.Length),
nameof(List<object>.Count),
nameof(List<object>.Capacity),
];

public static void BuildMappingBody(MappingBuilderContext ctx, INewInstanceEnumerableMapping mapping)
{
var mappingCtx = new NewInstanceContainerBuilderContext<INewInstanceEnumerableMapping>(ctx, mapping);
Expand All @@ -26,9 +33,14 @@ public static void BuildMappingBody(MappingBuilderContext ctx, IEnumerableMappin
InitContext(mappingCtx);

// include the target count as the target could already include elements
if (EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx, mapping.CollectionInfos, true) is { } ensureCapacity)
if (CapacitySetterBuilder.TryBuildCapacitySetter(ctx, mapping.CollectionInfos, true) is { } capacitySetter)
{
mapping.AddEnsureCapacity(ensureCapacity);
if (capacitySetter.CapacityTargetMember != null)
{
mappingCtx.IgnoreMembers(capacitySetter.CapacityTargetMember);
}

mapping.AddCapacitySetter(capacitySetter);
}

ObjectMemberMappingBodyBuilder.BuildMappingBody(mappingCtx);
Expand All @@ -47,7 +59,7 @@ private static void InitContext<T>(MembersMappingBuilderContext<T> ctx)
private static void IgnoreSystemMembers<T>(IMembersBuilderContext<T> ctx, ITypeSymbol type)
where T : IMapping
{
// ignore all members of collection classes of the System.Private.CoreLib assembly or of arrays
// ignore all members of collection classes of the System.Private.CoreLib assembly
// as these are considered mapped by the enumerable mapping itself
// these members can still be mapped with an explicit configuration.
var systemType = type.WalkTypeHierarchy().FirstOrDefault(x => x.IsArrayType() || x.IsInRootNamespace(SystemNamespaceName));
Expand All @@ -66,9 +78,10 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext<INewInsta
// named with a well known "count" name
if (ctx.Mapping.CollectionInfos.Source.CountIsKnown)
{
ctx.TryAddSourceMemberAlias(nameof(List<object>.Capacity), ctx.Mapping.CollectionInfos.Source.CountMember);
ctx.TryAddSourceMemberAlias(nameof(List<object>.Count), ctx.Mapping.CollectionInfos.Source.CountMember);
ctx.TryAddSourceMemberAlias(nameof(Array.Length), ctx.Mapping.CollectionInfos.Source.CountMember);
foreach (var countAlias in _sourceCountAlias)
{
ctx.TryAddSourceMemberAlias(countAlias, ctx.Mapping.CollectionInfos.Source.CountMember);
}
}

// always prefer parameterized constructor for system collections (to map capacity correctly)
Expand All @@ -87,15 +100,20 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext<INewInsta
// do not include the target count as the instance is just created by the ctor
if (
!countIsMapped
&& EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx.BuilderContext, ctx.Mapping.CollectionInfos, false) is { } ensureCapacity
&& CapacitySetterBuilder.TryBuildCapacitySetter(ctx.BuilderContext, ctx.Mapping.CollectionInfos, false) is { } capacitySetter
)
{
if (ctx.Mapping.CollectionInfos.Source.CountIsKnown)
{
ctx.IgnoreMembers(ctx.Mapping.CollectionInfos.Source.CountMember);
}

ctx.Mapping.AddEnsureCapacity(ensureCapacity);
if (capacitySetter.CapacityTargetMember != null)
{
ctx.IgnoreMembers(capacitySetter.CapacityTargetMember);
}

ctx.Mapping.AddCapacitySetter(capacitySetter);
}
}
}
Loading

0 comments on commit 2c3982e

Please sign in to comment.