Skip to content

Commit

Permalink
feat: improve ToString handling and use simplest overloads available (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Nov 28, 2023
1 parent cac2f2e commit 574cef7
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 120 deletions.
7 changes: 4 additions & 3 deletions docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict

### String format

The string format passed to `ToString` calls when converting to a string (using `IFormattable`) can be customized
The string format passed to `ToString` calls when converting to a string can be customized
by using the `StringFormat` property of the `MapPropertyAttribute`.

```csharp
Expand All @@ -245,13 +245,14 @@ public partial class CarMapper

### String format provider & culture

To customize the format provider / culture to be used by Mapperly when calling `ToString` (using `IFormattable`)
To customize the format provider / culture to be used by Mapperly when calling `ToString`
format providers can be used.
A format provider can be provided to Mapperly by simply annotating a field or property within the Mapper with the `FormatProviderAttribute`.
The field/property need to return a type implementing `System.IFormatProvider`.
Format providers can be referenced by the name of the property / field in `MapPropertyAttribute.FormatProvider`.
A format provider can be marked as default (set the default property of the `FormatProviderAttribute` to true).
A default format provider is used for all `ToString` conversions when the source implements `System.IFormattable`.
A default format provider is used for all `ToString` conversions
when the source implements has a `ToString` overload accepting a `System.IFormatProvider`.
In a mapper only one format provider can be marked as default.

```csharp
Expand Down
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ RMG051 | Mapper | Warning | Invalid ignore source member found, nested ignor
RMG052 | Mapper | Warning | Invalid ignore target member found, nested ignores are not supported
RMG053 | Mapper | Error | The flag MemberVisibility.Accessible cannot be disabled, this feature requires .NET 8.0 or greater
RMG054 | Mapper | Error | Mapper class containing 'static partial' method must not have any instance methods
RMG055 | Mapper | Error | The source type does not implement IFormattable, string format and format provider cannot be applied
RMG055 | Mapper | Error | The source type does not implement ToString with the provided formatting parameters, string format and format provider cannot be applied
RMG056 | Mapper | Error | Invalid format provider signature
RMG057 | Mapper | Error | Format provider not found
RMG058 | Mapper | Error | Multiple default format providers found, only one is allowed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ public class FormatProviderCollection(
FormatProvider? defaultFormatProvider
)
{
public FormatProvider? Get(string? reference)
public (FormatProvider? formatProvider, bool isDefault) Get(string? reference)
{
return reference == null ? defaultFormatProvider : formatProvidersByName.GetValueOrDefault(reference);
return reference == null ? (defaultFormatProvider, true) : (formatProvidersByName.GetValueOrDefault(reference), false);
}
}
6 changes: 3 additions & 3 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,15 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, params object[] me
public NullFallbackValue GetNullFallbackValue(ITypeSymbol? targetType = null) =>
GetNullFallbackValue(targetType ?? Target, MapperConfiguration.ThrowOnMappingNullMismatch);

public FormatProvider? GetFormatProvider(string? formatProviderName)
public (FormatProvider? formatProvider, bool isDefault) GetFormatProvider(string? formatProviderName)
{
var formatProvider = _formatProviders.Get(formatProviderName);
var (formatProvider, isDefault) = _formatProviders.Get(formatProviderName);
if (formatProviderName != null && formatProvider == null)
{
ReportDiagnostic(DiagnosticDescriptors.FormatProviderNotFound, formatProviderName);
}

return formatProvider;
return (formatProvider, isDefault);
}

protected virtual NullFallbackValue GetNullFallbackValue(ITypeSymbol targetType, bool throwOnMappingNullMismatch)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;
Expand All @@ -21,7 +20,6 @@ public static class ImplicitCastMappingBuilder
if (ctx.Target.IsTupleType)
return null;

var conversion = ctx.Compilation.ClassifyConversion(ctx.Source, ctx.Target);
return conversion.IsImplicit ? new CastMapping(ctx.Source, ctx.Target) : null;
return ctx.SymbolAccessor.HasImplicitConversion(ctx.Source, ctx.Target) ? new CastMapping(ctx.Source, ctx.Target) : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,89 @@ public static class ToStringMappingBuilder
if (ctx.Target.SpecialType != SpecialType.System_String)
return null;

var formatProvider = ctx.GetFormatProvider(ctx.MappingKey.Configuration.FormatProviderName);
if (ctx.MappingKey.Configuration.StringFormat == null && formatProvider == null)
var (formatProvider, formatProviderIsDefault) = ctx.GetFormatProvider(ctx.MappingKey.Configuration.FormatProviderName);
var stringFormat = ctx.MappingKey.Configuration.StringFormat;
if (stringFormat == null && formatProvider == null)
return new ToStringMapping(ctx.Source, ctx.Target);

if (!ctx.Source.Implements(ctx.Types.Get<IFormattable>()))
return (stringFormat, formatProvider, formatProviderIsDefault) switch
{
ctx.ReportDiagnostic(DiagnosticDescriptors.SourceDoesNotImplementIFormattable, ctx.Source);
return new ToStringMapping(ctx.Source, ctx.Target);
// ToString(string, IFormatProvider)
(not null, not null, _) when HasToStringMethod(ctx, true, true)
=> new ToStringMapping(ctx.Source, ctx.Target, stringFormat, formatProvider.Name),

// ToString(string)
(not null, not null, true) when HasToStringMethod(ctx, true, false)
=> new ToStringMapping(ctx.Source, ctx.Target, stringFormat),

// ToString(string)
(not null, null, _) when HasToStringMethod(ctx, true, false) => new ToStringMapping(ctx.Source, ctx.Target, stringFormat),

// ToString(string, null)
(not null, null, _) when HasToStringMethodWithNullableParameter(ctx, 1)
=> new ToStringMapping(ctx.Source, ctx.Target, stringFormat, simpleInvocation: false),

// ToString(IFormatProvider)
(null, not null, _) when HasToStringMethod(ctx, false, true)
=> new ToStringMapping(ctx.Source, ctx.Target, formatProviderName: formatProvider.Name),

// ToString(null, IFormatProvider)
(null, not null, _) when HasToStringMethodWithNullableParameter(ctx, 0)
=> new ToStringMapping(ctx.Source, ctx.Target, formatProviderName: formatProvider.Name, simpleInvocation: false),

// ToString()
(null, not null, true) => new ToStringMapping(ctx.Source, ctx.Target),

_ => ReportDiagnosticAndBuildUnformattedMapping(ctx),
};
}

private static ToStringMapping ReportDiagnosticAndBuildUnformattedMapping(MappingBuilderContext ctx)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.SourceDoesNotImplementToStringWithFormatParameters, ctx.Source);
return new ToStringMapping(ctx.Source, ctx.Target);
}

private static bool HasToStringMethod(MappingBuilderContext ctx, bool stringFormatParam, bool formatProviderParam) =>
FindToStringMethod(ctx, stringFormatParam, formatProviderParam) != null;

private static bool HasToStringMethodWithNullableParameter(MappingBuilderContext ctx, int nullableParameterIndex) =>
FindToStringMethod(ctx, true, true) is { } m && m.Parameters[nullableParameterIndex].NullableAnnotation.IsNullable();

private static IMethodSymbol? FindToStringMethod(MappingBuilderContext ctx, bool stringFormatParam, bool formatProviderParam)
{
return ctx.SymbolAccessor
.GetAllMethods(ctx.Source, nameof(ToString))
.FirstOrDefault(m => IsToStringMethod(ctx, m, stringFormatParam, formatProviderParam));
}

private static bool IsToStringMethod(MappingBuilderContext ctx, IMethodSymbol method, bool stringFormatParam, bool formatProviderParam)
{
if (
method
is not {
MethodKind: MethodKind.Ordinary,
IsAsync: false,
ReturnType.SpecialType: SpecialType.System_String,
Parameters.Length: 1 or 2,
IsGenericMethod: false
}
)
{
return false;
}

return new ToStringMapping(ctx.Source, ctx.Target, ctx.MappingKey.Configuration.StringFormat, formatProvider?.Name);
return (stringFormatParam, formatProviderParam) switch
{
(true, true)
=> method.Parameters.Length == 2
&& method.Parameters[0].Type.SpecialType == SpecialType.System_String
&& SymbolEqualityComparer.Default.Equals(method.Parameters[1].Type, ctx.Types.Get<IFormatProvider>()),
(true, false) => method.Parameters is [{ Type.SpecialType: SpecialType.System_String }],
(false, true)
=> method.Parameters.Length == 1
&& SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, ctx.Types.Get<IFormatProvider>()),
_ => false,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Helpers;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings;
Expand All @@ -21,9 +22,9 @@ public class SourceObjectMethodMapping(
{
public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
{
var sourceExpression = Invocation(MemberAccess(ctx.Source, methodName), BuildArguments(ctx).ToArray());
var sourceExpression = Invocation(MemberAccess(ctx.Source, methodName), BuildArguments(ctx).WhereNotNull().ToArray());
return delegateMapping == null ? sourceExpression : delegateMapping.Build(ctx.WithSource(sourceExpression));
}

protected virtual IEnumerable<ExpressionSyntax> BuildArguments(TypeMappingBuildContext ctx) => Enumerable.Empty<ExpressionSyntax>();
protected virtual IEnumerable<ExpressionSyntax?> BuildArguments(TypeMappingBuildContext ctx) => Enumerable.Empty<ExpressionSyntax?>();
}
29 changes: 21 additions & 8 deletions src/Riok.Mapperly/Descriptors/Mappings/ToStringMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,29 @@ namespace Riok.Mapperly.Descriptors.Mappings;
/// <code>
/// target = source.ToString();
/// </code>
/// <param name="simpleInvocation">
/// When true, <c>null</c> parameters are not emitted,
/// when false, <c>null</c> parameters are emitted as <c>null</c> literals..</param>
/// </summary>
public class ToStringMapping(ITypeSymbol sourceType, ITypeSymbol targetType, string? stringFormat = null, string? formatProviderName = null)
: SourceObjectMethodMapping(sourceType, targetType, nameof(ToString))
public class ToStringMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType,
string? stringFormat = null,
string? formatProviderName = null,
bool simpleInvocation = true
) : SourceObjectMethodMapping(sourceType, targetType, nameof(ToString))
{
protected override IEnumerable<ExpressionSyntax> BuildArguments(TypeMappingBuildContext ctx)
protected override IEnumerable<ExpressionSyntax?> BuildArguments(TypeMappingBuildContext ctx)
{
if (stringFormat == null && formatProviderName == null)
yield break;

yield return stringFormat == null ? NullLiteral() : StringLiteral(stringFormat);
yield return formatProviderName == null ? NullLiteral() : IdentifierName(formatProviderName);
yield return stringFormat != null
? StringLiteral(stringFormat)
: simpleInvocation
? null
: NullLiteral();
yield return formatProviderName != null
? IdentifierName(formatProviderName)
: simpleInvocation
? null
: NullLiteral();
}
}
6 changes: 3 additions & 3 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,10 +474,10 @@ public static class DiagnosticDescriptors
true
);

public static readonly DiagnosticDescriptor SourceDoesNotImplementIFormattable = new DiagnosticDescriptor(
public static readonly DiagnosticDescriptor SourceDoesNotImplementToStringWithFormatParameters = new DiagnosticDescriptor(
"RMG055",
$"The source type does not implement {nameof(IFormattable)}, string format and format provider cannot be applied",
$"The source type {{0}} does not implement {nameof(IFormattable)}, string format and format provider cannot be applied",
$"The source type does not implement {nameof(ToString)} with the provided formatting parameters, string format and format provider cannot be applied",
$"The source type {{0}} does not implement {nameof(ToString)} with the provided formatting parameters, string format and format provider cannot be applied",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
Expand Down
3 changes: 2 additions & 1 deletion src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,6 @@ internal static bool IsNullable(this ITypeParameterSymbol typeParameter, Nullabl
return typeParameter.HasReferenceTypeConstraint && typeParameter.ReferenceTypeConstraintNullableAnnotation.IsNullable();
}

private static bool IsNullable(this NullableAnnotation nullable) => nullable is NullableAnnotation.Annotated or NullableAnnotation.None;
internal static bool IsNullable(this NullableAnnotation nullable) =>
nullable is NullableAnnotation.Annotated or NullableAnnotation.None;
}
Loading

0 comments on commit 574cef7

Please sign in to comment.