Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate static methods in nonstatic classes #681

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/docs/configuration/static-mappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,24 @@ public static partial class CarMapper
private static int TimeSpanToHours(TimeSpan t) => t.Hours;
}
```

## Static methods in instantiable class

Static methods are supported in non-static mapper classes. This supports the static interface use case. When a static mapping method is present, to simplify mapping method resolution and reduce confusion about which mapping method Mapperly uses, all methods must be static.

```csharp
public interface ICarMapper
{
static abstract CarDto ToDto(Car car);
}

[Mapper]
// highlight-start
public partial class CarMapper : ICarMapper
// highlight-end
{
// highlight-start
public static partial CarDto ToDto(Car car);
// highlight-end
}
```
4 changes: 2 additions & 2 deletions docs/docs/contributing/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ The `DescriptorBuilder` is responsible to build a `MapperDescriptor` which holds
The `DescriptorBuilder` does this by following this process:

1. Extracting the configuration from the attributes
2. Extracting user implemented object factories
3. Extracting user implemented and user defined mapping methods.
2. 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.
3. Extracting user implemented object factories
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`.
Expand Down
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,9 @@ Rule ID | Category | Severity | Notes
RMG051 | Mapper | Warning | Invalid ignore source member found, nested ignores are not supported
RMG052 | Mapper | Warning | Invalid ignore target member found, nested ignores are not supported
RMG053 | Mapper | Error | The flag MemberVisibility.Accessible cannot be disabled, this feature requires .NET 8.0 or greater
RMG054 | Mapper | Error | Mapper class containing 'static partial' method must not have any instance methods

### Removed Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
RMG018 | Mapper | Disabled | Partial static mapping method in an instance mapper
52 changes: 43 additions & 9 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Riok.Mapperly.Descriptors.MappingBodyBuilders;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.ObjectFactories;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
using Riok.Mapperly.Templates;
Expand All @@ -28,8 +29,6 @@ public class DescriptorBuilder
private readonly UnsafeAccessorContext _unsafeAccessorContext;
private readonly MapperConfigurationReader _configurationReader;

private ObjectFactoryCollection _objectFactories = ObjectFactoryCollection.Empty;

public DescriptorBuilder(
CompilationContext compilationContext,
MapperDeclaration mapperDeclaration,
Expand Down Expand Up @@ -63,8 +62,10 @@ MapperConfiguration defaultMapperConfiguration
{
ConfigureMemberVisibility();
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
// ExtractObjectFactories needs to be called after ExtractUserMappings due to configuring mapperDescriptor.Static
var objectFactories = ExtractObjectFactories();
EnqueueUserMappings(objectFactories);
ExtractExternalMappings();
_mappingBodyBuilder.BuildMappingBodies(cancellationToken);
BuildMappingMethodNames();
Expand Down Expand Up @@ -106,24 +107,57 @@ private void ReserveMethodNames()
}
}

private void ExtractObjectFactories()
private void ExtractUserMappings()
{
_mapperDescriptor.Static = _mapperDescriptor.Symbol.IsStatic;
IMethodSymbol? firstNonStaticUserMapping = null;

foreach (var userMapping in UserMethodMappingExtractor.ExtractUserMappings(_builderContext, _mapperDescriptor.Symbol))
{
// if a user defined mapping method is static, all of them need to be static to avoid confusion for mapping method resolution
// however, user implemented mapping methods are allowed to be static in a non-static context.
// Therefore we are only interested in partial method definitions here.
if (userMapping.Method is { IsStatic: true, IsPartialDefinition: true })
trejjam marked this conversation as resolved.
Show resolved Hide resolved
{
_mapperDescriptor.Static = true;
}
else if (firstNonStaticUserMapping == null && !userMapping.Method.IsStatic)
{
firstNonStaticUserMapping = userMapping.Method;
}

_mappings.Add(userMapping);
}

if (_mapperDescriptor.Static && firstNonStaticUserMapping is not null)
{
_diagnostics.Add(
Diagnostic.Create(
DiagnosticDescriptors.MixingStaticPartialWithInstanceMethod,
firstNonStaticUserMapping.Locations.FirstOrDefault(),
_mapperDescriptor.Symbol.ToDisplayString()
)
);
}
}

private ObjectFactoryCollection ExtractObjectFactories()
{
_objectFactories = ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol);
return ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol);
}

private void ExtractUserMappings()
private void EnqueueUserMappings(ObjectFactoryCollection objectFactories)
{
foreach (var userMapping in UserMethodMappingExtractor.ExtractUserMappings(_builderContext, _mapperDescriptor.Symbol))
foreach (var userMapping in _mappings.UserMappings)
{
var ctx = new MappingBuilderContext(
_builderContext,
_objectFactories,
objectFactories,
userMapping.Method,
userMapping.SourceType,
userMapping.TargetType
);

_mappings.Add(userMapping);
_mappings.EnqueueToBuildBody(userMapping, ctx);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class MapperDescriptor
private readonly List<IUnsafeAccessor> _unsafeAccessors = new();
private readonly HashSet<TemplateReference> _requiredTemplates = new();

public bool Static { get; set; }

public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder)
{
_declaration = declaration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde
|| methodSymbol.IsPartialDefinition
|| methodSymbol.MethodKind != MethodKind.Ordinary
|| methodSymbol.ReturnsVoid
|| (!methodSymbol.IsStatic && ctx.Static)
)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidObjectFactorySignature, methodSymbol, methodSymbol.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories;

public class ObjectFactoryCollection
{
public static readonly ObjectFactoryCollection Empty = new(Array.Empty<ObjectFactory>());

private readonly IReadOnlyCollection<ObjectFactory> _objectFactories;
private readonly Dictionary<ITypeSymbol, ObjectFactory> _concreteObjectFactories = new(SymbolEqualityComparer.IncludeNullability);

Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx)

public WellKnownTypes Types => _compilationContext.Types;

public bool Static => _descriptor.Static;

public SymbolAccessor SymbolAccessor { get; }

public AttributeDataAccessor AttributeAccessor { get; }
Expand Down
13 changes: 3 additions & 10 deletions src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ internal static IEnumerable<IUserMapping> ExtractUserMappings(SimpleMappingBuild
foreach (var methodSymbol in ExtractMethods(mapperSymbol))
{
var mapping =
BuilderUserDefinedMapping(ctx, methodSymbol, mapperSymbol.IsStatic)
?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, mapperSymbol.IsStatic);
BuilderUserDefinedMapping(ctx, methodSymbol)
?? BuildUserImplementedMapping(ctx, methodSymbol, receiver: null, allowPartial: false, mapperSymbol.IsStatic);
if (mapping != null)
yield return mapping;
}
Expand Down Expand Up @@ -109,18 +109,11 @@ bool isStatic
: new UserImplementedMethodMapping(receiver, method, parameters.Source, parameters.ReferenceHandler);
}

private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, bool isStatic)
private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol)
{
if (!methodSymbol.IsPartialDefinition)
return null;

if (!isStatic && methodSymbol.IsStatic)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.PartialStaticMethodInInstanceMapper, methodSymbol, methodSymbol.Name);

return null;
}

if (methodSymbol.IsAsync)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, methodSymbol, methodSymbol.Name);
Expand Down
18 changes: 9 additions & 9 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,6 @@ public static class DiagnosticDescriptors
true
);

public static readonly DiagnosticDescriptor PartialStaticMethodInInstanceMapper = new DiagnosticDescriptor(
"RMG018",
"Partial static mapping method in an instance mapper",
"{0} is a partial static mapping method in an instance mapper. Static mapping methods are only supported in static mappers.",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
);

public static readonly DiagnosticDescriptor SourceMemberNotMapped = new DiagnosticDescriptor(
"RMG020",
"Source member is not mapped to any target member",
Expand Down Expand Up @@ -473,4 +464,13 @@ public static class DiagnosticDescriptors
DiagnosticSeverity.Error,
true
);

public static readonly DiagnosticDescriptor MixingStaticPartialWithInstanceMethod = new DiagnosticDescriptor(
"RMG054",
"Mapper class containing 'static partial' method must not have any instance methods",
"Mapper class {0} contains 'static partial' method. Use only instance method or only static methods.",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
);
}
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/Emit/SourceEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class SourceEmitter
public static CompilationUnitSyntax Build(MapperDescriptor descriptor, CancellationToken cancellationToken)
{
var ctx = new SourceEmitterContext(
descriptor.Symbol.IsStatic,
descriptor.Static,
descriptor.NameBuilder,
new SyntaxFactoryHelper(descriptor.Symbol.ContainingAssembly.Name)
);
Expand Down
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/MapperGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
Expand Down
18 changes: 15 additions & 3 deletions test/Riok.Mapperly.Tests/Mapping/DictionaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ public void DictionaryToCustomDictionary()
public void DictionaryToCustomDictionaryWithObjectFactory()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[ObjectFactory] A CreateA() => new();" + "partial A Map(IDictionary<string, int> source);",
"""
[ObjectFactory]
A CreateA() => new();
partial A Map(IDictionary<string, int> source);
""",
"class A : Dictionary<string, int> {}"
);
TestHelper
Expand Down Expand Up @@ -354,7 +358,11 @@ string IDictionary<string, string>.this[string key]
public void DictionaryToExplicitDictionaryWithObjectFactoryShouldCast()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[ObjectFactory] A CreateA() => new();" + "partial A Map(Dictionary<string, string> source);",
"""
[ObjectFactory]
A CreateA() => new();
partial A Map(Dictionary<string, string> source);
""",
"""
public class A : IDictionary<string, string>
{
Expand Down Expand Up @@ -387,7 +395,11 @@ string IDictionary<string, string>.this[string key]
public void DictionaryToImplicitDictionaryWithObjectFactoryShouldNotCast()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[ObjectFactory] A CreateA() => new();" + "partial A Map(Dictionary<string, string> source);",
"""
[ObjectFactory]
A CreateA() => new();
partial A Map(Dictionary<string, string> source);
""",
"""
public class A : IDictionary<string, string>
{
Expand Down
Loading
Loading