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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ When mapping properties with different enum types (e.g., `ContactType` → `Cust
- `ATCMCF010` - Assembly MapTypes source type not found (Error)
- `ATCMCF011` - Map() requires exactly two type arguments (Error)
- `ATCMCF013` - AddMappings() requires a lambda expression argument (Error)
- `ATCMCF014` - Type could not be resolved, possibly from sibling source generator (Warning)
- `ATCMAP005` - Duplicate mapping: attribute-based [MapTo] takes precedence over configuration (Warning)
- `ATCMAP006` - Duplicate configuration mapping for same source→target pair (Warning)

Expand Down
1 change: 1 addition & 0 deletions src/Atc.SourceGenerators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ ATCMCF011 | MappingConfiguration | Error | Map() requires exactly two type argum
ATCMCF013 | MappingConfiguration | Error | AddMappings() requires a lambda expression argument
ATCENUM006 | EnumMapping | Warning | Flag enum mapping may produce incorrect results for combined values
ATCMAP011 | ObjectMapping | Warning | Collection element type has no mapping method
ATCMCF014 | MappingConfiguration | Warning | Type could not be resolved (possibly from sibling generator)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal sealed record ConfiguredMappingInfo(
bool EnableFlattening,
IMethodSymbol? Constructor,
List<string> ConstructorParameterNames,
List<ConstructorDefaultParameter> ConstructorDefaultParameters,
PropertyNameStrategy PropertyNameStrategy,
List<string> IgnoredProperties,
List<(string Source, string Target)> PropertyRenames);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Atc.SourceGenerators.Generators.Internal;

/// <summary>
/// Represents a constructor parameter that has no matching source property
/// and should be filled with a default value.
/// </summary>
internal sealed record ConstructorDefaultParameter(
string Name,
ITypeSymbol Type,
string DefaultExpression);
133 changes: 108 additions & 25 deletions src/Atc.SourceGenerators/Generators/Internal/MappingTypeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,40 @@ internal static bool IsAttributeMatch(
attributeClass.ContainingNamespace?.ToDisplayString() == expectedNamespace;
}

internal static ITypeSymbol UnwrapNullable(ITypeSymbol type)
{
if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable)
{
return nullable.TypeArguments[0];
}

return type;
}

internal static bool IsNullableValueType(ITypeSymbol type)
=> type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T };

internal static bool IsEnumConversion(
ITypeSymbol sourceType,
ITypeSymbol targetType)
=> sourceType.TypeKind == TypeKind.Enum &&
targetType.TypeKind == TypeKind.Enum;
{
var unwrappedSource = UnwrapNullable(sourceType);
var unwrappedTarget = UnwrapNullable(targetType);
return unwrappedSource.TypeKind == TypeKind.Enum &&
unwrappedTarget.TypeKind == TypeKind.Enum;
}

internal static bool IsNestedMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
var st = sourceType.SpecialType.ToString();
var tt = targetType.SpecialType.ToString();
var unwrappedSource = UnwrapNullable(sourceType);
var unwrappedTarget = UnwrapNullable(targetType);
var st = unwrappedSource.SpecialType.ToString();
var tt = unwrappedTarget.SpecialType.ToString();

return sourceType.TypeKind is TypeKind.Class or TypeKind.Struct &&
targetType.TypeKind is TypeKind.Class or TypeKind.Struct &&
return unwrappedSource.TypeKind is TypeKind.Class or TypeKind.Struct &&
unwrappedTarget.TypeKind is TypeKind.Class or TypeKind.Struct &&
!st.StartsWith("System", StringComparison.Ordinal) &&
!tt.StartsWith("System", StringComparison.Ordinal);
}
Expand All @@ -47,55 +66,81 @@ internal static bool IsBuiltInTypeConversion(
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
var sourceTypeName = sourceType.ToDisplayString();
var targetTypeName = targetType.ToDisplayString();
var sourceTypeName = UnwrapNullable(sourceType).ToDisplayString();
var targetTypeName = UnwrapNullable(targetType).ToDisplayString();

// DateTime/DateTimeOffset -> string
return IsStringToFromSystemTypeConversion(sourceTypeName, targetTypeName) ||
IsStringToFromUriConversion(sourceTypeName, targetTypeName) ||
IsStringToFromEnumConversion(sourceType, targetType, sourceTypeName, targetTypeName);
}

private static bool IsStringToFromSystemTypeConversion(
string sourceTypeName,
string targetTypeName)
{
// DateTime/DateTimeOffset <-> string
if ((sourceTypeName is "System.DateTime" or "System.DateTimeOffset") &&
targetTypeName == "string")
{
return true;
}

// string -> DateTime/DateTimeOffset
if (sourceTypeName == "string" &&
(targetTypeName is "System.DateTime" or "System.DateTimeOffset"))
{
return true;
}

// Guid -> string
if (sourceTypeName == "System.Guid" && targetTypeName == "string")
// Guid <-> string
if ((sourceTypeName == "System.Guid" && targetTypeName == "string") ||
(sourceTypeName == "string" && targetTypeName == "System.Guid"))
{
return true;
}

// string -> Guid
if (sourceTypeName == "string" && targetTypeName == "System.Guid")
// Numeric types <-> string
if ((IsNumericType(sourceTypeName) && targetTypeName == "string") ||
(sourceTypeName == "string" && IsNumericType(targetTypeName)))
{
return true;
}

// Numeric types -> string
if (IsNumericType(sourceTypeName) && targetTypeName == "string")
// bool <-> string
if ((sourceTypeName == "bool" && targetTypeName == "string") ||
(sourceTypeName == "string" && targetTypeName == "bool"))
{
return true;
}

// string -> Numeric types
if (sourceTypeName == "string" && IsNumericType(targetTypeName))
{
return true;
}
return false;
}

private static bool IsStringToFromUriConversion(
string sourceTypeName,
string targetTypeName)
{
var normalizedSource = sourceTypeName.TrimEnd('?');
var normalizedTarget = targetTypeName.TrimEnd('?');

return (normalizedSource == "string" && normalizedTarget == "System.Uri") ||
(normalizedSource == "System.Uri" && normalizedTarget == "string");
}

private static bool IsStringToFromEnumConversion(
ITypeSymbol sourceType,
ITypeSymbol targetType,
string sourceTypeName,
string targetTypeName)
{
var normalizedSource = sourceTypeName.TrimEnd('?');
var normalizedTarget = targetTypeName.TrimEnd('?');

// bool -> string
if (sourceTypeName == "bool" && targetTypeName == "string")
if (normalizedSource == "string" && UnwrapNullable(targetType).TypeKind == TypeKind.Enum)
{
return true;
}

// string -> bool
if (sourceTypeName == "string" && targetTypeName == "bool")
if (UnwrapNullable(sourceType).TypeKind == TypeKind.Enum && normalizedTarget == "string")
{
return true;
}
Expand All @@ -108,6 +153,15 @@ internal static bool IsNumericType(string typeName)
"uint" or "ulong" or "ushort" or
"decimal" or "double" or "float";

internal static bool IsNumericTypeConversion(
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
var sourceName = UnwrapNullable(sourceType).ToDisplayString();
var targetName = UnwrapNullable(targetType).ToDisplayString();
return sourceName != targetName && IsNumericType(sourceName) && IsNumericType(targetName);
}

internal static bool IsCollectionType(
ITypeSymbol type,
out ITypeSymbol? elementType)
Expand Down Expand Up @@ -189,4 +243,33 @@ internal static bool HasMapIgnoreAttribute(IPropertySymbol property)
return attributes.Any(attr =>
IsAttributeMatch(attr.AttributeClass, "MapIgnoreAttribute"));
}

internal static bool IsImplicitlyConvertible(
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
if (targetType.TypeKind == TypeKind.Interface)
{
return sourceType.AllInterfaces.Any(i =>
SymbolEqualityComparer.Default.Equals(i, targetType));
}

var current = sourceType.BaseType;
while (current is not null)
{
if (SymbolEqualityComparer.Default.Equals(current, targetType))
{
return true;
}

current = current.BaseType;
}

if (targetType is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedTarget)
{
return SymbolEqualityComparer.Default.Equals(sourceType, namedTarget.TypeArguments[0]);
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal sealed record PropertyMapping(
bool HasEnumMapping,
bool IsCollection,
ITypeSymbol? CollectionElementType,
ITypeSymbol? SourceCollectionElementType,
string? CollectionTargetType,
bool IsFlattened,
IPropertySymbol? FlattenedNestedProperty,
Expand Down
Loading