diff --git a/Riok.Mapperly.sln b/Riok.Mapperly.sln index 1104608627..4312128ae0 100644 --- a/Riok.Mapperly.sln +++ b/Riok.Mapperly.sln @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Benchmarks", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{961BAABA-0672-48E7-A5B3-30A676146BE3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Templates", "src\Riok.Mapperly.Templates\Riok.Mapperly.Templates.csproj", "{FF31D522-6A62-4466-90F7-6B297F82FCF3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Templates.Tests", "test\Riok.Mapperly.Templates.Tests\Riok.Mapperly.Templates.Tests.csproj", "{1500A843-37E3-4DBA-8BAB-A40CF14678ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +63,14 @@ Global {F2214C71-15A7-46EB-A3AA-D02EF4B705EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2214C71-15A7-46EB-A3AA-D02EF4B705EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2214C71-15A7-46EB-A3AA-D02EF4B705EF}.Release|Any CPU.Build.0 = Release|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Release|Any CPU.Build.0 = Release|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -71,6 +83,8 @@ Global {C3C40A0A-168F-4A66-B9F9-FC80D2F26306} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C} {43A2E8E0-5A2C-45E9-84EF-CF934EC946FA} = {0FBD6C81-7E7A-4915-90D2-896F11C89FF0} {F2214C71-15A7-46EB-A3AA-D02EF4B705EF} = {961BAABA-0672-48E7-A5B3-30A676146BE3} + {FF31D522-6A62-4466-90F7-6B297F82FCF3} = {B65AF89A-4A3B-473C-83C8-5F0CB0EED30E} + {1500A843-37E3-4DBA-8BAB-A40CF14678ED} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BAAC5976-BEE6-440E-8DDC-90916A1001A1} diff --git a/docs/docs/contributing/architecture.md b/docs/docs/contributing/architecture.md index a21b727619..45c7fe2d56 100644 --- a/docs/docs/contributing/architecture.md +++ b/docs/docs/contributing/architecture.md @@ -8,14 +8,19 @@ description: The architecture of Mapperly. Mapperly is an incremental .NET source generator implementation. Source generators are explained [here](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md) and [here](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md). -However, the incremental source generator of Mapperly is not yet optimal (see [#72](https://github.com/riok/mapperly/issues/72)). -## Projects +## Solution -Mapperly is structured in two projects. -`Riok.Mapperly.Abstractions` includes abstractions and attributes to be used by the application code, -this is referenced by the source generator. -`Riok.Mapperly` includes the implementation of the source generator. +- `benchmarks` Benchmarks to analyze the performance of the generated code and the source generator itself +- `build` Build scripts +- `docs` Documentation of Mapperly +- `samples` Sample implementations of Mappers using Mapperly +- `src` Source code of Mapperly + - `Riok.Mapperly` The source generator implementation + - `Riok.Mapperly.Abstractions` Abstractions and attributes to be used by the application code to configure Mapperly. + This is referenced by the source generator but is not needed at runtime. + - `Riok.Mapperly.Templates` Templates of code files which are embedded as resources into `Riok.Mapperly` and may be emitted during source generation depending on enabled features. +- `test` Unit- and integration tests of Mapperly ## Flow @@ -24,11 +29,12 @@ For each discovered `MapperAttribute` a new `DescriptorBuilder` is created. The `DescriptorBuilder` is responsible to build a `MapperDescriptor` which holds all the mappings. The `DescriptorBuilder` does this by following this process: -1. Extracting the configuration from the attribute +1. Extracting the configuration from the attributes 2. Extracting user implemented object factories 3. Extracting user implemented and user defined mapping methods. It instantiates a `User*Mapping` (eg. `UserDefinedNewInstanceMethodMapping`) for each discovered mapping method and adds it to the queue of mappings to work on. -4. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies. +4. Extracting external mappings +5. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies. This is done by a so called `*MappingBodyBuilder`. A mapping body builder tries to map each property from the source to the target. To do this, it asks the `DescriptorBuilder` to create mappings for the according types. @@ -36,7 +42,7 @@ The `DescriptorBuilder` does this by following this process: Each of the mapping builders try to create a mapping (an `ITypeMapping` implementation) for the asked type mapping by using one approach on how to map types (eg. an explicit cast is implemented by the `ExplicitCastMappingBuilder`). These mappings are queued in the queue of mappings which need the body to be built (currently body builders are only used for object to object (property-based) mappings). -5. The `SourceEmitter` emits the code described by the `MapperDescriptor` and all its mappings. +6. The `SourceEmitter` emits the code described by the `MapperDescriptor` and all its mappings. The syntax objects are created by using `SyntaxFactory` and `SyntaxFactoryHelper`. The `SyntaxFactoryHelper` tries to simplify creating formatted syntax trees. If indentation is needed, diff --git a/docs/docs/contributing/common-tasks.md b/docs/docs/contributing/common-tasks.md index 9967dc4450..1da4bad0d3 100644 --- a/docs/docs/contributing/common-tasks.md +++ b/docs/docs/contributing/common-tasks.md @@ -50,3 +50,4 @@ To support a new roslyn version via multi targeting follow these steps (see also Mapperly Mappings use Roslyn syntax trees. [RoslynQuoter](https://roslynquoter.azurewebsites.net/) and [SharpLab](https://sharplab.io/) are fantastic tools to understand and work with Roslyn syntax trees. +The `Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper` and `Microsoft.CodeAnalysis.CSharp.SyntaxFactory` classes help building these syntax trees. diff --git a/docs/docs/getting-started/installation.mdx b/docs/docs/getting-started/installation.mdx index eedfcf3590..a49f022f85 100644 --- a/docs/docs/getting-started/installation.mdx +++ b/docs/docs/getting-started/installation.mdx @@ -12,13 +12,13 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; ## Add the NuGet Package to your project -All you need to do, to install Mapperly is to add a NuGet reference pointing to the package `Riok.Mapperly`. +All you need to do, to install Mapperly is to add a NuGet reference pointing to the package [`Riok.Mapperly`](https://www.nuget.org/packages/Riok.Mapperly). - {``} + {``} @@ -26,6 +26,8 @@ All you need to do, to install Mapperly is to add a NuGet reference pointing to dotnet add package Riok.Mapperly ``` +Make sure to set `PrivateAssets="all" ExcludeAssets="runtime"` on the added `PackageReference`. + @@ -33,5 +35,13 @@ dotnet add package Riok.Mapperly Install-Package Riok.Mapperly ``` +Make sure to set `PrivateAssets="all" ExcludeAssets="runtime"` on the added `PackageReference`. + + +:::info +`PrivateAssets="all"` ensures projects referencing this project will not also get a reference to `Riok.Mapperly`. +`ExcludeAssets="runtime"` ensures the `Riok.Mapperly.Abstractions.dll` file is not copied to your build output +(it is not required at runtime). +::: diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 102b672cd7..66eeb02125 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -39,6 +39,7 @@ For example, Mapperly can report a warning when there is an added property in a - Mapperly does not use reflection - Mapperly is trimming and AoT safe - Mapperly runs at build time +- Mapperly does not have a runtime dependency - The generated mappings are amazingly fast with minimal memory overhead - The generated mapping code is readable and debuggable - No need to write and maintain boilerplate code by hand diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 71b8efc981..80b1edf4ec 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -81,10 +81,6 @@ Riok.Mapperly.Abstractions.MappingConversionType.Tuple = 32768 -> Riok.Mapperly. Riok.Mapperly.Abstractions.MappingConversionType.Queryable = 1024 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.set -> void -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.PreserveReferenceHandler() -> void -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.SetReference(TSource source, TTarget target) -> void -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.SetReference(TSource source, TTarget target) -> void Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool diff --git a/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs b/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs deleted file mode 100644 index efa5159eba..0000000000 --- a/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Riok.Mapperly.Abstractions.ReferenceHandling.Internal; - -/// -/// A implementation -/// which returns the same target object instance if encountered the same source object instance. -/// Do not use directly. Should only be used by Mapperly generated code. -/// API surface is not subject to semantic releases and may break in any release. -/// -public sealed class PreserveReferenceHandler : IReferenceHandler -{ - private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); - - /// - public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) - where TSource : notnull - where TTarget : notnull - { - var refHolder = GetReferenceHolder(); - return refHolder.TryGetRef(source, out target); - } - - /// - public void SetReference(TSource source, TTarget target) - where TSource : notnull - where TTarget : notnull => GetReferenceHolder().SetRef(source, target); - - private ReferenceHolder GetReferenceHolder() - { - var mapping = (typeof(TSource), typeof(TTarget)); - if (_referenceHolders.TryGetValue(mapping, out var refHolder)) - return refHolder; - - return _referenceHolders[mapping] = new(); - } - - private class ReferenceHolder - { - private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); - - public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) - where TSource : notnull - where TTarget : notnull - { - if (_references.TryGetValue(source, out var targetObj)) - { - target = (TTarget)targetObj; - return true; - } - - target = default; - return false; - } - - public void SetRef(TSource source, TTarget target) - where TSource : notnull - where TTarget : notnull - { - _references[source] = target; - } - } -} diff --git a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj index 57f77bad1e..c55ede897d 100644 --- a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj +++ b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj @@ -2,6 +2,7 @@ false true + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute diff --git a/src/Riok.Mapperly.Templates/.editorconfig b/src/Riok.Mapperly.Templates/.editorconfig new file mode 100644 index 0000000000..392159068b --- /dev/null +++ b/src/Riok.Mapperly.Templates/.editorconfig @@ -0,0 +1,3 @@ +# keep namespaces block scoped, because .NET Framework language version does not support this +[*.cs] +csharp_style_namespace_declarations = block_scoped:warning diff --git a/src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs b/src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs new file mode 100644 index 0000000000..c7b6d4cf38 --- /dev/null +++ b/src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.AssemblyName +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj b/src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj new file mode 100644 index 0000000000..7b128289e1 --- /dev/null +++ b/src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj @@ -0,0 +1,29 @@ + + + + Riok.Mapperly.Internal + + + disable + + + disable + + + * + + + + + <_Parameter1>Riok.Mapperly.Templates.Tests + + + + + + + + diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 26a506b508..83e882daa4 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -4,9 +4,12 @@ using Riok.Mapperly.Descriptors.ExternalMappings; using Riok.Mapperly.Descriptors.MappingBodyBuilders; using Riok.Mapperly.Descriptors.MappingBuilders; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.UserMappings; using Riok.Mapperly.Descriptors.ObjectFactories; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Descriptors; @@ -55,6 +58,7 @@ MapperConfiguration defaultMapperConfiguration ExtractExternalMappings(); _mappingBodyBuilder.BuildMappingBodies(cancellationToken); BuildMappingMethodNames(); + AddRequiredTemplates(); BuildReferenceHandlingParameters(); AddMappingsToDescriptor(); return (_mapperDescriptor, _diagnostics); @@ -117,6 +121,20 @@ private void BuildReferenceHandlingParameters() } } + private void AddRequiredTemplates() + { + // if reference handling is enabled and any user defined method mapping + // does not have a reference handling parameter, + // emit the preserve reference handler template which gets instantiated as reference handler. + if ( + _builderContext.MapperConfiguration.UseReferenceHandling + && _mappings.UserMappings.OfType().Any(x => !x.HasReferenceHandlingParameter()) + ) + { + _mapperDescriptor.AddRequiredTemplate(TemplateName.PreserveReferenceHandler); + } + } + private void AddMappingsToDescriptor() { // add generated mappings to the mapper diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index fb90f72da5..da2f37479e 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -4,6 +4,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Descriptors; @@ -11,6 +12,7 @@ public class MapperDescriptor { private readonly MapperDeclaration _declaration; private readonly List _methodMappings = new(); + private readonly HashSet _requiredTemplates = new(); public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder) { @@ -34,10 +36,14 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui public UniqueNameBuilder NameBuilder { get; } + public IReadOnlyCollection RequiredTemplates => _requiredTemplates; + public IReadOnlyCollection MethodTypeMappings => _methodMappings; public void AddTypeMapping(MethodMapping mapping) => _methodMappings.Add(mapping); + public void AddRequiredTemplate(TemplateName template) => _requiredTemplates.Add(template); + private string BuildName(INamedTypeSymbol symbol) { if (symbol.ContainingType == null) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 48d6f6b680..df6fe36ca0 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -90,6 +90,8 @@ internal void SetMethodNameIfNeeded(Func methodNameBuilde _methodName ??= methodNameBuilder(this); } + public bool HasReferenceHandlingParameter() => ReferenceHandlerParameter.HasValue; + internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) { ReferenceHandlerParameter ??= new MethodParameter( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 59c32ac45b..b583624f1e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -3,6 +3,7 @@ using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -13,9 +14,6 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; /// public class UserDefinedExistingTargetMethodMapping : MethodMapping, IUserMapping { - private const string ReferenceHandlerTypeName = - "global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler"; - private readonly bool _enableReferenceHandling; private IExistingTargetMapping? _delegateMapping; @@ -66,7 +64,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c { // var refHandler = new RefHandler(); var referenceHandlerName = ctx.NameBuilder.New(DefaultReferenceHandlerParameterName); - var createRefHandler = CreateInstance(ReferenceHandlerTypeName); + var createRefHandler = ctx.SyntaxFactory.CreateInstance(TemplateName.PreserveReferenceHandler); yield return ctx.SyntaxFactory.DeclareLocalVariable(referenceHandlerName, createRefHandler); ctx = ctx.WithRefHandler(referenceHandlerName); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 4c4f88e0a4..7c49c33b7a 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; @@ -11,9 +12,6 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; /// public class UserDefinedNewInstanceMethodMapping : MethodMapping, IDelegateUserMapping { - private const string ReferenceHandlerTypeName = - "global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler"; - private readonly bool _enableReferenceHandling; public UserDefinedNewInstanceMethodMapping( @@ -47,7 +45,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c if (_enableReferenceHandling && ReferenceHandlerParameter == null) { // new RefHandler(); - var createRefHandler = CreateInstance(ReferenceHandlerTypeName); + var createRefHandler = ctx.SyntaxFactory.CreateInstance(TemplateName.PreserveReferenceHandler); ctx = ctx.WithRefHandler(createRefHandler); return new[] { ctx.SyntaxFactory.Return(DelegateMapping.Build(ctx)) }; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index ba127b314c..86832df1cd 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -3,6 +3,7 @@ using Riok.Mapperly.Emit.Syntax; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -16,8 +17,6 @@ public abstract class UserDefinedNewInstanceRuntimeTargetTypeMapping : MethodMap { private const string IsAssignableFromMethodName = nameof(Type.IsAssignableFrom); private const string GetTypeMethodName = nameof(GetType); - private const string ReferenceHandlerTypeName = - "global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler"; private readonly List _mappings = new(); private readonly bool _enableReferenceHandling; @@ -61,7 +60,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c { // var refHandler = new RefHandler(); var referenceHandlerName = ctx.NameBuilder.New(DefaultReferenceHandlerParameterName); - var createRefHandler = CreateInstance(ReferenceHandlerTypeName); + var createRefHandler = ctx.SyntaxFactory.CreateInstance(TemplateName.PreserveReferenceHandler); yield return ctx.SyntaxFactory.DeclareLocalVariable(referenceHandlerName, createRefHandler); ctx = ctx.WithRefHandler(referenceHandlerName); diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs index a6d99f1a03..374333aa5b 100644 --- a/src/Riok.Mapperly/Emit/SourceEmitter.cs +++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs @@ -13,7 +13,11 @@ public static class SourceEmitter public static CompilationUnitSyntax Build(MapperDescriptor descriptor, CancellationToken cancellationToken) { - var ctx = new SourceEmitterContext(descriptor.Symbol.IsStatic, descriptor.NameBuilder, new SyntaxFactoryHelper()); + var ctx = new SourceEmitterContext( + descriptor.Symbol.IsStatic, + descriptor.NameBuilder, + new SyntaxFactoryHelper(descriptor.Symbol.ContainingAssembly.Name) + ); ctx = IndentForMapper(ctx, descriptor.Symbol); var memberCtx = ctx.AddIndentation(); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs index 65f8c40b54..3d689737bb 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs @@ -1,14 +1,16 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Templates; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Riok.Mapperly.Emit.Syntax; public partial struct SyntaxFactoryHelper { - public static ObjectCreationExpressionSyntax CreateInstance(string typeName) + public ObjectCreationExpressionSyntax CreateInstance(TemplateName template) { + var typeName = TemplateReader.GetTypeName(template, _assemblyName); var type = IdentifierName(typeName); return CreateObject(type, SyntaxFactory.ArgumentList()); } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs index aabb9ccec9..34eb862c8b 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs @@ -10,16 +10,24 @@ public readonly partial struct SyntaxFactoryHelper { public static readonly IdentifierNameSyntax VarIdentifier = IdentifierName("var").AddTrailingSpace(); - private SyntaxFactoryHelper(int indentation) + private readonly string _assemblyName; + + public SyntaxFactoryHelper(string assemblyName) + { + _assemblyName = assemblyName; + } + + private SyntaxFactoryHelper(int indentation, string assemblyName) { Indentation = indentation; + _assemblyName = assemblyName; } public int Indentation { get; } - public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1); + public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1, _assemblyName); - public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1); + public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1, _assemblyName); public static SyntaxToken Accessibility(Accessibility accessibility) { diff --git a/src/Riok.Mapperly/Helpers/FileNameBuilder.cs b/src/Riok.Mapperly/Helpers/FileNameBuilder.cs index 14e500b6a7..4e1bfb82ed 100644 --- a/src/Riok.Mapperly/Helpers/FileNameBuilder.cs +++ b/src/Riok.Mapperly/Helpers/FileNameBuilder.cs @@ -4,7 +4,7 @@ namespace Riok.Mapperly.Helpers; public class FileNameBuilder { - private const string GeneratedFileSuffix = ".g.cs"; + public const string GeneratedFileSuffix = ".g.cs"; private readonly UniqueNameBuilder _uniqueNameBuilder = new(); diff --git a/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs b/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs index bdc0d01c44..596fb13e79 100644 --- a/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs +++ b/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs @@ -11,8 +11,6 @@ namespace Riok.Mapperly.Helpers; public sealed class ImmutableEquatableArray : IEquatable>, IReadOnlyList where T : IEquatable { - public static ImmutableEquatableArray Empty { get; } = new(Array.Empty()); - private readonly T[] _values; public T this[int index] => _values[index]; public int Count => _values.Length; @@ -68,12 +66,6 @@ public bool MoveNext() public static class ImmutableEquatableArray { - public static ImmutableEquatableArray Empty() - where T : IEquatable => ImmutableEquatableArray.Empty; - public static ImmutableEquatableArray ToImmutableEquatableArray(this IEnumerable values) where T : IEquatable => new(values); - - public static ImmutableEquatableArray Create(params T[] values) - where T : IEquatable => values is { Length: > 0 } ? new(values) : ImmutableEquatableArray.Empty; } diff --git a/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs b/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs index 7a5a3196e2..332e5a594b 100644 --- a/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs +++ b/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Riok.Mapperly.Output; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Helpers; @@ -70,6 +71,17 @@ IncrementalValuesProvider mappers ); } + public static void EmitTemplates( + this IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider templates + ) + { + context.RegisterImplementationSourceOutput( + templates, + static (spc, template) => spc.AddSource(template.FileName, SourceText.From(template.Content, Encoding.UTF8)) + ); + } + #if !ROSLYN4_4_OR_GREATER public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) { diff --git a/src/Riok.Mapperly/MapperGenerator.cs b/src/Riok.Mapperly/MapperGenerator.cs index e7c2d5e2dc..c5f1754ee5 100644 --- a/src/Riok.Mapperly/MapperGenerator.cs +++ b/src/Riok.Mapperly/MapperGenerator.cs @@ -8,6 +8,7 @@ using Riok.Mapperly.Helpers; using Riok.Mapperly.Output; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; namespace Riok.Mapperly; @@ -22,6 +23,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) #if DEBUG_SOURCE_GENERATOR DebuggerUtil.AttachDebugger(); #endif + var assemblyName = context.CompilationProvider.Select((x, _) => x.Assembly.Name); + // report compilation diagnostics var compilationDiagnostics = context.CompilationProvider.SelectMany( static (compilation, _) => BuildCompilationDiagnostics(compilation) @@ -57,6 +60,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // output the mappers var mappers = mappersAndDiagnostics.Select(static (x, _) => x.Mapper).WithTrackingName(MapperGeneratorStepNames.BuildMappers); context.EmitMapperSource(mappers); + + // output the templates + var templates = mappersAndDiagnostics + .SelectMany(static (x, _) => x.Templates) + .Collect() + .SelectMany(static (x, _) => x.DistinctBy(tm => tm)) + .Combine(assemblyName) + .WithTrackingName(MapperGeneratorStepNames.BuildTemplates) + .Select(static (x, _) => TemplateReader.ReadContent(x.Left, x.Right)) + .WithTrackingName(MapperGeneratorStepNames.BuildTemplatesContent); + context.EmitTemplates(templates); } private static MapperAndDiagnostics? BuildDescriptor( @@ -82,7 +96,7 @@ CancellationToken cancellationToken compilationContext.FileNameBuilder.Build(descriptor), SourceEmitter.Build(descriptor, cancellationToken) ); - return new MapperAndDiagnostics(mapper, descriptorDiagnostics.ToImmutableEquatableArray()); + return new MapperAndDiagnostics(mapper, descriptorDiagnostics.ToImmutableEquatableArray(), descriptor.RequiredTemplates); } catch (OperationCanceledException) { diff --git a/src/Riok.Mapperly/MapperGeneratorStepNames.cs b/src/Riok.Mapperly/MapperGeneratorStepNames.cs index 2cb986a932..d625898d8b 100644 --- a/src/Riok.Mapperly/MapperGeneratorStepNames.cs +++ b/src/Riok.Mapperly/MapperGeneratorStepNames.cs @@ -6,5 +6,6 @@ public static class MapperGeneratorStepNames public const string BuildMapperDefaults = nameof(BuildMapperDefaults); public const string ReportDiagnostics = nameof(ReportDiagnostics); public const string BuildMappers = nameof(BuildMappers); - public const string ImplementationSourceOutput = nameof(ImplementationSourceOutput); + public const string BuildTemplates = nameof(BuildTemplates); + public const string BuildTemplatesContent = nameof(BuildTemplatesContent); } diff --git a/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs b/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs index fed10768d1..439abee1aa 100644 --- a/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs +++ b/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs @@ -1,14 +1,19 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Helpers; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Output; -public readonly record struct MapperAndDiagnostics(MapperNode Mapper, ImmutableEquatableArray Diagnostics) +public readonly record struct MapperAndDiagnostics( + MapperNode Mapper, + ImmutableEquatableArray Diagnostics, + IReadOnlyCollection Templates +) { public bool Equals(MapperAndDiagnostics other) { - return Mapper.Equals(other.Mapper) && Diagnostics.Equals(other.Diagnostics); + return Mapper.Equals(other.Mapper) && Diagnostics.Equals(other.Diagnostics) && Templates.SequenceEqual(other.Templates); } - public override int GetHashCode() => HashCode.Combine(Mapper.GetHashCode(), Diagnostics.GetHashCode()); + public override int GetHashCode() => HashCode.Combine(Mapper.GetHashCode(), Diagnostics.GetHashCode(), Templates.Count); } diff --git a/src/Riok.Mapperly/Riok.Mapperly.csproj b/src/Riok.Mapperly/Riok.Mapperly.csproj index 33081b7f5d..d00626ba96 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.csproj +++ b/src/Riok.Mapperly/Riok.Mapperly.csproj @@ -33,19 +33,25 @@ - + all analyzers - + + + + +