diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs index e9a6259fb8..fc4965360b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs @@ -123,7 +123,13 @@ public static bool ValidateMappingSpecification( } // cannot access non public member in initializer - if (allowInitOnlyMember && !ctx.BuilderContext.SymbolAccessor.IsAccessible(targetMemberPath.Member.MemberSymbol)) + if ( + allowInitOnlyMember + && ( + !ctx.BuilderContext.SymbolAccessor.IsAccessible(targetMemberPath.Member.MemberSymbol) + || !targetMemberPath.Member.CanAccessiblySet + ) + ) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToReadOnlyMember, @@ -138,7 +144,14 @@ public static bool ValidateMappingSpecification( } // 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.IsAccessible(p.MemberSymbol)) + ) + ) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToWriteOnlyMemberPath, @@ -174,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.IsAccessible(p.MemberSymbol)) + ) + ) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapFromWriteOnlyMember, diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs index 1bb39ca8fe..c9b9ed28e2 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; using Riok.Mapperly.Helpers; @@ -80,26 +81,21 @@ private string GetValidMethodName(ITypeSymbol source, string name) return uniqueName; } - // TODO: Refactor, rename, optimise, handle only underscore??? + // strip the leading underscore and capitalise the first letter private string FormatAccessorName(string name) { - var result = new StringBuilder(); + if (name.Length == 0) + return name; var index = 0; - foreach (var c in name) - { + if (name[0] == '_') index++; - if (c == '_') - { - continue; - } - result.Append(char.ToUpper(c)); - result.Append(name.AsSpan().Slice(index, name.Length - index)); - break; - } + if (name.Length - index == 0) + return string.Empty; - return result.ToString(); + var ch = char.ToUpper(name[index], CultureInfo.InvariantCulture); + return $"{ch}{name.Substring(index + 1, name.Length - index - 1)}"; } private readonly struct UnsafeAccessorKey : IEquatable diff --git a/src/Riok.Mapperly/Symbols/FieldMember.cs b/src/Riok.Mapperly/Symbols/FieldMember.cs index 0d37960507..14a75d28ac 100644 --- a/src/Riok.Mapperly/Symbols/FieldMember.cs +++ b/src/Riok.Mapperly/Symbols/FieldMember.cs @@ -21,7 +21,7 @@ public FieldMember(IFieldSymbol fieldSymbol) public bool IsIndexer => false; public bool CanGet => !_fieldSymbol.IsReadOnly; public bool CanSet => true; - + public bool CanAccessiblySet => true; public bool IsInitOnly => false; public bool IsRequired diff --git a/src/Riok.Mapperly/Symbols/IMappableMember.cs b/src/Riok.Mapperly/Symbols/IMappableMember.cs index 6c0cdcbc11..3c2e4156b6 100644 --- a/src/Riok.Mapperly/Symbols/IMappableMember.cs +++ b/src/Riok.Mapperly/Symbols/IMappableMember.cs @@ -23,6 +23,8 @@ public interface IMappableMember bool CanSet { get; } + bool CanAccessiblySet { get; } + bool IsInitOnly { get; } bool IsRequired { get; } diff --git a/src/Riok.Mapperly/Symbols/MethodMember.cs b/src/Riok.Mapperly/Symbols/MethodMember.cs index 8a97cf5a1c..734b9fdf48 100644 --- a/src/Riok.Mapperly/Symbols/MethodMember.cs +++ b/src/Riok.Mapperly/Symbols/MethodMember.cs @@ -24,6 +24,7 @@ public MethodMember(IMappableMember mappableMember, string methodName, bool isMe public bool IsIndexer => _mappableMember.IsIndexer; public bool CanGet => _mappableMember.CanGet; public bool CanSet => _mappableMember.CanSet; + public bool CanAccessiblySet => _mappableMember.CanAccessiblySet; public bool IsInitOnly => _mappableMember.IsInitOnly; public bool IsRequired => _mappableMember.IsRequired; diff --git a/src/Riok.Mapperly/Symbols/PropertyMember.cs b/src/Riok.Mapperly/Symbols/PropertyMember.cs index a93505dca1..bdd561d889 100644 --- a/src/Riok.Mapperly/Symbols/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/PropertyMember.cs @@ -28,6 +28,9 @@ internal PropertyMember(IPropertySymbol propertySymbol, SymbolAccessor symbolAcc public bool CanSet => !_propertySymbol.IsReadOnly && (_propertySymbol.SetMethod == null || _symbolAccessor.IsUnsafeAccessible(_propertySymbol.SetMethod)); + public bool CanAccessiblySet => + !_propertySymbol.IsReadOnly && (_propertySymbol.SetMethod == null || _symbolAccessor.IsAccessible(_propertySymbol.SetMethod)); + public bool IsInitOnly => _propertySymbol.SetMethod?.IsInitOnly == true; public bool IsRequired diff --git a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs index 147331ea07..dcd1f84db7 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs @@ -192,6 +192,69 @@ public void InitPrivateValueShouldNotMap() ); } + [Fact] + public void RequiredPrivateSetShouldDiagnostic() + { + 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 QueryablePrivateToPrivateShouldNotGenerate() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.AllAccessible & ~MemberVisibility.Accessible), + "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 QueryablePrivateToPublicShouldNotGenerate() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.AllAccessible & ~MemberVisibility.Accessible), + "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() {