Skip to content
Merged
4 changes: 4 additions & 0 deletions src/TypeGenerationModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ internal TypeGenerationModel(
string qualifiedSourceTypeName,
string viewTypeName,
bool isEffectivelyPublic,
bool emitShapeTag,
bool isBlittableStruct,
int blittableStructByteCount)
{
Symbol = symbol;
QualifiedSourceTypeName = qualifiedSourceTypeName;
ViewTypeName = viewTypeName;
IsEffectivelyPublic = isEffectivelyPublic;
EmitShapeTag = emitShapeTag;
IsBlittableStruct = isBlittableStruct;
BlittableStructByteCount = blittableStructByteCount;
}
Expand All @@ -34,6 +36,8 @@ internal TypeGenerationModel(

internal bool IsEffectivelyPublic { get; }

internal bool EmitShapeTag { get; }

internal bool IsBlittableStruct { get; }

internal int BlittableStructByteCount { get; }
Expand Down
60 changes: 56 additions & 4 deletions src/ZeroSerializerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ public void Execute(GeneratorExecutionContext executionContext)
injectedAttributeSourceBuilder.AppendLine(" Inherited = false)]");
injectedAttributeSourceBuilder.AppendLine($"internal sealed class {SerializerAttributeName} : Attribute");
injectedAttributeSourceBuilder.OpenBlock();
injectedAttributeSourceBuilder.AppendLine("[Obsolete(\"Emitting string representation of the type will expose internal details in the resulting assembly. Consider using `ShapeHash` instead, or using `#if DEBUG` directive to prevent emitting on release build.\")]");
injectedAttributeSourceBuilder.AppendLine("public bool EmitShapeTag;");
injectedAttributeSourceBuilder.AppendLine();
injectedAttributeSourceBuilder.AppendLine($"public {SerializerAttributeName}() {{ }}");
injectedAttributeSourceBuilder.CloseBlock();
injectedAttributeSourceBuilder.CloseBlock();
executionContext.AddSource(
Expand Down Expand Up @@ -285,6 +289,7 @@ private static TypeGenerationModel CreateGenerationModel(
qualifiedSourceTypeName,
serializableType.Name + "View",
IsEffectivelyPublic(serializableType),
ShouldEmitShapeTag(serializableType),
isBlittableStruct,
isBlittableStruct ? blittableStructByteCount : 0);

Expand Down Expand Up @@ -365,6 +370,46 @@ private static TypeGenerationModel CreateGenerationModel(
return generationModel;
}

private static bool ShouldEmitShapeTag(INamedTypeSymbol serializableType)
{
foreach (SyntaxReference syntaxReference in serializableType.DeclaringSyntaxReferences)
{
if (syntaxReference.GetSyntax() is not TypeDeclarationSyntax declaration)
{
continue;
}

foreach (AttributeListSyntax attributeList in declaration.AttributeLists)
{
foreach (AttributeSyntax attribute in attributeList.Attributes)
{
string attributeName = attribute.Name.ToString();
if (!attributeName.EndsWith(SerializerName, StringComparison.Ordinal) &&
!attributeName.EndsWith(SerializerAttributeName, StringComparison.Ordinal))
{
continue;
}

if (attribute.ArgumentList == null)
{
continue;
}

foreach (AttributeArgumentSyntax argument in attribute.ArgumentList.Arguments)
{
if (argument.NameEquals?.Name.Identifier.ValueText == "EmitShapeTag" &&
argument.Expression.IsKind(SyntaxKind.TrueLiteralExpression))
{
return true;
}
}
Comment thread
sator-imaging marked this conversation as resolved.
}
}
}

return false;
}

private static FieldGenerationModel? CreatePropertyGenerationModel(
IPropertySymbol serializableProperty,
HashSet<INamedTypeSymbol> allSerializableTypes)
Expand Down Expand Up @@ -913,9 +958,12 @@ private static void EmitView(
sourceBuilder.AppendLine("/// <summary>");
sourceBuilder.AppendLine($"/// Provides a deserialized view of <see cref=\"{generationModel.QualifiedSourceTypeName}\"/>.");
sourceBuilder.AppendLine("/// </summary>");
sourceBuilder.AppendLine("/// <remarks>");
sourceBuilder.AppendLine($"/// ShapeTag: {shapeTag}");
sourceBuilder.AppendLine("/// </remarks>");
if (generationModel.EmitShapeTag)
{
sourceBuilder.AppendLine("/// <remarks>");
sourceBuilder.AppendLine($"/// ShapeTag: {shapeTag}");
sourceBuilder.AppendLine("/// </remarks>");
}
sourceBuilder.AppendLine($"{viewAccessibility} readonly struct {generationModel.ViewTypeName}");
sourceBuilder.OpenBlock();
sourceBuilder.AppendLine("/// <summary>");
Expand All @@ -925,7 +973,11 @@ private static void EmitView(
sourceBuilder.AppendLine($"public const int RequiredByteLength = {requiredByteLength};");
sourceBuilder.AppendLine($"public const bool IsBlittable = {generationModel.IsBlittableStruct.ToString().ToLowerInvariant()};");
uint shapeHash = XXHash32.HashToUInt32(shapeTag);
sourceBuilder.AppendLine($"public const string ShapeTag = \"{shapeTag}\";");
if (!generationModel.EmitShapeTag)
{
sourceBuilder.AppendLine("// To emit this, `set EmitShapeTag = true` on ZeroSerializerAttribute.");
}
sourceBuilder.AppendLine($"{(!generationModel.EmitShapeTag ? "// " : string.Empty)}public const string ShapeTag = \"{shapeTag}\";");
sourceBuilder.AppendLine($"public const uint ShapeHash = {shapeHash}U;");
sourceBuilder.AppendLine();
// ReadOnlyMemory keeps the borrowed byte array reusable by ordinary and nested View structs without allocation.
Expand Down
10 changes: 5 additions & 5 deletions tests/SerializationModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
Positive = 5,
}

[ZeroSerializer]
[ZeroSerializer(EmitShapeTag = true)]
public sealed class PrimitiveRecord
{
public bool Boolean { get; init; }
Expand Down Expand Up @@ -50,7 +50,7 @@
public double Double { get; init; }
}

[ZeroSerializer]
[ZeroSerializer(EmitShapeTag = true)]
public sealed class EnumClass
{
public ByteState ByteState { get; init; }
Expand All @@ -59,7 +59,7 @@
}

[ZeroSerializer]
public readonly struct EnumStruct

Check warning on line 62 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Debug)

Struct 'EnumStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization

Check warning on line 62 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Release)

Struct 'EnumStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization
{
public EnumStruct(ByteState byteState, SignedState signedState)
{
Expand All @@ -73,15 +73,15 @@
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
[ZeroSerializer]
[ZeroSerializer(EmitShapeTag = true)]
public struct PackedRecord
{
public int Number { get; init; }

public SignedState State { get; init; }
}

[ZeroSerializer]
[ZeroSerializer(EmitShapeTag = true)]
public sealed class PackedContainer
{
public PackedRecord Value { get; init; }
Expand Down Expand Up @@ -116,7 +116,7 @@
}

[ZeroSerializer]
public readonly struct SmallFixedStruct

Check warning on line 119 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Debug)

Struct 'SmallFixedStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization

Check warning on line 119 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Release)

Struct 'SmallFixedStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization
{
public SmallFixedStruct(int value)
{
Expand All @@ -127,7 +127,7 @@
}

[ZeroSerializer]
public readonly struct LargeFixedStruct

Check warning on line 130 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Debug)

Struct 'LargeFixedStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization

Check warning on line 130 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Release)

Struct 'LargeFixedStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization
{
public LargeFixedStruct(long value, ByteState state)
{
Expand Down Expand Up @@ -166,7 +166,7 @@
}

[ZeroSerializer]
public struct EmptyStruct

Check warning on line 169 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Debug)

Struct 'EmptyStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization

Check warning on line 169 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Release)

Struct 'EmptyStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization
{
}

Expand Down Expand Up @@ -267,7 +267,7 @@
{
}

[ZeroSerializer]
[ZeroSerializer(EmitShapeTag = true)]
public sealed class SchemaSignatureTestsModel
{
// 1. blittable and non-blittable nested type combo
Expand Down Expand Up @@ -331,14 +331,14 @@

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
[ZeroSerializer]
public struct SequentialPackOneWithCharSetStruct

Check warning on line 334 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Debug)

Struct 'SequentialPackOneWithCharSetStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization

Check warning on line 334 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Release)

Struct 'SequentialPackOneWithCharSetStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization
{
public int Value { get; init; }
}

[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 7)]
[ZeroSerializer]
public struct SequentialPackOneWithSizeStruct

Check warning on line 341 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Debug)

Struct 'SequentialPackOneWithSizeStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization

Check warning on line 341 in tests/SerializationModels.cs

View workflow job for this annotation

GitHub Actions / test (Release)

Struct 'SequentialPackOneWithSizeStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization
{
public int Value { get; init; }
}
74 changes: 74 additions & 0 deletions tests/ShapeTagEmissionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed under the Apache-2.0 License
// https://github.com/sator-imaging/ZeroSerializer

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Linq;
using Xunit;
using ZeroSerializer.Generator;

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

namespace ZeroSerializer.Tests;

public class ShapeTagEmissionTests
{
[Fact]
public void ShapeTagIsCommentedOutByDefault()
{
string generatedView = GenerateView("[ZeroSerializer.ZeroSerializer] public class Record { public int Value { get; set; } }");

Assert.DoesNotContain("/// <remarks>", generatedView);
Assert.DoesNotContain(" public const string ShapeTag", generatedView);
Assert.Contains("// To emit this, `set EmitShapeTag = true` on ZeroSerializerAttribute.", generatedView);
Assert.Contains("// public const string ShapeTag = \"v1/{int}\";", generatedView);
Assert.Contains("public const uint ShapeHash = ", generatedView);
}

[Fact]
public void ShapeTagAndRemarksAreEmittedWhenRequested()
{
string generatedView = GenerateView("[ZeroSerializer.ZeroSerializer(EmitShapeTag = true)] public class Record { public int Value { get; set; } }");

Assert.DoesNotContain("// public const string ShapeTag", generatedView);
Assert.Contains("/// <remarks>", generatedView);
Assert.Contains("/// ShapeTag: v1/{int}", generatedView);
Assert.Contains("public const string ShapeTag = \"v1/{int}\";", generatedView);
Assert.Contains("public const uint ShapeHash = ", generatedView);
Comment thread
sator-imaging marked this conversation as resolved.
}
Comment thread
sator-imaging marked this conversation as resolved.

[Fact]
public void InjectedAttributeExposesObsoleteEmitShapeTagField()
{
GeneratorDriverRunResult result = RunGenerator("public class Record { }");
string generatedAttribute = result.Results[0].GeneratedSources
.Single(source => source.HintName == "- ZeroSerializerAttribute.g.cs")
.SourceText
.ToString();

Assert.Contains("public bool EmitShapeTag;", generatedAttribute);
Assert.Contains("[Obsolete(\"Emitting string representation of the type will expose internal details", generatedAttribute);
Assert.Contains("public ZeroSerializerAttribute() { }", generatedAttribute);
}

private static string GenerateView(string source)
{
GeneratorDriverRunResult result = RunGenerator(source);
return result.Results[0].GeneratedSources
.Single(generatedSource => generatedSource.HintName == "Record.ZeroSerializer.g.cs")
.SourceText
.ToString();
}

private static GeneratorDriverRunResult RunGenerator(string source)
{
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source);
CSharpCompilation compilation = CSharpCompilation.Create(
"ShapeTagEmissionTests",
new[] { syntaxTree },
new[] { MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location) });
GeneratorDriver driver = CSharpGeneratorDriver.Create(new ZeroSerializerGenerator());
return driver.RunGenerators(compilation).GetRunResult();
}
}
Loading