Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support additional mapping method parameters #1400

Merged
merged 1 commit into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions docs/docs/configuration/additional-mapping-parameters.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
sidebar_position: 6
description: Additional mapping parameters
---

# Additional mapping parameters

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

A mapping method declaration can have additional parameters.
Each additional parameter is considered the same as a source member and matched by its case-insensitive name.
An additional mapping parameter has lower priority than a `MapProperty` mapping,
but higher than a by-name matched regular member mapping.

<Tabs>
<TabItem default label="Declaration" value="declaration">
```csharp
[Mapper]
public partial class CarMapper
{
// highlight-start
public partial CarDto Map(Car source, string name);
CommonGuy marked this conversation as resolved.
Show resolved Hide resolved
// highlight-end
}

public class Car
{
public string Brand { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}

public class CarDto
{
public string Brand { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
```

</TabItem>
<TabItem default label="Generated code" value="generated">
```csharp
[Mapper]
public partial class CarMapper
{
// highlight-start
public partial CarDto Map(Car source, string name)
// highlight-end
{
var target = new CarDto();
target.Brand = source.Brand;
target.Model = source.Model;
// highlight-start
target.Name = name;
// highlight-end
return target;
}
}
```
</TabItem>
</Tabs>

:::info
Mappings with additional parameters do have some limitions:

- The additional parameters are not passed to nested mappings.
- A mapping with additional mapping parameters cannot be the default mapping
(it is not used by Mapperly when encountering a nested mapping for the given types),
see also [default mapping methods](./user-implemented-methods.mdx##default-mapping-methods).
- Generic and runtime target type mappings do not support additional type parameters.
:::
2 changes: 1 addition & 1 deletion docs/docs/configuration/analyzer-diagnostics/index.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 15
sidebar_position: 18
description: A list of all analyzer diagnostics used by Mapperly and how to configure them.
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/conversions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 15
sidebar_position: 17
description: A list of conversions supported by Mapperly
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/ctor-mappings.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
description: Constructor mappings
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/derived-type-mapping.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
description: Map derived types and interfaces
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/existing-target.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
description: Map to an existing target object
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/generic-mapping.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 11
sidebar_position: 12
description: Create a generic mapping method
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/object-factories.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
description: Construct and resolve objects using object factories
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/private-member-mapping.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 13
sidebar_position: 14
description: Private member mapping
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/queryable-projections.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 14
sidebar_position: 16
description: Use queryable projections to map queryable objects and optimize ORM performance
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/reference-handling.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 12
sidebar_position: 13
description: Use reference handling to handle reference loops
---

Expand Down
10 changes: 5 additions & 5 deletions docs/docs/configuration/user-implemented-methods.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
description: Manually implement mappings
---

Expand Down Expand Up @@ -27,7 +27,7 @@ The types of the user-implemented mapping method need to match the types to map

If there are multiple user-implemented mapping methods suiting the given type-pair, by default, the first one is used.
This can be customized by using [automatic user-implemented mapping method discovery](#automatic-user-implemented-mapping-method-discovery)
and [default user-implemented mapping method](#default-user-implemented-mapping-method).
and [default mapping method](#default-mapping-methods).

## Automatic user-implemented mapping method discovery

Expand Down Expand Up @@ -82,11 +82,11 @@ public partial class CarMapper
}
```

## Default user-implemented mapping method
## Default mapping methods

Whenever Mapperly will need a mapping for a given type-pair,
it will use the default user-implemented mapping.
A user-implemented mapping is considered the default mapping for a type-pair
it will use the default mapping.
A user-implemented or user-defined mapping is considered the default mapping for a type-pair
if `Default = true` is set on the `UserMapping` attribute.
If no user-implemented mapping with `Default = true` exists and `AutoUserMappings` is enabled,
the first user-implemented mapping which has an unspecified `Default` value is used.
Expand Down
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ RMG012 | Mapper | Warning | Source member was not found for target member
RMG020 | Mapper | Warning | Source member is not mapped to any target member
RMG037 | Mapper | Warning | An enum member could not be found on the source enum
RMG038 | Mapper | Warning | An enum member could not be found on the target enum
RMG081 | Mapper | Error | A mapping method with additional parameters cannot be a default mapping
RMG082 | Mapper | Warning | An additional mapping method parameter is not mapped

### Removed Rules

Expand All @@ -192,3 +194,4 @@ RMG017 | Mapper | Warning | An init only member can have one configuration a
RMG026 | Mapper | Info | Cannot map from indexed member
RMG027 | Mapper | Warning | A constructor parameter can have one configuration at max
RMG028 | Mapper | Warning | Constructor parameter cannot handle target paths

1 change: 1 addition & 0 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
using Riok.Mapperly.Descriptors.ObjectFactories;
using Riok.Mapperly.Descriptors.UnsafeAccess;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
Expand Down
9 changes: 4 additions & 5 deletions src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Enumerables;

Expand All @@ -8,19 +9,17 @@ public record CollectionInfo(
CollectionType CollectionType,
CollectionType ImplementedTypes,
ITypeSymbol EnumeratedType,
string? CountPropertyName,
IMappableMember? CountMember,
bool HasImplicitCollectionAddMethod,
bool IsImmutableCollectionType
)
{
public bool ImplementsIEnumerable => ImplementedTypes.HasFlag(CollectionType.IEnumerable);

public bool ImplementsDictionary =
ImplementedTypes.HasFlag(CollectionType.IDictionary) || ImplementedTypes.HasFlag(CollectionType.IReadOnlyDictionary);
public bool IsArray => CollectionType is CollectionType.Array;
public bool IsMemory => CollectionType is CollectionType.Memory or CollectionType.ReadOnlyMemory;
public bool IsSpan => CollectionType is CollectionType.Span or CollectionType.ReadOnlySpan;

[MemberNotNullWhen(true, nameof(CountPropertyName))]
public bool CountIsKnown => CountPropertyName != null;
[MemberNotNullWhen(true, nameof(CountMember))]
public bool CountIsKnown => CountMember != null;
}
21 changes: 10 additions & 11 deletions src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Enumerables;

Expand Down Expand Up @@ -137,7 +138,7 @@ ITypeSymbol enumeratedType
typeInfo,
implementedTypes,
symbolAccessor.UpgradeNullable(enumeratedType),
FindCountProperty(symbolAccessor, type, typeInfo),
FindCountMember(symbolAccessor, type, typeInfo),
HasValidAddMethod(wellKnownTypes, type, typeInfo, implementedTypes),
collectionTypeInfo?.Immutable == true
);
Expand Down Expand Up @@ -218,7 +219,7 @@ or CollectionType.SortedSet
return false;
}

private static string? FindCountProperty(SymbolAccessor symbolAccessor, ITypeSymbol t, CollectionType typeInfo)
private static IMappableMember? FindCountMember(SymbolAccessor symbolAccessor, ITypeSymbol t, CollectionType typeInfo)
{
if (typeInfo is CollectionType.IEnumerable)
return null;
Expand All @@ -231,17 +232,15 @@ or CollectionType.ReadOnlySpan
or CollectionType.Memory
or CollectionType.ReadOnlyMemory
)
return "Length";
{
return symbolAccessor.GetMappableMember(t, "Length");
}

if (typeInfo is not CollectionType.None)
return "Count";

var member = symbolAccessor
.GetAllAccessibleMappableMembers(t)
.FirstOrDefault(x =>
x.Type.SpecialType == SpecialType.System_Int32 && x.Name is nameof(ICollection<object>.Count) or nameof(Array.Length)
);
return member?.Name;
return symbolAccessor.GetMappableMember(t, "Count");

var member = symbolAccessor.GetMappableMember(t, "Count") ?? symbolAccessor.GetMappableMember(t, "Length");
return member?.Type.SpecialType == SpecialType.System_Int32 ? member : null;
}

private static CollectionTypeInfo? GetCollectionTypeInfo(WellKnownTypes types, ITypeSymbol type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public static class EnsureCapacityBuilder

// if source count is known, create a simple EnsureCapacity statement
if (source.CountIsKnown)
return new EnsureCapacityMember(target.CountPropertyName, source.CountPropertyName);
{
var targetCount = target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext);
var sourceCount = source.CountMember.BuildGetter(ctx.UnsafeAccessorContext);
return new EnsureCapacityMember(targetCount, sourceCount);
}

var nonEnumeratedCountMethod = ctx
.Types.Get(typeof(Enumerable))
Expand All @@ -40,6 +44,6 @@ public static class EnsureCapacityBuilder
return null;

// if source does not have a count use GetNonEnumeratedCount, calling EnsureCapacity if count is available
return new EnsureCapacityNonEnumerated(target.CountPropertyName, nonEnumeratedCountMethod);
return new EnsureCapacityNonEnumerated(target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext), nonEnumeratedCountMethod);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;

Expand All @@ -12,15 +12,15 @@ namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
/// target.EnsureCapacity(source.Length + target.Count);
/// </code>
/// </remarks>
public class EnsureCapacityMember(string? targetAccessor, string sourceAccessor) : EnsureCapacityInfo
public class EnsureCapacityMember(IMemberGetter? targetAccessor, IMemberGetter sourceAccessor) : EnsureCapacityInfo
{
public override StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
return EnsureCapacityStatement(
ctx.SyntaxFactory,
target,
MemberAccess(ctx.Source, sourceAccessor),
targetAccessor != null ? MemberAccess(target, targetAccessor) : null
sourceAccessor.BuildAccess(ctx.Source),
targetAccessor?.BuildAccess(target)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Symbols.Members;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

Expand All @@ -18,26 +18,24 @@ namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
/// target.EnsureCapacity(sourceCount + target.Count);
/// </code>
/// </remarks>
public class EnsureCapacityNonEnumerated(string? targetAccessor, IMethodSymbol getNonEnumeratedMethod) : EnsureCapacityInfo
public class EnsureCapacityNonEnumerated(IMemberGetter? targetAccessor, IMethodSymbol getNonEnumeratedMethod) : EnsureCapacityInfo
{
private const string SourceCountVariableName = "sourceCount";

public override StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
var targetCount = targetAccessor == null ? null : MemberAccess(target, targetAccessor);
var targetCount = targetAccessor?.BuildAccess(target);

var sourceCountIdentifier = Identifier(ctx.NameBuilder.New(SourceCountVariableName));
var sourceCountName = ctx.NameBuilder.New(SourceCountVariableName);

var enumerableArgument = Argument(ctx.Source);

var outVarArgument = Argument(DeclarationExpression(VarIdentifier, SingleVariableDesignation(sourceCountIdentifier)))
.WithRefOrOutKeyword(TrailingSpacedToken(SyntaxKind.OutKeyword));
var outVarArgument = OutVarArgument(sourceCountName);

var getNonEnumeratedInvocation = StaticInvocation(getNonEnumeratedMethod, enumerableArgument, outVarArgument);
var ensureCapacity = EnsureCapacityStatement(
ctx.SyntaxFactory.AddIndentation(),
target,
IdentifierName(sourceCountIdentifier),
IdentifierName(sourceCountName),
targetCount
);
return ctx.SyntaxFactory.If(getNonEnumeratedInvocation, ensureCapacity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.FormatProviders;

Expand Down Expand Up @@ -33,7 +33,7 @@ public static FormatProviderCollection ExtractFormatProviders(SimpleMappingBuild
if (memberSymbol == null)
return null;

if (!memberSymbol.CanGet || symbol.IsStatic != isStatic || !memberSymbol.Type.Implements(ctx.Types.Get<IFormatProvider>()))
if (!memberSymbol.CanGetDirectly || symbol.IsStatic != isStatic || !memberSymbol.Type.Implements(ctx.Types.Get<IFormatProvider>()))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFormatProviderSignature, symbol, symbol.Name);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
// for inline expression mappings.
// This is not needed for regular mappings as these user defined method mappings
// are directly built (with KeepUserSymbol) and called by the other mappings.
userMapping ??= (MappingBuilder.Find(mappingKey) as IUserMapping);
userMapping ??= MappingBuilder.Find(mappingKey) as IUserMapping;
options &= ~MappingBuildingOptions.KeepUserSymbol;
return BuildMapping(userMapping, mappingKey, options, diagnosticLocation);
}
Expand Down
Loading