diff --git a/src/generator/Generator.Impl.cs b/src/generator/Generator.Impl.cs index 433d9f83..69200aaf 100644 --- a/src/generator/Generator.Impl.cs +++ b/src/generator/Generator.Impl.cs @@ -57,30 +57,64 @@ internal void AddSource(string fileName, string content) partial class SerdeImplRoslynGenerator { internal static void GenerateImpl( + AttributeData attributeData, SerdeUsage usage, BaseTypeDeclarationSyntax typeDecl, - SemanticModel semanticModel, + SemanticModel model, GeneratorExecutionContext context, ImmutableList inProgress) { - var receiverType = semanticModel.GetDeclaredSymbol(typeDecl); - if (receiverType is null) + var typeSymbol = model.GetDeclaredSymbol(typeDecl); + if (typeSymbol is null) { return; } - if (!typeDecl.IsKind(SyntaxKind.EnumDeclaration) && !typeDecl.Modifiers.Any(tok => tok.IsKind(SyntaxKind.PartialKeyword))) + + ITypeSymbol receiverType; + ExpressionSyntax receiverExpr; + // If the Through property is set, then we are implementing a wrapper type + if (attributeData.NamedArguments is [ (nameof(GenerateSerialize.Through), { Value: string memberName }) ]) { - // Type must be partial - context.ReportDiagnostic(CreateDiagnostic( - DiagId.ERR_TypeNotPartial, - typeDecl.Identifier.GetLocation(), - typeDecl.Identifier.ValueText)); - return; - } + var members = model.LookupSymbols(typeDecl.SpanStart, typeSymbol, memberName); + if (members.Length != 1) + { + // TODO: Error about bad lookup + return; + } + receiverType = SymbolUtilities.GetSymbolType(members[0]); + receiverExpr = IdentifierName(memberName); - var receiverExpr = typeDecl.IsKind(SyntaxKind.EnumDeclaration) - ? (ExpressionSyntax)IdentifierName("Value") - : ThisExpression(); + if (usage.HasFlag(SerdeUsage.Serialize)) + { + // If we're implementing ISerialize, also implement ISerializeWrap + GenerateISerializeWrapImpl( + typeDecl.Identifier.ValueText, + receiverType.ToDisplayString(), + typeDecl, + context); + } + } + // Enums are also always wrapped, but the attribute is on the enum itself + else if (typeDecl.IsKind(SyntaxKind.EnumDeclaration)) + { + receiverType = typeSymbol; + receiverExpr = IdentifierName("Value"); + } + // Just a normal interface implementation + else + { + if (!typeDecl.Modifiers.Any(tok => tok.IsKind(SyntaxKind.PartialKeyword))) + { + // Type must be partial + context.ReportDiagnostic(CreateDiagnostic( + DiagId.ERR_TypeNotPartial, + typeDecl.Identifier.GetLocation(), + typeDecl.Identifier.ValueText)); + return; + } + receiverType = typeSymbol; + receiverExpr = ThisExpression(); + } GenerateImpl( usage, @@ -124,6 +158,34 @@ private static void GenerateEnumWrapper( context.AddSource(fullWrapperName, Environment.NewLine + tree.ToFullString()); } + private static void GenerateISerializeWrapImpl( + string wrapperName, + string wrappedName, + BaseTypeDeclarationSyntax typeDecl, + GeneratorExecutionContext context) + { + var typeDeclContext = new TypeDeclContext(typeDecl); + var newType = SyntaxFactory.ParseMemberDeclaration($$""" +partial record struct {{wrapperName}}({{wrappedName}} Value) : ISerializeWrap<{{wrappedName}}, {{wrapperName}}> +{ + {{wrapperName}} ISerializeWrap<{{wrappedName}}, {{wrapperName}}>.Wrap({{wrappedName}} value) => new(value); +} +""")!; + newType = typeDeclContext.WrapNewType(newType); + string fullWrapperName = string.Join(".", typeDeclContext.NamespaceNames + .Concat(typeDeclContext.ParentTypeInfo.Select(x => x.Name)) + .Concat(new[] { wrapperName })); + + var tree = CompilationUnit( + externs: default, + usings: default, + attributeLists: default, + members: List(new[] { newType })); + tree = tree.NormalizeWhitespace(eol: Environment.NewLine); + + context.AddSource($"{fullWrapperName}.ISerializeWrap", Environment.NewLine + tree.ToFullString()); + } + private static void GenerateImpl( SerdeUsage usage, TypeDeclContext typeDeclContext, diff --git a/src/generator/Generator.cs b/src/generator/Generator.cs index ac6c5b95..f349d334 100644 --- a/src/generator/Generator.cs +++ b/src/generator/Generator.cs @@ -83,6 +83,7 @@ static GenerationOutput GenerateForCtx( if (usage.HasFlag(SerdeUsage.Serialize)) { SerdeImplRoslynGenerator.GenerateImpl( + attrCtx.Attributes.Single(), SerdeUsage.Serialize, (BaseTypeDeclarationSyntax)attrCtx.TargetNode, attrCtx.SemanticModel, @@ -92,6 +93,7 @@ static GenerationOutput GenerateForCtx( if (usage.HasFlag(SerdeUsage.Deserialize)) { SerdeImplRoslynGenerator.GenerateImpl( + attrCtx.Attributes.Single(), SerdeUsage.Deserialize, (BaseTypeDeclarationSyntax)attrCtx.TargetNode, attrCtx.SemanticModel, diff --git a/src/serde/Attributes.cs b/src/serde/Attributes.cs index 1040d5d0..7e225ca3 100644 --- a/src/serde/Attributes.cs +++ b/src/serde/Attributes.cs @@ -18,7 +18,13 @@ namespace Serde; internal #endif sealed class GenerateSerialize : Attribute -{ } +{ + /// + /// If non-null, the name of the member used to implement serialization. This is used to + /// implement serialization for a wrapper type. + /// + public string? Through { get; init; } +} /// /// Generates an implementation of . @@ -31,7 +37,14 @@ sealed class GenerateSerialize : Attribute internal #endif sealed class GenerateDeserialize : Attribute -{ } +{ + /// + /// If non-null, the name of the member used to implement deserialization. This is used to + /// implement deserialization for a wrapper type. + /// + public string? Through { get; init; } + +} /// /// Generates an implementation of both and . @@ -44,7 +57,13 @@ sealed class GenerateDeserialize : Attribute internal #endif sealed class GenerateSerde : Attribute -{ } +{ + /// + /// If non-null, the name of the member used to implement serialization and deserialization. + /// This is used to implement serialization and deserialization for a wrapper type. + /// + public string? Through { get; init; } +} /// /// Generates the equivalent of , but delegated to a member of the name @@ -72,6 +91,26 @@ public GenerateWrapper(string memberName) MemberName = memberName; } } + +[AttributeUsage( + AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct, + AllowMultiple = false, + Inherited = false)] +[Conditional("EMIT_GENERATE_SERDE_ATTRIBUTE")] +#if !SRCGEN +public +#else +internal +#endif +sealed class SerdeWrapAttribute : Attribute +{ + public SerdeWrapAttribute(Type wrapper) + { + Wrapper = wrapper; + } + public Type Wrapper { get; } +} + #pragma warning restore CS1574 /// diff --git a/src/serde/Wrappers.cs b/src/serde/Wrappers.cs index 51e9d777..cd0f9414 100644 --- a/src/serde/Wrappers.cs +++ b/src/serde/Wrappers.cs @@ -6,20 +6,6 @@ namespace Serde { - [AttributeUsage( - AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct, - AllowMultiple = false, - Inherited = false)] - [Conditional("EMIT_GENERATE_SERDE_ATTRIBUTE")] - public sealed class SerdeWrapAttribute : Attribute - { - public SerdeWrapAttribute(Type wrapper) - { - Wrapper = wrapper; - } - public Type Wrapper { get; } - } - public interface ISerializeWrap where TWrap : ISerialize { abstract static TWrap Create(T t); // Should be abstract static diff --git a/test/Serde.Generation.Test/DeserializeTests.cs b/test/Serde.Generation.Test/DeserializeTests.cs index 0cc9d6f8..8afc1e0d 100644 --- a/test/Serde.Generation.Test/DeserializeTests.cs +++ b/test/Serde.Generation.Test/DeserializeTests.cs @@ -11,6 +11,20 @@ namespace Serde.Test [UsesVerify] public class DeserializeTests { + [Fact] + public Task DeserializeOnlyWrap() + { + var src = """ +using Serde; +using System.Collections.Specialized; + +[GenerateDeserialize(Through = nameof(Value))] +readonly partial record struct SectionWrap(BitVector32.Section Value); + +"""; + return VerifyDeserialize(src); + } + [Fact] public Task MemberSkip() { diff --git a/test/Serde.Generation.Test/SerializeTests.cs b/test/Serde.Generation.Test/SerializeTests.cs index a2df306d..1c2bd160 100644 --- a/test/Serde.Generation.Test/SerializeTests.cs +++ b/test/Serde.Generation.Test/SerializeTests.cs @@ -12,6 +12,20 @@ namespace Serde.Test [UsesVerify] public class SerializeTests { + [Fact] + public Task SerializeOnlyWrapper() + { + var src = """ +using Serde; +using System.Collections.Specialized; + +[GenerateSerialize(Through = nameof(Value))] +readonly partial record struct SectionWrap(BitVector32.Section Value); + +"""; + return VerifyMultiFile(src); + } + [Fact] public Task MemberSkip() { diff --git a/test/Serde.Generation.Test/WrapperTests.cs b/test/Serde.Generation.Test/WrapperTests.cs index 5179bbb3..0cd36319 100644 --- a/test/Serde.Generation.Test/WrapperTests.cs +++ b/test/Serde.Generation.Test/WrapperTests.cs @@ -11,6 +11,20 @@ namespace Serde.Test [UsesVerify] public class WrapperTests { + [Fact] + public Task GenerateSerdeWrap() + { + var src = """ +using System.Collections.Specialized; +using Serde; + +[GenerateSerde(Through = nameof(Value))] +readonly partial record struct SectionWrap(BitVector32.Section Value); + +"""; + return VerifyMultiFile(src); + } + [Fact] public Task StringWrap() { @@ -67,18 +81,6 @@ public PointWrap(Point point) return VerifyMultiFile(src); } - [Fact] - public Task NestedUnimplementedSerializeWrap() - { - var src = @" -using System.Collections.Specialized; -[Serde.GenerateSerialize] -partial class C -{ - public BitVector32.Section S = new BitVector32.Section(); -}"; - return VerifyMultiFile(src); - } [Fact] public Task NestedDeserializeWrap() @@ -227,7 +229,7 @@ internal partial record struct RecursiveWrap(Recursive Value); [GenerateSerde] public partial record Parent( - [property: SerdeWrap(typeof(RecursiveWrap)] + [property: SerdeWrap(typeof(RecursiveWrap))] Recursive R); """; await VerifyMultiFile(src, new[] { comp.EmitToImageReference() }); diff --git a/test/Serde.Generation.Test/test_output/DeserializeTests/DeserializeOnlyWrap#SectionWrap.IDeserialize.verified.cs b/test/Serde.Generation.Test/test_output/DeserializeTests/DeserializeOnlyWrap#SectionWrap.IDeserialize.verified.cs new file mode 100644 index 00000000..015a8605 --- /dev/null +++ b/test/Serde.Generation.Test/test_output/DeserializeTests/DeserializeOnlyWrap#SectionWrap.IDeserialize.verified.cs @@ -0,0 +1,78 @@ +//HintName: SectionWrap.IDeserialize.cs + +#nullable enable +using System; +using Serde; + +partial record struct SectionWrap : Serde.IDeserialize +{ + static System.Collections.Specialized.BitVector32.Section Serde.IDeserialize.Deserialize(ref D deserializer) + { + var visitor = new SerdeVisitor(); + var fieldNames = new[] + { + "Mask", + "Offset" + }; + return deserializer.DeserializeType("Section", fieldNames, visitor); + } + + private sealed class SerdeVisitor : Serde.IDeserializeVisitor + { + public string ExpectedTypeName => "System.Collections.Specialized.BitVector32.Section"; + + private struct FieldNameVisitor : Serde.IDeserialize, Serde.IDeserializeVisitor + { + public static byte Deserialize(ref D deserializer) + where D : IDeserializer => deserializer.DeserializeString(new FieldNameVisitor()); + public string ExpectedTypeName => "string"; + + byte Serde.IDeserializeVisitor.VisitString(string s) => VisitUtf8Span(System.Text.Encoding.UTF8.GetBytes(s)); + public byte VisitUtf8Span(System.ReadOnlySpan s) + { + switch (s[0]) + { + case (byte)'m'when s.SequenceEqual("mask"u8): + return 1; + case (byte)'o'when s.SequenceEqual("offset"u8): + return 2; + default: + return 0; + } + } + } + + System.Collections.Specialized.BitVector32.Section Serde.IDeserializeVisitor.VisitDictionary(ref D d) + { + short _l_mask = default !; + short _l_offset = default !; + byte _r_assignedValid = 0b0; + while (d.TryGetNextKey(out byte key)) + { + switch (key) + { + case 1: + _l_mask = d.GetNextValue(); + _r_assignedValid |= ((byte)1) << 0; + break; + case 2: + _l_offset = d.GetNextValue(); + _r_assignedValid |= ((byte)1) << 1; + break; + } + } + + if (_r_assignedValid != 0b11) + { + throw new Serde.InvalidDeserializeValueException("Not all members were assigned"); + } + + var newType = new System.Collections.Specialized.BitVector32.Section() + { + Mask = _l_mask, + Offset = _l_offset, + }; + return newType; + } + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/SerializeTests.SerializeOnlyWrapper/SectionWrap.ISerialize.verified.cs b/test/Serde.Generation.Test/test_output/SerializeTests.SerializeOnlyWrapper/SectionWrap.ISerialize.verified.cs new file mode 100644 index 00000000..4958ea9f --- /dev/null +++ b/test/Serde.Generation.Test/test_output/SerializeTests.SerializeOnlyWrapper/SectionWrap.ISerialize.verified.cs @@ -0,0 +1,16 @@ +//HintName: SectionWrap.ISerialize.cs + +#nullable enable +using System; +using Serde; + +partial record struct SectionWrap : Serde.ISerialize +{ + void Serde.ISerialize.Serialize(ISerializer serializer) + { + var type = serializer.SerializeType("Section", 2); + type.SerializeField("mask"u8, new Int16Wrap(Value.Mask)); + type.SerializeField("offset"u8, new Int16Wrap(Value.Offset)); + type.End(); + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/SerializeTests.SerializeOnlyWrapper/SectionWrap.ISerializeWrap.verified.cs b/test/Serde.Generation.Test/test_output/SerializeTests.SerializeOnlyWrapper/SectionWrap.ISerializeWrap.verified.cs new file mode 100644 index 00000000..eb9906bf --- /dev/null +++ b/test/Serde.Generation.Test/test_output/SerializeTests.SerializeOnlyWrapper/SectionWrap.ISerializeWrap.verified.cs @@ -0,0 +1,6 @@ +//HintName: SectionWrap.ISerializeWrap.cs + +partial record struct SectionWrap(System.Collections.Specialized.BitVector32.Section Value) : ISerializeWrap +{ + SectionWrap ISerializeWrap.Wrap(System.Collections.Specialized.BitVector32.Section value) => new(value); +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.IDeserialize.verified.cs b/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.IDeserialize.verified.cs new file mode 100644 index 00000000..015a8605 --- /dev/null +++ b/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.IDeserialize.verified.cs @@ -0,0 +1,78 @@ +//HintName: SectionWrap.IDeserialize.cs + +#nullable enable +using System; +using Serde; + +partial record struct SectionWrap : Serde.IDeserialize +{ + static System.Collections.Specialized.BitVector32.Section Serde.IDeserialize.Deserialize(ref D deserializer) + { + var visitor = new SerdeVisitor(); + var fieldNames = new[] + { + "Mask", + "Offset" + }; + return deserializer.DeserializeType("Section", fieldNames, visitor); + } + + private sealed class SerdeVisitor : Serde.IDeserializeVisitor + { + public string ExpectedTypeName => "System.Collections.Specialized.BitVector32.Section"; + + private struct FieldNameVisitor : Serde.IDeserialize, Serde.IDeserializeVisitor + { + public static byte Deserialize(ref D deserializer) + where D : IDeserializer => deserializer.DeserializeString(new FieldNameVisitor()); + public string ExpectedTypeName => "string"; + + byte Serde.IDeserializeVisitor.VisitString(string s) => VisitUtf8Span(System.Text.Encoding.UTF8.GetBytes(s)); + public byte VisitUtf8Span(System.ReadOnlySpan s) + { + switch (s[0]) + { + case (byte)'m'when s.SequenceEqual("mask"u8): + return 1; + case (byte)'o'when s.SequenceEqual("offset"u8): + return 2; + default: + return 0; + } + } + } + + System.Collections.Specialized.BitVector32.Section Serde.IDeserializeVisitor.VisitDictionary(ref D d) + { + short _l_mask = default !; + short _l_offset = default !; + byte _r_assignedValid = 0b0; + while (d.TryGetNextKey(out byte key)) + { + switch (key) + { + case 1: + _l_mask = d.GetNextValue(); + _r_assignedValid |= ((byte)1) << 0; + break; + case 2: + _l_offset = d.GetNextValue(); + _r_assignedValid |= ((byte)1) << 1; + break; + } + } + + if (_r_assignedValid != 0b11) + { + throw new Serde.InvalidDeserializeValueException("Not all members were assigned"); + } + + var newType = new System.Collections.Specialized.BitVector32.Section() + { + Mask = _l_mask, + Offset = _l_offset, + }; + return newType; + } + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.ISerialize.verified.cs b/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.ISerialize.verified.cs new file mode 100644 index 00000000..4958ea9f --- /dev/null +++ b/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.ISerialize.verified.cs @@ -0,0 +1,16 @@ +//HintName: SectionWrap.ISerialize.cs + +#nullable enable +using System; +using Serde; + +partial record struct SectionWrap : Serde.ISerialize +{ + void Serde.ISerialize.Serialize(ISerializer serializer) + { + var type = serializer.SerializeType("Section", 2); + type.SerializeField("mask"u8, new Int16Wrap(Value.Mask)); + type.SerializeField("offset"u8, new Int16Wrap(Value.Offset)); + type.End(); + } +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.ISerializeWrap.verified.cs b/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.ISerializeWrap.verified.cs new file mode 100644 index 00000000..eb9906bf --- /dev/null +++ b/test/Serde.Generation.Test/test_output/WrapperTests.GenerateSerdeWrap/SectionWrap.ISerializeWrap.verified.cs @@ -0,0 +1,6 @@ +//HintName: SectionWrap.ISerializeWrap.cs + +partial record struct SectionWrap(System.Collections.Specialized.BitVector32.Section Value) : ISerializeWrap +{ + SectionWrap ISerializeWrap.Wrap(System.Collections.Specialized.BitVector32.Section value) => new(value); +} \ No newline at end of file diff --git a/test/Serde.Generation.Test/test_output/_diagnostics/WrapperTests.RecursiveType/target.verified.txt b/test/Serde.Generation.Test/test_output/_diagnostics/WrapperTests.RecursiveType/target.verified.txt deleted file mode 100644 index aa5ba8f6..00000000 --- a/test/Serde.Generation.Test/test_output/_diagnostics/WrapperTests.RecursiveType/target.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - Id: CS1026, - Title: , - Severity: Error, - WarningLevel: 0, - Location: : (8,46)-(8,47), - Description: , - HelpLink: https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS1026), - MessageFormat: ) expected, - Message: ) expected, - Category: Compiler, - CustomTags: [ - Compiler, - Telemetry, - NotConfigurable - ] - } -] \ No newline at end of file