Skip to content
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
15 changes: 15 additions & 0 deletions docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ The `MapperDefaultsAttribute` allows to set default configurations applied to al
[assembly: MapperDefaults(EnumMappingIgnoreCase = true)]
```

### MSBuild configuration

Mapperly configurations can also be set via MSBuild properties.
This allows to configure defaults in the project file (e.g. `.csproj`) or `Directory.Build.props`.
Property names correspond to the `MapperAttribute` properties prefixed with `Mapperly`.

```xml
<PropertyGroup>
<MapperlyEnumMappingIgnoreCase>true</MapperlyEnumMappingIgnoreCase>
<MapperlyRequiredMappingStrategy>Both</MapperlyRequiredMappingStrategy>
</PropertyGroup>
```

Configurations set via `MapperDefaultsAttribute` take precedence over MSBuild properties.

## Copy behavior

By default, Mapperly does not create deep copies of objects to improve performance.
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,4 @@ RMG091 | Mapper | Error | Circular included mapping configuration detected
RMG092 | Mapper | Error | Source type is not assignable to the included source type
RMG093 | Mapper | Error | Target type is not assignable to the included target type
RMG094 | Mapper | Error | Circular existing target mapping without setter detected
RMG095 | Mapper | Warning | Invalid MSBuild configuration option
93 changes: 93 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperBuildConfigurationReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Configuration;

internal static class MapperBuildConfigurationReader
{
private const string MapperlyBuildPropertyNamePrefix = "Mapperly";
private const string BuildPropertyPrefix = "build_property.";

private static readonly IReadOnlyCollection<PropertyInfo> _properties = typeof(MapperConfiguration).GetProperties(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly
);

public static (MapperConfiguration Configuration, ImmutableEquatableArray<Diagnostic> Diagnostics) Read(AnalyzerConfigOptions options)
{
var configuration = new MapperConfiguration();
var diagnostics = new List<Diagnostic>();

foreach (var property in _properties)
{
var configName = MapperlyBuildPropertyNamePrefix + property.Name;
if (!options.TryGetValue(BuildPropertyPrefix + configName, out var value) || string.IsNullOrWhiteSpace(value))
continue;

var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
if (propertyType.IsEnum)
{
if (!TryParseEnum(propertyType, value, out var enumValue))
{
diagnostics.Add(
Diagnostic.Create(
DiagnosticDescriptors.ConfiguredMSBuildOptionInvalid,
null,
configName,
value,
propertyType.FullName
)
);
continue;
}

property.SetValue(configuration, enumValue);
}
else if (propertyType == typeof(bool))
{
if (!bool.TryParse(value, out var boolValue))
{
diagnostics.Add(
Diagnostic.Create(
DiagnosticDescriptors.ConfiguredMSBuildOptionInvalid,
null,
configName,
value,
propertyType.FullName
)
);
continue;
}

property.SetValue(configuration, boolValue);
}
else
{
diagnostics.Add(
Diagnostic.Create(DiagnosticDescriptors.ConfiguredMSBuildOptionInvalid, null, configName, value, propertyType.FullName)
);
}
}

return (configuration, diagnostics.ToImmutableEquatableArray());
}

private static bool TryParseEnum(Type enumType, string value, out object? result)
{
try
{
// The Enum.Parse method only supports commas as a separator for flags.
// MSBuild (and humans) may use other separators.
var normalizedValue = value.Replace(';', ',').Replace('|', ',');
result = Enum.Parse(enumType, normalizedValue, true);
return true;
}
catch (ArgumentException)
{
result = null;
return false;
}
}
}
28 changes: 27 additions & 1 deletion src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,33 @@ namespace Riok.Mapperly.Configuration;

public static class MapperConfigurationMerger
{
public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, MapperConfiguration defaultMapperConfiguration)
public static MapperConfiguration Merge(MapperConfiguration highPriority, MapperConfiguration lowPriority)
{
return new MapperConfiguration
{
PropertyNameMappingStrategy = highPriority.PropertyNameMappingStrategy ?? lowPriority.PropertyNameMappingStrategy,
EnumMappingStrategy = highPriority.EnumMappingStrategy ?? lowPriority.EnumMappingStrategy,
EnumMappingIgnoreCase = highPriority.EnumMappingIgnoreCase ?? lowPriority.EnumMappingIgnoreCase,
ThrowOnMappingNullMismatch = highPriority.ThrowOnMappingNullMismatch ?? lowPriority.ThrowOnMappingNullMismatch,
ThrowOnPropertyMappingNullMismatch =
highPriority.ThrowOnPropertyMappingNullMismatch ?? lowPriority.ThrowOnPropertyMappingNullMismatch,
AllowNullPropertyAssignment = highPriority.AllowNullPropertyAssignment ?? lowPriority.AllowNullPropertyAssignment,
UseDeepCloning = highPriority.UseDeepCloning ?? lowPriority.UseDeepCloning,
StackCloningStrategy = highPriority.StackCloningStrategy ?? lowPriority.StackCloningStrategy,
EnabledConversions = highPriority.EnabledConversions ?? lowPriority.EnabledConversions,
UseReferenceHandling = highPriority.UseReferenceHandling ?? lowPriority.UseReferenceHandling,
IgnoreObsoleteMembersStrategy = highPriority.IgnoreObsoleteMembersStrategy ?? lowPriority.IgnoreObsoleteMembersStrategy,
RequiredMappingStrategy = highPriority.RequiredMappingStrategy ?? lowPriority.RequiredMappingStrategy,
RequiredEnumMappingStrategy = highPriority.RequiredEnumMappingStrategy ?? lowPriority.RequiredEnumMappingStrategy,
IncludedMembers = highPriority.IncludedMembers ?? lowPriority.IncludedMembers,
IncludedConstructors = highPriority.IncludedConstructors ?? lowPriority.IncludedConstructors,
PreferParameterlessConstructors = highPriority.PreferParameterlessConstructors ?? lowPriority.PreferParameterlessConstructors,
AutoUserMappings = highPriority.AutoUserMappings ?? lowPriority.AutoUserMappings,
EnumNamingStrategy = highPriority.EnumNamingStrategy ?? lowPriority.EnumNamingStrategy,
};
}

public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfiguration, MapperConfiguration defaultMapperConfiguration)
{
var mapper = new MapperAttribute();
mapper.PropertyNameMappingStrategy =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ SupportedFeatures supportedFeatures
_types = types;

var mapperConfiguration = _dataAccessor.AccessSingle<MapperAttribute, MapperConfiguration>(mapperSymbol);
var mapper = MapperConfigurationMerger.Merge(mapperConfiguration, defaultMapperConfiguration);
var mapper = MapperConfigurationMerger.MergeToAttribute(mapperConfiguration, defaultMapperConfiguration);

MapperConfiguration = new MappingConfiguration(
mapper,
Expand Down
9 changes: 9 additions & 0 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,15 @@ public static class DiagnosticDescriptors
true
);

public static readonly DiagnosticDescriptor ConfiguredMSBuildOptionInvalid = new(
"RMG095",
"Invalid MSBuild configuration option",
"The MSBuild option {0} with value {1} could not be parsed as {2}",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Warning,
true
);

private static string BuildHelpUri(string id)
{
#if ENV_NEXT
Expand Down
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ public bool MoveNext()

public static class ImmutableEquatableArray
{
public static ImmutableEquatableArray<T> ToImmutableEquatableArray<T>(this IReadOnlyCollection<T> values)
where T : IEquatable<T>
{
return values.Count == 0 ? ImmutableEquatableArray<T>.Empty : new ImmutableEquatableArray<T>(values);
}

public static ImmutableEquatableArray<T> ToImmutableEquatableArray<T>(this IEnumerable<T> values)
where T : IEquatable<T> => new(values);

Expand Down
31 changes: 23 additions & 8 deletions src/Riok.Mapperly/MapperGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors;
Expand Down Expand Up @@ -54,11 +55,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

// build the assembly default configurations
var mapperDefaultsAssembly = SyntaxProvider.GetMapperDefaultDeclarations(context);
var mapperDefaults = compilationContext
var mapperDefaultsAndDiagnostics = compilationContext
.Combine(mapperDefaultsAssembly)
.Select(static (x, _) => BuildDefaults(x.Left, x.Right))
.Combine(context.AnalyzerConfigOptionsProvider)
.Select(static (x, _) => BuildDefaults(x.Left.Left, x.Left.Right, x.Right.GlobalOptions))
.WithTrackingName(MapperGeneratorStepNames.BuildMapperDefaults);
context.ReportDiagnostics(mapperDefaultsAndDiagnostics.SelectMany(static (x, _) => x.Diagnostics));

var mapperDefaults = mapperDefaultsAndDiagnostics.Select(static (x, _) => x.MapperConfiguration);
var useStaticMappers = SyntaxProvider
.GetUseStaticMapperDeclarations(context)
.Select(BuildStaticMappers)
Expand Down Expand Up @@ -172,21 +176,32 @@ CSharpCompilation compilation
return staticMappersBuilder.ToImmutable();
}

private static MapperConfiguration BuildDefaults(CompilationContext compilationContext, IAssemblySymbol? assemblySymbol)
private static (MapperConfiguration MapperConfiguration, ImmutableEquatableArray<Diagnostic> Diagnostics) BuildDefaults(
CompilationContext compilationContext,
IAssemblySymbol? assemblySymbol,
AnalyzerConfigOptions options
)
{
var (msbuildMapperConfiguration, diagnostics) = MapperBuildConfigurationReader.Read(options);

if (assemblySymbol == null)
return MapperConfiguration.Default;
return (msbuildMapperConfiguration, diagnostics);

var mapperDefaultsAttribute = compilationContext.Types.TryGet(MapperDefaultsAttributeName);
if (mapperDefaultsAttribute == null)
return MapperConfiguration.Default;
return (msbuildMapperConfiguration, diagnostics);

var assemblyMapperDefaultsAttribute = SymbolAccessor
.GetAttributesSkipCache(assemblySymbol, mapperDefaultsAttribute)
.FirstOrDefault();
return assemblyMapperDefaultsAttribute == null
? MapperConfiguration.Default
: AttributeDataAccessor.Access<MapperDefaultsAttribute, MapperConfiguration>(assemblyMapperDefaultsAttribute);
if (assemblyMapperDefaultsAttribute == null)
return (msbuildMapperConfiguration, diagnostics);

var attributeMapperConfiguration = AttributeDataAccessor.Access<MapperDefaultsAttribute, MapperConfiguration>(
assemblyMapperDefaultsAttribute
);
var defaultMapperConfiguration = MapperConfigurationMerger.Merge(attributeMapperConfiguration, msbuildMapperConfiguration);
return (defaultMapperConfiguration, diagnostics);
}

private static IEnumerable<Diagnostic> BuildCompilationDiagnostics(Compilation compilation)
Expand Down
21 changes: 21 additions & 0 deletions src/Riok.Mapperly/Riok.Mapperly.targets
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,25 @@
>$(DefineConstants);MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME</DefineConstants
>
</PropertyGroup>

<ItemGroup>
<CompilerVisibleProperty Include="MapperlyPropertyNameMappingStrategy" />
<CompilerVisibleProperty Include="MapperlyEnumMappingStrategy" />
<CompilerVisibleProperty Include="MapperlyEnumNamingStrategy" />
<CompilerVisibleProperty Include="MapperlyEnumMappingIgnoreCase" />
<CompilerVisibleProperty Include="MapperlyThrowOnMappingNullMismatch" />
<CompilerVisibleProperty Include="MapperlyThrowOnPropertyMappingNullMismatch" />
<CompilerVisibleProperty Include="MapperlyAllowNullPropertyAssignment" />
<CompilerVisibleProperty Include="MapperlyUseDeepCloning" />
<CompilerVisibleProperty Include="MapperlyStackCloningStrategy" />
<CompilerVisibleProperty Include="MapperlyEnabledConversions" />
<CompilerVisibleProperty Include="MapperlyUseReferenceHandling" />
<CompilerVisibleProperty Include="MapperlyIgnoreObsoleteMembersStrategy" />
<CompilerVisibleProperty Include="MapperlyRequiredMappingStrategy" />
<CompilerVisibleProperty Include="MapperlyRequiredEnumMappingStrategy" />
<CompilerVisibleProperty Include="MapperlyIncludedMembers" />
<CompilerVisibleProperty Include="MapperlyIncludedConstructors" />
<CompilerVisibleProperty Include="MapperlyPreferParameterlessConstructors" />
<CompilerVisibleProperty Include="MapperlyAutoUserMappings" />
</ItemGroup>
</Project>
Loading
Loading