diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 8f60d49..1cef116 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -39,4 +39,6 @@ jobs: - if: failure() name: TEST FAILURE LOG - run: cat test-output.txt + run: | + cat test-output.txt + exit 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2ef75d..73cac0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,9 @@ jobs: - if: failure() name: TEST FAILURE LOG - run: cat test-output.txt + run: | + cat test-output.txt + exit 1 # Place after test to make step summary order better (test -> code preview) diff --git a/README.ja.md b/README.ja.md index 99c031e..e2ac899 100644 --- a/README.ja.md +++ b/README.ja.md @@ -73,7 +73,7 @@ Blittable Struct は全ケースで offset table を持たない raw payload と `[ZeroSerializer]` が付いていても Blittable Struct はネスト View 化しません。親が非 Blittable 型なら親の field offset table は存在しますが、Blittable Struct payload 内部には table を生成しません。 -全フィールド型が Blittable 対応済みで、自身の `StructLayout(LayoutKind.Sequential, Pack = 1)` だけが不足する `[ZeroSerializer]` struct には、型名 identifier へ `ZEROS006` warning を出します。その struct が有効な `[ZeroSerializer]` ネスト型として使われている場合は、raw payload 化による性能改善を案内する `ZEROS007` info もネスト型の identifier へ1回だけ出します。`[ZeroSerializer]` がない間は親型に `ZEROS003` error が発生し、`ZEROS007` は生成エラーが解消されるまで出ません。 +全フィールド型が Blittable 対応済みで、自身の `StructLayout(LayoutKind.Sequential, Pack = 1)` だけが不足する `[ZeroSerializer]` struct には、型名 identifier へ `ZEROS006` warning を出します。また、`bool` 型のプロパティーに対しては、flags enum (byte) の使用によるペイロードサイズの削減を促す `ZEROS007` info をプロパティーの型位置に出します。 ネストした class/struct にも `[ZeroSerializer]` が必要です。未修飾の型は `ZEROS003` error になります。 diff --git a/src/ZeroSerializerGenerator.cs b/src/ZeroSerializerGenerator.cs index 52628b7..8147b99 100644 --- a/src/ZeroSerializerGenerator.cs +++ b/src/ZeroSerializerGenerator.cs @@ -76,10 +76,10 @@ public sealed class ZeroSerializerGenerator : ISourceGenerator DiagnosticSeverity.Warning, isEnabledByDefault: true); - private static readonly DiagnosticDescriptor BlittableCompatibleNestedStruct = new( + private static readonly DiagnosticDescriptor BoolPropertyTypeUseFlagsEnum = new( "ZEROS007", - "Nested struct can use faster Blittable serialization", - "Nested struct '{0}' can use StructLayout(LayoutKind.Sequential, Pack = 1) to improve serialization performance with raw payload serialization", + "Use flags enum to reduce payload size", + "Property '{0}' uses bool type; consider using a flags enum (byte) to reduce payload size by combining up to 8 booleans into one byte", SerializerName, DiagnosticSeverity.Info, isEnabledByDefault: true); @@ -249,11 +249,6 @@ private static void ExecuteCore( return; } - ReportBlittableCompatibleNestedStructDiagnostics( - executionContext, - validModels, - generationModels); - var modelLookup = new Dictionary(SymbolEqualityComparer.Default); // Each type owns one generated file and contributes one method to its namespace-local partial extension class. foreach (TypeGenerationModel validModel in validModels) @@ -351,13 +346,21 @@ private static TypeGenerationModel CreateGenerationModel( continue; } + if (serializableProperty.Type.SpecialType == SpecialType.System_Boolean) + { + executionContext.ReportDiagnostic(Diagnostic.Create( + BoolPropertyTypeUseFlagsEnum, + GetPropertyTypeLocation(serializableProperty), + serializableProperty.Name)); + } + FieldGenerationModel? propertyModel = CreatePropertyGenerationModel(serializableProperty, allSerializableTypes); if (propertyModel is null) { generationModel.IsValid = false; executionContext.ReportDiagnostic(Diagnostic.Create( UnsupportedSerializableField, - serializableProperty.Locations.IsDefaultOrEmpty ? null : serializableProperty.Locations[0], + GetPropertyTypeLocation(serializableProperty), serializableProperty.Name, serializableProperty.Type.ToDisplayString())); continue; @@ -368,7 +371,7 @@ private static TypeGenerationModel CreateGenerationModel( generationModel.IsValid = false; executionContext.ReportDiagnostic(Diagnostic.Create( InvalidBlittableArrayElement, - serializableProperty.Locations.IsDefaultOrEmpty ? null : serializableProperty.Locations[0], + GetPropertyTypeLocation(serializableProperty), serializableProperty.Name)); continue; } @@ -747,47 +750,30 @@ private static bool HasBlittableCompatibleFieldShape(INamedTypeSymbol candidateS return true; } - private static void ReportBlittableCompatibleNestedStructDiagnostics( - GeneratorExecutionContext executionContext, - IReadOnlyList validGenerationModels, - IReadOnlyDictionary generationModels) + private static Location? GetTypeIdentifierLocation(INamedTypeSymbol declaredType) { - var reportedNestedStructs = new HashSet(SymbolEqualityComparer.Default); - foreach (TypeGenerationModel containingGenerationModel in validGenerationModels) + foreach (SyntaxReference declaringSyntaxReference in declaredType.DeclaringSyntaxReferences) { - foreach (FieldGenerationModel nestedField in containingGenerationModel.Fields) + if (declaringSyntaxReference.GetSyntax() is TypeDeclarationSyntax typeDeclaration) { - if (nestedField.Kind != FieldSerializationKind.Nested - || nestedField.NestedSerializableType is not INamedTypeSymbol nestedSerializableStruct - || nestedSerializableStruct.TypeKind != TypeKind.Struct - || !generationModels.TryGetValue(nestedSerializableStruct, out TypeGenerationModel? nestedGenerationModel) - || nestedGenerationModel.IsBlittableStruct - || !HasBlittableCompatibleFieldShape(nestedSerializableStruct) - || !reportedNestedStructs.Add(nestedSerializableStruct)) - { - continue; - } - - // Report only after dependency validation so this performance advice never replaces ZEROS003 or another generation error. - executionContext.ReportDiagnostic(Diagnostic.Create( - BlittableCompatibleNestedStruct, - GetTypeIdentifierLocation(nestedSerializableStruct), - nestedSerializableStruct.ToDisplayString())); + return typeDeclaration.Identifier.GetLocation(); } } + + return declaredType.Locations.IsDefaultOrEmpty ? null : declaredType.Locations[0]; } - private static Location? GetTypeIdentifierLocation(INamedTypeSymbol declaredType) + private static Location? GetPropertyTypeLocation(IPropertySymbol propertySymbol) { - foreach (SyntaxReference declaringSyntaxReference in declaredType.DeclaringSyntaxReferences) + foreach (SyntaxReference declaringSyntaxReference in propertySymbol.DeclaringSyntaxReferences) { - if (declaringSyntaxReference.GetSyntax() is TypeDeclarationSyntax typeDeclaration) + if (declaringSyntaxReference.GetSyntax() is PropertyDeclarationSyntax propertyDeclaration) { - return typeDeclaration.Identifier.GetLocation(); + return propertyDeclaration.Type.GetLocation(); } } - return declaredType.Locations.IsDefaultOrEmpty ? null : declaredType.Locations[0]; + return propertySymbol.Locations.IsDefaultOrEmpty ? null : propertySymbol.Locations[0]; } private static bool TryGetPrimitiveByteCount(ITypeSymbol candidateType, out int byteCount) diff --git a/tests/Diagnostics/DiagnosticTests.cs b/tests/Diagnostics/DiagnosticTests.cs index 54f16fd..57df297 100644 --- a/tests/Diagnostics/DiagnosticTests.cs +++ b/tests/Diagnostics/DiagnosticTests.cs @@ -250,8 +250,8 @@ public struct PackedValue [ZeroSerializer] public class Container { - public PackedValue {|#0:Value|} { get; set; } - public PackedValue? {|#1:OptionalValue|} { get; set; } + public {|#0:PackedValue|} Value { get; set; } + public {|#1:PackedValue?|} OptionalValue { get; set; } } "; @@ -280,7 +280,7 @@ public class UnmarkedClass [ZeroSerializer] public class Container { - public UnmarkedClass {|#0:Value|} { get; set; } + public {|#0:UnmarkedClass|} Value { get; set; } } "; @@ -335,7 +335,7 @@ public struct PackedValue [ZeroSerializer] public class Container { - public PackedValue[] {|#0:Values|} { get; set; } + public {|#0:PackedValue[]|} Values { get; set; } } "; @@ -389,7 +389,7 @@ public struct PackedValue [ZeroSerializer] public class InvalidType { - public PackedValue {|#0:Value|} { get; set; } + public {|#0:PackedValue|} Value { get; set; } } [ZeroSerializer] @@ -484,53 +484,36 @@ await CSharpSourceGeneratorVerifier.VerifySourceGenerat } [Fact] - public async Task ZEROS007_Violation_BlittableCompatibleNestedStruct() + public async Task ZEROS007_Violation_BoolProperty() { string source = @" using ZeroSerializer; [ZeroSerializer] -public struct {|#0:NestedStruct|} +public class BoolPropertyContainer { - public int Value { get; set; } -} - -[ZeroSerializer] -public class ParentClass -{ - public NestedStruct Child { get; set; } + public {|#0:bool|} IsActive { get; set; } } "; await CSharpSourceGeneratorVerifier.VerifySourceGeneratorAsync( source, - new DiagnosticResult("ZEROS006", DiagnosticSeverity.Warning) - .WithLocation(0) - .WithMessage("Struct 'NestedStruct' has a Blittable-compatible field shape; use StructLayout(LayoutKind.Sequential, Pack = 1) to enable raw payload serialization"), new DiagnosticResult("ZEROS007", DiagnosticSeverity.Info) .WithLocation(0) - .WithMessage("Nested struct 'NestedStruct' can use StructLayout(LayoutKind.Sequential, Pack = 1) to improve serialization performance with raw payload serialization") + .WithMessage("Property 'IsActive' uses bool type; consider using a flags enum (byte) to reduce payload size by combining up to 8 booleans into one byte") ); } [Fact] - public async Task ZEROS007_Compliant_BlittableNestedStructWithLayout() + public async Task ZEROS007_Compliant_NonBoolProperty() { string source = @" -using System.Runtime.InteropServices; using ZeroSerializer; [ZeroSerializer] -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public struct NestedStructWithLayout -{ - public int Value { get; set; } -} - -[ZeroSerializer] -public class ParentClassWithBlittable +public class BytePropertyContainer { - public NestedStructWithLayout Child { get; set; } + public byte Flags { get; set; } } ";