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

Allow generating wrappers using Generate* attributes #139

Merged
merged 1 commit into from
Sep 5, 2023
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
90 changes: 76 additions & 14 deletions src/generator/Generator.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITypeSymbol> 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,
Expand Down Expand Up @@ -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<MemberDeclarationSyntax>(new[] { newType }));
tree = tree.NormalizeWhitespace(eol: Environment.NewLine);

context.AddSource($"{fullWrapperName}.ISerializeWrap", Environment.NewLine + tree.ToFullString());
}

private static void GenerateImpl(
SerdeUsage usage,
TypeDeclContext typeDeclContext,
Expand Down
2 changes: 2 additions & 0 deletions src/generator/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ static GenerationOutput GenerateForCtx(
if (usage.HasFlag(SerdeUsage.Serialize))
{
SerdeImplRoslynGenerator.GenerateImpl(
attrCtx.Attributes.Single(),
SerdeUsage.Serialize,
(BaseTypeDeclarationSyntax)attrCtx.TargetNode,
attrCtx.SemanticModel,
Expand All @@ -92,6 +93,7 @@ static GenerationOutput GenerateForCtx(
if (usage.HasFlag(SerdeUsage.Deserialize))
{
SerdeImplRoslynGenerator.GenerateImpl(
attrCtx.Attributes.Single(),
SerdeUsage.Deserialize,
(BaseTypeDeclarationSyntax)attrCtx.TargetNode,
attrCtx.SemanticModel,
Expand Down
45 changes: 42 additions & 3 deletions src/serde/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ namespace Serde;
internal
#endif
sealed class GenerateSerialize : Attribute
{ }
{
/// <summary>
/// If non-null, the name of the member used to implement serialization. This is used to
/// implement serialization for a wrapper type.
/// </summary>
public string? Through { get; init; }
}

/// <summary>
/// Generates an implementation of <see cref="Serde.IDeserialize" />.
Expand All @@ -31,7 +37,14 @@ sealed class GenerateSerialize : Attribute
internal
#endif
sealed class GenerateDeserialize : Attribute
{ }
{
/// <summary>
/// If non-null, the name of the member used to implement deserialization. This is used to
/// implement deserialization for a wrapper type.
/// </summary>
public string? Through { get; init; }

}

/// <summary>
/// Generates an implementation of both <see cref="Serde.ISerialize" /> and <see cref="Serde.IDeserialize" />.
Expand All @@ -44,7 +57,13 @@ sealed class GenerateDeserialize : Attribute
internal
#endif
sealed class GenerateSerde : Attribute
{ }
{
/// <summary>
/// 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.
/// </summary>
public string? Through { get; init; }
}

/// <summary>
/// Generates the equivalent of <see cref="GenerateSerde" />, but delegated to a member of the name
Expand Down Expand Up @@ -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

/// <summary>
Expand Down
14 changes: 0 additions & 14 deletions src/serde/Wrappers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, TWrap> where TWrap : ISerialize
{
abstract static TWrap Create(T t); // Should be abstract static
Expand Down
14 changes: 14 additions & 0 deletions test/Serde.Generation.Test/DeserializeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
14 changes: 14 additions & 0 deletions test/Serde.Generation.Test/SerializeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
28 changes: 15 additions & 13 deletions test/Serde.Generation.Test/WrapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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() });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//HintName: SectionWrap.IDeserialize.cs

#nullable enable
using System;
using Serde;

partial record struct SectionWrap : Serde.IDeserialize<System.Collections.Specialized.BitVector32.Section>
{
static System.Collections.Specialized.BitVector32.Section Serde.IDeserialize<System.Collections.Specialized.BitVector32.Section>.Deserialize<D>(ref D deserializer)
{
var visitor = new SerdeVisitor();
var fieldNames = new[]
{
"Mask",
"Offset"
};
return deserializer.DeserializeType<System.Collections.Specialized.BitVector32.Section, SerdeVisitor>("Section", fieldNames, visitor);
}

private sealed class SerdeVisitor : Serde.IDeserializeVisitor<System.Collections.Specialized.BitVector32.Section>
{
public string ExpectedTypeName => "System.Collections.Specialized.BitVector32.Section";

private struct FieldNameVisitor : Serde.IDeserialize<byte>, Serde.IDeserializeVisitor<byte>
{
public static byte Deserialize<D>(ref D deserializer)
where D : IDeserializer => deserializer.DeserializeString<byte, FieldNameVisitor>(new FieldNameVisitor());
public string ExpectedTypeName => "string";

byte Serde.IDeserializeVisitor<byte>.VisitString(string s) => VisitUtf8Span(System.Text.Encoding.UTF8.GetBytes(s));
public byte VisitUtf8Span(System.ReadOnlySpan<byte> 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<System.Collections.Specialized.BitVector32.Section>.VisitDictionary<D>(ref D d)
{
short _l_mask = default !;
short _l_offset = default !;
byte _r_assignedValid = 0b0;
while (d.TryGetNextKey<byte, FieldNameVisitor>(out byte key))
{
switch (key)
{
case 1:
_l_mask = d.GetNextValue<short, Int16Wrap>();
_r_assignedValid |= ((byte)1) << 0;
break;
case 2:
_l_offset = d.GetNextValue<short, Int16Wrap>();
_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;
}
}
}
Loading
Loading