diff --git a/src/generator/Generator.Serialize.Generic.cs b/src/generator/Generator.Serialize.Generic.cs index 606e5d0e..a7bd7c66 100644 --- a/src/generator/Generator.Serialize.Generic.cs +++ b/src/generator/Generator.Serialize.Generic.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -164,7 +165,7 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ Identifier("name"), argumentList: null, EqualsValueClause(SwitchExpression(receiverExpr, SeparatedList(cases)))) })))); - var wrapper = TryGetPrimitiveWrapper(enumType.EnumUnderlyingType!, SerdeUsage.Serialize)!; + var wrapper = TryGetPrimitiveWrapper(enumType.EnumUnderlyingType!, SerdeUsage.Serialize).Unwrap().Wrapper; statements.Add(ExpressionStatement(InvocationExpression( QualifiedName(IdentifierName("serializer"), IdentifierName("SerializeEnumValue")), ArgumentList(SeparatedList(new[] { @@ -205,8 +206,8 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ { // Generate statements of the form `type.SerializeField("FieldName", receiver.FieldValue)` var memberExpr = MakeMemberAccessExpr(m, receiverExpr); - var serializeImpl = MakeSerializeType(m, context, memberExpr, inProgress); - if (serializeImpl is null) + var typeAndWrapperOpt = MakeSerializeType(m, context, memberExpr, inProgress); + if (typeAndWrapperOpt is not {} typeAndWrapper) { // No built-in handling and doesn't implement ISerialize, error context.ReportDiagnostic(CreateDiagnostic( @@ -218,7 +219,7 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ } else { - statements.Add(MakeSerializeFieldStmt(m, memberExpr, serializeImpl, receiverExpr)); + statements.Add(MakeSerializeFieldStmt(m, memberExpr, typeAndWrapper, receiverExpr)); } } @@ -260,7 +261,7 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ static ExpressionStatementSyntax MakeSerializeFieldStmt( DataMemberSymbol member, ExpressionSyntax value, - TypeSyntax serializeType, + TypeAndWrapper typeAndWrapper, ExpressionSyntax receiver) { var arguments = new List() { @@ -270,8 +271,8 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ value, }; var typeArgs = new List() { - member.Type.ToFqnSyntax(), - serializeType + typeAndWrapper.Type, + typeAndWrapper.Wrapper }; string methodName; @@ -341,7 +342,7 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ /// implements ISerialize. SerdeDn provides wrappers for primitives and common types in the /// framework. If found, we generate and initialize the wrapper. /// - private static TypeSyntax? MakeSerializeType( + private static TypeAndWrapper? MakeSerializeType( DataMemberSymbol member, GeneratorExecutionContext context, ExpressionSyntax memberExpr, @@ -350,23 +351,18 @@ partial record struct {{wrapperName}} : Serde.ISerializeWrap<{{wrappedName}}, {{ // 1. Check for an explicit wrapper if (TryGetExplicitWrapper(member, context, SerdeUsage.Serialize, inProgress) is {} wrapper) { - return wrapper; + return new(member.Type.ToFqnSyntax(), wrapper); } // 2. Check for a direct implementation of ISerialize if (ImplementsSerde(member.Type, context, SerdeUsage.Serialize)) { - return GenericName(Identifier("IdWrap"), TypeArgumentList(SeparatedList(new[] { member.Type.ToFqnSyntax() }))); + return new(member.Type.ToFqnSyntax(), + GenericName(Identifier("IdWrap"), TypeArgumentList(SeparatedList(new[] { member.Type.ToFqnSyntax() })))); } // 3. A wrapper that implements ISerialize - var wrapperType = TryGetAnyWrapper(member.Type, context, SerdeUsage.Serialize, inProgress); - if (wrapperType is not null) - { - return wrapperType; - } - - return null; + return TryGetAnyWrapper(member.Type, context, SerdeUsage.Serialize, inProgress); } /// @@ -553,9 +549,9 @@ private static bool ImplementsSerde(ITypeSymbol memberType, GeneratorExecutionCo // Otherwise we'll need to wrap the element type as well e.g., // ArrayWrap<`elemType`, `elemTypeWrapper`> - var wrapper = TryGetAnyWrapper(elemType, context, usage, inProgress); + var typeAndWrapper = TryGetAnyWrapper(elemType, context, usage, inProgress); - if (wrapper is null) + if (typeAndWrapper is not (_, var wrapper)) { // Could not find a wrapper return null; @@ -575,7 +571,7 @@ private static bool ImplementsSerde(ITypeSymbol memberType, GeneratorExecutionCo return wrapperSyntax; } - private static TypeSyntax? TryGetAnyWrapper( + private static TypeAndWrapper? TryGetAnyWrapper( ITypeSymbol elemType, GeneratorExecutionContext context, SerdeUsage usage, @@ -592,16 +588,16 @@ private static bool ImplementsSerde(ITypeSymbol memberType, GeneratorExecutionCo allTypes = parent.Name + allTypes; } var wrapperName = $"{allTypes}Wrap"; - return IdentifierName(wrapperName); + return new(elemType.ToFqnSyntax(), IdentifierName(wrapperName)); } - var nameSyntax = TryGetPrimitiveWrapper(elemType, usage) + var typeAndWrapper = TryGetPrimitiveWrapper(elemType, usage) ?? TryGetEnumWrapper(elemType, usage) ?? TryGetCompoundWrapper(elemType, context, usage, inProgress); - if (nameSyntax is null) + if (typeAndWrapper is null) { return null; } - return nameSyntax; + return typeAndWrapper; } @@ -654,7 +650,7 @@ namespace Serde } // If the target is a core type, we can wrap it - private static TypeSyntax? TryGetPrimitiveWrapper(ITypeSymbol type, SerdeUsage usage) + private static TypeAndWrapper? TryGetPrimitiveWrapper(ITypeSymbol type, SerdeUsage usage) { if (type.NullableAnnotation == NullableAnnotation.Annotated) { @@ -678,10 +674,10 @@ namespace Serde SpecialType.System_Decimal => "DecimalWrap", _ => null }; - return name is null ? null : IdentifierName(name); + return name is null ? null : new(type.ToFqnSyntax(), IdentifierName(name)); } - private static TypeSyntax? TryGetEnumWrapper(ITypeSymbol type, SerdeUsage usage) + private static TypeAndWrapper? TryGetEnumWrapper(ITypeSymbol type, SerdeUsage usage) { if (type.TypeKind is not TypeKind.Enum) { @@ -704,7 +700,7 @@ namespace Serde ? containing + "." + wrapperName : "global::" + wrapperName; - return SyntaxFactory.ParseTypeName(wrapperFqn); + return new(type.ToFqnSyntax(), ParseTypeName(wrapperFqn)); } private static bool HasGenerateAttribute(ITypeSymbol memberType, SerdeUsage usage) @@ -735,17 +731,18 @@ private static bool HasGenerateAttribute(ITypeSymbol memberType, SerdeUsage usag return false; } - private static TypeSyntax? TryGetCompoundWrapper(ITypeSymbol type, GeneratorExecutionContext context, SerdeUsage usage, ImmutableList inProgress) + private static TypeAndWrapper? TryGetCompoundWrapper(ITypeSymbol type, GeneratorExecutionContext context, SerdeUsage usage, ImmutableList inProgress) { - return type switch + (TypeSyntax?, TypeSyntax?)? valueTypeAndWrapper = type switch { { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } => - MakeWrappedExpression( + (null, + MakeWrappedExpression( context.Compilation.GetTypeByMetadataName("Serde.NullableWrap+" + GetImplName(usage) + "`2")!, ImmutableArray.Create(((INamedTypeSymbol)type).TypeArguments[0]), context, usage, - inProgress), + inProgress)), // This is rather subtle. One might think that we would want to use a // NullableRefWrapper for any reference type that could contain null. In fact, we @@ -759,27 +756,36 @@ private static bool HasGenerateAttribute(ITypeSymbol memberType, SerdeUsage usag // ISerialize, and therefore the substitution to provide the appropriate nullable // wrapper. { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated} => - MakeWrappedExpression( + (null, + MakeWrappedExpression( context.Compilation.GetTypeByMetadataName("Serde.NullableRefWrap+" + GetImplName(usage) + "`2")!, ImmutableArray.Create(type.WithNullableAnnotation(NullableAnnotation.NotAnnotated)), context, usage, - inProgress), + inProgress)), IArrayTypeSymbol and { IsSZArray: true, Rank: 1, ElementType: { } elemType } - => MakeWrappedExpression( + => (null, + MakeWrappedExpression( context.Compilation.GetTypeByMetadataName("Serde.ArrayWrap+" + GetImplName(usage) + "`2")!, ImmutableArray.Create(elemType), context, usage, - inProgress), + inProgress)), - INamedTypeSymbol t when TryGetWrapperName(t, context, usage) is { } tuple - => MakeWrappedExpression( - tuple.WrapperType, tuple.Args, context, usage, inProgress), + INamedTypeSymbol t when TryGetWrapperName(t, context, usage) is (var ValueType, (var WrapperType, var Args)) + => (ValueType, + MakeWrappedExpression( + WrapperType, Args, context, usage, inProgress)), _ => null, }; + return valueTypeAndWrapper switch { + null => null, + (null, {} wrapper) => new(type.ToFqnSyntax(), wrapper), + ({ } value, { } wrapper) => new(value, wrapper), + (_, null) => throw ExceptionUtilities.Unreachable + }; } private static string GetImplName(SerdeUsage usage) => usage switch @@ -789,7 +795,7 @@ private static bool HasGenerateAttribute(ITypeSymbol memberType, SerdeUsage usag _ => throw ExceptionUtilities.Unreachable }; - private static (INamedTypeSymbol WrapperType, ImmutableArray Args)? TryGetWrapperName( + private static (TypeSyntax MemberType, (INamedTypeSymbol WrapperType, ImmutableArray Args))? TryGetWrapperName( ITypeSymbol typeSymbol, GeneratorExecutionContext context, SerdeUsage usage) @@ -797,11 +803,12 @@ private static bool HasGenerateAttribute(ITypeSymbol memberType, SerdeUsage usag if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated) { var nullableRefWrap = context.Compilation.GetTypeByMetadataName("Serde.NullableRefWrap+" + GetImplName(usage) + "`1")!; - return (nullableRefWrap, ImmutableArray.Create(typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated))); + return (typeSymbol.ToFqnSyntax(), + (nullableRefWrap, ImmutableArray.Create(typeSymbol.WithNullableAnnotation(NullableAnnotation.NotAnnotated)))); } if (typeSymbol is INamedTypeSymbol named && TryGetWellKnownType(named, context) is {} wk) { - return (ToWrapper(wk, context.Compilation, usage), named.TypeArguments); + return (typeSymbol.ToFqnSyntax(), (ToWrapper(wk, context.Compilation, usage), named.TypeArguments)); } // Check if it implements well-known interfaces @@ -813,13 +820,15 @@ private static bool HasGenerateAttribute(ITypeSymbol memberType, SerdeUsage usag if (impl.OriginalDefinition.Equals(iface, SymbolEqualityComparer.Default) && ToWrapper(TryGetWellKnownType(iface, context), context.Compilation, usage) is { } wrap) { - return (wrap, impl.TypeArguments); + return (impl.ToFqnSyntax(), (wrap, impl.TypeArguments)); } } } return null; } + private readonly record struct TypeAndWrapper(TypeSyntax Type, TypeSyntax Wrapper); + [return: NotNullIfNotNull(nameof(wk))] internal static INamedTypeSymbol? ToWrapper(WellKnownType? wk, Compilation comp, SerdeUsage usage) { diff --git a/src/generator/Utilities.cs b/src/generator/Utilities.cs index b53bd9bd..0de5f5b7 100644 --- a/src/generator/Utilities.cs +++ b/src/generator/Utilities.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Serde { @@ -32,6 +33,8 @@ public static bool IsSorted(this ReadOnlySpan span, IComparer comparer) internal static class Utilities { + public static T Unwrap(this T? value) where T : struct => value!.Value; + public static string Concat(this string recv, string other) { return recv + other; diff --git a/src/serde/ISerialize.cs b/src/serde/ISerialize.cs index 59819fdc..f8c2c648 100644 --- a/src/serde/ISerialize.cs +++ b/src/serde/ISerialize.cs @@ -8,7 +8,7 @@ public interface ISerialize void Serialize(ISerializer serializer); } -public interface ISerialize : ISerialize +public interface ISerialize : ISerialize { void Serialize(T value, ISerializer serializer); diff --git a/test/Serde.Generation.Test/test_output/SerializeTests/IDictionaryImplGenerate#C.ISerialize`1.verified.cs b/test/Serde.Generation.Test/test_output/SerializeTests/IDictionaryImplGenerate#C.ISerialize`1.verified.cs index 8af66c89..69be2ce3 100644 --- a/test/Serde.Generation.Test/test_output/SerializeTests/IDictionaryImplGenerate#C.ISerialize`1.verified.cs +++ b/test/Serde.Generation.Test/test_output/SerializeTests/IDictionaryImplGenerate#C.ISerialize`1.verified.cs @@ -9,7 +9,7 @@ partial class C : Serde.ISerialize void ISerialize.Serialize(C value, ISerializer serializer) { var type = serializer.SerializeType("C", 1); - type.SerializeField>("rDictionary", value.RDictionary); + type.SerializeField, Serde.IDictWrap.SerializeImpl>("rDictionary", value.RDictionary); type.End(); } } \ No newline at end of file diff --git a/test/Serde.Test/JsonSerializerTests.cs b/test/Serde.Test/JsonSerializerTests.cs index f1d04b1b..af88d0d8 100644 --- a/test/Serde.Test/JsonSerializerTests.cs +++ b/test/Serde.Test/JsonSerializerTests.cs @@ -147,7 +147,7 @@ public void NullableString() string? s = null; var js = Serde.Json.JsonSerializer.Serialize>(s); Assert.Equal("null", js); - js = Serde.Json.JsonSerializer.Serialize(JsonValue.Null.Instance); + js = Serde.Json.JsonSerializer.Serialize(JsonValue.Null.Instance); Assert.Equal("null", js); }