From e82e85818a04c9b4157f99681ce2c42958801730 Mon Sep 17 00:00:00 2001 From: latonz Date: Wed, 26 Jul 2023 15:09:28 +0200 Subject: [PATCH] fix: handle internal visibility correctly --- .../Enumerables/CollectionInfoBuilder.cs | 1 + .../MembersContainerBuilderContext.cs | 3 +- ...wInstanceObjectMemberMappingBodyBuilder.cs | 2 +- .../NewValueTupleMappingBodyBuilder.cs | 26 +-- .../RuntimeTargetTypeMappingBodyBuilder.cs | 15 +- .../Descriptors/MappingBuilderContext.cs | 2 +- .../MappingBuilders/CtorMappingBuilder.cs | 2 +- .../DerivedTypeMappingBuilder.cs | 8 +- .../DictionaryMappingBuilder.cs | 2 +- .../EnumerableMappingBuilder.cs | 2 +- ...NewInstanceObjectPropertyMappingBuilder.cs | 2 +- .../MappingBuilders/SpanMappingBuilder.cs | 2 +- .../GenericSourceObjectFactory.cs | 16 +- .../GenericSourceTargetObjectFactory.cs | 23 +-- .../GenericTargetObjectFactory.cs | 16 +- .../GenericTargetObjectFactoryWithSource.cs | 4 +- .../ObjectFactories/ObjectFactory.cs | 7 +- .../ObjectFactories/ObjectFactoryBuilder.cs | 12 +- .../ObjectFactories/SimpleObjectFactory.cs | 4 +- .../SimpleObjectFactoryWithSource.cs | 4 +- .../SimpleMappingBuilderContext.cs | 1 + .../Descriptors/SymbolAccessor.cs | 49 ++++- .../Descriptors/UserMethodMappingExtractor.cs | 16 +- .../Descriptors/WellKnownTypes.cs | 2 +- .../Helpers/CompilationExtensions.cs | 120 +++++++++++ src/Riok.Mapperly/Helpers/SymbolExtensions.cs | 41 ---- src/Riok.Mapperly/MapperGenerator.cs | 2 +- src/Riok.Mapperly/Symbols/FieldMember.cs | 4 +- .../Symbols/GenericMappingTypeParameters.cs | 13 +- src/Riok.Mapperly/Symbols/MappableMember.cs | 8 +- src/Riok.Mapperly/Symbols/PropertyMember.cs | 11 +- .../Generator/IncrementalGeneratorTest.cs | 8 +- .../Mapping/ObjectVisibilityTest.cs | 193 ++++++++++++++++++ test/Riok.Mapperly.Tests/TestAssembly.cs | 20 ++ test/Riok.Mapperly.Tests/TestHelper.cs | 70 +++++-- test/Riok.Mapperly.Tests/TestHelperOptions.cs | 3 +- 36 files changed, 544 insertions(+), 170 deletions(-) create mode 100644 src/Riok.Mapperly/Helpers/CompilationExtensions.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/ObjectVisibilityTest.cs create mode 100644 test/Riok.Mapperly.Tests/TestAssembly.cs diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs index 964cba6540..b56e765c06 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs @@ -163,6 +163,7 @@ or CollectionType.SortedSet { return true; } + // has valid add if type implements ISet and has implicit Add method if ( implementedTypes.HasFlag(CollectionType.ISet) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index 00996eb00a..1b8f2fc62d 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -1,6 +1,5 @@ using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -39,7 +38,7 @@ private void AddNullMemberInitializers(IMemberAssignmentMappingContainer contain { var nullablePath = new MemberPath(nullableTrailPath); var type = nullablePath.Member.Type; - if (!type.HasAccessibleParameterlessConstructor()) + if (!BuilderContext.SymbolAccessor.HasAccessibleParameterlessConstructor(type)) { BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, type); continue; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs index ed4bf0e7d8..609c39b0d7 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs @@ -189,7 +189,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 => ctor.IsAccessible()) + .Where(ctor => ctx.BuilderContext.SymbolAccessor.IsAccessible(ctor)) .OrderByDescending(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x)) .ThenBy(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x)) .ThenByDescending(x => x.Parameters.Length == 0) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs index 0256960cee..95735f8f19 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs @@ -196,23 +196,17 @@ out sourcePath if (!ctx.Mapping.SourceType.IsTupleType || ctx.Mapping.SourceType is not INamedTypeSymbol namedType) return false; - var mappableMember = namedType.TupleElements - .Where( - x => - x.CorrespondingTupleField != default - && !ctx.IgnoredSourceMemberNames.Contains(x.Name) - && string.Equals(field.CorrespondingTupleField!.Name, x.CorrespondingTupleField!.Name) - ) - .Select(MappableMember.Create) - .WhereNotNull() - .FirstOrDefault(); + var mappableField = namedType.TupleElements.FirstOrDefault( + x => + x.CorrespondingTupleField != default + && !ctx.IgnoredSourceMemberNames.Contains(x.Name) + && string.Equals(field.CorrespondingTupleField!.Name, x.CorrespondingTupleField!.Name) + ); - if (mappableMember != default) - { - sourcePath = new MemberPath(new[] { mappableMember }); - return true; - } + if (mappableField == default) + return false; - return false; + sourcePath = new MemberPath(new[] { new FieldMember(mappableField) }); + return true; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs index cc92fee709..e2ce7394a3 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs @@ -14,7 +14,14 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns // therefore set source type always to nun-nullable // as non-nullables are also assignable to nullables. var mappings = GetUserMappingCandidates(ctx) - .Where(x => mapping.TypeParameters.CanConsumeTypes(ctx.Compilation, x.SourceType.NonNullable(), x.TargetType)); + .Where( + x => + mapping.TypeParameters.DoesTypesSatisfyTypeParameterConstraints( + ctx.SymbolAccessor, + x.SourceType.NonNullable(), + x.TargetType + ) + ); BuildMappingBody(ctx, mapping, mappings); } @@ -27,8 +34,8 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns var mappings = GetUserMappingCandidates(ctx) .Where( x => - x.SourceType.NonNullable().IsAssignableTo(ctx.Compilation, mapping.SourceType) - && x.TargetType.IsAssignableTo(ctx.Compilation, mapping.TargetType) + ctx.SymbolAccessor.HasImplicitConversion(x.SourceType.NonNullable(), mapping.SourceType) + && ctx.SymbolAccessor.HasImplicitConversion(x.TargetType, mapping.TargetType) ); BuildMappingBody(ctx, mapping, mappings); @@ -74,7 +81,7 @@ IEnumerable childMappings .ThenBy(x => x.TargetType.IsNullable()) .GroupBy(x => new TypeMappingKey(x, false)) .Select(x => x.First()) - .Select(x => new RuntimeTargetTypeMapping(x, x.TargetType.IsAssignableTo(ctx.Compilation, ctx.Target))); + .Select(x => new RuntimeTargetTypeMapping(x, ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target))); mapping.AddMappings(runtimeTargetTypeMappings); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 28cfc8a47c..eba5a5afd3 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -176,7 +176,7 @@ protected virtual NullFallbackValue GetNullFallbackValue(ITypeSymbol targetType, if (targetType.SpecialType == SpecialType.System_String) return NullFallbackValue.EmptyString; - if (targetType.HasAccessibleParameterlessConstructor()) + if (SymbolAccessor.HasAccessibleParameterlessConstructor(targetType)) return NullFallbackValue.CreateInstance; ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, targetType); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs index 8c1da83deb..4769ffe0f9 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(x => x.IsAccessible()) + .Where(ctx.SymbolAccessor.IsAccessible) .FirstOrDefault( m => m.Parameters.Length == 1 diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs index 38d673dede..45e7d36482 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs @@ -38,11 +38,11 @@ bool duplicatedSourceTypesAllowed var derivedTypeMappingSourceTypes = new HashSet(SymbolEqualityComparer.Default); var derivedTypeMappings = new List(configs.Count); Func isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter - ? t => sourceTypeParameter.CanConsumeType(ctx.Compilation, ctx.Source.NullableAnnotation, t) - : t => t.IsAssignableTo(ctx.Compilation, ctx.Source); + ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t, ctx.Source.NullableAnnotation) + : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source); Func isAssignableToTarget = ctx.Target is ITypeParameterSymbol targetTypeParameter - ? t => targetTypeParameter.CanConsumeType(ctx.Compilation, ctx.Target.NullableAnnotation, t) - : t => t.IsAssignableTo(ctx.Compilation, ctx.Target); + ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(targetTypeParameter, t, ctx.Target.NullableAnnotation) + : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Target); foreach (var config in configs) { diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs index 1d9d1af1c8..f3a961c6d9 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs @@ -64,7 +64,7 @@ or CollectionType.IReadOnlyDictionary // it should have a an object factory or a parameterless public ctor if ( !ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory) - && !ctx.Target.HasAccessibleParameterlessConstructor() + && !ctx.SymbolAccessor.HasAccessibleParameterlessConstructor(ctx.Target) ) { ctx.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, ctx.Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index be110137c0..bac0361ea7 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -165,7 +165,7 @@ ITypeMapping elementMapping { if ( !ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory) - && !ctx.Target.HasAccessibleParameterlessConstructor() + && !ctx.SymbolAccessor.HasAccessibleParameterlessConstructor(ctx.Target) ) { ctx.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, ctx.Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs index 97a3952494..be84352298 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs @@ -21,7 +21,7 @@ public static class NewInstanceObjectPropertyMappingBuilder ctx.MapperConfiguration.UseReferenceHandling ); - if (ctx.Target is not INamedTypeSymbol namedTarget || namedTarget.Constructors.All(x => !x.IsAccessible())) + if (ctx.Target is not INamedTypeSymbol namedTarget || namedTarget.Constructors.All(x => !ctx.SymbolAccessor.IsAccessible(x))) return null; if (ctx.Source.IsEnum() || ctx.Target.IsEnum()) diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs index dce98ec9a9..ae7d34ad04 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs @@ -130,7 +130,7 @@ ForEachAddEnumerableExistingTargetMapping CreateForEach(string methodName) if ( !ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory) - && !ctx.Target.HasAccessibleParameterlessConstructor() + && !ctx.SymbolAccessor.HasAccessibleParameterlessConstructor(ctx.Target) ) { return MapSpanArrayToEnumerableMethod(ctx); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs index 392c5efde9..ba3ea6d72f 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs @@ -1,6 +1,5 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; @@ -12,17 +11,16 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public class GenericSourceObjectFactory : ObjectFactory { - private readonly Compilation _compilation; - - public GenericSourceObjectFactory(IMethodSymbol method, Compilation compilation) - : base(method) - { - _compilation = compilation; - } + public GenericSourceObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) + : base(symbolAccessor, method) { } public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => SymbolEqualityComparer.Default.Equals(Method.ReturnType, targetTypeToCreate) - && Method.TypeParameters[0].CanConsumeType(_compilation, Method.Parameters[0].Type.NullableAnnotation, sourceType); + && SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints( + Method.TypeParameters[0], + sourceType, + Method.Parameters[0].Type.NullableAnnotation + ); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => GenericInvocation(Method.Name, new[] { NonNullableIdentifier(sourceType) }, source); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs index 776933e97e..6c1b1f7cd1 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs @@ -1,34 +1,31 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; public class GenericSourceTargetObjectFactory : ObjectFactory { - private readonly Compilation _compilation; private readonly int _sourceTypeParameterIndex; private readonly int _targetTypeParameterIndex; - public GenericSourceTargetObjectFactory(IMethodSymbol method, Compilation compilation, int sourceTypeParameterIndex) - : base(method) + public GenericSourceTargetObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method, int sourceTypeParameterIndex) + : base(symbolAccessor, method) { - _compilation = compilation; _sourceTypeParameterIndex = sourceTypeParameterIndex; _targetTypeParameterIndex = (sourceTypeParameterIndex + 1) % 2; } public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => - Method.TypeParameters[_sourceTypeParameterIndex].CanConsumeType( - _compilation, - Method.Parameters[0].Type.NullableAnnotation, - sourceType + SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints( + Method.TypeParameters[_sourceTypeParameterIndex], + sourceType, + Method.Parameters[0].Type.NullableAnnotation ) - && Method.TypeParameters[_targetTypeParameterIndex].CanConsumeType( - _compilation, - Method.ReturnType.NullableAnnotation, - targetTypeToCreate + && SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints( + Method.TypeParameters[_targetTypeParameterIndex], + targetTypeToCreate, + Method.ReturnType.NullableAnnotation ); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs index 07d0388d26..a5a3adc3d1 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs @@ -1,6 +1,5 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; @@ -12,16 +11,15 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public class GenericTargetObjectFactory : ObjectFactory { - private readonly Compilation _compilation; - - public GenericTargetObjectFactory(IMethodSymbol method, Compilation compilation) - : base(method) - { - _compilation = compilation; - } + public GenericTargetObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) + : base(symbolAccessor, method) { } public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => - Method.TypeParameters[0].CanConsumeType(_compilation, Method.ReturnType.NullableAnnotation, targetTypeToCreate); + SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints( + Method.TypeParameters[0], + targetTypeToCreate, + Method.ReturnType.NullableAnnotation + ); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => GenericInvocation(Method.Name, new[] { NonNullableIdentifier(targetTypeToCreate) }); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs index 002e407888..607340a362 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs @@ -11,8 +11,8 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public class GenericTargetObjectFactoryWithSource : GenericTargetObjectFactory { - public GenericTargetObjectFactoryWithSource(IMethodSymbol method, Compilation compilation) - : base(method, compilation) { } + public GenericTargetObjectFactoryWithSource(SymbolAccessor symbolAccessor, IMethodSymbol method) + : base(symbolAccessor, method) { } public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => base.CanCreateType(sourceType, targetTypeToCreate) && SymbolEqualityComparer.Default.Equals(Method.Parameters[0].Type, sourceType); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs index 6efe49fea4..72d5256cb7 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs @@ -10,11 +10,14 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public abstract class ObjectFactory { - protected ObjectFactory(IMethodSymbol method) + protected ObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) { Method = method; + SymbolAccessor = symbolAccessor; } + protected SymbolAccessor SymbolAccessor { get; } + protected IMethodSymbol Method { get; } public ExpressionSyntax CreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => @@ -38,7 +41,7 @@ private ExpressionSyntax HandleNull(ExpressionSyntax expression, ITypeSymbol typ if (!Method.ReturnType.UpgradeNullable().IsNullable()) return expression; - ExpressionSyntax nullFallback = typeToCreate.HasAccessibleParameterlessConstructor() + ExpressionSyntax nullFallback = SymbolAccessor.HasAccessibleParameterlessConstructor(typeToCreate) ? CreateInstance(typeToCreate) : ThrowNullReferenceException($"The object factory {Method.Name} returned null"); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs index 95e2120f24..2f6bd4b8d3 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs @@ -37,8 +37,8 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde if (!methodSymbol.IsGenericMethod) { return methodSymbol.Parameters.Length == 1 - ? new SimpleObjectFactoryWithSource(methodSymbol) - : new SimpleObjectFactory(methodSymbol); + ? new SimpleObjectFactoryWithSource(ctx.SymbolAccessor, methodSymbol) + : new SimpleObjectFactory(ctx.SymbolAccessor, methodSymbol); } switch (methodSymbol.TypeParameters.Length) @@ -76,12 +76,12 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde if (returnTypeIsGeneric) { return hasSourceParameter - ? new GenericTargetObjectFactoryWithSource(methodSymbol, ctx.Compilation) - : new GenericTargetObjectFactory(methodSymbol, ctx.Compilation); + ? new GenericTargetObjectFactoryWithSource(ctx.SymbolAccessor, methodSymbol) + : new GenericTargetObjectFactory(ctx.SymbolAccessor, methodSymbol); } if (hasSourceParameter) - return new GenericSourceObjectFactory(methodSymbol, ctx.Compilation); + return new GenericSourceObjectFactory(ctx.SymbolAccessor, methodSymbol); ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidObjectFactorySignature, methodSymbol, methodSymbol.Name); return null; @@ -109,6 +109,6 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde return null; } - return new GenericSourceTargetObjectFactory(methodSymbol, ctx.Compilation, sourceParameterIndex); + return new GenericSourceTargetObjectFactory(ctx.SymbolAccessor, methodSymbol, sourceParameterIndex); } } diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs index 956837079e..dd5e7ec4da 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs @@ -10,8 +10,8 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public class SimpleObjectFactory : ObjectFactory { - public SimpleObjectFactory(IMethodSymbol method) - : base(method) { } + public SimpleObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) + : base(symbolAccessor, method) { } public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => SymbolEqualityComparer.Default.Equals(Method.ReturnType, targetTypeToCreate); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs index 805662f2a9..e4b6f855ed 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs @@ -11,8 +11,8 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public class SimpleObjectFactoryWithSource : SimpleObjectFactory { - public SimpleObjectFactoryWithSource(IMethodSymbol method) - : base(method) { } + public SimpleObjectFactoryWithSource(SymbolAccessor symbolAccessor, IMethodSymbol method) + : base(symbolAccessor, method) { } public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => base.CanCreateType(sourceType, targetTypeToCreate) && SymbolEqualityComparer.Default.Equals(sourceType, Method.Parameters[0].Type); diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index b20b41b61e..ed81d08f73 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -52,6 +52,7 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx) public MapperAttribute MapperConfiguration => _configuration.Mapper; public WellKnownTypes Types { get; } + public SymbolAccessor SymbolAccessor { get; } protected MappingBuilder MappingBuilder { get; } diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 401d9e4580..9f9e8b1f72 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; @@ -9,14 +10,54 @@ namespace Riok.Mapperly.Descriptors; public class SymbolAccessor { private readonly WellKnownTypes _types; + private readonly Compilation _compilation; + private readonly INamedTypeSymbol _mapperSymbol; private readonly Dictionary> _attributes = new(SymbolEqualityComparer.Default); private readonly Dictionary> _allMembers = new(SymbolEqualityComparer.Default); private readonly Dictionary> _allAccessibleMembers = new(SymbolEqualityComparer.Default); - public SymbolAccessor(WellKnownTypes types) + public SymbolAccessor(WellKnownTypes types, Compilation compilation, INamedTypeSymbol mapperSymbol) { _types = types; + _compilation = compilation; + _mapperSymbol = mapperSymbol; + } + + public bool HasAccessibleParameterlessConstructor(ITypeSymbol symbol) => + symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol + && namedTypeSymbol.InstanceConstructors.Any(c => c.Parameters.IsDefaultOrEmpty && IsAccessible(c)); + + public bool IsAccessible(ISymbol symbol) => _compilation.IsSymbolAccessibleWithin(symbol, _mapperSymbol); + + public bool HasImplicitConversion(ITypeSymbol source, ITypeSymbol destination) => + _compilation.ClassifyConversion(source, destination).IsImplicit && (destination.IsNullable() || !source.IsNullable()); + + public bool DoesTypeSatisfyTypeParameterConstraints( + ITypeParameterSymbol typeParameter, + ITypeSymbol type, + NullableAnnotation typeParameterUsageNullableAnnotation + ) + { + if (typeParameter.HasConstructorConstraint && !HasAccessibleParameterlessConstructor(type)) + return false; + + if (!typeParameter.IsNullable(typeParameterUsageNullableAnnotation) && type.IsNullable()) + return false; + + if (typeParameter.HasValueTypeConstraint && !type.IsValueType) + return false; + + if (typeParameter.HasReferenceTypeConstraint && !type.IsReferenceType) + return false; + + foreach (var constraintType in typeParameter.ConstraintTypes) + { + if (!_compilation.ClassifyConversion(type, constraintType.UpgradeNullable()).IsImplicit) + return false; + } + + return true; } internal IEnumerable GetAttributes(ISymbol symbol) @@ -187,13 +228,13 @@ private IEnumerable GetAllAccessibleMappableMembersCore(ITypeSy { if (symbol.IsTupleType && symbol is INamedTypeSymbol namedType) { - return namedType.TupleElements.Select(MappableMember.Create).WhereNotNull(); + return namedType.TupleElements.Select(x => MappableMember.Create(this, x)).WhereNotNull(); } return GetAllMembers(symbol) - .Where(x => x is { IsStatic: false, Kind: SymbolKind.Property or SymbolKind.Field } && x.IsAccessible()) + .Where(x => x is { IsStatic: false, Kind: SymbolKind.Property or SymbolKind.Field }) .DistinctBy(x => x.Name) - .Select(MappableMember.Create) + .Select(x => MappableMember.Create(this, x)) .WhereNotNull(); } } diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 887c1c1905..549a766a76 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -28,7 +28,7 @@ public static IEnumerable ExtractUserMappings(SimpleMappingBuilder yield break; // extract user implemented mappings from base methods - foreach (var method in ExtractBaseMethods(ctx.Compilation.ObjectType, mapperSymbol, ctx.SymbolAccessor)) + foreach (var method in ExtractBaseMethods(ctx, mapperSymbol)) { // Partial method declarations are allowed for base classes, // but still treated as user implemented methods, @@ -42,15 +42,11 @@ public static IEnumerable ExtractUserMappings(SimpleMappingBuilder private static IEnumerable ExtractMethods(ITypeSymbol mapperSymbol) => mapperSymbol.GetMembers().OfType(); - private static IEnumerable ExtractBaseMethods( - INamedTypeSymbol objectType, - ITypeSymbol mapperSymbol, - SymbolAccessor symbolAccessor - ) + private static IEnumerable ExtractBaseMethods(SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol) { var baseMethods = - mapperSymbol.BaseType != null ? symbolAccessor.GetAllMethods(mapperSymbol.BaseType!) : Enumerable.Empty(); - var intfMethods = mapperSymbol.AllInterfaces.SelectMany(symbolAccessor.GetAllMethods); + mapperSymbol.BaseType != null ? ctx.SymbolAccessor.GetAllMethods(mapperSymbol.BaseType!) : Enumerable.Empty(); + var intfMethods = mapperSymbol.AllInterfaces.SelectMany(ctx.SymbolAccessor.GetAllMethods); return baseMethods .Concat(intfMethods) .OfType() @@ -58,8 +54,8 @@ SymbolAccessor symbolAccessor .Where( x => x.MethodKind == MethodKind.Ordinary - && x.IsAccessible(true) - && !SymbolEqualityComparer.Default.Equals(x.ReceiverType, objectType) + && ctx.SymbolAccessor.IsAccessible(x) + && !SymbolEqualityComparer.Default.Equals(x.ReceiverType, ctx.Compilation.ObjectType) ); } diff --git a/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs index 4dac8e7644..a9396cdc46 100644 --- a/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs +++ b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs @@ -40,7 +40,7 @@ public INamedTypeSymbol Get(Type type) return typeSymbol; } - typeSymbol = _compilation.GetTypeByMetadataName(typeFullName); + typeSymbol = _compilation.GetBestTypeByMetadataName(typeFullName); _cachedTypes.Add(typeFullName, typeSymbol); return typeSymbol; diff --git a/src/Riok.Mapperly/Helpers/CompilationExtensions.cs b/src/Riok.Mapperly/Helpers/CompilationExtensions.cs new file mode 100644 index 0000000000..62c3a9becb --- /dev/null +++ b/src/Riok.Mapperly/Helpers/CompilationExtensions.cs @@ -0,0 +1,120 @@ +using Microsoft.CodeAnalysis; + +namespace Riok.Mapperly.Helpers; + +internal static class CompilationExtensions +{ + // Copy from https://github.com/dotnet/roslyn/blob/d2ff1d83e8fde6165531ad83f0e5b1ae95908289/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/CompilationExtensions.cs#L11-L68 + /// + /// Gets a type by its metadata name to use for code analysis within a . This method + /// attempts to find the "best" symbol to use for code analysis, which is the symbol matching the first of the + /// following rules. + /// + /// + /// + /// If only one type with the given name is found within the compilation and its referenced assemblies, that + /// type is returned regardless of accessibility. + /// + /// + /// If the current defines the symbol, that symbol is returned. + /// + /// + /// If exactly one referenced assembly defines the symbol in a manner that makes it visible to the current + /// , that symbol is returned. + /// + /// + /// Otherwise, this method returns . + /// + /// + /// + /// The to consider for analysis. + /// The fully-qualified metadata type name to find. + /// The symbol to use for code analysis; otherwise, . + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string fullyQualifiedMetadataName) + { +#if ROSLYN4_4_OR_GREATER + INamedTypeSymbol? type = null; + + foreach (var currentType in compilation.GetTypesByMetadataName(fullyQualifiedMetadataName)) + { + if (ReferenceEquals(currentType.ContainingAssembly, compilation.Assembly)) + return currentType; + + switch (currentType.GetResultantVisibility()) + { + case SymbolVisibility.Public: + case SymbolVisibility.Internal when currentType.ContainingAssembly.GivesAccessTo(compilation.Assembly): + break; + + default: + continue; + } + + if (type != null) + return null; + + type = currentType; + } + + return type; +#else + return compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); +#endif + } + + // Copy from https://github.com/dotnet/roslyn/blob/d2ff1d83e8fde6165531ad83f0e5b1ae95908289/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs#L28-L73 + private static SymbolVisibility GetResultantVisibility(this ISymbol symbol) + { + while (true) + { + // Start by assuming it's visible. + var visibility = SymbolVisibility.Public; + switch (symbol.Kind) + { + case SymbolKind.Alias: + // Aliases are uber private. They're only visible in the same file that they + // were declared in. + return SymbolVisibility.Private; + + case SymbolKind.Parameter: + // Parameters are only as visible as their containing symbol + symbol = symbol.ContainingSymbol; + continue; + + case SymbolKind.TypeParameter: + // Type Parameters are private. + return SymbolVisibility.Private; + } + + while (symbol != null && symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + // If we see anything private, then the symbol is private. + case Accessibility.NotApplicable: + case Accessibility.Private: + return SymbolVisibility.Private; + + // If we see anything internal, then knock it down from public to + // internal. + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + visibility = SymbolVisibility.Internal; + break; + // For anything else (Public, Protected, ProtectedOrInternal), the + // symbol stays at the level we've gotten so far. + } + symbol = symbol.ContainingSymbol; + } + + return visibility; + } + } + + private enum SymbolVisibility + { + Public, + Internal, + Private, + } +} diff --git a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs index ef5f1c99a2..b46b765dce 100644 --- a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs +++ b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs @@ -1,7 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; namespace Riok.Mapperly.Helpers; @@ -20,15 +19,6 @@ symbol is INamedTypeSymbol namedSymbol || _wellKnownImmutableTypes.Contains(namedSymbol.ToDisplayString()) ); - internal static bool IsAccessible(this ISymbol symbol, bool allowProtected = false) => - symbol.DeclaredAccessibility.HasFlag(Accessibility.Internal) - || symbol.DeclaredAccessibility.HasFlag(Accessibility.Public) - || (symbol.DeclaredAccessibility.HasFlag(Accessibility.Protected) && allowProtected); - - internal static bool HasAccessibleParameterlessConstructor(this ITypeSymbol symbol, bool allowProtected = false) => - symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol - && namedTypeSymbol.InstanceConstructors.Any(c => c.Parameters.IsDefaultOrEmpty && c.IsAccessible(allowProtected)); - internal static int GetInheritanceLevel(this ITypeSymbol symbol) { var level = 0; @@ -140,35 +130,4 @@ out bool isExplicit internal static bool HasImplicitGenericImplementation(this ITypeSymbol symbol, INamedTypeSymbol inter, string methodName) => symbol.ImplementsGeneric(inter, methodName, out _, out var isExplicit) && !isExplicit; - - internal static bool IsAssignableTo(this ITypeSymbol symbol, Compilation compilation, ITypeSymbol type) => - compilation.ClassifyConversion(symbol, type).IsImplicit && (type.IsNullable() || !symbol.IsNullable()); - - internal static bool CanConsumeType( - this ITypeParameterSymbol typeParameter, - Compilation compilation, - NullableAnnotation typeParameterUsageNullableAnnotation, - ITypeSymbol type - ) - { - if (typeParameter.HasConstructorConstraint && !type.HasAccessibleParameterlessConstructor()) - return false; - - if (!typeParameter.IsNullable(typeParameterUsageNullableAnnotation) && type.IsNullable()) - return false; - - if (typeParameter.HasValueTypeConstraint && !type.IsValueType) - return false; - - if (typeParameter.HasReferenceTypeConstraint && !type.IsReferenceType) - return false; - - foreach (var constraintType in typeParameter.ConstraintTypes) - { - if (!compilation.ClassifyConversion(type, constraintType.UpgradeNullable()).IsImplicit) - return false; - } - - return true; - } } diff --git a/src/Riok.Mapperly/MapperGenerator.cs b/src/Riok.Mapperly/MapperGenerator.cs index a204a47524..0018a2b103 100644 --- a/src/Riok.Mapperly/MapperGenerator.cs +++ b/src/Riok.Mapperly/MapperGenerator.cs @@ -67,7 +67,6 @@ CancellationToken cancellationToken if (mapperAttributeSymbol == null) return MapperResults.Empty; - var symbolAccessor = new SymbolAccessor(wellKnownTypes); var uniqueNameBuilder = new UniqueNameBuilder(); var diagnostics = new List(); @@ -80,6 +79,7 @@ CancellationToken cancellationToken if (mapperModel.GetDeclaredSymbol(mapperSyntax) is not INamedTypeSymbol mapperSymbol) continue; + var symbolAccessor = new SymbolAccessor(wellKnownTypes, compilation, mapperSymbol); if (!symbolAccessor.HasAttribute(mapperSymbol)) continue; diff --git a/src/Riok.Mapperly/Symbols/FieldMember.cs b/src/Riok.Mapperly/Symbols/FieldMember.cs index 67065063b2..b63a20cef7 100644 --- a/src/Riok.Mapperly/Symbols/FieldMember.cs +++ b/src/Riok.Mapperly/Symbols/FieldMember.cs @@ -17,8 +17,8 @@ public FieldMember(IFieldSymbol fieldSymbol) public ISymbol MemberSymbol => _fieldSymbol; public bool IsNullable => _fieldSymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable(); public bool IsIndexer => false; - public bool CanGet => !_fieldSymbol.IsReadOnly && _fieldSymbol.IsAccessible(); - public bool CanSet => _fieldSymbol.IsAccessible(); + public bool CanGet => !_fieldSymbol.IsReadOnly; + public bool CanSet => true; public bool IsInitOnly => false; public bool IsRequired diff --git a/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs b/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs index a746ba7d7f..dc767658b2 100644 --- a/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs +++ b/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors; using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Symbols; @@ -35,9 +36,15 @@ NullableAnnotation targetNullableAnnotation public bool? TargetNullable { get; } - public bool CanConsumeTypes(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType) + public bool DoesTypesSatisfyTypeParameterConstraints(SymbolAccessor symbolAccessor, ITypeSymbol sourceType, ITypeSymbol targetType) { - return SourceType?.CanConsumeType(compilation, _sourceNullableAnnotation, sourceType) != false - && TargetType?.CanConsumeType(compilation, _targetNullableAnnotation, targetType) != false; + return ( + SourceType == null + || symbolAccessor.DoesTypeSatisfyTypeParameterConstraints(SourceType, sourceType, _sourceNullableAnnotation) + ) + && ( + TargetType == null + || symbolAccessor.DoesTypeSatisfyTypeParameterConstraints(TargetType, targetType, _targetNullableAnnotation) + ); } } diff --git a/src/Riok.Mapperly/Symbols/MappableMember.cs b/src/Riok.Mapperly/Symbols/MappableMember.cs index 6ca929c096..66e88485c2 100644 --- a/src/Riok.Mapperly/Symbols/MappableMember.cs +++ b/src/Riok.Mapperly/Symbols/MappableMember.cs @@ -1,14 +1,18 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors; namespace Riok.Mapperly.Symbols; internal static class MappableMember { - public static IMappableMember? Create(ISymbol symbol) + public static IMappableMember? Create(SymbolAccessor accessor, ISymbol symbol) { + if (!accessor.IsAccessible(symbol)) + return null; + return symbol switch { - IPropertySymbol property => new PropertyMember(property), + IPropertySymbol property => new PropertyMember(property, accessor), IFieldSymbol field => new FieldMember(field), _ => null, }; diff --git a/src/Riok.Mapperly/Symbols/PropertyMember.cs b/src/Riok.Mapperly/Symbols/PropertyMember.cs index 4180059569..4b98e2f0cb 100644 --- a/src/Riok.Mapperly/Symbols/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/PropertyMember.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors; using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Symbols; @@ -6,10 +7,12 @@ namespace Riok.Mapperly.Symbols; internal class PropertyMember : IMappableMember { private readonly IPropertySymbol _propertySymbol; + private readonly SymbolAccessor _symbolAccessor; - internal PropertyMember(IPropertySymbol propertySymbol) + internal PropertyMember(IPropertySymbol propertySymbol, SymbolAccessor symbolAccessor) { _propertySymbol = propertySymbol; + _symbolAccessor = symbolAccessor; } public string Name => _propertySymbol.Name; @@ -17,8 +20,10 @@ internal PropertyMember(IPropertySymbol propertySymbol) public ISymbol MemberSymbol => _propertySymbol; public bool IsNullable => _propertySymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable(); public bool IsIndexer => _propertySymbol.IsIndexer; - public bool CanGet => !_propertySymbol.IsWriteOnly && _propertySymbol.GetMethod?.IsAccessible() != false; - public bool CanSet => !_propertySymbol.IsReadOnly && _propertySymbol.SetMethod?.IsAccessible() != false; + public bool CanGet => + !_propertySymbol.IsWriteOnly && (_propertySymbol.GetMethod == null || _symbolAccessor.IsAccessible(_propertySymbol.GetMethod)); + public bool CanSet => + !_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/Generator/IncrementalGeneratorTest.cs b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs index 7af95b50eb..6239dfe68a 100644 --- a/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs +++ b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs @@ -13,7 +13,7 @@ public void AddingUnrelatedTypeDoesNotRegenerateOriginal() var source = TestSourceBuilder.Mapping("string", "string"); var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); - var compilation1 = TestHelper.BuildCompilation(TestHelperOptions.NoDiagnostics.NullableOption, syntaxTree); + var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -49,7 +49,7 @@ internal partial class BarFooMapper ); var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); - var compilation1 = TestHelper.BuildCompilation(TestHelperOptions.NoDiagnostics.NullableOption, syntaxTree); + var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -65,7 +65,7 @@ public void AppendingUnrelatedTypeDoesNotRegenerateOriginal() { var source = TestSourceBuilder.Mapping("string", "string"); var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); - var compilation1 = TestHelper.BuildCompilation(TestHelperOptions.NoDiagnostics.NullableOption, syntaxTree); + var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); @@ -90,7 +90,7 @@ public void ModifyingMapperDoesRegenerateOriginal() "class B { }" ); var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); - var compilation1 = TestHelper.BuildCompilation(TestHelperOptions.NoDiagnostics.NullableOption, syntaxTree); + var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectVisibilityTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectVisibilityTest.cs new file mode 100644 index 0000000000..cbcd64727e --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectVisibilityTest.cs @@ -0,0 +1,193 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +public class ObjectVisibilityTest +{ + [Fact] + public void PrivateToPublicShouldIgnore() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "class A { private string Value { get; set; } }", + "class B { public string Value { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + return target; + """ + ); + } + + [Fact] + public void PublicToPrivateShouldIgnore() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "class A { public string Value { get; set; } }", + "class B { private string Value { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + return target; + """ + ); + } + + [Fact] + public void InternalToInternalShouldMap() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "class A { internal string Value { get; set; } }", + "class B { internal string Value { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void InternalOtherAssemblyToInternalShouldIgnore() + { + var aSource = TestSourceBuilder.SyntaxTree("namespace A; public class A { internal string Value { get; set; } }"); + using var aAssembly = TestHelper.BuildAssembly("A", aSource); + + var source = TestSourceBuilder.Mapping("A.A", "B", "class B { internal string Value { get; set; } }"); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics, new[] { aAssembly }) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + return target; + """ + ); + } + + [Fact] + public void InternalToInternalOtherAssemblyShouldIgnore() + { + var aSource = TestSourceBuilder.SyntaxTree( + """ + namespace A; + public class A { public string PublicValue { get; set; } internal string InternalValue { get; set; } private string PrivateValue { get; set; } } + """ + ); + + using var aAssembly = TestHelper.BuildAssembly("A", aSource); + + var source = TestSourceBuilder.Mapping( + "A.A", + "B", + "class B { public string PublicValue { get; set; } internal string InternalValue { get; set; } private string PrivateValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics, new[] { aAssembly }) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.PublicValue = source.PublicValue; + return target; + """ + ); + } + + [Fact] + public void InternalOtherAssemblyWithGrantedVisibilityToInternalShouldMap() + { + var aSource = TestSourceBuilder.SyntaxTree( + """ + [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Tests")] + + namespace A; + public class A { public string PublicValue { get; set; } internal string InternalValue { get; set; } private string PrivateValue { get; set; } } + """ + ); + + using var aAssembly = TestHelper.BuildAssembly("A", aSource); + + var source = TestSourceBuilder.Mapping( + "A.A", + "B", + "class B { public string PublicValue { get; set; } internal string InternalValue { get; set; } private string PrivateValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.NoDiagnostics, new[] { aAssembly }) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.PublicValue = source.PublicValue; + target.InternalValue = source.InternalValue; + return target; + """ + ); + } + + [Fact] + public void InternalClassOtherAssemblyWithGrantedVisibilityToInternalShouldMap() + { + var aSource = TestSourceBuilder.SyntaxTree( + """ + [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Tests")] + + namespace A; + internal class A { public string PublicValue { get; set; } internal string InternalValue { get; set; } private string PrivateValue { get; set; } } + """ + ); + + using var aAssembly = TestHelper.BuildAssembly("A", aSource); + + var source = TestSourceBuilder.Mapping( + "A.A", + "B", + "class B { public string PublicValue { get; set; } internal string InternalValue { get; set; } private string PrivateValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.NoDiagnostics, new[] { aAssembly }) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.PublicValue = source.PublicValue; + target.InternalValue = source.InternalValue; + return target; + """ + ); + } +} diff --git a/test/Riok.Mapperly.Tests/TestAssembly.cs b/test/Riok.Mapperly.Tests/TestAssembly.cs new file mode 100644 index 0000000000..498b51fbb8 --- /dev/null +++ b/test/Riok.Mapperly.Tests/TestAssembly.cs @@ -0,0 +1,20 @@ +using Microsoft.CodeAnalysis; + +namespace Riok.Mapperly.Tests; + +public class TestAssembly : IDisposable +{ + private readonly MemoryStream _data = new(); + + internal TestAssembly(Compilation compilation) + { + compilation.Emit(_data).Success.Should().BeTrue(); + + _data.Seek(0, SeekOrigin.Begin); + MetadataReference = MetadataReference.CreateFromStream(_data); + } + + public MetadataReference MetadataReference { get; } + + public void Dispose() => _data.Dispose(); +} diff --git a/test/Riok.Mapperly.Tests/TestHelper.cs b/test/Riok.Mapperly.Tests/TestHelper.cs index 02a98f2e43..24e61f4673 100644 --- a/test/Riok.Mapperly.Tests/TestHelper.cs +++ b/test/Riok.Mapperly.Tests/TestHelper.cs @@ -23,11 +23,15 @@ public static Task VerifyGenerator(string source, TestHelperOption return verify.ToTask(); } - public static MapperGenerationResult GenerateMapper(string source, TestHelperOptions? options = null) + public static MapperGenerationResult GenerateMapper( + string source, + TestHelperOptions? options = null, + IReadOnlyCollection? additionalAssemblies = null + ) { options ??= TestHelperOptions.NoDiagnostics; - var result = Generate(source, options).GetRunResult(); + var result = Generate(source, options, additionalAssemblies).GetRunResult(); var mapperClassImpl = result.GeneratedTrees .Single() @@ -53,23 +57,13 @@ public static MapperGenerationResult GenerateMapper(string source, TestHelperOpt return mapperResult; } - public static CSharpCompilation BuildCompilation(NullableContextOptions nullableOption, params SyntaxTree[] syntaxTrees) - { - var references = AppDomain.CurrentDomain - .GetAssemblies() - .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) - .Select(x => MetadataReference.CreateFromFile(x.Location)) - .Concat( - new[] - { - MetadataReference.CreateFromFile(typeof(MapperGenerator).Assembly.Location), - MetadataReference.CreateFromFile(typeof(MapperAttribute).Assembly.Location) - } - ); - - var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableOption); + public static CSharpCompilation BuildCompilation(params SyntaxTree[] syntaxTrees) => + BuildCompilation("Tests", NullableContextOptions.Enable, true, syntaxTrees); - return CSharpCompilation.Create("Tests", syntaxTrees, references, compilationOptions); + public static TestAssembly BuildAssembly(string name, params SyntaxTree[] syntaxTrees) + { + var compilation = BuildCompilation(name, NullableContextOptions.Enable, false, syntaxTrees); + return new TestAssembly(compilation); } public static GeneratorDriver GenerateTracked(Compilation compilation) @@ -83,15 +77,51 @@ public static GeneratorDriver GenerateTracked(Compilation compilation) return driver.RunGenerators(compilation); } - private static GeneratorDriver Generate(string source, TestHelperOptions? options) + private static GeneratorDriver Generate( + string source, + TestHelperOptions? options, + IReadOnlyCollection? additionalAssemblies = null + ) { options ??= TestHelperOptions.NoDiagnostics; var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion)); - var compilation = BuildCompilation(options.NullableOption, syntaxTree); + var compilation = BuildCompilation(options.AssemblyName, options.NullableOption, true, syntaxTree); + if (additionalAssemblies != null) + { + compilation = compilation.AddReferences(additionalAssemblies.Select(x => x.MetadataReference)); + } + var generator = new MapperGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); return driver.RunGenerators(compilation); } + + private static CSharpCompilation BuildCompilation( + string name, + NullableContextOptions nullableOption, + bool addMapperlyReferences, + params SyntaxTree[] syntaxTrees + ) + { + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableOption); + var compilation = CSharpCompilation.Create(name, syntaxTrees, options: compilationOptions); + + var references = AppDomain.CurrentDomain + .GetAssemblies() + .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) + .Select(x => MetadataReference.CreateFromFile(x.Location)); + compilation = compilation.AddReferences(references); + + if (addMapperlyReferences) + { + compilation = compilation.AddReferences( + MetadataReference.CreateFromFile(typeof(MapperGenerator).Assembly.Location), + MetadataReference.CreateFromFile(typeof(MapperAttribute).Assembly.Location) + ); + } + + return compilation; + } } diff --git a/test/Riok.Mapperly.Tests/TestHelperOptions.cs b/test/Riok.Mapperly.Tests/TestHelperOptions.cs index 1ec8142abd..5a50c0bb2a 100644 --- a/test/Riok.Mapperly.Tests/TestHelperOptions.cs +++ b/test/Riok.Mapperly.Tests/TestHelperOptions.cs @@ -6,7 +6,8 @@ namespace Riok.Mapperly.Tests; public record TestHelperOptions( NullableContextOptions NullableOption = NullableContextOptions.Enable, LanguageVersion LanguageVersion = LanguageVersion.Default, - IReadOnlySet? AllowedDiagnostics = null + IReadOnlySet? AllowedDiagnostics = null, + string AssemblyName = "Tests" ) { public static readonly TestHelperOptions AllowDiagnostics = new();