Skip to content
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ So the benefits of Mappit are:
### Properties
* **Implicit property mappings** (properties with matching names and compatible types)
* **Custom property mappings** - mapping from one property to another property with a different name, including support for custom value transformations.
* **Implicit nullable mappings** - automatic mapping between a type with it's `Nullable<T>` counterpart. The reverse is supported too, but will be configured to throw
`ArgumentNullException` at runtime is the source value is null.

### Enums
* **Implicit enum mappings** where all the enum names match
Expand All @@ -30,6 +32,7 @@ So the benefits of Mappit are:
### Type construction
* **Constructor initialization**, including constructors that only cover some of the properties. Any remaining properties will be initialized via their setters.
* **Missing properties on the target type** - by default you'll get compile-time errors, but can opt in to ignore them.
* Support for both structs and classes, including records.

## Getting started

Expand Down
10 changes: 5 additions & 5 deletions src/Mappit.Generator/MappingTypeInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand All @@ -20,20 +20,20 @@ public MappingTypeInfo(IMethodSymbol methodSymbol, ITypeSymbol sourceType, IType
TargetType = targetType;
MethodDeclaration = methodDeclaration;
RequiresPartialMethod = true;
IsEnum = sourceType.TypeKind == TypeKind.Enum || targetType.TypeKind == TypeKind.Enum;
IsEnum = sourceType.IsEnum() || targetType.IsEnum();

if (IsEnum)
{
if (sourceType.TypeKind != TypeKind.Enum || targetType.TypeKind != TypeKind.Enum)
if (!sourceType.IsEnum() || !targetType.IsEnum())
{
ValidationErrors.Add((MappitErrorCode.EnumTypeMismatch, $"{(sourceType.TypeKind == TypeKind.Enum ? "source type" : "target type")} is an enum, but the other is not."));
ValidationErrors.Add((MappitErrorCode.EnumTypeMismatch, $"{(sourceType.IsEnum() ? "source type" : "target type")} is an enum, but the other is not."));
}
}
}

public List<(MappitErrorCode, string)> ValidationErrors { get; set; } = [];
public bool RequiresGeneration => _methodSymbol.IsPartialDefinition;
public bool IsEnum { get; init; }
public bool IsEnum { get; }
public string MethodName => _methodSymbol.Name;

public ITypeSymbol SourceType { get; init; }
Expand Down
2 changes: 1 addition & 1 deletion src/Mappit.Generator/Mappit.Generator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<IsPackable>true</IsPackable>
<Version>0.0.5</Version>
<Version>0.0.6</Version>
<WarningsNotAsErrors>NU5128</WarningsNotAsErrors>
</PropertyGroup>

Expand Down
220 changes: 141 additions & 79 deletions src/Mappit.Generator/MappitGenerator.MappingValidation.cs

Large diffs are not rendered by default.

86 changes: 66 additions & 20 deletions src/Mappit.Generator/MappitGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private static void GenerateMapper(SourceProductionContext context, MapperClassI
source.AppendLine("{");

var classModifiers = string.Join(
" ",
" ",
mapperClass.ClassDeclarationSyntax.Modifiers
.Where(m => !m.IsKind(SyntaxKind.PartialKeyword) && !m.IsKind(SyntaxKind.SealedKeyword))
.Select(m => m.Text));
Expand Down Expand Up @@ -83,6 +83,11 @@ private static void GenerateMapper(SourceProductionContext context, MapperClassI
}
}

foreach (var mapping in validatedMap.NullableMappings)
{
EmitNullableMappingMethod(source, validatedMap, mapping);
}

source.AppendLine(" }");
source.AppendLine("}");

Expand All @@ -101,7 +106,9 @@ private static void GenerateMapperInterface(StringBuilder source, string classMo
// Generate interface methods for each mapping type
foreach (var mapping in validatedMap.EnumMappings
.Concat<ValidatedMappingInfo>(validatedMap.TypeMappings)
.Concat(validatedMap.CollectionMappings.Where(cm => !cm.IsImplicitMapping)))
.Concat(validatedMap.CollectionMappings)
.Concat(validatedMap.NullableMappings)
.Where(cm => !cm.IsImplicitMapping))
{
source.AppendLine($" /// <summary>");
source.AppendLine($" /// Maps {mapping.SourceType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)} to {mapping.TargetType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}");
Expand Down Expand Up @@ -142,16 +149,50 @@ private static void EmitSourcePropertyReference(StringBuilder source, ValidatedM
}
}

private static void EmitNullableMappingMethod(StringBuilder source, ValidatedMapperClassInfo classInfo, ValidatedNullableMappingTypeInfo mapping)
{
EmitMappingMethodDeclaration(source, mapping);

bool needsElementMapping = classInfo.TryGetMappedType(
mapping.SourceNullableUnderlyingType ?? mapping.SourceType,
mapping.TargetNullableUnderlyingType ?? mapping.TargetType,
out var underlyingTypeMapping);

source.AppendLine(" return source is {} notNullSource");
source.Append(" ? ");
if (needsElementMapping)
{
// If we need to map the underlying type, we need to call the mapping method
source.AppendLine($"{underlyingTypeMapping!.MethodName}(notNullSource)");
}
else
{
// No mapping needed; just return the value
source.AppendLine($"notNullSource");
}

source.Append(" : ");
if (mapping.TargetNullableUnderlyingType is null)
{
// If the target type is not nullable, there's a runtime conversion error
source.AppendLine($"throw new global::System.ArgumentNullException(nameof(source), \"Cannot map null to non-nullable type {FormatTypeForErrorMessage(mapping.TargetType)}\");");
}
else
{
// If the target type is nullable, we can just return null
source.AppendLine($"default({mapping.TargetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)});");
}

source.AppendLine(" }");
}

private static void EmitCollectionMappingMethod(StringBuilder source, ValidatedMapperClassInfo classInfo, ValidatedCollectionMappingTypeInfo mapping)
{
var (sourceElementType, targetElementType) = mapping.ElementTypeMap;

EmitMappingMethodDeclaration(source, mapping);
source.AppendLine(" if (source is null)");
source.AppendLine(" {");
source.AppendLine(" return default;");
source.AppendLine(" }");
source.AppendLine();
EmitSourceNullCheck(source, mapping);

source.Append(" ");

bool needsElementMapping = classInfo.TryGetMappedType(sourceElementType, targetElementType, out var elementMapping);
Expand Down Expand Up @@ -201,13 +242,9 @@ private static void EmitDictionaryMappingMethod(StringBuilder source, ValidatedM
bool needsValueMapping = classInfo.TryGetMappedType(sourceElementType, targetElementType, out var valueMapping);

EmitMappingMethodDeclaration(source, mapping);
source.AppendLine(" if (source is null)");
source.AppendLine(" {");
source.AppendLine(" return default;");
source.AppendLine(" }");
source.AppendLine();
EmitSourceNullCheck(source, mapping);

source.Append(" ");

var concreteReturnType = TypeHelpers.InferConcreteDictionaryType(mapping.TargetType, targetKeyType, targetElementType);
if (needsKeyMapping || needsValueMapping)
{
Expand Down Expand Up @@ -248,11 +285,7 @@ private static void GenerateTypeMappingMethod(StringBuilder source, ValidatedMap
}

EmitMappingMethodDeclaration(source, mapping);
source.AppendLine(" if (source is null)");
source.AppendLine(" {");
source.AppendLine(" return default;");
source.AppendLine(" }");
source.AppendLine();
EmitSourceNullCheck(source, mapping);

// Start object initialization
source.Append($" return new {mapping.TargetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}");
Expand Down Expand Up @@ -286,14 +319,14 @@ private static void GenerateTypeMappingMethod(StringBuilder source, ValidatedMap
source.Append("()");
}

if (mapping.MemberMappings.Values.Any(m => m.TargetMapping == TargetMapping.Initialization))
if (mapping.MemberMappings.Any(m => m.TargetMapping == TargetMapping.Initialization))
{
// Start object initializer
source.AppendLine();
source.AppendLine(" {");

// Handle custom property mappings first (skip those already set by constructor)
foreach (var propertyMapping in mapping.MemberMappings.Values.Where(x => x.TargetMapping == TargetMapping.Initialization))
foreach (var propertyMapping in mapping.MemberMappings.Where(x => x.TargetMapping == TargetMapping.Initialization))
{
source.Append($" {propertyMapping.TargetProperty.Name} = ");
EmitSourcePropertyReference(source, classInfo, propertyMapping);
Expand All @@ -314,6 +347,19 @@ private static void GenerateTypeMappingMethod(StringBuilder source, ValidatedMap
source.AppendLine(" }");
}

private static void EmitSourceNullCheck(StringBuilder source, ValidatedMappingInfo mapping)
{
var targetType = mapping.TargetType;
if (!targetType.IsValueType)
{
source.AppendLine(" if (source is null)");
source.AppendLine(" {");
source.AppendLine($" return default({mapping.TargetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)});");
source.AppendLine(" }");
source.AppendLine();
}
}

private static void GenerateEnumMappingMethod(StringBuilder source, ValidatedMappingEnumInfo mapping)
{
if (!mapping.RequiresGeneration)
Expand Down
64 changes: 64 additions & 0 deletions src/Mappit.Generator/TypeCompatibilityChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.CodeAnalysis;

namespace Mappit.Generator
{
internal static class TypeCompatibilityChecker
{
/// <summary>
/// Determines if two types are compatible for mapping purposes
/// </summary>
public static bool AreCompatibleTypes(MapperClassInfoBase mapperClass, ITypeSymbol sourceType, ITypeSymbol targetType)
{
// Simple case: if the types are the same, they're compatible
if (sourceType.Equals(targetType, SymbolEqualityComparer.Default))
{
return true;
}

// Check if the types are compatible because they've already been mapped
if (mapperClass.HasHapping(sourceType, targetType))
{
return true;
}

// Check for nullable types
var sourceIsNullable = sourceType.IsNullableType();
var targetIsNullable = targetType.IsNullableType();

// If one or the other is nullable, check if their underlying types are compatible
if (sourceIsNullable || targetIsNullable)
{
var sourceUnderlyingType = sourceIsNullable ? sourceType.GetNullableUnderlyingType() : sourceType;
var targetUnderlyingType = targetIsNullable ? targetType.GetNullableUnderlyingType() : targetType;

// If the underlying types are compatible, then the nullable and non-nullable types are compatible
return AreCompatibleTypes(mapperClass, sourceUnderlyingType, targetUnderlyingType);
}

// Dictionary types - these also have collection/enumerable interfaces, so we need to check them first
if (TypeHelpers.IsDictionaryType(sourceType, out var sourceKeyType, out var sourceValueType) &&
TypeHelpers.IsDictionaryType(targetType, out var targetKeyType, out var targetValueType))
{
if (sourceKeyType != null && targetKeyType != null && sourceValueType != null && targetValueType != null)
{
// Dictionaries are compatible if their key and value types are compatible
return AreCompatibleTypes(mapperClass, sourceKeyType, targetKeyType) &&
AreCompatibleTypes(mapperClass, sourceValueType, targetValueType);
}
}

// Check for collection types
if (TypeHelpers.IsCollectionType(sourceType, out var sourceElementType) &&
TypeHelpers.IsCollectionType(targetType, out var targetElementType))
{
if (sourceElementType != null && targetElementType != null)
{
// Collections are compatible if their element types are compatible
return AreCompatibleTypes(mapperClass, sourceElementType, targetElementType);
}
}

return false;
}
}
}
46 changes: 46 additions & 0 deletions src/Mappit.Generator/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.CodeAnalysis;

namespace Mappit.Generator
{
internal static class TypeSymbolExtensions
{
/// <summary>
/// Determines if this type is a nullable type (Nullable<T>)
/// </summary>
public static bool IsNullableType(this ITypeSymbol type)
{
return type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
}

/// <summary>
/// If this is a nullable type, returns the underlying type.
/// Otherwise, returns the type itself.
/// </summary>
public static ITypeSymbol GetNullableUnderlyingType(this ITypeSymbol type)
{
if (type is INamedTypeSymbol namedType && namedType.IsNullableType())
{
return namedType.TypeArguments[0];
}

return type;
}

/// <summary>
/// Determines if this type is a struct
/// </summary>
public static bool IsStruct(this ITypeSymbol type)
{
return type.IsValueType && type.TypeKind == TypeKind.Struct;
}

/// <summary>
/// Determines if this type is an enum
/// </summary>
public static bool IsEnum(this ITypeSymbol type)
{
return type.TypeKind == TypeKind.Enum
|| (type.IsNullableType() && type.GetNullableUnderlyingType().TypeKind == TypeKind.Enum);
}
}
}
Loading