diff --git a/src/TypeGenerationModel.cs b/src/TypeGenerationModel.cs index 65327d8..f2ba27c 100644 --- a/src/TypeGenerationModel.cs +++ b/src/TypeGenerationModel.cs @@ -15,6 +15,7 @@ internal TypeGenerationModel( string qualifiedSourceTypeName, string viewTypeName, bool isEffectivelyPublic, + bool emitShapeTag, bool isBlittableStruct, int blittableStructByteCount) { @@ -22,6 +23,7 @@ internal TypeGenerationModel( QualifiedSourceTypeName = qualifiedSourceTypeName; ViewTypeName = viewTypeName; IsEffectivelyPublic = isEffectivelyPublic; + EmitShapeTag = emitShapeTag; IsBlittableStruct = isBlittableStruct; BlittableStructByteCount = blittableStructByteCount; } @@ -34,6 +36,8 @@ internal TypeGenerationModel( internal bool IsEffectivelyPublic { get; } + internal bool EmitShapeTag { get; } + internal bool IsBlittableStruct { get; } internal int BlittableStructByteCount { get; } diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 3a63d4e..8590075 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -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( @@ -285,6 +289,7 @@ private static TypeGenerationModel CreateGenerationModel( qualifiedSourceTypeName, serializableType.Name + "View", IsEffectivelyPublic(serializableType), + ShouldEmitShapeTag(serializableType), isBlittableStruct, isBlittableStruct ? blittableStructByteCount : 0); @@ -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; + } + } + } + } + } + + return false; + } + private static FieldGenerationModel? CreatePropertyGenerationModel( IPropertySymbol serializableProperty, HashSet allSerializableTypes) @@ -913,9 +958,12 @@ private static void EmitView( sourceBuilder.AppendLine("/// "); sourceBuilder.AppendLine($"/// Provides a deserialized view of ."); sourceBuilder.AppendLine("/// "); - sourceBuilder.AppendLine("/// "); - sourceBuilder.AppendLine($"/// ShapeTag: {shapeTag}"); - sourceBuilder.AppendLine("/// "); + if (generationModel.EmitShapeTag) + { + sourceBuilder.AppendLine("/// "); + sourceBuilder.AppendLine($"/// ShapeTag: {shapeTag}"); + sourceBuilder.AppendLine("/// "); + } sourceBuilder.AppendLine($"{viewAccessibility} readonly struct {generationModel.ViewTypeName}"); sourceBuilder.OpenBlock(); sourceBuilder.AppendLine("/// "); @@ -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. diff --git a/tests/SerializationModels.cs b/tests/SerializationModels.cs index a253de6..f210139 100644 --- a/tests/SerializationModels.cs +++ b/tests/SerializationModels.cs @@ -22,7 +22,7 @@ public enum SignedState : short Positive = 5, } -[ZeroSerializer] +[ZeroSerializer(EmitShapeTag = true)] public sealed class PrimitiveRecord { public bool Boolean { get; init; } @@ -50,7 +50,7 @@ public sealed class PrimitiveRecord public double Double { get; init; } } -[ZeroSerializer] +[ZeroSerializer(EmitShapeTag = true)] public sealed class EnumClass { public ByteState ByteState { get; init; } @@ -73,7 +73,7 @@ public EnumStruct(ByteState byteState, SignedState signedState) } [StructLayout(LayoutKind.Sequential, Pack = 1)] -[ZeroSerializer] +[ZeroSerializer(EmitShapeTag = true)] public struct PackedRecord { public int Number { get; init; } @@ -81,7 +81,7 @@ public struct PackedRecord public SignedState State { get; init; } } -[ZeroSerializer] +[ZeroSerializer(EmitShapeTag = true)] public sealed class PackedContainer { public PackedRecord Value { get; init; } @@ -267,7 +267,7 @@ public struct EmptyBlittableStruct { } -[ZeroSerializer] +[ZeroSerializer(EmitShapeTag = true)] public sealed class SchemaSignatureTestsModel { // 1. blittable and non-blittable nested type combo diff --git a/tests/ShapeTagEmissionTests.cs b/tests/ShapeTagEmissionTests.cs new file mode 100644 index 0000000..02a4d38 --- /dev/null +++ b/tests/ShapeTagEmissionTests.cs @@ -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("/// ", 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("/// ", generatedView); + Assert.Contains("/// ShapeTag: v1/{int}", generatedView); + Assert.Contains("public const string ShapeTag = \"v1/{int}\";", generatedView); + Assert.Contains("public const uint ShapeHash = ", generatedView); + } + + [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(); + } +}