diff --git a/.csharpierrc.yaml b/.csharpierrc.yaml
index 2a6d37ae080..d9b5ea2bb20 100644
--- a/.csharpierrc.yaml
+++ b/.csharpierrc.yaml
@@ -8,6 +8,7 @@ preprocessorSymbolSets:
- "SYSTEM_PRIVATE_CORELIB"
- "NETSTANDARD2_0"
- "ROSLYN4_4_OR_GREATER"
+ - "ROSLYN4_7_OR_GREATER"
- "NET6_0_OR_GREATER"
- "NET7_0_OR_GREATER"
- "NET8_0_OR_GREATER"
diff --git a/docs/docs/configuration/analyzer-diagnostics.mdx b/docs/docs/configuration/analyzer-diagnostics.mdx
index 93e2873e9c3..35f93be09b9 100644
--- a/docs/docs/configuration/analyzer-diagnostics.mdx
+++ b/docs/docs/configuration/analyzer-diagnostics.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 14
+sidebar_position: 15
description: A list of all analyzer diagnostics used by Mapperly and how to configure them.
---
diff --git a/docs/docs/configuration/conversions.md b/docs/docs/configuration/conversions.md
index 44dfe121c0b..66f836d09f1 100644
--- a/docs/docs/configuration/conversions.md
+++ b/docs/docs/configuration/conversions.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 13
+sidebar_position: 14
description: A list of conversions supported by Mapperly
---
diff --git a/docs/docs/configuration/generated-source.mdx b/docs/docs/configuration/generated-source.mdx
index 781d16fc1d3..224c222f025 100644
--- a/docs/docs/configuration/generated-source.mdx
+++ b/docs/docs/configuration/generated-source.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 15
+sidebar_position: 16
description: How to inspect and check in the generated source into a version control system (VCS, GIT, ...)
---
diff --git a/docs/docs/configuration/private-member-mapping.md b/docs/docs/configuration/private-member-mapping.md
new file mode 100644
index 00000000000..b72ff02405f
--- /dev/null
+++ b/docs/docs/configuration/private-member-mapping.md
@@ -0,0 +1,62 @@
+---
+sidebar_position: 12
+description: Private member mapping
+---
+
+# Private member mapping
+
+As of .NET 8.0, Mapperly supports mapping members that are normally inaccessible like `private` or `protected` properties. This is made possible by using the [UnsafeAccessorAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute) which lets Mapperly access normally inaccessible members with zero overhead while being completely AOT safe.
+
+By default `IncludedMembers` is set to `MemberVisibility.AllAccessible` which will configure Mapperly to map members of all accessibility levels as long as they are ordinarily accessible. To enable unsafe accessor usage, set `IncludedMembers` to `MemberVisibility.All`. Mapperly will then try to map members of all accessibilities, including ones that are not usually visible to external types.
+
+```csharp
+public class Fruit
+{
+ private bool _isSeeded;
+
+ public string Name { get; set; }
+
+ private int Sweetness { get; set; }
+}
+
+// highlight-start
+[Mapper(IncludedMembers = MemberVisibility.All)]
+// highlight-end
+public partial class FruitMapper
+{
+ public partial FruitDto ToDto(Fruit fruit);
+}
+```
+
+## Generated code
+
+```csharp
+public partial class FruitMapper
+{
+ private partial global::FruitDto ToDto(global::Fruit source)
+ {
+ var target = new global::FruitDto();
+ target.GetIsSeeded1() = source.GetIsSeeded();
+ target.Name = source.Name;
+ target.SetSweetness(source.GetSweetness());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")]
+ public static extern ref bool GetSeeded(this global::Fruit target);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")]
+ public static extern ref bool GetSeeded1(this global::FruitDto target);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Sweetness")]
+ public static extern int GetSweetness(this global::Fruit source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Sweetness")]
+ public static extern void SetSweetness(this global::FruitDto target, int value);
+}
+```
+
+Here Mapperly generates a file scoped class containing extension method for each internal member for both the source and target. Mapperly then uses the extension methods to get and set the members. Note that this uses zero reflection and is as performant as using an ordinary property or field.
diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx
index 6454cec41cb..b31c3a61d48 100644
--- a/docs/docs/configuration/queryable-projections.mdx
+++ b/docs/docs/configuration/queryable-projections.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 12
+sidebar_position: 13
description: Use queryable projections to map queryable objects and optimize ORM performance
---
diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index f6832b3e5d1..daeb593530a 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -92,4 +92,9 @@ public class MapperAttribute : Attribute
/// By default this is , emitting warnings for unmapped source and target members.
///
public RequiredMappingStrategy RequiredMappingStrategy { get; set; } = RequiredMappingStrategy.Both;
+
+ ///
+ /// Determines the access level of members that Mapperly will map.
+ ///
+ public MemberVisibility IncludedMembers { get; set; } = MemberVisibility.AllAccessible;
}
diff --git a/src/Riok.Mapperly.Abstractions/MemberVisibility.cs b/src/Riok.Mapperly.Abstractions/MemberVisibility.cs
new file mode 100644
index 00000000000..cfec5fc98c1
--- /dev/null
+++ b/src/Riok.Mapperly.Abstractions/MemberVisibility.cs
@@ -0,0 +1,45 @@
+namespace Riok.Mapperly.Abstractions;
+
+///
+/// Determines what member accessibility Mapperly will attempt to map.
+///
+[Flags]
+public enum MemberVisibility
+{
+ ///
+ /// Maps all accessible members.
+ ///
+ AllAccessible = All | Accessible,
+
+ ///
+ /// Maps all members, even members which are not directly accessible by the mapper are mapped
+ /// by using accessors with the UnsafeAccessorAttribute. This can only be used for .NET 8.0 and later.
+ ///
+ All = Public | Internal | Protected | Private,
+
+ ///
+ /// Maps only accessible members.
+ /// If not set, the UnsafeAccessorAttribute is used to generate mappings for inaccessible members.
+ ///
+ Accessible = 1 << 0,
+
+ ///
+ /// Maps public members.
+ ///
+ Public = 1 << 1,
+
+ ///
+ /// Maps internal members.
+ ///
+ Internal = 1 << 2,
+
+ ///
+ /// Maps protected members.
+ ///
+ Protected = 1 << 3,
+
+ ///
+ /// Maps private members.
+ ///
+ Private = 1 << 4,
+}
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index 80be9349f8b..d0014895899 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -119,3 +119,13 @@ Riok.Mapperly.Abstractions.RequiredMappingStrategy.Both = -1 -> Riok.Mapperly.Ab
Riok.Mapperly.Abstractions.RequiredMappingStrategy.None = 0 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
Riok.Mapperly.Abstractions.RequiredMappingStrategy.Source = 1 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
Riok.Mapperly.Abstractions.RequiredMappingStrategy.Target = 2 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.MapperAttribute.IncludedMembers.get -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MapperAttribute.IncludedMembers.set -> void
+Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.Accessible = 1 -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.All = Riok.Mapperly.Abstractions.MemberVisibility.Public | Riok.Mapperly.Abstractions.MemberVisibility.Internal | Riok.Mapperly.Abstractions.MemberVisibility.Protected | Riok.Mapperly.Abstractions.MemberVisibility.Private -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.AllAccessible = Riok.Mapperly.Abstractions.MemberVisibility.Accessible | Riok.Mapperly.Abstractions.MemberVisibility.All -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.Internal = 4 -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.Private = 16 -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.Protected = 8 -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.MemberVisibility.Public = 2 -> Riok.Mapperly.Abstractions.MemberVisibility
diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
index 82ed09a6c4d..d8f8df9e79c 100644
--- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
+++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
@@ -125,3 +125,4 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
RMG051 | Mapper | Warning | Invalid ignore source member found, nested ignores are not supported
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
diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
index 270049f41a6..2000d030deb 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
@@ -98,4 +98,9 @@ public record MapperConfiguration
/// By default this is , emitting warnings for unmapped source and target members.
///
public RequiredMappingStrategy? RequiredMappingStrategy { get; init; }
+
+ ///
+ /// Determines the access level of members that Mapperly will map.
+ ///
+ public MemberVisibility? IncludedMembers { get; init; }
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
index f83f5843b0a..0986667d1be 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
@@ -51,6 +51,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map
?? defaultMapperConfiguration.RequiredMappingStrategy
?? mapper.RequiredMappingStrategy;
+ mapper.IncludedMembers =
+ mapperConfiguration.IncludedMembers ?? defaultMapperConfiguration.IncludedMembers ?? mapper.IncludedMembers;
+
return mapper;
}
}
diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
index 9378494ed66..e6bf94e264a 100644
--- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Abstractions.ReferenceHandling;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.ExternalMappings;
@@ -21,6 +22,8 @@ public class DescriptorBuilder
private readonly MappingBodyBuilder _mappingBodyBuilder;
private readonly SimpleMappingBuilderContext _builderContext;
private readonly List _diagnostics = new();
+ private readonly UnsafeAccessorContext _unsafeAccessorContext;
+ private readonly MapperConfigurationReader _configurationReader;
private ObjectFactoryCollection _objectFactories = ObjectFactoryCollection.Empty;
@@ -34,14 +37,18 @@ MapperConfiguration defaultMapperConfiguration
_mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder);
_symbolAccessor = symbolAccessor;
_mappingBodyBuilder = new MappingBodyBuilder(_mappings);
+ _unsafeAccessorContext = new UnsafeAccessorContext(_methodNameBuilder, symbolAccessor);
var attributeAccessor = new AttributeDataAccessor(symbolAccessor);
+ _configurationReader = new MapperConfigurationReader(attributeAccessor, mapperDeclaration.Symbol, defaultMapperConfiguration);
+
_builderContext = new SimpleMappingBuilderContext(
compilationContext,
- new MapperConfigurationReader(attributeAccessor, mapperDeclaration.Symbol, defaultMapperConfiguration),
+ _configurationReader,
_symbolAccessor,
attributeAccessor,
_mapperDescriptor,
+ _unsafeAccessorContext,
_diagnostics,
new MappingBuilder(_mappings),
new ExistingTargetMappingBuilder(_mappings)
@@ -50,6 +57,7 @@ MapperConfiguration defaultMapperConfiguration
public (MapperDescriptor descriptor, IReadOnlyCollection diagnostics) Build(CancellationToken cancellationToken)
{
+ ConfigureMemberVisibility();
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
@@ -59,9 +67,33 @@ MapperConfiguration defaultMapperConfiguration
TemplateResolver.AddRequiredTemplates(_builderContext.MapperConfiguration, _mappings, _mapperDescriptor);
BuildReferenceHandlingParameters();
AddMappingsToDescriptor();
+ AddAccessorsToDescriptor();
return (_mapperDescriptor, _diagnostics);
}
+ ///
+ /// If is not set and the roslyn version does not have UnsafeAccessors
+ /// then emit a diagnostic and update the for .
+ ///
+ private void ConfigureMemberVisibility()
+ {
+ var includedMembers = _configurationReader.Mapper.IncludedMembers;
+#if ROSLYN4_7_OR_GREATER
+ _symbolAccessor.SetMemberVisibility(includedMembers);
+#else
+ if (includedMembers.HasFlag(MemberVisibility.Accessible))
+ return;
+
+ _diagnostics.Add(
+ Diagnostic.Create(
+ Riok.Mapperly.Diagnostics.DiagnosticDescriptors.UnsafeAccessorNotAvailable,
+ _mapperDescriptor.Syntax.GetLocation()
+ )
+ );
+ _symbolAccessor.SetMemberVisibility(includedMembers | MemberVisibility.Accessible);
+#endif
+ }
+
private void ReserveMethodNames()
{
foreach (var methodSymbol in _symbolAccessor.GetAllMembers(_mapperDescriptor.Symbol))
@@ -127,4 +159,10 @@ private void AddMappingsToDescriptor()
_mapperDescriptor.AddTypeMapping(mapping);
}
}
+
+ private void AddAccessorsToDescriptor()
+ {
+ // add generated accessors to the mapper
+ _mapperDescriptor.AddUnsafeAccessors(_unsafeAccessorContext.UnsafeAccessors);
+ }
}
diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
index f5005297f27..66c6d531b2d 100644
--- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
+++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
@@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
using Riok.Mapperly.Templates;
@@ -12,6 +13,7 @@ public class MapperDescriptor
{
private readonly MapperDeclaration _declaration;
private readonly List _methodMappings = new();
+ private readonly List _unsafeAccessors = new();
private readonly HashSet _requiredTemplates = new();
public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder)
@@ -40,10 +42,14 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui
public IReadOnlyCollection MethodTypeMappings => _methodMappings;
- public void AddTypeMapping(MethodMapping mapping) => _methodMappings.Add(mapping);
+ public IReadOnlyCollection UnsafeAccessors => _unsafeAccessors;
public void AddRequiredTemplate(TemplateReference template) => _requiredTemplates.Add(template);
+ public void AddTypeMapping(MethodMapping mapping) => _methodMappings.Add(mapping);
+
+ public void AddUnsafeAccessors(IEnumerable accessors) => _unsafeAccessors.AddRange(accessors);
+
private string BuildName(INamedTypeSymbol symbol)
{
if (symbol.ContainingType == null)
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs
index 76c6eb4eb4c..da1b86e2801 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs
@@ -48,7 +48,18 @@ private void AddNullMemberInitializers(IMemberAssignmentMappingContainer contain
continue;
}
- container.AddMemberMappingContainer(new MemberNullAssignmentInitializerMapping(nullablePath));
+ var setterNullablePath = SetterMemberPath.Build(BuilderContext, nullablePath);
+
+ if (setterNullablePath.IsMethod)
+ {
+ var getterNullablePath = GetterMemberPath.Build(BuilderContext, nullablePath);
+ container.AddMemberMappingContainer(
+ new MethodMemberNullAssignmentInitializerMapping(setterNullablePath, getterNullablePath)
+ );
+ continue;
+ }
+
+ container.AddMemberMappingContainer(new MemberNullAssignmentInitializerMapping(setterNullablePath));
}
}
@@ -75,7 +86,7 @@ private MemberNullDelegateAssignmentMapping GetOrCreateNullDelegateMappingForPat
}
mapping = new MemberNullDelegateAssignmentMapping(
- nullConditionSourcePath,
+ GetterMemberPath.Build(BuilderContext, nullConditionSourcePath),
parentMapping,
BuilderContext.MapperConfiguration.ThrowOnPropertyMappingNullMismatch,
needsNullSafeAccess
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs
index 1154db4e38e..60b53d971d2 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs
@@ -179,14 +179,17 @@ MemberPath sourcePath
nullFallback = ctx.BuilderContext.GetNullFallbackValue(targetMember.Type);
}
+ var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourcePath);
+ var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, targetPath);
+
var memberMapping = new NullMemberMapping(
delegateMapping,
- sourcePath,
+ getterSourcePath,
targetMember.Type,
nullFallback,
!ctx.BuilderContext.IsExpression
);
- var memberAssignmentMapping = new MemberAssignmentMapping(targetPath, memberMapping);
+ var memberAssignmentMapping = new MemberAssignmentMapping(setterTargetPath, memberMapping);
ctx.AddInitMemberMapping(memberAssignmentMapping);
}
@@ -203,7 +206,7 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext
// then by descending parameter count
// ctors annotated with [Obsolete] are considered last unless they have a MapperConstructor attribute set
var ctorCandidates = namedTargetType.InstanceConstructors
- .Where(ctor => ctx.BuilderContext.SymbolAccessor.IsAccessible(ctor))
+ .Where(ctor => ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(ctor))
.OrderByDescending(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x))
.ThenBy(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x))
.ThenByDescending(x => x.Parameters.Length == 0)
@@ -285,9 +288,11 @@ private static bool TryBuildConstructorMapping(
return false;
}
+ var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourcePath);
+
var memberMapping = new NullMemberMapping(
delegateMapping,
- sourcePath,
+ getterSourcePath,
paramType,
ctx.BuilderContext.GetNullFallbackValue(paramType),
!ctx.BuilderContext.IsExpression
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs
index 8cd2d733458..4276a030f1c 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs
@@ -111,9 +111,11 @@ out HashSet mappedTargetMemberNames
return false;
}
+ var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourcePath);
+
var memberMapping = new NullMemberMapping(
delegateMapping,
- sourcePath,
+ getterSourcePath,
paramType,
ctx.BuilderContext.GetNullFallbackValue(paramType),
!ctx.BuilderContext.IsExpression
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs
index 164276c1919..d7cfd9c4dbc 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs
@@ -122,8 +122,36 @@ public static bool ValidateMappingSpecification(
return false;
}
+ // cannot access non public member in initializer
+ if (
+ allowInitOnlyMember
+ && (
+ !ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(targetMemberPath.Member.MemberSymbol)
+ || !targetMemberPath.Member.CanSetDirectly
+ )
+ )
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.CannotMapToReadOnlyMember,
+ ctx.Mapping.SourceType,
+ sourceMemberPath.FullName,
+ sourceMemberPath.Member.Type,
+ ctx.Mapping.TargetType,
+ targetMemberPath.FullName,
+ targetMemberPath.Member.Type
+ );
+ return false;
+ }
+
// a target member path part is write only or not accessible
- if (targetMemberPath.ObjectPath.Any(p => !p.CanGet))
+ // an expressions target member path is only accessible with unsafe access
+ if (
+ targetMemberPath.ObjectPath.Any(p => !p.CanGet)
+ || (
+ ctx.BuilderContext.IsExpression
+ && targetMemberPath.ObjectPath.Any(p => !ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(p.MemberSymbol))
+ )
+ )
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapToWriteOnlyMemberPath,
@@ -159,7 +187,14 @@ public static bool ValidateMappingSpecification(
}
// a source member path is write only or not accessible
- if (sourceMemberPath.Path.Any(p => !p.CanGet))
+ // an expressions source member path is only accessible with unsafe access
+ if (
+ sourceMemberPath.Path.Any(p => !p.CanGet)
+ || (
+ ctx.BuilderContext.IsExpression
+ && sourceMemberPath.Path.Any(p => !ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(p.MemberSymbol))
+ )
+ )
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapFromWriteOnlyMember,
@@ -261,11 +296,14 @@ MemberPath targetMemberPath
return;
}
+ var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMemberPath);
+ var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, targetMemberPath);
+
// no member of the source path is nullable, no null handling needed
if (!sourceMemberPath.IsAnyNullable())
{
- var memberMapping = new MemberMapping(delegateMapping, sourceMemberPath, false, true);
- ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(targetMemberPath, memberMapping));
+ var memberMapping = new MemberMapping(delegateMapping, getterSourcePath, false, true);
+ ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(setterTargetPath, memberMapping));
return;
}
@@ -278,15 +316,15 @@ MemberPath targetMemberPath
&& (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMemberPath.Member.IsNullable)
)
{
- var memberMapping = new MemberMapping(delegateMapping, sourceMemberPath, true, false);
- ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(targetMemberPath, memberMapping));
+ var memberMapping = new MemberMapping(delegateMapping, getterSourcePath, true, false);
+ ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(setterTargetPath, memberMapping));
return;
}
// additional null condition check
// (only map if source is not null, else may throw depending on settings)
ctx.AddNullDelegateMemberAssignmentMapping(
- new MemberAssignmentMapping(targetMemberPath, new MemberMapping(delegateMapping, sourceMemberPath, false, true))
+ new MemberAssignmentMapping(setterTargetPath, new MemberMapping(delegateMapping, getterSourcePath, false, true))
);
}
@@ -311,7 +349,10 @@ MemberPath targetMemberPath
if (existingTargetMapping == null)
return false;
- var memberMapping = new MemberExistingTargetMapping(existingTargetMapping, sourceMemberPath, targetMemberPath);
+ var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMemberPath);
+ var setterTargetPath = GetterMemberPath.Build(ctx.BuilderContext, targetMemberPath);
+
+ var memberMapping = new MemberExistingTargetMapping(existingTargetMapping, getterSourcePath, setterTargetPath);
ctx.AddMemberAssignmentMapping(memberMapping);
return true;
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs
index 4769ffe0f90..b6824c28401 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs
@@ -17,7 +17,7 @@ public static class CtorMappingBuilder
// resolve ctors which have the source as single argument
var ctorMethod = namedTarget.InstanceConstructors
- .Where(ctx.SymbolAccessor.IsAccessible)
+ .Where(ctx.SymbolAccessor.IsDirectlyAccessible)
.FirstOrDefault(
m =>
m.Parameters.Length == 1
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs
index 432585b39f1..dae22bd8132 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs
@@ -21,7 +21,9 @@ public static class NewInstanceObjectPropertyMappingBuilder
ctx.MapperConfiguration.UseReferenceHandling
);
- if (ctx.Target is not INamedTypeSymbol namedTarget || namedTarget.Constructors.All(x => !ctx.SymbolAccessor.IsAccessible(x)))
+ if (
+ ctx.Target is not INamedTypeSymbol namedTarget || namedTarget.Constructors.All(x => !ctx.SymbolAccessor.IsDirectlyAccessible(x))
+ )
return null;
if (ctx.Source.IsEnum() || ctx.Target.IsEnum())
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs
index 9bb0a111313..18ac055d23d 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs
@@ -8,7 +8,7 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
///
public interface IMemberAssignmentMapping
{
- MemberPath SourcePath { get; }
+ GetterMemberPath SourcePath { get; }
MemberPath TargetPath { get; }
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs
index c91582c64ac..67cb858a1b0 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs
@@ -9,7 +9,7 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
///
public interface IMemberMapping
{
- MemberPath SourcePath { get; }
+ GetterMemberPath SourcePath { get; }
ExpressionSyntax Build(TypeMappingBuildContext ctx);
}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs
index 548ebbdf503..3b4224dc41e 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs
@@ -1,7 +1,6 @@
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Symbols;
-using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
@@ -13,27 +12,27 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
public class MemberAssignmentMapping : IMemberAssignmentMapping
{
private readonly IMemberMapping _mapping;
+ private readonly SetterMemberPath _targetPath;
- public MemberAssignmentMapping(MemberPath targetPath, IMemberMapping mapping)
+ public MemberAssignmentMapping(SetterMemberPath targetPath, IMemberMapping mapping)
{
- TargetPath = targetPath;
+ _targetPath = targetPath;
_mapping = mapping;
}
- public MemberPath SourcePath => _mapping.SourcePath;
+ public GetterMemberPath SourcePath => _mapping.SourcePath;
- public MemberPath TargetPath { get; }
+ public MemberPath TargetPath => _targetPath;
public IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) =>
ctx.SyntaxFactory.SingleStatement(BuildExpression(ctx, targetAccess));
public ExpressionSyntax BuildExpression(TypeMappingBuildContext ctx, ExpressionSyntax? targetAccess)
{
- var targetMemberAccess = TargetPath.BuildAccess(targetAccess);
var mappedValue = _mapping.Build(ctx);
- // target.Member = mappedValue;
- return Assignment(targetMemberAccess, mappedValue);
+ // target.SetValue(source.Value); or target.Value = source.Value;
+ return _targetPath.BuildAssignment(targetAccess, mappedValue);
}
public override bool Equals(object? obj)
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs
index 57ee5cb540f..6fdc65503fe 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs
@@ -10,22 +10,23 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
public class MemberExistingTargetMapping : IMemberAssignmentMapping
{
private readonly IExistingTargetMapping _delegateMapping;
+ private readonly GetterMemberPath _targetPath;
- public MemberExistingTargetMapping(IExistingTargetMapping delegateMapping, MemberPath sourcePath, MemberPath targetPath)
+ public MemberExistingTargetMapping(IExistingTargetMapping delegateMapping, GetterMemberPath sourcePath, GetterMemberPath targetPath)
{
_delegateMapping = delegateMapping;
SourcePath = sourcePath;
- TargetPath = targetPath;
+ _targetPath = targetPath;
}
- public MemberPath SourcePath { get; }
+ public GetterMemberPath SourcePath { get; }
- public MemberPath TargetPath { get; }
+ public MemberPath TargetPath => _targetPath;
public IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess)
{
var source = SourcePath.BuildAccess(ctx.Source);
- var target = TargetPath.BuildAccess(targetAccess);
+ var target = _targetPath.BuildAccess(targetAccess);
return _delegateMapping.Build(ctx.WithSource(source), target);
}
}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs
index 4c37f1594ca..84a252afaff 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs
@@ -15,7 +15,7 @@ public class MemberMapping : IMemberMapping
public MemberMapping(
INewInstanceMapping delegateMapping,
- MemberPath sourcePath,
+ GetterMemberPath sourcePath,
bool nullConditionalAccess,
bool addValuePropertyOnNullable
)
@@ -26,7 +26,7 @@ bool addValuePropertyOnNullable
_addValuePropertyOnNullable = addValuePropertyOnNullable;
}
- public MemberPath SourcePath { get; }
+ public GetterMemberPath SourcePath { get; }
public ExpressionSyntax Build(TypeMappingBuildContext ctx)
{
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs
index c0569630275..f1c687d0dce 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs
@@ -2,7 +2,6 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Symbols;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
-using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
@@ -12,9 +11,9 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
[DebuggerDisplay("MemberNullAssignmentInitializerMapping({_pathToInitialize} ??= new())")]
public class MemberNullAssignmentInitializerMapping : MemberAssignmentMappingContainer
{
- private readonly MemberPath _pathToInitialize;
+ private readonly SetterMemberPath _pathToInitialize;
- public MemberNullAssignmentInitializerMapping(MemberPath pathToInitialize)
+ public MemberNullAssignmentInitializerMapping(SetterMemberPath pathToInitialize)
{
_pathToInitialize = pathToInitialize;
}
@@ -23,7 +22,7 @@ public override IEnumerable Build(TypeMappingBuildContext ctx,
{
// source.Value ??= new();
var initializer = ctx.SyntaxFactory.ExpressionStatement(
- CoalesceAssignment(_pathToInitialize.BuildAccess(targetAccess), ImplicitObjectCreationExpression())
+ _pathToInitialize.BuildAssignment(targetAccess, ImplicitObjectCreationExpression(), true)
);
return base.Build(ctx, targetAccess).Prepend(initializer);
}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs
index 8b2e636311d..df08150dda7 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs
@@ -11,12 +11,12 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
[DebuggerDisplay("MemberNullDelegateAssignmentMapping({_nullConditionalSourcePath} != null)")]
public class MemberNullDelegateAssignmentMapping : MemberAssignmentMappingContainer
{
- private readonly MemberPath _nullConditionalSourcePath;
+ private readonly GetterMemberPath _nullConditionalSourcePath;
private readonly bool _throwInsteadOfConditionalNullMapping;
private readonly bool _needsNullSafeAccess;
public MemberNullDelegateAssignmentMapping(
- MemberPath nullConditionalSourcePath,
+ GetterMemberPath nullConditionalSourcePath,
IMemberAssignmentMappingContainer parent,
bool throwInsteadOfConditionalNullMapping,
bool needsNullSafeAccess
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs
new file mode 100644
index 00000000000..0025e313baa
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Emit.Syntax;
+using Riok.Mapperly.Symbols;
+
+namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
+
+///
+/// A member initializer which initializes null members to new objects.
+///
+[DebuggerDisplay("MemberNullAssignmentInitializerMapping({_targetPathToInitialize} ??= new())")]
+public class MethodMemberNullAssignmentInitializerMapping : MemberAssignmentMappingContainer
+{
+ private readonly SetterMemberPath _targetPathToInitialize;
+ private readonly GetterMemberPath _sourcePathToInitialize;
+
+ public MethodMemberNullAssignmentInitializerMapping(SetterMemberPath targetPathToInitialize, GetterMemberPath sourcePathToInitialize)
+ {
+ _targetPathToInitialize = targetPathToInitialize;
+ _sourcePathToInitialize = sourcePathToInitialize;
+ }
+
+ public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess)
+ {
+ // target.Value ?? new()
+ var initializer = SyntaxFactoryHelper.Coalesce(
+ _sourcePathToInitialize.BuildAccess(targetAccess),
+ SyntaxFactory.ImplicitObjectCreationExpression()
+ );
+
+ // target.SetValue(source.Value ?? new());
+ var setTarget = ctx.SyntaxFactory.ExpressionStatement(_targetPathToInitialize.BuildAssignment(targetAccess, initializer));
+ return base.Build(ctx, targetAccess).Prepend(setTarget);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ if (ReferenceEquals(this, obj))
+ return true;
+
+ if (obj.GetType() != GetType())
+ return false;
+
+ return Equals((MethodMemberNullAssignmentInitializerMapping)obj);
+ }
+
+ public override int GetHashCode() => _targetPathToInitialize.GetHashCode();
+
+ public static bool operator ==(
+ MethodMemberNullAssignmentInitializerMapping? left,
+ MethodMemberNullAssignmentInitializerMapping? right
+ ) => Equals(left, right);
+
+ public static bool operator !=(
+ MethodMemberNullAssignmentInitializerMapping? left,
+ MethodMemberNullAssignmentInitializerMapping? right
+ ) => !Equals(left, right);
+
+ protected bool Equals(MethodMemberNullAssignmentInitializerMapping other) =>
+ _targetPathToInitialize.Equals(other._targetPathToInitialize);
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs
index ea9b6485fab..bc159fdd743 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs
@@ -21,7 +21,7 @@ public class NullMemberMapping : IMemberMapping
public NullMemberMapping(
INewInstanceMapping delegateMapping,
- MemberPath sourcePath,
+ GetterMemberPath sourcePath,
ITypeSymbol targetType,
NullFallbackValue nullFallback,
bool useNullConditionalAccess
@@ -34,7 +34,7 @@ bool useNullConditionalAccess
_targetType = targetType;
}
- public MemberPath SourcePath { get; }
+ public GetterMemberPath SourcePath { get; }
public ExpressionSyntax Build(TypeMappingBuildContext ctx)
{
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/IUnsafeAccessor.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/IUnsafeAccessor.cs
new file mode 100644
index 00000000000..d19afa5ffa0
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/IUnsafeAccessor.cs
@@ -0,0 +1,16 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Emit;
+
+namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess;
+
+///
+/// Represents a method accessor for inaccessible members.
+/// This uses the .NET 8.0 UnsafeAccessorAttribute to access members that are not public or visible without the use of reflection.
+/// See here
+///
+public interface IUnsafeAccessor
+{
+ MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx);
+
+ string MethodName { get; }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs
new file mode 100644
index 00000000000..9ffa8f4faef
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs
@@ -0,0 +1,51 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Emit;
+using Riok.Mapperly.Emit.Syntax;
+using Riok.Mapperly.Helpers;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess;
+
+///
+/// Creates an extension method to access an objects non public field using .Net 8's UnsafeAccessor.
+/// ///
+/// [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
+/// public extern static int GetValue(this global::MyClass source);
+///
+///
+public class UnsafeFieldAccessor : IUnsafeAccessor
+{
+ private const string DefaultTargetParameterName = "target";
+
+ private readonly string _targetType;
+ private readonly string _result;
+ private readonly string _memberName;
+
+ public UnsafeFieldAccessor(IFieldSymbol value, string methodName)
+ {
+ MethodName = methodName;
+ _targetType = value.ContainingType.FullyQualifiedIdentifierName();
+ _result = value.Type.FullyQualifiedIdentifierName();
+ _memberName = value.Name;
+ }
+
+ public string MethodName { get; }
+
+ public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
+ {
+ var nameBuilder = ctx.NameBuilder.NewScope();
+ var targetName = nameBuilder.New(DefaultTargetParameterName);
+
+ var target = Parameter(_targetType, targetName, true);
+
+ var parameters = ParameterList(CommaSeparatedList(target));
+ var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Field, _memberName);
+ var returnType = RefType(IdentifierName(_result).AddTrailingSpace())
+ .WithRefKeyword(Token(TriviaList(), SyntaxKind.RefKeyword, TriviaList(Space)));
+
+ return PublicStaticExternMethod(returnType, MethodName, parameters, attributeList);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs
new file mode 100644
index 00000000000..928024752d0
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs
@@ -0,0 +1,47 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Emit;
+using Riok.Mapperly.Emit.Syntax;
+using Riok.Mapperly.Helpers;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess;
+
+///
+/// Creates an extension method to access an objects non public property using .Net 8's UnsafeAccessor.
+/// ///
+/// [UnsafeAccessor(UnsafeAccessorKind.Property, Name = "get_value")]
+/// public extern static int GetValue(this global::MyClass source);
+///
+///
+public class UnsafeGetPropertyAccessor : IUnsafeAccessor
+{
+ private const string DefaultSourceParameterName = "source";
+
+ private readonly string _result;
+ private readonly string _sourceType;
+ private readonly string _memberName;
+
+ public UnsafeGetPropertyAccessor(IPropertySymbol result, string methodName)
+ {
+ _sourceType = result.ContainingType.FullyQualifiedIdentifierName();
+ _result = result.Type.FullyQualifiedIdentifierName();
+ _memberName = result.Name;
+ MethodName = methodName;
+ }
+
+ public string MethodName { get; }
+
+ public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
+ {
+ var nameBuilder = ctx.NameBuilder.NewScope();
+ var sourceName = nameBuilder.New(DefaultSourceParameterName);
+
+ var source = Parameter(_sourceType, sourceName, true);
+
+ var parameters = ParameterList(CommaSeparatedList(source));
+ var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"get_{_memberName}");
+ return PublicStaticExternMethod(IdentifierName(_result).AddTrailingSpace(), MethodName, parameters, attributeList);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeSetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeSetPropertyAccessor.cs
new file mode 100644
index 00000000000..74684191442
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeSetPropertyAccessor.cs
@@ -0,0 +1,57 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Emit;
+using Riok.Mapperly.Emit.Syntax;
+using Riok.Mapperly.Helpers;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess;
+
+///
+/// Creates an extension method to set an objects non public property using .Net 8's UnsafeAccessor.
+/// ///
+/// [UnsafeAccessor(UnsafeAccessorKind.Property, Name = "set_value")]
+/// public extern static void SetValue(this global::MyClass source, int value);
+///
+///
+public class UnsafeSetPropertyAccessor : IUnsafeAccessor
+{
+ private const string DefaultTargetParameterName = "target";
+ private const string DefaultValueParameterName = "value";
+
+ private readonly string _targetType;
+ private readonly string _valueType;
+ private readonly string _memberName;
+
+ public UnsafeSetPropertyAccessor(IPropertySymbol value, string methodName)
+ {
+ MethodName = methodName;
+ _targetType = value.ContainingType.FullyQualifiedIdentifierName();
+ _valueType = value.Type.FullyQualifiedIdentifierName();
+ _memberName = value.Name;
+ }
+
+ public string MethodName { get; }
+
+ public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
+ {
+ var nameBuilder = ctx.NameBuilder.NewScope();
+ var targetName = nameBuilder.New(DefaultTargetParameterName);
+ var valueName = nameBuilder.New(DefaultValueParameterName);
+
+ var target = Parameter(_targetType, targetName, true);
+ var value = Parameter(_valueType, valueName);
+
+ var parameters = ParameterList(CommaSeparatedList(target, value));
+ var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"set_{_memberName}");
+
+ return PublicStaticExternMethod(
+ PredefinedType(Token(SyntaxKind.VoidKeyword)).AddTrailingSpace(),
+ MethodName,
+ parameters,
+ attributeList
+ );
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
index b3216946994..c00de0464f5 100644
--- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
@@ -22,6 +22,7 @@ public SimpleMappingBuilderContext(
SymbolAccessor symbolAccessor,
AttributeDataAccessor attributeAccessor,
MapperDescriptor descriptor,
+ UnsafeAccessorContext unsafeAccessorContext,
List diagnostics,
MappingBuilder mappingBuilder,
ExistingTargetMappingBuilder existingTargetMappingBuilder
@@ -35,6 +36,7 @@ ExistingTargetMappingBuilder existingTargetMappingBuilder
MappingBuilder = mappingBuilder;
ExistingTargetMappingBuilder = existingTargetMappingBuilder;
AttributeAccessor = attributeAccessor;
+ UnsafeAccessorContext = unsafeAccessorContext;
}
protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx)
@@ -44,6 +46,7 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx)
ctx.SymbolAccessor,
ctx.AttributeAccessor,
ctx._descriptor,
+ ctx.UnsafeAccessorContext,
ctx._diagnostics,
ctx.MappingBuilder,
ctx.ExistingTargetMappingBuilder
@@ -59,6 +62,8 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx)
public AttributeDataAccessor AttributeAccessor { get; }
+ public UnsafeAccessorContext UnsafeAccessorContext { get; }
+
protected MappingBuilder MappingBuilder { get; }
protected ExistingTargetMappingBuilder ExistingTargetMappingBuilder { get; }
diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
index c2ed55ea3ed..e4af8a547e2 100644
--- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
+++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
@@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
+using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
@@ -20,6 +21,8 @@ public class SymbolAccessor
private readonly Dictionary> _allAccessibleMembersCaseSensitive =
new(SymbolEqualityComparer.Default);
+ private MemberVisibility _memberVisibility = MemberVisibility.AllAccessible;
+
public SymbolAccessor(CompilationContext compilationContext, INamedTypeSymbol mapperSymbol)
{
_compilationContext = compilationContext;
@@ -28,11 +31,32 @@ public SymbolAccessor(CompilationContext compilationContext, INamedTypeSymbol ma
private Compilation Compilation => _compilationContext.Compilation;
+ internal void SetMemberVisibility(MemberVisibility visibility) => _memberVisibility = visibility;
+
public bool HasAccessibleParameterlessConstructor(ITypeSymbol symbol) =>
symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol
- && namedTypeSymbol.InstanceConstructors.Any(c => c.Parameters.IsDefaultOrEmpty && IsAccessible(c));
+ && namedTypeSymbol.InstanceConstructors.Any(c => c.Parameters.IsDefaultOrEmpty && IsDirectlyAccessible(c));
+
+ public bool IsDirectlyAccessible(ISymbol symbol) => Compilation.IsSymbolAccessibleWithin(symbol, _mapperSymbol);
+
+ public bool IsAccessibleToMemberVisibility(ISymbol symbol)
+ {
+ if (_memberVisibility.HasFlag(MemberVisibility.Accessible) && !IsDirectlyAccessible(symbol))
+ return false;
- public bool IsAccessible(ISymbol symbol) => Compilation.IsSymbolAccessibleWithin(symbol, _mapperSymbol);
+ return symbol.DeclaredAccessibility switch
+ {
+ Accessibility.Private => _memberVisibility.HasFlag(MemberVisibility.Private),
+ Accessibility.ProtectedAndInternal
+ => _memberVisibility.HasFlag(MemberVisibility.Protected) && _memberVisibility.HasFlag(MemberVisibility.Internal),
+ Accessibility.Protected => _memberVisibility.HasFlag(MemberVisibility.Protected),
+ Accessibility.Internal => _memberVisibility.HasFlag(MemberVisibility.Internal),
+ Accessibility.ProtectedOrInternal
+ => _memberVisibility.HasFlag(MemberVisibility.Protected) || _memberVisibility.HasFlag(MemberVisibility.Internal),
+ Accessibility.Public => _memberVisibility.HasFlag(MemberVisibility.Public),
+ _ => false,
+ };
+ }
public bool HasImplicitConversion(ITypeSymbol source, ITypeSymbol destination) =>
Compilation.ClassifyConversion(source, destination).IsImplicit && (destination.IsNullable() || !source.IsNullable());
diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs
new file mode 100644
index 00000000000..514387afe8b
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs
@@ -0,0 +1,104 @@
+using System.Globalization;
+using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess;
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Descriptors;
+
+public class UnsafeAccessorContext
+{
+ private readonly UniqueNameBuilder _nameBuilder;
+ private readonly SymbolAccessor _symbolAccessor;
+ private readonly Dictionary _unsafeAccessors = new();
+
+ public UnsafeAccessorContext(UniqueNameBuilder nameBuilder, SymbolAccessor symbolAccessor)
+ {
+ _nameBuilder = nameBuilder.NewScope();
+ _symbolAccessor = symbolAccessor;
+ }
+
+ public IReadOnlyCollection UnsafeAccessors => _unsafeAccessors.Values;
+
+ public IUnsafeAccessor GetOrBuildAccessor(UnsafeAccessorType type, ISymbol symbol)
+ {
+ var key = new UnsafeAccessorKey(symbol, type);
+ if (_unsafeAccessors.TryGetValue(key, out var value))
+ return value;
+
+ var formatted = FormatAccessorName(symbol.Name);
+
+ var defaultMethodName = type switch
+ {
+ UnsafeAccessorType.GetField => $"Get{formatted}",
+ UnsafeAccessorType.GetProperty => $"Get{formatted}",
+ UnsafeAccessorType.SetProperty => $"Set{formatted}",
+ _ => throw new Exception($"Unknown {nameof(UnsafeAccessorType)}."),
+ };
+ var methodName = GetValidMethodName(symbol.ContainingType, defaultMethodName);
+
+ IUnsafeAccessor accessor = type switch
+ {
+ UnsafeAccessorType.GetField => new UnsafeFieldAccessor((IFieldSymbol)symbol, methodName),
+ UnsafeAccessorType.GetProperty => new UnsafeGetPropertyAccessor((IPropertySymbol)symbol, methodName),
+ UnsafeAccessorType.SetProperty => new UnsafeSetPropertyAccessor((IPropertySymbol)symbol, methodName),
+ _ => throw new Exception($"Unknown {nameof(UnsafeAccessorType)}."),
+ };
+
+ _unsafeAccessors.Add(key, accessor);
+ return accessor;
+ }
+
+ private string GetValidMethodName(ITypeSymbol symbol, string name)
+ {
+ var memberNames = _symbolAccessor.GetAllMembers(symbol).Select(x => x.Name);
+ return _nameBuilder.New(name, memberNames);
+ }
+
+ ///
+ /// Strips the leading underscore and capitalise the first letter.
+ ///
+ /// Accessor name to be formatted.
+ /// Formatted accessor name.
+ private string FormatAccessorName(string name)
+ {
+ name = name.TrimStart('_');
+ if (name.Length == 0)
+ return name;
+
+ return char.ToUpper(name[0], CultureInfo.InvariantCulture) + name[1..];
+ }
+
+ public enum UnsafeAccessorType
+ {
+ GetProperty,
+ SetProperty,
+ GetField,
+ }
+
+ private readonly struct UnsafeAccessorKey : IEquatable
+ {
+ private readonly ISymbol _member;
+ private readonly UnsafeAccessorType _type;
+
+ public UnsafeAccessorKey(ISymbol member, UnsafeAccessorType type)
+ {
+ _member = member;
+ _type = type;
+ }
+
+ public bool Equals(UnsafeAccessorKey other) =>
+ SymbolEqualityComparer.Default.Equals(_member, other._member) && _type == other._type;
+
+ public override bool Equals(object? obj) => obj is UnsafeAccessorKey other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = SymbolEqualityComparer.Default.GetHashCode(_member);
+ hashCode = (hashCode * 397) ^ (int)_type;
+ return hashCode;
+ }
+ }
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
index ae858eec447..273a8fbf925 100644
--- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
+++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
@@ -79,7 +79,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM
{
// ignore all non ordinary methods (eg. ctor, operators, etc.) and methods declared on the object type (eg. ToString)
return method.MethodKind == MethodKind.Ordinary
- && ctx.SymbolAccessor.IsAccessible(method)
+ && ctx.SymbolAccessor.IsDirectlyAccessible(method)
&& !SymbolEqualityComparer.Default.Equals(method.ReceiverType, ctx.Compilation.ObjectType);
}
diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
index c80826752f5..36b45233ae4 100644
--- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
@@ -464,4 +464,13 @@ public static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
true
);
+
+ public static readonly DiagnosticDescriptor UnsafeAccessorNotAvailable = new DiagnosticDescriptor(
+ "RMG053",
+ $"The flag {nameof(MemberVisibility)}.{nameof(MemberVisibility.Accessible)} cannot be disabled, this feature requires .NET 8.0 or greater",
+ $"The flag {nameof(MemberVisibility)}.{nameof(MemberVisibility.Accessible)} cannot be disabled, this feature requires .NET 8.0 or greater",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Error,
+ true
+ );
}
diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs
index 374333aa5b4..a91c7614038 100644
--- a/src/Riok.Mapperly/Emit/SourceEmitter.cs
+++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs
@@ -23,14 +23,26 @@ public static CompilationUnitSyntax Build(MapperDescriptor descriptor, Cancellat
var memberCtx = ctx.AddIndentation();
var members = BuildMembers(memberCtx, descriptor, cancellationToken);
members = members.SeparateByLineFeed(memberCtx.SyntaxFactory.Indentation);
- MemberDeclarationSyntax member = ctx.SyntaxFactory.Class(descriptor.Symbol.Name, descriptor.Syntax.Modifiers, List(members));
+ MemberDeclarationSyntax mapperClass = ctx.SyntaxFactory.Class(descriptor.Symbol.Name, descriptor.Syntax.Modifiers, List(members));
+
+ var compilationUnitMembers = new List(2) { mapperClass };
+
+#if ROSLYN4_7_OR_GREATER
+ if (descriptor.UnsafeAccessors.Count > 0)
+ {
+ var unsafeAccessorClass = UnsafeAccessorEmitter.BuildUnsafeAccessorClass(descriptor, cancellationToken, ctx);
+ compilationUnitMembers.Add(unsafeAccessorClass);
+ }
+#endif
+
+ var compilationUnitMemberSyntaxList = List(compilationUnitMembers.SeparateByTrailingLineFeed(memberCtx.SyntaxFactory.Indentation));
ctx = ctx.RemoveIndentation();
- member = WrapInClassesAsNeeded(ref ctx, descriptor.Symbol, member);
- member = WrapInNamespaceIfNeeded(ctx, descriptor.Namespace, member);
+ compilationUnitMemberSyntaxList = WrapInClassesAsNeeded(ref ctx, descriptor.Symbol, compilationUnitMemberSyntaxList);
+ compilationUnitMemberSyntaxList = WrapInNamespaceIfNeeded(ctx, descriptor.Namespace, compilationUnitMemberSyntaxList);
return CompilationUnit()
- .WithMembers(SingletonList(member))
+ .WithMembers(compilationUnitMemberSyntaxList)
.WithLeadingTrivia(Comment(AutoGeneratedComment), ElasticCarriageReturnLineFeed, Nullable(true), ElasticCarriageReturnLineFeed);
}
@@ -47,10 +59,10 @@ CancellationToken cancellationToken
}
}
- private static MemberDeclarationSyntax WrapInClassesAsNeeded(
+ private static SyntaxList WrapInClassesAsNeeded(
ref SourceEmitterContext ctx,
INamedTypeSymbol symbol,
- MemberDeclarationSyntax syntax
+ SyntaxList members
)
{
var containingType = symbol.ContainingType;
@@ -59,24 +71,26 @@ MemberDeclarationSyntax syntax
if (containingType.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not ClassDeclarationSyntax containingTypeSyntax)
break;
- syntax = ctx.SyntaxFactory.Class(containingType.Name, containingTypeSyntax.Modifiers, SingletonList(syntax));
+ members = SingletonList(
+ ctx.SyntaxFactory.Class(containingType.Name, containingTypeSyntax.Modifiers, members)
+ );
ctx = ctx.RemoveIndentation();
containingType = containingType.ContainingType;
}
- return syntax;
+ return members;
}
- private static MemberDeclarationSyntax WrapInNamespaceIfNeeded(
+ private static SyntaxList WrapInNamespaceIfNeeded(
SourceEmitterContext ctx,
string? namespaceName,
- MemberDeclarationSyntax classDeclaration
+ SyntaxList members
)
{
if (namespaceName == null)
- return classDeclaration;
+ return members;
- return ctx.SyntaxFactory.Namespace(namespaceName).WithMembers(SingletonList(classDeclaration));
+ return SingletonList(ctx.SyntaxFactory.Namespace(namespaceName).WithMembers(members));
}
private static SourceEmitterContext IndentForMapper(SourceEmitterContext ctx, INamedTypeSymbol symbol)
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs
new file mode 100644
index 00000000000..e52ff97dade
--- /dev/null
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs
@@ -0,0 +1,44 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Riok.Mapperly.Emit.Syntax;
+
+public partial struct SyntaxFactoryHelper
+{
+ private const string UnsafeAccessorName = "global::System.Runtime.CompilerServices.UnsafeAccessor";
+ private const string UnsafeAccessorKindName = "global::System.Runtime.CompilerServices.UnsafeAccessorKind";
+ private const string UnsafeAccessorNameArgument = "Name";
+
+ private static readonly IdentifierNameSyntax _unsafeAccessorKindName = IdentifierName(UnsafeAccessorKindName);
+
+ public SyntaxList UnsafeAccessorAttributeList(UnsafeAccessorType type, string name)
+ {
+ var unsafeAccessType = type switch
+ {
+ UnsafeAccessorType.Field => "Field",
+ UnsafeAccessorType.Method => "Method",
+ _ => throw new Exception($"Unknown {nameof(UnsafeAccessorType)}."),
+ };
+ var arguments = CommaSeparatedList(
+ AttributeArgument(
+ MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, _unsafeAccessorKindName, IdentifierName(unsafeAccessType))
+ ),
+ AttributeArgument(
+ Assignment(IdentifierName(UnsafeAccessorNameArgument), LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(name)))
+ )
+ );
+
+ var attribute = Attribute(IdentifierName(UnsafeAccessorName))
+ .WithArgumentList(AttributeArgumentList(CommaSeparatedList(arguments)));
+
+ return SingletonList(AttributeList(SingletonSeparatedList(attribute)).AddTrailingLineFeed(Indentation));
+ }
+
+ public enum UnsafeAccessorType
+ {
+ Method,
+ Field
+ }
+}
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs
index 4a2a0e0afd9..63fab7d5782 100644
--- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs
@@ -90,11 +90,14 @@ public static ParameterListSyntax ParameterList(bool extensionMethod, params Met
private static ParameterSyntax Parameter(bool addThisKeyword, MethodParameter parameter)
{
- var param = SyntaxFactory
- .Parameter(Identifier(parameter.Name))
- .WithType(FullyQualifiedIdentifier(parameter.Type).AddTrailingSpace());
+ return Parameter(parameter.Type.FullyQualifiedIdentifierName(), parameter.Name, addThisKeyword);
+ }
+
+ public static ParameterSyntax Parameter(string type, string identifier, bool addThisKeyword = false)
+ {
+ var param = SyntaxFactory.Parameter(Identifier(identifier)).WithType(IdentifierName(type).AddTrailingSpace());
- if (addThisKeyword && parameter.Ordinal == 0)
+ if (addThisKeyword)
{
param = param.WithModifiers(TokenList(TrailingSpacedToken(SyntaxKind.ThisKeyword)));
}
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs
new file mode 100644
index 00000000000..598518ddf37
--- /dev/null
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs
@@ -0,0 +1,29 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Riok.Mapperly.Emit.Syntax;
+
+public partial struct SyntaxFactoryHelper
+{
+ public static MethodDeclarationSyntax PublicStaticExternMethod(
+ TypeSyntax returnType,
+ string methodName,
+ ParameterListSyntax parameterList,
+ SyntaxList attributeList
+ )
+ {
+ return MethodDeclaration(returnType, Identifier(methodName))
+ .WithModifiers(
+ TokenList(
+ TrailingSpacedToken(SyntaxKind.PublicKeyword),
+ TrailingSpacedToken(SyntaxKind.StaticKeyword),
+ TrailingSpacedToken(SyntaxKind.ExternKeyword)
+ )
+ )
+ .WithParameterList(parameterList)
+ .WithAttributeLists(attributeList)
+ .WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
+ }
+}
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs
index afed3afa9b3..e8f893c1e29 100644
--- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs
@@ -64,6 +64,14 @@ public static SeparatedSyntaxList CommaSeparatedList(IEnumerable nodes,
return SeparatedList(joinedNodes);
}
+ public static SeparatedSyntaxList CommaSeparatedList(params T[] nodes)
+ where T : SyntaxNode
+ {
+ var sep = TrailingSpacedToken(SyntaxKind.CommaToken);
+ var joinedNodes = Join(sep, false, nodes);
+ return SeparatedList(joinedNodes);
+ }
+
private SeparatedSyntaxList CommaLineFeedSeparatedList(IEnumerable nodes)
where T : SyntaxNode
{
diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs
index cf3d87d36c3..b3ae2a1b4c1 100644
--- a/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs
+++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs
@@ -34,6 +34,24 @@ public static IEnumerable SeparateByLineFeed(this IEnumerable<
}
}
+ public static IEnumerable SeparateByTrailingLineFeed(this IEnumerable syntax, int indentation)
+ where TSyntax : SyntaxNode
+ {
+ using var enumerator = syntax.GetEnumerator();
+ if (!enumerator.MoveNext())
+ yield break;
+
+ var current = enumerator.Current!;
+
+ while (enumerator.MoveNext())
+ {
+ yield return current.AddTrailingLineFeed(indentation);
+ current = enumerator.Current!;
+ }
+
+ yield return current;
+ }
+
///
/// Adds a leading line feed to the first found trivia.
/// If the first token is known by the caller, use for the first token instead
diff --git a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs
new file mode 100644
index 00000000000..47009756a29
--- /dev/null
+++ b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs
@@ -0,0 +1,45 @@
+#if ROSLYN4_7_OR_GREATER
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors;
+using Riok.Mapperly.Emit.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Emit;
+
+public static class UnsafeAccessorEmitter
+{
+ private const string AccessorClassName = "UnsafeAccessor";
+
+ public static MemberDeclarationSyntax BuildUnsafeAccessorClass(
+ MapperDescriptor descriptor,
+ CancellationToken cancellationToken,
+ SourceEmitterContext ctx
+ )
+ {
+ var accessorCtx = ctx.AddIndentation();
+ var accessorClassName = descriptor.NameBuilder.New(AccessorClassName);
+ var accessors = BuildUnsafeAccessors(accessorCtx, descriptor, cancellationToken);
+ accessors = accessors.SeparateByLineFeed(accessorCtx.SyntaxFactory.Indentation);
+ return ctx.SyntaxFactory.Class(
+ accessorClassName,
+ TokenList(TrailingSpacedToken(SyntaxKind.StaticKeyword), TrailingSpacedToken(SyntaxKind.FileKeyword)),
+ List(accessors)
+ );
+ }
+
+ private static IEnumerable BuildUnsafeAccessors(
+ SourceEmitterContext ctx,
+ MapperDescriptor descriptor,
+ CancellationToken cancellationToken
+ )
+ {
+ foreach (var accessor in descriptor.UnsafeAccessors)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return accessor.BuildMethod(ctx);
+ }
+ }
+}
+#endif
diff --git a/src/Riok.Mapperly/Helpers/HashSetExtensions.cs b/src/Riok.Mapperly/Helpers/HashSetExtensions.cs
new file mode 100644
index 00000000000..9340c616fd2
--- /dev/null
+++ b/src/Riok.Mapperly/Helpers/HashSetExtensions.cs
@@ -0,0 +1,12 @@
+namespace Riok.Mapperly.Helpers;
+
+public static class HashSetExtensions
+{
+ public static void AddRange(this HashSet hashSet, IEnumerable items)
+ {
+ foreach (var key in items)
+ {
+ hashSet.Add(key);
+ }
+ }
+}
diff --git a/src/Riok.Mapperly/Helpers/UniqueNameBuilder.cs b/src/Riok.Mapperly/Helpers/UniqueNameBuilder.cs
index 6a4a53d53a3..3e9d3e11985 100644
--- a/src/Riok.Mapperly/Helpers/UniqueNameBuilder.cs
+++ b/src/Riok.Mapperly/Helpers/UniqueNameBuilder.cs
@@ -35,6 +35,17 @@ public string New(string name)
return uniqueName;
}
+ public string New(string name, IEnumerable reservedNames)
+ {
+ var scope = NewScope();
+ scope.Reserve(reservedNames);
+ var uniqueName = scope.New(name);
+ _usedNames.Add(uniqueName);
+ return uniqueName;
+ }
+
+ private void Reserve(IEnumerable names) => _usedNames.AddRange(names);
+
private bool Contains(string name)
{
if (_usedNames.Contains(name))
diff --git a/src/Riok.Mapperly/Riok.Mapperly.Roslyn4.7.props b/src/Riok.Mapperly/Riok.Mapperly.Roslyn4.7.props
index 1135a89bf88..c99d945a48a 100644
--- a/src/Riok.Mapperly/Riok.Mapperly.Roslyn4.7.props
+++ b/src/Riok.Mapperly/Riok.Mapperly.Roslyn4.7.props
@@ -2,6 +2,7 @@
$(DefineConstants);ROSLYN4_4_OR_GREATER
+ $(DefineConstants);ROSLYN4_7_OR_GREATER
diff --git a/src/Riok.Mapperly/Symbols/FieldMember.cs b/src/Riok.Mapperly/Symbols/FieldMember.cs
index 6fc08b11672..e2e8457f49b 100644
--- a/src/Riok.Mapperly/Symbols/FieldMember.cs
+++ b/src/Riok.Mapperly/Symbols/FieldMember.cs
@@ -1,5 +1,7 @@
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Helpers;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
namespace Riok.Mapperly.Symbols;
@@ -19,6 +21,7 @@ public FieldMember(IFieldSymbol fieldSymbol)
public bool IsIndexer => false;
public bool CanGet => !_fieldSymbol.IsReadOnly;
public bool CanSet => true;
+ public bool CanSetDirectly => true;
public bool IsInitOnly => false;
public bool IsRequired
@@ -28,6 +31,11 @@ public bool IsRequired
=> false;
#endif
+ public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false)
+ {
+ return nullConditional ? ConditionalAccess(source, Name) : MemberAccess(source, Name);
+ }
+
public override bool Equals(object? obj) =>
obj is FieldMember other && SymbolEqualityComparer.IncludeNullability.Equals(_fieldSymbol, other._fieldSymbol);
diff --git a/src/Riok.Mapperly/Symbols/GetterMemberPath.cs b/src/Riok.Mapperly/Symbols/GetterMemberPath.cs
new file mode 100644
index 00000000000..689eb707cb8
--- /dev/null
+++ b/src/Riok.Mapperly/Symbols/GetterMemberPath.cs
@@ -0,0 +1,89 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors;
+using Riok.Mapperly.Emit.Syntax;
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Symbols;
+
+public class GetterMemberPath : MemberPath
+{
+ private GetterMemberPath(IReadOnlyList path)
+ : base(path) { }
+
+ public static GetterMemberPath Build(MappingBuilderContext ctx, MemberPath memberPath)
+ {
+ var memberPathArray = memberPath.Path.Select(item => BuildMappableMember(ctx, item)).ToArray();
+ return new GetterMemberPath(memberPathArray);
+ }
+
+ public static IEnumerable Build(MappingBuilderContext ctx, IEnumerable path)
+ {
+ return path.Select(item => BuildMappableMember(ctx, item));
+ }
+
+ private static IMappableMember BuildMappableMember(MappingBuilderContext ctx, IMappableMember item)
+ {
+ if (ctx.SymbolAccessor.IsDirectlyAccessible(item.MemberSymbol))
+ {
+ return item;
+ }
+
+ if (item.MemberSymbol.Kind == SymbolKind.Field)
+ {
+ var unsafeAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor(
+ UnsafeAccessorContext.UnsafeAccessorType.GetField,
+ (IFieldSymbol)item.MemberSymbol
+ );
+
+ return new MethodAccessorMember(item, unsafeAccessor.MethodName);
+ }
+ else
+ {
+ var unsafeAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor(
+ UnsafeAccessorContext.UnsafeAccessorType.GetProperty,
+ (IPropertySymbol)item.MemberSymbol
+ );
+
+ return new MethodAccessorMember(item, unsafeAccessor.MethodName);
+ }
+ }
+
+ public ExpressionSyntax BuildAccess(
+ ExpressionSyntax? baseAccess,
+ bool addValuePropertyOnNullable = false,
+ bool nullConditional = false,
+ bool skipTrailingNonNullable = false
+ )
+ {
+ var path = skipTrailingNonNullable ? PathWithoutTrailingNonNullable() : Path;
+
+ if (baseAccess == null)
+ {
+ baseAccess = SyntaxFactory.IdentifierName(path.First().Name);
+ path = path.Skip(1);
+ }
+
+ if (nullConditional)
+ {
+ return path.AggregateWithPrevious(
+ baseAccess,
+ (expr, prevProp, prop) => prevProp?.IsNullable == true ? prop.BuildAccess(expr, true) : prop.BuildAccess(expr)
+ );
+ }
+
+ if (addValuePropertyOnNullable)
+ {
+ return path.Aggregate(
+ baseAccess,
+ (a, b) =>
+ b.Type.IsNullableValueType()
+ ? SyntaxFactoryHelper.MemberAccess(b.BuildAccess(a), NullableValueProperty)
+ : b.BuildAccess(a)
+ );
+ }
+
+ return path.Aggregate(baseAccess, (a, b) => b.BuildAccess(a));
+ }
+}
diff --git a/src/Riok.Mapperly/Symbols/IMappableMember.cs b/src/Riok.Mapperly/Symbols/IMappableMember.cs
index 9eb6a873ee9..3491e9b40bd 100644
--- a/src/Riok.Mapperly/Symbols/IMappableMember.cs
+++ b/src/Riok.Mapperly/Symbols/IMappableMember.cs
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Riok.Mapperly.Symbols;
@@ -20,9 +21,19 @@ public interface IMappableMember
bool CanGet { get; }
+ ///
+ /// Whether the member can be modified using assignment or an unsafe accessor method.
+ ///
bool CanSet { get; }
+ ///
+ /// Whether the member can be modified using simple assignment.
+ ///
+ bool CanSetDirectly { get; }
+
bool IsInitOnly { get; }
bool IsRequired { get; }
+
+ ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false);
}
diff --git a/src/Riok.Mapperly/Symbols/MappableMember.cs b/src/Riok.Mapperly/Symbols/MappableMember.cs
index 66e88485c22..6be46167c5e 100644
--- a/src/Riok.Mapperly/Symbols/MappableMember.cs
+++ b/src/Riok.Mapperly/Symbols/MappableMember.cs
@@ -7,7 +7,7 @@ internal static class MappableMember
{
public static IMappableMember? Create(SymbolAccessor accessor, ISymbol symbol)
{
- if (!accessor.IsAccessible(symbol))
+ if (!accessor.IsAccessibleToMemberVisibility(symbol))
return null;
return symbol switch
diff --git a/src/Riok.Mapperly/Symbols/MemberPath.cs b/src/Riok.Mapperly/Symbols/MemberPath.cs
index 9b3d858e303..12a5f755dbc 100644
--- a/src/Riok.Mapperly/Symbols/MemberPath.cs
+++ b/src/Riok.Mapperly/Symbols/MemberPath.cs
@@ -15,7 +15,7 @@ namespace Riok.Mapperly.Symbols;
public class MemberPath
{
private const string MemberAccessSeparator = ".";
- private const string NullableValueProperty = "Value";
+ protected const string NullableValueProperty = "Value";
public MemberPath(IReadOnlyList path)
{
@@ -73,41 +73,6 @@ public IEnumerable> ObjectPathNullableSubPaths()
public bool IsAnyObjectPathNullable() => ObjectPath.Any(p => p.IsNullable);
- public ExpressionSyntax BuildAccess(
- ExpressionSyntax? baseAccess,
- bool addValuePropertyOnNullable = false,
- bool nullConditional = false,
- bool skipTrailingNonNullable = false
- )
- {
- var path = skipTrailingNonNullable ? PathWithoutTrailingNonNullable() : Path;
-
- if (baseAccess == null)
- {
- baseAccess = IdentifierName(path.First().Name);
- path = path.Skip(1);
- }
-
- if (nullConditional)
- {
- return path.AggregateWithPrevious(
- baseAccess,
- (expr, prevProp, prop) => prevProp?.IsNullable == true ? ConditionalAccess(expr, prop.Name) : MemberAccess(expr, prop.Name)
- );
- }
-
- if (addValuePropertyOnNullable)
- {
- return path.Aggregate(
- baseAccess,
- (a, b) =>
- b.Type.IsNullableValueType() ? MemberAccess(MemberAccess(a, b.Name), NullableValueProperty) : MemberAccess(a, b.Name)
- );
- }
-
- return path.Aggregate(baseAccess, (a, b) => MemberAccess(a, b.Name));
- }
-
///
/// Builds a condition (the resulting expression evaluates to a boolean)
/// whether the path is non-null.
diff --git a/src/Riok.Mapperly/Symbols/MethodAccessorMember.cs b/src/Riok.Mapperly/Symbols/MethodAccessorMember.cs
new file mode 100644
index 00000000000..540df82ffc0
--- /dev/null
+++ b/src/Riok.Mapperly/Symbols/MethodAccessorMember.cs
@@ -0,0 +1,46 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Symbols;
+
+public class MethodAccessorMember : IMappableMember
+{
+ private readonly IMappableMember _mappableMember;
+ private readonly string _methodName;
+ private readonly bool _isMethod;
+
+ public MethodAccessorMember(IMappableMember mappableMember, string methodName, bool isMethod = false)
+ {
+ _mappableMember = mappableMember;
+ _methodName = methodName;
+ _isMethod = isMethod;
+ }
+
+ public string Name => _mappableMember.Name;
+ public ITypeSymbol Type => _mappableMember.Type;
+ public ISymbol MemberSymbol => _mappableMember.MemberSymbol;
+ public bool IsNullable => _mappableMember.IsNullable;
+ public bool IsIndexer => _mappableMember.IsIndexer;
+ public bool CanGet => _mappableMember.CanGet;
+ public bool CanSet => _mappableMember.CanSet;
+ public bool CanSetDirectly => _mappableMember.CanSetDirectly;
+ public bool IsInitOnly => _mappableMember.IsInitOnly;
+ public bool IsRequired => _mappableMember.IsRequired;
+
+ public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false)
+ {
+ if (_isMethod)
+ {
+ return nullConditional ? ConditionalAccess(source, _methodName) : MemberAccess(source, _methodName);
+ }
+
+ return nullConditional ? Invocation(ConditionalAccess(source, _methodName)) : Invocation(MemberAccess(source, _methodName));
+ }
+
+ public override bool Equals(object? obj) =>
+ obj is MethodAccessorMember other
+ && SymbolEqualityComparer.IncludeNullability.Equals(_mappableMember.MemberSymbol, other.MemberSymbol);
+
+ public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(_mappableMember.MemberSymbol);
+}
diff --git a/src/Riok.Mapperly/Symbols/PropertyMember.cs b/src/Riok.Mapperly/Symbols/PropertyMember.cs
index 5f4c24ad689..9ec35c6233d 100644
--- a/src/Riok.Mapperly/Symbols/PropertyMember.cs
+++ b/src/Riok.Mapperly/Symbols/PropertyMember.cs
@@ -1,6 +1,8 @@
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors;
using Riok.Mapperly.Helpers;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
namespace Riok.Mapperly.Symbols;
@@ -21,9 +23,16 @@ internal PropertyMember(IPropertySymbol propertySymbol, SymbolAccessor symbolAcc
public bool IsNullable => _propertySymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable();
public bool IsIndexer => _propertySymbol.IsIndexer;
public bool CanGet =>
- !_propertySymbol.IsWriteOnly && (_propertySymbol.GetMethod == null || _symbolAccessor.IsAccessible(_propertySymbol.GetMethod));
+ !_propertySymbol.IsWriteOnly
+ && (_propertySymbol.GetMethod == null || _symbolAccessor.IsAccessibleToMemberVisibility(_propertySymbol.GetMethod));
public bool CanSet =>
- !_propertySymbol.IsReadOnly && (_propertySymbol.SetMethod == null || _symbolAccessor.IsAccessible(_propertySymbol.SetMethod));
+ !_propertySymbol.IsReadOnly
+ && (_propertySymbol.SetMethod == null || _symbolAccessor.IsAccessibleToMemberVisibility(_propertySymbol.SetMethod));
+
+ public bool CanSetDirectly =>
+ !_propertySymbol.IsReadOnly
+ && (_propertySymbol.SetMethod == null || _symbolAccessor.IsDirectlyAccessible(_propertySymbol.SetMethod));
+
public bool IsInitOnly => _propertySymbol.SetMethod?.IsInitOnly == true;
public bool IsRequired
@@ -33,6 +42,11 @@ public bool IsRequired
=> false;
#endif
+ public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false)
+ {
+ return nullConditional ? ConditionalAccess(source, Name) : MemberAccess(source, Name);
+ }
+
public override bool Equals(object? obj) =>
obj is PropertyMember other && SymbolEqualityComparer.IncludeNullability.Equals(_propertySymbol, other._propertySymbol);
diff --git a/src/Riok.Mapperly/Symbols/SetterMemberPath.cs b/src/Riok.Mapperly/Symbols/SetterMemberPath.cs
new file mode 100644
index 00000000000..c1f61d52f01
--- /dev/null
+++ b/src/Riok.Mapperly/Symbols/SetterMemberPath.cs
@@ -0,0 +1,92 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Symbols;
+
+public class SetterMemberPath : MemberPath
+{
+ private SetterMemberPath(IReadOnlyList path, bool isMethod)
+ : base(path)
+ {
+ IsMethod = isMethod;
+ }
+
+ ///
+ /// Indicates whether this setter is an UnsafeAccessor for a property, ie. target.SetValue(source.Value);
+ /// False for standard properties, fields and UnsafeAccessor fields.
+ ///
+ public bool IsMethod { get; }
+
+ public static SetterMemberPath Build(MappingBuilderContext ctx, MemberPath memberPath)
+ {
+ // object path is the same as a getter
+ var setterPath = GetterMemberPath.Build(ctx, memberPath.ObjectPath).ToList();
+ // build the final member in the path and add it to the setter path
+ var (member, isMethod) = BuildMemberSetter(ctx, memberPath.Member);
+ setterPath.Add(member);
+
+ return new SetterMemberPath(setterPath, isMethod);
+ }
+
+ private static (IMappableMember, bool) BuildMemberSetter(MappingBuilderContext ctx, IMappableMember member)
+ {
+ if (ctx.SymbolAccessor.IsDirectlyAccessible(member.MemberSymbol))
+ {
+ return (member, false);
+ }
+
+ if (member.MemberSymbol.Kind == SymbolKind.Field)
+ {
+ var unsafeAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor(
+ UnsafeAccessorContext.UnsafeAccessorType.GetField,
+ (IFieldSymbol)member.MemberSymbol
+ );
+
+ return (new MethodAccessorMember(member, unsafeAccessor.MethodName), false);
+ }
+ else
+ {
+ var unsafeAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor(
+ UnsafeAccessorContext.UnsafeAccessorType.SetProperty,
+ (IPropertySymbol)member.MemberSymbol
+ );
+
+ return (new MethodAccessorMember(member, unsafeAccessor.MethodName, isMethod: true), true);
+ }
+ }
+
+ public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax sourceValue, bool coalesceAssignment = false)
+ {
+ IEnumerable path = Path;
+
+ if (baseAccess == null)
+ {
+ baseAccess = SyntaxFactory.IdentifierName(path.First().Name);
+ path = path.Skip(1);
+ }
+
+ var memberPath = path.Aggregate(baseAccess, (a, b) => b.BuildAccess(a));
+
+ if (coalesceAssignment)
+ {
+ // cannot use coalesce assignment within a setter method invocation.
+ Debug.Assert(!IsMethod);
+
+ // target.Value ??= mappedValue;
+ return CoalesceAssignment(memberPath, sourceValue);
+ }
+
+ if (IsMethod)
+ {
+ // target.SetValue(source.Value);
+ return Invocation(memberPath, sourceValue);
+ }
+
+ // target.Value = source.Value;
+ return Assignment(memberPath, sourceValue);
+ }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
index 6ce3d979e8c..b6e60d56a38 100644
--- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
@@ -11,6 +11,7 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
{
CtorValue = ctorValue;
CtorValue2 = ctorValue2;
+ PrivateValue = ctorValue + 21;
}
public int CtorValue { get; set; }
@@ -121,5 +122,9 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public DateOnly DateTimeValueTargetDateOnly { get; set; }
public TimeOnly DateTimeValueTargetTimeOnly { get; set; }
+
+ public int ExposePrivateValue => PrivateValue;
+
+ private int PrivateValue { get; set; }
}
}
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
index 2cbffa2900e..03c08c3d722 100644
--- a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
@@ -11,7 +11,7 @@
namespace Riok.Mapperly.IntegrationTests.Mapper
{
- [Mapper]
+ [Mapper(IncludedMembers = MemberVisibility.All)]
public partial class TestMapper
{
public partial int DirectInt(int value);
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
index 21e1d94064f..bd797aa3c6f 100644
--- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
@@ -10,6 +10,7 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
{
CtorValue = ctorValue;
CtorValue2 = ctorValue2;
+ PrivateValue = ctorValue + 11;
}
public int CtorValue { get; set; }
@@ -118,5 +119,9 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public DateTime DateTimeValueTargetDateOnly { get; set; }
public DateTime DateTimeValueTargetTimeOnly { get; set; }
+
+ public int ExposePrivateValue => PrivateValue;
+
+ private int PrivateValue { get; set; }
}
}
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
index a6fbf2c826d..4aa16dc0345 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -40,7 +40,8 @@
ImmutableStackValue: [],
EnumValue: Value10,
EnumName: Value30,
- EnumReverseStringValue: DtoValue3
+ EnumReverseStringValue: DtoValue3,
+ ExposePrivateValue: 16
},
SourceTargetSameObjectType: {
CtorValue: 8,
@@ -57,7 +58,8 @@
ImmutableArrayValue: null,
ImmutableQueueValue: [],
ImmutableStackValue: [],
- EnumReverseStringValue:
+ EnumReverseStringValue: ,
+ ExposePrivateValue: 19
},
NullableReadOnlyObjectCollection: [
{
@@ -167,5 +169,6 @@
BaseIntValue: 1
},
DateTimeValueTargetDateOnly: 2020-01-03 15:10:05 Utc,
- DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc
+ DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc,
+ ExposePrivateValue: 18
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 23d9668a6fb..93274d630b5 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -47,7 +47,8 @@
EnumValue: DtoValue1,
EnumName: Value30,
EnumStringValue: 0,
- EnumReverseStringValue: DtoValue3
+ EnumReverseStringValue: DtoValue3,
+ ExposePrivateValue: 16
},
SourceTargetSameObjectType: {
CtorValue: 8,
@@ -61,7 +62,8 @@
ImmutableArrayValue: null,
ImmutableQueueValue: [],
ImmutableStackValue: [],
- EnumReverseStringValue:
+ EnumReverseStringValue: ,
+ ExposePrivateValue: 19
},
NullableReadOnlyObjectCollection: [
{
@@ -181,5 +183,6 @@
BaseIntValue: 1
},
DateTimeValueTargetDateOnly: 2020-01-03,
- DateTimeValueTargetTimeOnly: 3:10 PM
+ DateTimeValueTargetTimeOnly: 3:10 PM,
+ ExposePrivateValue: 18
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
index 443e51fee26..1d345352d60 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
@@ -137,6 +137,7 @@ public partial int ParseableInt(string value)
target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue);
target.DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(testObject.DateTimeValueTargetDateOnly);
target.DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(testObject.DateTimeValueTargetTimeOnly);
+ target.SetPrivateValue(DirectInt(testObject.GetPrivateValue()));
return target;
}
@@ -216,6 +217,7 @@ public partial int ParseableInt(string value)
target.EnumRawValue = (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)dto.EnumRawValue;
target.EnumStringValue = MapToTestEnum(dto.EnumStringValue);
target.EnumReverseStringValue = MapToString1(dto.EnumReverseStringValue);
+ target.SetPrivateValue1(DirectInt(dto.GetPrivateValue1()));
return target;
}
@@ -302,6 +304,7 @@ public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.Test
target.EnumReverseStringValue = MapToTestEnumDtoByValue(source.EnumReverseStringValue);
target.DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(source.DateTimeValueTargetDateOnly);
target.DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(source.DateTimeValueTargetTimeOnly);
+ target.SetPrivateValue(DirectInt(source.GetPrivateValue()));
}
public partial global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName MapToEnumDtoByName(global::Riok.Mapperly.IntegrationTests.Models.TestEnum v)
@@ -464,4 +467,19 @@ private string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumD
return target;
}
}
+
+ static file class UnsafeAccessor
+ {
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_PrivateValue")]
+ public static extern int GetPrivateValue(this global::Riok.Mapperly.IntegrationTests.Models.TestObject source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_PrivateValue")]
+ public static extern void SetPrivateValue(this global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target, int value);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_PrivateValue")]
+ public static extern int GetPrivateValue1(this global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_PrivateValue")]
+ public static extern void SetPrivateValue1(this global::Riok.Mapperly.IntegrationTests.Models.TestObject target, int value);
+ }
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
index 257a3273fd3..9e396839a6a 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
@@ -42,7 +42,8 @@
EnumValue: DtoValue1,
EnumName: Value30,
EnumStringValue: 0,
- EnumReverseStringValue: DtoValue3
+ EnumReverseStringValue: DtoValue3,
+ ExposePrivateValue: 26
},
SourceTargetSameObjectType: {
CtorValue: 8,
@@ -56,7 +57,8 @@
ImmutableArrayValue: null,
ImmutableQueueValue: [],
ImmutableStackValue: [],
- EnumReverseStringValue:
+ EnumReverseStringValue: ,
+ ExposePrivateValue: 19
},
NullableReadOnlyObjectCollection: [
{
@@ -176,5 +178,6 @@
BaseIntValue: 1
},
DateTimeValueTargetDateOnly: 2020-01-03,
- DateTimeValueTargetTimeOnly: 3:10 PM
+ DateTimeValueTargetTimeOnly: 3:10 PM,
+ ExposePrivateValue: 28
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 9f4d61564e8..0e81ca42613 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -47,7 +47,8 @@
EnumValue: DtoValue1,
EnumName: Value30,
EnumStringValue: 0,
- EnumReverseStringValue: DtoValue3
+ EnumReverseStringValue: DtoValue3,
+ ExposePrivateValue: 26
},
SourceTargetSameObjectType: {
CtorValue: 8,
@@ -61,7 +62,8 @@
ImmutableArrayValue: null,
ImmutableQueueValue: [],
ImmutableStackValue: [],
- EnumReverseStringValue:
+ EnumReverseStringValue: ,
+ ExposePrivateValue: 19
},
NullableReadOnlyObjectCollection: [
{
@@ -181,5 +183,6 @@
BaseIntValue: 1
},
DateTimeValueTargetDateOnly: 2020-01-03,
- DateTimeValueTargetTimeOnly: 3:10 PM
+ DateTimeValueTargetTimeOnly: 3:10 PM,
+ ExposePrivateValue: 28
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/Helpers/HashSetExtensionsTest.cs b/test/Riok.Mapperly.Tests/Helpers/HashSetExtensionsTest.cs
new file mode 100644
index 00000000000..0e1562e54fe
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Helpers/HashSetExtensionsTest.cs
@@ -0,0 +1,14 @@
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Tests.Helpers;
+
+public class HashSetExtensionsTest
+{
+ [Fact]
+ public void AddRangeShouldAddAllItems()
+ {
+ var h = new HashSet { 1, 2, 3 };
+ h.AddRange(new[] { 3, 4, 5, 5 });
+ h.Should().Contain(new[] { 1, 2, 3, 4, 5 });
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs
new file mode 100644
index 00000000000..6940f7387b9
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs
@@ -0,0 +1,410 @@
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.Diagnostics;
+
+namespace Riok.Mapperly.Tests.Mapping;
+
+[UsesVerify]
+public class UnsafeAccessorTest
+{
+ [Fact]
+ public Task PrivateProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "partial B Map(A source);",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int _value { get; set; } }",
+ "class B { private int _value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task ProtectedProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "partial B Map(A source);",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { protected int value { get; set; } }",
+ "class B { protected int value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task PrivateNestedProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("nested.value", "value")]
+ partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private C nested { get; set; } }",
+ "class B { private int value { get; set; } }",
+ "class C { private int value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task PrivateExistingTargetProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ partial void Map(A source, B dest);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int value { get; set; } }",
+ "class B { private int value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task PrivateExistingTargetEnumerableProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ partial void Map(A source, B dest);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private List value { get; } }",
+ "class B { private List value { get;} }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task PrivateNestedNullableProperty()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("nested.value", "value")]
+ partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private C? nested { get; set; } }",
+ "class B { private int value { get; set; } }",
+ "class C { private int? value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task PrivateNestedNullablePropertyShouldInitialize()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("nested.value", "nested.value")]
+ partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private C nested { get; set; } }",
+ "class B { private D nested { get; set; } }",
+ "class C { private int value { get; set; } }",
+ "class D { private int value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void ManualUnflattenedPropertyNullablePath()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("MyValueId", "Value.Id")]
+ [MapProperty("MyValueId2", "Value.Id2")]
+ partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }",
+ "class B { private C? Value { get; set; } }",
+ "class C { public string Id { get; set; } public string Id2 { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveMapMethodBody(
+ """
+ var target = new global::B();
+ target.SetValue(target.GetValue() ?? new());
+ target.GetValue().Id = source.MyValueId;
+ target.GetValue().Id2 = source.MyValueId2;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public Task PrivateField()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "partial B Map(A source);",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int value }",
+ "class B { private int value }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void ManualUnflattenedFieldNullablePath()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("MyValueId", "Value.Id")]
+ [MapProperty("MyValueId2", "Value.Id2")]
+ partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }",
+ "class B { private C? Value }",
+ "class C { public string Id { get; set; } public string Id2 { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveMapMethodBody(
+ """
+ var target = new global::B();
+ target.GetValue() ??= new();
+ target.GetValue().Id = source.MyValueId;
+ target.GetValue().Id2 = source.MyValueId2;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public Task GeneratedMethodShouldNotHaveConflictingName()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "partial B Map(A source);",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int value { get; set; } public void GetValue() { } public void GetValue1() { } }",
+ "class B { private int value { get; set; } public void Setvalue() { } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void InitPrivatePropertyShouldNotMap()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "partial B Map(A source);",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int value { get; set; } }",
+ "class B { private int value { get; init; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveMapMethodBody(
+ """
+ var target = new global::B();
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void RequiredPrivateSetPropertyShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.AllAccessible),
+ "class A { public int Value { get; set; } }",
+ "class B { public required int Value { get; private set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember)
+ .HaveAssertedAllDiagnostics()
+ .HaveMapMethodBody(
+ """
+ var target = new global::B();
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void QueryablePrivateToPrivatePropertyShouldNotGenerate()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int Value { get; set; } }",
+ "class B { private int Value { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics();
+ }
+
+ [Fact]
+ public void QueryablePrivateToPublicPropertyShouldNotGenerate()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int Value { get; set; } }",
+ "class B { public int Value { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.CannotMapFromWriteOnlyMember)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics();
+ }
+
+ [Fact]
+ public void UnmappedMembersShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "partial B Map(A source);",
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int _value1 { get; set; } }",
+ "class B { private int _value2 { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveMapMethodBody(
+ """
+ var target = new global::B();
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void IgnoreUnmappedMembers()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapperIgnoreSource("_value1")]
+ [MapperIgnoreTarget("_value2")]
+ partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All),
+ "class A { private int _value1 { get; set; } }",
+ "class B { private int _value2 { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveMapMethodBody(
+ """
+ var target = new global::B();
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public Task AssemblyDefaultShouldWork()
+ {
+ var source = TestSourceBuilder.CSharp(
+ """
+ using Riok.Mapperly.Abstractions;
+
+ [assembly: MapperDefaultsAttribute(IncludedMembers = MemberVisibility.All)]
+ [Mapper()]
+ public partial class MyMapper
+ {
+ private partial B Map(A source);
+ }
+
+ class A { private int value { get; set; } }
+
+ class B { private int value { get; set; } }
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task AttributeShouldOverrideAssemblyDefault()
+ {
+ var source = TestSourceBuilder.CSharp(
+ """
+ using Riok.Mapperly.Abstractions;
+
+ [assembly: MapperDefaultsAttribute(IncludedMembers = MemberVisibility.All)]
+ [Mapper(IncludedMembers = MemberVisibility.AllAccessible)]
+ public partial class MyMapper
+ {
+ private partial B Map(A value);
+ }
+
+ class A { private int value { get; set; } }
+
+ class B { private int value { get; set; } }
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MapperInNestedClassShouldWork()
+ {
+ var source = TestSourceBuilder.CSharp(
+ """
+ using Riok.Mapperly.Abstractions;
+
+ class A { private int value { get; set; } }
+
+ class B { private int value { get; set; } }
+
+ public static partial class CarFeature
+ {
+ public static partial class Mappers
+ {
+ [Mapper(IncludedMembers = MemberVisibility.All)]
+ public partial class CarMapper
+ {
+ public partial B Map(A value);
+ }
+ }
+ },
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
index 41f2ad5545c..62579524b35 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
@@ -91,6 +91,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options)
Attribute(options.EnumMappingIgnoreCase),
Attribute(options.IgnoreObsoleteMembersStrategy),
Attribute(options.RequiredMappingStrategy),
+ Attribute(options.IncludedMembers),
}.WhereNotNull();
return $"[Mapper({string.Join(", ", attrs)})]";
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
index 88248468298..0060d5008df 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
@@ -15,7 +15,8 @@ public record TestSourceBuilderOptions(
EnumMappingStrategy? EnumMappingStrategy = null,
bool? EnumMappingIgnoreCase = null,
IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null,
- RequiredMappingStrategy? RequiredMappingStrategy = null
+ RequiredMappingStrategy? RequiredMappingStrategy = null,
+ MemberVisibility? IncludedMembers = null
)
{
public const string DefaultMapperClassName = "Mapper";
@@ -30,6 +31,9 @@ public static TestSourceBuilderOptions WithIgnoreObsolete(IgnoreObsoleteMembersS
public static TestSourceBuilderOptions WithRequiredMappingStrategy(RequiredMappingStrategy requiredMappingStrategy) =>
new(RequiredMappingStrategy: requiredMappingStrategy);
+ public static TestSourceBuilderOptions WithMemberVisibility(MemberVisibility memberVisibility) =>
+ new(IncludedMembers: memberVisibility);
+
public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes)
{
var enabled = MappingConversionType.All;
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs
new file mode 100644
index 00000000000..7113b4ddeed
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: MyMapper.g.cs
+//
+#nullable enable
+public partial class MyMapper
+{
+ private partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.SetValue(source.GetValue());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs
new file mode 100644
index 00000000000..356416f47ad
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs
@@ -0,0 +1,11 @@
+//HintName: MyMapper.g.cs
+//
+#nullable enable
+public partial class MyMapper
+{
+ private partial global::B Map(global::A value)
+ {
+ var target = new global::B();
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.GeneratedMethodShouldNotHaveConflictingName#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.GeneratedMethodShouldNotHaveConflictingName#Mapper.g.verified.cs
new file mode 100644
index 00000000000..0d94bf9703a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.GeneratedMethodShouldNotHaveConflictingName#Mapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.SetValue(source.GetValue2());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue2(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.MapperInNestedClassShouldWork#CarFeature.Mappers.CarMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.MapperInNestedClassShouldWork#CarFeature.Mappers.CarMapper.g.verified.cs
new file mode 100644
index 00000000000..b7d35c42ead
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.MapperInNestedClassShouldWork#CarFeature.Mappers.CarMapper.g.verified.cs
@@ -0,0 +1,27 @@
+//HintName: CarFeature.Mappers.CarMapper.g.cs
+//
+#nullable enable
+public static partial class CarFeature
+{
+ public static partial class Mappers
+ {
+ public partial class CarMapper
+ {
+ public partial global::B Map(global::A value)
+ {
+ var target = new global::B();
+ target.SetValue(value.GetValue());
+ return target;
+ }
+ }
+
+ static file class UnsafeAccessor
+ {
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateExistingTargetEnumerableProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateExistingTargetEnumerableProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..8c31c95abc6
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateExistingTargetEnumerableProperty#Mapper.g.verified.cs
@@ -0,0 +1,23 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial void Map(global::A source, global::B dest)
+ {
+ dest.GetValue1().EnsureCapacity(source.GetValue().Count + dest.GetValue1().Count);
+ foreach (var item in source.GetValue())
+ {
+ dest.GetValue1().Add(item);
+ }
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern global::System.Collections.Generic.List GetValue(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern global::System.Collections.Generic.List GetValue1(this global::B source);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateExistingTargetProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateExistingTargetProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..c7c35566f29
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateExistingTargetProperty#Mapper.g.verified.cs
@@ -0,0 +1,19 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial void Map(global::A source, global::B dest)
+ {
+ dest.SetValue(source.GetValue());
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateField#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateField#Mapper.g.verified.cs
new file mode 100644
index 00000000000..80b4499f1a7
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateField#Mapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.GetValue1() = source.GetValue();
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "value")]
+ public static extern ref int GetValue(this global::A target);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "value")]
+ public static extern ref int GetValue1(this global::B target);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullableProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullableProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..99a46aeb1ce
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullableProperty#Mapper.g.verified.cs
@@ -0,0 +1,27 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ if (source.GetNested()?.GetValue() != null)
+ {
+ target.SetValue(source.GetNested().GetValue().Value);
+ }
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_nested")]
+ public static extern global::C? GetNested(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int? GetValue(this global::C source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyShouldInitialize#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyShouldInitialize#Mapper.g.verified.cs
new file mode 100644
index 00000000000..5749b85a65f
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedNullablePropertyShouldInitialize#Mapper.g.verified.cs
@@ -0,0 +1,27 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.GetNested1().SetValue(source.GetNested().GetValue());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_nested")]
+ public static extern global::C GetNested(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue(this global::C source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_nested")]
+ public static extern global::D GetNested1(this global::B source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::D target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..c6d850580fc
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateNestedProperty#Mapper.g.verified.cs
@@ -0,0 +1,24 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.SetValue(source.GetNested().GetValue());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_nested")]
+ public static extern global::C GetNested(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue(this global::C source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..fd559d7eacf
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateProperty#Mapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.SetValue(source.GetValue());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get__value")]
+ public static extern int GetValue(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set__value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ProtectedProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ProtectedProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..1d0e73c8e07
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ProtectedProperty#Mapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::B Map(global::A source)
+ {
+ var target = new global::B();
+ target.SetValue(source.GetValue());
+ return target;
+ }
+}
+
+static file class UnsafeAccessor
+{
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_value")]
+ public static extern int GetValue(this global::A source);
+
+ [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_value")]
+ public static extern void SetValue(this global::B target, int value);
+}
\ No newline at end of file