From ce3bbfe4f13aea55b140c616a1af2333255d07dc Mon Sep 17 00:00:00 2001 From: RejectKid Date: Wed, 22 Jul 2026 15:38:40 -0400 Subject: [PATCH] Harden source generation and diagnostics --- .../Analysers/BitFieldAnalyser.cs | 11 +- .../Analysers/BitObjectAnalyser.cs | 376 ++++++++++++++++-- .../AnalyzerReleases.Unshipped.md | 14 +- BitsKit.Generator/BitObjectGenerator.cs | 47 +-- BitsKit.Generator/DiagnosticDescriptors.cs | 35 ++ BitsKit.Generator/Extensions.cs | 37 ++ BitsKit.Generator/Models/BackingFieldModel.cs | 53 ++- BitsKit.Generator/Models/BitFieldModel.cs | 194 ++++----- BitsKit.Generator/Models/BooleanFieldModel.cs | 4 +- BitsKit.Generator/Models/EnumFieldModel.cs | 14 +- .../Models/IntegralFieldModel.cs | 20 +- BitsKit.Generator/StringConstants.cs | 16 +- BitsKit.Generator/SymbolFormatting.cs | 59 +++ BitsKit.Generator/TypeSymbolProcessor.cs | 195 +++++++-- BitsKit.Tests/AnalyzerTests.cs | 112 +++++- BitsKit.Tests/GeneratorTests.Models.cs | 16 + BitsKit.Tests/GeneratorTests.cs | 174 +++++++- BitsKit.Tests/Helpers.cs | 17 + BitsKit.Tests/UnsafeAccessTests.cs | 5 + CHANGELOG.md | 8 + README.md | 25 ++ 21 files changed, 1192 insertions(+), 240 deletions(-) create mode 100644 BitsKit.Generator/SymbolFormatting.cs diff --git a/BitsKit.Generator/Analysers/BitFieldAnalyser.cs b/BitsKit.Generator/Analysers/BitFieldAnalyser.cs index 3a243d7..0b78e4b 100644 --- a/BitsKit.Generator/Analysers/BitFieldAnalyser.cs +++ b/BitsKit.Generator/Analysers/BitFieldAnalyser.cs @@ -100,11 +100,8 @@ private static void AnalyzeField(SymbolAnalysisContext context, ITypeSymbol bitF } private static bool RequiresExplicitFieldType(IFieldSymbol fieldSymbol) => - fieldSymbol.Type.ToDisplayString() is - "System.Memory" or - "System.ReadOnlyMemory" or - "System.Span" or - "System.ReadOnlySpan" or - "byte[]" or - "byte*"; + BackingFieldModel.Classify(fieldSymbol) is + BackingFieldType.Memory or + BackingFieldType.Span or + BackingFieldType.Pointer; } diff --git a/BitsKit.Generator/Analysers/BitObjectAnalyser.cs b/BitsKit.Generator/Analysers/BitObjectAnalyser.cs index 9eb3cb2..4b469b8 100644 --- a/BitsKit.Generator/Analysers/BitObjectAnalyser.cs +++ b/BitsKit.Generator/Analysers/BitObjectAnalyser.cs @@ -1,59 +1,351 @@ -using System.Collections.Immutable; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using BitsKit.Generator.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace BitsKit.Generator.Analysers +namespace BitsKit.Generator.Analysers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class BitObjectAnalyser : DiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class BitObjectAnalyser : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + [ + DiagnosticDescriptors.MustBePartial, + DiagnosticDescriptors.NestedNotAllowed, + DiagnosticDescriptors.InvalidBitObjectOption, + DiagnosticDescriptors.UnsupportedBackingField, + DiagnosticDescriptors.RawPointerRequiresUnsafe, + DiagnosticDescriptors.InvalidFieldName, + DiagnosticDescriptors.GeneratedMemberConflict, + DiagnosticDescriptors.InvalidFieldWidth, + DiagnosticDescriptors.LayoutExceedsBacking, + DiagnosticDescriptors.InvalidModifiers + ]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(startContext => + { + INamedTypeSymbol? bitObjectAttribute = startContext.Compilation.GetTypeByMetadataName( + StringConstants.BitObjectAttributeFullName); + INamedTypeSymbol? bitFieldAttribute = startContext.Compilation.GetTypeByMetadataName( + StringConstants.BitFieldAttributeFullName); + if (bitObjectAttribute is null || bitFieldAttribute is null) + return; + + startContext.RegisterSymbolAction( + symbolContext => AnalyzeType( + symbolContext, + bitObjectAttribute, + bitFieldAttribute), + SymbolKind.NamedType); + }); + } + + private static void AnalyzeType( + SymbolAnalysisContext context, + INamedTypeSymbol bitObjectAttribute, + INamedTypeSymbol bitFieldAttribute) + { + var type = (INamedTypeSymbol)context.Symbol; + if (!type.TryGetAttributeWithType(bitObjectAttribute, out AttributeData? bitObjectData)) + return; + + TypeDeclarationSyntax? declaration = type.DeclaringSyntaxReferences + .Select(reference => reference.GetSyntax(context.CancellationToken)) + .OfType() + .FirstOrDefault(); + if (declaration is null) + return; + + if (!declaration.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.MustBePartial, + declaration.GetLocation(), + type.Name)); + } + + if (type.ContainingType is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.NestedNotAllowed, + declaration.GetLocation(), + type.Name)); + } + + AnalyzeOptions(context, type, bitObjectData, declaration.GetLocation()); + AnalyzeFields(context, type, bitObjectData, bitFieldAttribute); + } + + private static void AnalyzeOptions( + SymbolAnalysisContext context, + INamedTypeSymbol type, + AttributeData attribute, + Location location) { - public override ImmutableArray SupportedDiagnostics { get; } = [ - DiagnosticDescriptors.MustBePartial, - DiagnosticDescriptors.NestedNotAllowed - ]; + if (!TypeSymbolProcessor.TryGetBitOrder(attribute, out _)) + { + object? value = attribute.ConstructorArguments.Length == 0 + ? null + : attribute.ConstructorArguments[0].Value; + ReportInvalidOption(context, type, location, "BitOrder", value); + } + + if (!TypeSymbolProcessor.TryGetAccessMode(attribute, out _)) + { + object? value = attribute.NamedArguments + .FirstOrDefault(argument => argument.Key == "AccessMode") + .Value.Value; + ReportInvalidOption(context, type, location, "AccessMode", value); + } + } + + private static void ReportInvalidOption( + SymbolAnalysisContext context, + INamedTypeSymbol type, + Location location, + string option, + object? value) => context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidBitObjectOption, + location, + type.Name, + option, + value ?? "null")); + + private static void AnalyzeFields( + SymbolAnalysisContext context, + INamedTypeSymbol type, + AttributeData bitObjectAttribute, + INamedTypeSymbol bitFieldAttribute) + { + TypeSymbolProcessor.TryGetAccessMode(bitObjectAttribute, out BitObjectAccessMode accessMode); + var generatedNames = new HashSet(); + var existingNames = new HashSet(type.GetMembers() + .Where(member => !IsBitsKitGeneratedMember(member)) + .Select(member => member.Name)); + existingNames.Add(type.Name); + int inlineArrayLength = GetInlineArrayLength(type); + + foreach (IFieldSymbol field in type.GetMembers().OfType()) + { + if (!field.TryGetAttributesWithBaseType(bitFieldAttribute, out List? attributes)) + continue; + + BackingFieldType backingType = BackingFieldModel.Classify(field); + if (backingType == BackingFieldType.Integral && inlineArrayLength > 0) + backingType = BackingFieldType.InlineArray; + + Location fieldLocation = field.Locations.FirstOrDefault() ?? Location.None; + if (backingType == BackingFieldType.Invalid) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UnsupportedBackingField, + fieldLocation, + type.Name, + field.Name, + field.Type.ToDisplayString())); + continue; + } + + var backing = new BackingFieldModel(field, backingType); + if (backing.IsRawPointer && accessMode != BitObjectAccessMode.Unsafe) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.RawPointerRequiresUnsafe, + fieldLocation, + type.Name, + field.Name)); + } - public override void Initialize(AnalysisContext context) + AnalyzeFieldAttributes( + context, + type, + field, + backing, + backingType, + attributes, + inlineArrayLength, + existingNames, + generatedNames, + fieldLocation); + } + } + + private static void AnalyzeFieldAttributes( + SymbolAnalysisContext context, + INamedTypeSymbol type, + IFieldSymbol field, + BackingFieldModel backing, + BackingFieldType backingType, + List attributes, + int inlineArrayLength, + HashSet existingNames, + HashSet generatedNames, + Location fieldLocation) + { + int offset = 0; + foreach (AttributeData attribute in attributes) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + BitFieldModel? model = TypeSymbolProcessor.CreateBitFieldFromAttribute(attribute, null); + if (model is null) + continue; - context.RegisterCompilationStartAction(context => + Location location = attribute.ApplicationSyntaxReference? + .GetSyntax(context.CancellationToken).GetLocation() ?? fieldLocation; + bool isPadding = model.FieldType == BitFieldType.Padding; + BitFieldType? declaredFieldType = model.FieldType; + BitFieldType? effectiveFieldType = model.FieldType; + if (backingType == BackingFieldType.Integral) + effectiveFieldType = field.Type.SpecialType.ToBitFieldType(); + else if (backingType == BackingFieldType.InlineArray) + effectiveFieldType ??= field.Type.SpecialType.ToBitFieldType(); + + if (!isPadding) { - var bitObjectAttribute = context.Compilation.GetTypeByMetadataName(StringConstants.BitObjectAttributeFullName); - if (bitObjectAttribute == null) return; + AnalyzeName(context, type, model, location, existingNames, generatedNames); + AnalyzeModifiers(context, type, field, backing, model, location); - context.RegisterSymbolAction(context => + int maximumWidth = model is EnumFieldModel + ? declaredFieldType?.GetBitWidth() ?? 0 + : effectiveFieldType?.GetBitWidth() ?? 0; + if (model.BitCount <= 0 || maximumWidth == 0 || model.BitCount > maximumWidth) { - var type = (INamedTypeSymbol)context.Symbol; - - if (!type.TryGetAttributeWithType(bitObjectAttribute, out _)) - { - return; - } - - if (type.DeclaringSyntaxReferences[0].GetSyntax() is not TypeDeclarationSyntax typeDeclarationSyntax) - { - return; - } - - if (!typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - { - context.ReportDiagnostic( - Diagnostic.Create(DiagnosticDescriptors.MustBePartial, typeDeclarationSyntax.GetLocation(), type.Name) - ); - } - - if (type.ContainingType != null) - { - context.ReportDiagnostic( - Diagnostic.Create(DiagnosticDescriptors.NestedNotAllowed, typeDeclarationSyntax.GetLocation(), type.Name) - ); - } - }, SymbolKind.NamedType); - }); + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidFieldWidth, + location, + type.Name, + model.Name, + model.BitCount, + model.ReturnType ?? effectiveFieldType?.ToString() ?? "unknown type", + maximumWidth)); + } + } + + int capacity = GetCapacity(backingType, backing, field, inlineArrayLength); + long end = (long)offset + model.BitCount; + if (capacity != int.MaxValue && end > capacity) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.LayoutExceedsBacking, + location, + type.Name, + isPadding ? "" : model.Name, + end, + capacity, + field.Name)); + } + + offset += model.BitCount; + } + } + + private static void AnalyzeName( + SymbolAnalysisContext context, + INamedTypeSymbol type, + BitFieldModel model, + Location location, + HashSet existingNames, + HashSet generatedNames) + { + if (!SymbolFormatting.IsValidGeneratedName(model.Name)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidFieldName, + location, + type.Name, + model.Name ?? "")); + return; + } + + string name = model.Name.StartsWith("@") ? model.Name.Substring(1) : model.Name; + if (existingNames.Contains(name) || !generatedNames.Add(name)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedMemberConflict, + location, + type.Name, + name)); } } + + private static void AnalyzeModifiers( + SymbolAnalysisContext context, + INamedTypeSymbol type, + IFieldSymbol field, + BackingFieldModel backing, + BitFieldModel model, + Location location) + { + const BitFieldModifiers knownModifiers = + BitFieldModifiers.AccessorMask | + BitFieldModifiers.ReadOnly | + BitFieldModifiers.InitOnly | + BitFieldModifiers.Required; + string? reason = null; + + if ((model.Modifiers & ~knownModifiers) != 0) + reason = "unknown modifier bits"; + else if (type.TypeKind == TypeKind.Struct && + (model.Modifiers & BitFieldModifiers.AccessorMask) is + BitFieldModifiers.Protected or + BitFieldModifiers.ProtectedInternal or + BitFieldModifiers.PrivateProtected) + reason = "struct members cannot be protected"; + else if (model.Modifiers.HasFlag(BitFieldModifiers.Required) && + (model.Modifiers.HasFlag(BitFieldModifiers.ReadOnly) || + field.IsReadOnly || backing.IsReadOnlyStorage)) + reason = "required fields must have a writable setter"; + + if (reason is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidModifiers, + location, + type.Name, + model.Name, + reason)); + } + } + + private static int GetCapacity( + BackingFieldType backingType, + BackingFieldModel backing, + IFieldSymbol field, + int inlineArrayLength) => backingType switch + { + BackingFieldType.Integral => field.Type.SpecialType.GetBitWidth(), + BackingFieldType.Pointer when backing.FixedSize > 0 => backing.FixedSize * 8, + BackingFieldType.InlineArray => inlineArrayLength * field.Type.SpecialType.GetBitWidth(), + _ => int.MaxValue + }; + + private static int GetInlineArrayLength(INamedTypeSymbol type) => + (int?)type.GetAttributes() + .FirstOrDefault(attribute => + attribute.AttributeClass?.ToDisplayString() == StringConstants.InlineArrayAttributeFullName)? + .ConstructorArguments[0].Value ?? 0; + + private static bool IsBitsKitGeneratedMember(ISymbol member) + { + foreach (Location location in member.Locations) + { + if (location.SourceTree?.FilePath is not string path) + continue; + + int separator = System.Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\')); + string fileName = path.Substring(separator + 1); + if (fileName.StartsWith("BitsKit.") && fileName.EndsWith(".g.cs")) + return true; + } + + return false; + } } diff --git a/BitsKit.Generator/AnalyzerReleases.Unshipped.md b/BitsKit.Generator/AnalyzerReleases.Unshipped.md index 5f28270..f351a23 100644 --- a/BitsKit.Generator/AnalyzerReleases.Unshipped.md +++ b/BitsKit.Generator/AnalyzerReleases.Unshipped.md @@ -1 +1,13 @@ - \ No newline at end of file + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +BITSKIT007 | BitsKit.Generator | Error | Invalid bit object option +BITSKIT008 | BitsKit.Generator | Error | Unsupported bit-field backing type +BITSKIT009 | BitsKit.Generator | Error | Raw pointer backing requires unsafe access +BITSKIT010 | BitsKit.Generator | Error | Invalid generated bit-field name +BITSKIT011 | BitsKit.Generator | Error | Generated bit-field member conflicts with another member +BITSKIT012 | BitsKit.Generator | Error | Invalid bit-field width +BITSKIT013 | BitsKit.Generator | Error | Bit-field layout exceeds its backing storage +BITSKIT014 | BitsKit.Generator | Error | Invalid bit-field modifiers diff --git a/BitsKit.Generator/BitObjectGenerator.cs b/BitsKit.Generator/BitObjectGenerator.cs index f3827ee..6becfc0 100644 --- a/BitsKit.Generator/BitObjectGenerator.cs +++ b/BitsKit.Generator/BitObjectGenerator.cs @@ -22,8 +22,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(x => x is not null) .WithTrackingName("Main")!; - var model = typeDeclarations.Collect(); - context.RegisterSourceOutput(model, GenerateSourceCode); + context.RegisterSourceOutput(typeDeclarations, GenerateSourceCode); } private static TypeSymbolProcessor? ProcessSyntaxNode(GeneratorAttributeSyntaxContext syntaxContext, CancellationToken token) @@ -36,47 +35,33 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (symbol is not INamedTypeSymbol typeSymbol) return null; - AttributeData attribute = typeSymbol + AttributeData? attribute = typeSymbol .GetAttributes() - .Single(a => a.AttributeClass?.ToDisplayString() == StringConstants.BitObjectAttributeFullName); + .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == StringConstants.BitObjectAttributeFullName); - return new(typeSymbol, attribute); + return attribute is null ? null : new(typeSymbol, attribute); } - private static void GenerateSourceCode(SourceProductionContext context, ImmutableArray processors) + private static void GenerateSourceCode(SourceProductionContext context, TypeSymbolProcessor processor) { - if (processors.Length == 0) + if (!processor.IsValid) return; StringBuilder stringBuilder = new(StringConstants.Header); + stringBuilder.AppendLine(); - // group the objects by their respective namespace - var namespaceGroups = processors.GroupBy(x => x.Namespace); + if (processor.Namespace is not null) + stringBuilder + .AppendLine($"namespace {processor.Namespace}") + .AppendLine("{"); - foreach (var namespaceGroup in namespaceGroups) - { - stringBuilder.AppendLine(); + processor.GenerateCSharpSource(stringBuilder); + stringBuilder.RemoveLastLine(); - // print the current namespace - if (namespaceGroup.Key is not null) - stringBuilder - .AppendLine($"namespace {namespaceGroup.Key}") - .AppendLine("{"); + if (processor.Namespace is not null) + stringBuilder.AppendLine("}"); - foreach (TypeSymbolProcessor processor in namespaceGroup) - { - processor.GenerateCSharpSource(stringBuilder); - } - - // remove typesymbol seperator - stringBuilder.RemoveLastLine(); - - // apply closing namespace bracket - if (namespaceGroup.Key is not null) - stringBuilder.AppendLine("}"); - } - - context.AddSource("BitsKitGeneratedFields.g.cs", stringBuilder.ToString()); + context.AddSource(processor.HintName, stringBuilder.ToString()); } private static bool IsValidTypeDeclaration(SyntaxNode node, CancellationToken _) => diff --git a/BitsKit.Generator/DiagnosticDescriptors.cs b/BitsKit.Generator/DiagnosticDescriptors.cs index 828230e..b263f47 100644 --- a/BitsKit.Generator/DiagnosticDescriptors.cs +++ b/BitsKit.Generator/DiagnosticDescriptors.cs @@ -53,4 +53,39 @@ internal static class DiagnosticDescriptors category: Category, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidBitObjectOption = new( + "BITSKIT007", "Invalid bit object option", "'{0}' has an invalid {1} value '{2}'", + Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor UnsupportedBackingField = new( + "BITSKIT008", "Unsupported bit-field backing type", "'{0}.{1}' uses unsupported backing type '{2}'", + Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor RawPointerRequiresUnsafe = new( + "BITSKIT009", "Raw pointer backing requires unsafe access", + "'{0}.{1}' is a raw byte pointer and requires BitObjectAccessMode.Unsafe; use a fixed buffer for checked access", + Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor InvalidFieldName = new( + "BITSKIT010", "Invalid generated bit-field name", "'{0}.{1}' is not a valid generated member name", + Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor GeneratedMemberConflict = new( + "BITSKIT011", "Generated bit-field member conflicts with another member", + "'{0}' already contains a member named '{1}'", Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor InvalidFieldWidth = new( + "BITSKIT012", "Invalid bit-field width", + "'{0}.{1}' width {2} is invalid for '{3}', whose maximum width is {4} bits", + Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor LayoutExceedsBacking = new( + "BITSKIT013", "Bit-field layout exceeds its backing storage", + "'{0}.{1}' ends at bit {2}, beyond the {3}-bit capacity of backing field '{4}'", + Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor InvalidModifiers = new( + "BITSKIT014", "Invalid bit-field modifiers", "'{0}.{1}' has invalid modifiers: {2}", + Category, DiagnosticSeverity.Error, true); } diff --git a/BitsKit.Generator/Extensions.cs b/BitsKit.Generator/Extensions.cs index 9737f38..d7653ff 100644 --- a/BitsKit.Generator/Extensions.cs +++ b/BitsKit.Generator/Extensions.cs @@ -85,6 +85,43 @@ SpecialType.System_IntPtr or _ => throw new NotSupportedException() }; + public static int GetBitWidth(this SpecialType type) => type switch + { + SpecialType.System_SByte or SpecialType.System_Byte => 8, + SpecialType.System_Int16 or SpecialType.System_UInt16 => 16, + SpecialType.System_Int32 or SpecialType.System_UInt32 => 32, + SpecialType.System_Int64 or SpecialType.System_UInt64 => 64, + SpecialType.System_IntPtr or SpecialType.System_UIntPtr => 64, + _ => 0 + }; + + public static int GetBitWidth(this BitFieldType type) => type switch + { + BitFieldType.SByte or BitFieldType.Byte => 8, + BitFieldType.Int16 or BitFieldType.UInt16 => 16, + BitFieldType.Int32 or BitFieldType.UInt32 => 32, + BitFieldType.Int64 or BitFieldType.UInt64 => 64, + BitFieldType.IntPtr or BitFieldType.UIntPtr => 64, + BitFieldType.Boolean => 1, + _ => 0 + }; + + public static string ToTypeName(this BitFieldType type) => type switch + { + BitFieldType.SByte => "global::System.SByte", + BitFieldType.Byte => "global::System.Byte", + BitFieldType.Int16 => "global::System.Int16", + BitFieldType.UInt16 => "global::System.UInt16", + BitFieldType.Int32 => "global::System.Int32", + BitFieldType.UInt32 => "global::System.UInt32", + BitFieldType.Int64 => "global::System.Int64", + BitFieldType.UInt64 => "global::System.UInt64", + BitFieldType.IntPtr => "global::System.IntPtr", + BitFieldType.UIntPtr => "global::System.UIntPtr", + BitFieldType.Boolean => "global::System.Boolean", + _ => throw new NotSupportedException() + }; + public static string ToIntegralName(this BitFieldType type) => type switch { BitFieldType.SByte => "Int8", diff --git a/BitsKit.Generator/Models/BackingFieldModel.cs b/BitsKit.Generator/Models/BackingFieldModel.cs index 32e4ecd..1605495 100644 --- a/BitsKit.Generator/Models/BackingFieldModel.cs +++ b/BitsKit.Generator/Models/BackingFieldModel.cs @@ -4,21 +4,70 @@ namespace BitsKit.Generator.Models { internal record BackingFieldModel { + public readonly string OriginalName; public readonly string Name; public readonly string TypeString; public readonly int FixedSize; public readonly bool IsReadOnly; + public readonly bool IsReadOnlyStorage; + public readonly bool IsByteArray; + public readonly bool IsRawPointer; public readonly BackingFieldType Type; public BackingFieldModel(IFieldSymbol fieldSymbol, BackingFieldType type) { - Name = fieldSymbol.Name; - TypeString = fieldSymbol.Type.ToDisplayString(); + OriginalName = fieldSymbol.Name; + Name = SymbolFormatting.EscapeIdentifier(fieldSymbol.Name); + TypeString = SymbolFormatting.GetTypeName(fieldSymbol.Type); FixedSize = fieldSymbol.FixedSize; IsReadOnly = fieldSymbol.IsReadOnly; + IsReadOnlyStorage = fieldSymbol.Type is INamedTypeSymbol + { + Name: "ReadOnlySpan" or "ReadOnlyMemory", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } + }; + IsByteArray = fieldSymbol.Type is IArrayTypeSymbol; + IsRawPointer = type == BackingFieldType.Pointer && fieldSymbol.FixedSize == 0; Type = type; } + + public static BackingFieldType Classify(IFieldSymbol field) + { + if (field.Type is IArrayTypeSymbol + { + Rank: 1, + ElementType.SpecialType: SpecialType.System_Byte + }) + { + return BackingFieldType.Span; + } + + if (field.Type is IPointerTypeSymbol + { + PointedAtType.SpecialType: SpecialType.System_Byte + }) + { + return BackingFieldType.Pointer; + } + + if (field.Type is INamedTypeSymbol named && + named.TypeArguments.Length == 1 && + named.TypeArguments[0].SpecialType == SpecialType.System_Byte && + named.ContainingNamespace is { Name: "System", ContainingNamespace.IsGlobalNamespace: true }) + { + return named.Name switch + { + "Memory" or "ReadOnlyMemory" => BackingFieldType.Memory, + "Span" or "ReadOnlySpan" => BackingFieldType.Span, + _ => BackingFieldType.Invalid + }; + } + + return field.Type.IsSupportedIntegralType() + ? BackingFieldType.Integral + : BackingFieldType.Invalid; + } } } diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index 693ef8f..117a135 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -37,10 +37,12 @@ public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbo switch (attributeData.NamedArguments[i].Key) { case "ReverseBitOrder": - ReverseBitOrder = (bool)attributeData.NamedArguments[i].Value.Value!; + if (attributeData.NamedArguments[i].Value.Value is bool reverseBitOrder) + ReverseBitOrder = reverseBitOrder; break; case "Modifiers": - Modifiers = (BitFieldModifiers)attributeData.NamedArguments[i].Value.Value!; + if (attributeData.NamedArguments[i].Value.Value is int modifiers) + Modifiers = (BitFieldModifiers)modifiers; break; } } @@ -55,8 +57,8 @@ public void GenerateCSharpSource(StringBuilder sb) GetPropertyTemplate(), accessor, Modifiers.HasFlag(BitFieldModifiers.Required) ? "required" : "", - ReturnType ?? FieldType?.ToString(), - Name) + ReturnType ?? FieldType?.ToTypeName(), + SymbolFormatting.EscapeIdentifier(Name)) .AppendIndentedLine(2, "{"); // getter @@ -97,39 +99,40 @@ public void GenerateCSharpSource(StringBuilder sb) public void GenerateBatchAccessors(StringBuilder sb) { string accessor = GetAccessor(); - string valueType = ReturnType ?? FieldType!.Value.ToString(); + string methodName = Name.StartsWith("@") ? Name.Substring(1) : Name; + string valueType = ReturnType ?? FieldType!.Value.ToTypeName(); string primitiveName = this is BooleanFieldModel ? "Bit" : FieldType!.Value.ToIntegralName(); string bitCountArgument = this is BooleanFieldModel ? string.Empty : $", {BitCount}"; string readDestination = this is EnumFieldModel - ? $"MemoryMarshal.Cast<{valueType}, {FieldType!.Value}>(destination)" + ? $"global::System.Runtime.InteropServices.MemoryMarshal.Cast<{valueType}, {FieldType!.Value.ToTypeName()}>(destination)" : "destination"; string writeValues = this is EnumFieldModel - ? $"MemoryMarshal.Cast<{valueType}, {FieldType!.Value}>(values)" + ? $"global::System.Runtime.InteropServices.MemoryMarshal.Cast<{valueType}, {FieldType!.Value.ToTypeName()}>(values)" : "values"; sb.AppendIndentedLine(2, - $"{accessor} static void Read{Name}Batch(ReadOnlySpan source, Span<{valueType}> destination) =>") + $"{accessor} static void Read{methodName}Batch(global::System.ReadOnlySpan source, global::System.Span<{valueType}> destination) =>") .AppendIndentedLine(3, - $"BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, {readDestination});") + $"global::BitsKit.Primitives.BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, {readDestination});") .AppendLine() .AppendIndentedLine(2, - $"{accessor} static void Read{Name}Batch(ReadOnlySpan source, Int32 bitStride, Span<{valueType}> destination) =>") + $"{accessor} static void Read{methodName}Batch(global::System.ReadOnlySpan source, global::System.Int32 bitStride, global::System.Span<{valueType}> destination) =>") .AppendIndentedLine(3, - $"BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, bitStride, {readDestination});") + $"global::BitsKit.Primitives.BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, bitStride, {readDestination});") .AppendLine(); if (IsReadOnly()) return; sb.AppendIndentedLine(2, - $"{accessor} static void Write{Name}Batch(Span destination, ReadOnlySpan<{valueType}> values) =>") + $"{accessor} static void Write{methodName}Batch(global::System.Span destination, global::System.ReadOnlySpan<{valueType}> values) =>") .AppendIndentedLine(3, - $"BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, {writeValues});") + $"global::BitsKit.Primitives.BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, {writeValues});") .AppendLine() .AppendIndentedLine(2, - $"{accessor} static void Write{Name}Batch(Span destination, Int32 bitStride, ReadOnlySpan<{valueType}> values) =>") + $"{accessor} static void Write{methodName}Batch(global::System.Span destination, global::System.Int32 bitStride, global::System.ReadOnlySpan<{valueType}> values) =>") .AppendIndentedLine(3, - $"BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, bitStride, {writeValues});") + $"global::BitsKit.Primitives.BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, bitStride, {writeValues});") .AppendLine(); } @@ -206,8 +209,8 @@ BackingFieldType.Span or BackingFieldType.Integral => "{4}", BackingFieldType.Memory => "{4}.Span", BackingFieldType.Span => "{4}", - BackingFieldType.Pointer => "MemoryMarshal.CreateReadOnlySpan(ref {4}[0], {7})", - BackingFieldType.InlineArray => "MemoryMarshal.AsBytes<{8}>(this)", + BackingFieldType.Pointer => "global::System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan(ref {4}[0], {7})", + BackingFieldType.InlineArray => "global::System.Runtime.InteropServices.MemoryMarshal.AsBytes<{8}>(this)", _ => throw new NotSupportedException() }; @@ -219,8 +222,8 @@ BackingFieldType.Span or BackingFieldType.Integral => "ref {4}", BackingFieldType.Memory => "{4}.Span", BackingFieldType.Span => "{4}", - BackingFieldType.Pointer => "MemoryMarshal.CreateSpan(ref {4}[0], {7})", - BackingFieldType.InlineArray => "MemoryMarshal.AsBytes((Span<{8}>)this)", + BackingFieldType.Pointer => "global::System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref {4}[0], {7})", + BackingFieldType.InlineArray => "global::System.Runtime.InteropServices.MemoryMarshal.AsBytes((global::System.Span<{8}>)this)", _ => throw new NotSupportedException() }; @@ -236,7 +239,7 @@ protected bool TryGetUnsafeReadExpression(out string expression) if (!TryGetUnsafeStorageReference(writable: false, out string source)) return false; - expression = $"UnsafeBitPrimitives.Read{FieldType!.Value.ToIntegralName()}{BitOrder.ToShortName()}" + + expression = $"global::BitsKit.Primitives.UnsafeBitPrimitives.Read{FieldType!.Value.ToIntegralName()}{BitOrder.ToShortName()}" + $"({source}, {BitOffset}, {BitCount})"; return true; } @@ -253,7 +256,7 @@ protected bool TryGetUnsafeWriteExpression(string valueExpression, out string ex if (!TryGetUnsafeStorageReference(writable: true, out string destination)) return false; - expression = $"UnsafeBitPrimitives.Write{FieldType!.Value.ToIntegralName()}{BitOrder.ToShortName()}" + + expression = $"global::BitsKit.Primitives.UnsafeBitPrimitives.Write{FieldType!.Value.ToIntegralName()}{BitOrder.ToShortName()}" + $"({destination}, {BitOffset}, unchecked(({FieldType.Value})({valueExpression})), {BitCount})"; return true; } @@ -272,7 +275,7 @@ protected bool TryGetUnsafeBooleanReadExpression(out string expression) int mask = 1 << (BitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); string target = byteOffset == 0 ? source - : $"System.Runtime.CompilerServices.Unsafe.Add(ref {source}, {byteOffset})"; + : $"global::System.Runtime.CompilerServices.Unsafe.Add(ref {source}, {byteOffset})"; expression = $"({target} & 0x{mask:X2}) != 0"; return true; } @@ -291,11 +294,11 @@ protected bool TryGetUnsafeBooleanWriteTemplate(out string template) int mask = 1 << (BitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); string target = byteOffset == 0 ? destination - : $"System.Runtime.CompilerServices.Unsafe.Add(ref {destination}, {byteOffset})"; + : $"global::System.Runtime.CompilerServices.Unsafe.Add(ref {destination}, {byteOffset})"; template = "{0} {1}\n" + "{{\n" + - $" ref Byte target = ref {target};\n" + + $" ref global::System.Byte target = ref {target};\n" + " if (value)\n" + $" target |= 0x{mask:X2};\n" + " else\n" + @@ -320,10 +323,16 @@ private bool TryGetUnsafeStorageTarget(bool writable, out string target) if (!UsesUnsafeAccess || BackingFieldType == BackingFieldType.Integral) return false; - string source = BackingField.TypeString == "byte[]" - ? writable ? "((Span){4})" : "((ReadOnlySpan){4})" + if (BackingField.IsRawPointer) + { + target = "{4}[0]"; + return true; + } + + string source = BackingField.IsByteArray + ? writable ? "((global::System.Span){4})" : "((global::System.ReadOnlySpan){4})" : writable ? SetterSource() : GetterSource(); - target = $"MemoryMarshal.GetReference({source})"; + target = $"global::System.Runtime.InteropServices.MemoryMarshal.GetReference({source})"; return true; } @@ -362,7 +371,7 @@ protected bool TryGetDirectStorageReadExpression(out string expression) } string endianness = BitOrder == BitOrder.MostSignificant ? "BigEndian" : "LittleEndian"; - expression = $"BinaryPrimitives.Read{typeName}{endianness}({source})"; + expression = $"global::System.Buffers.Binary.BinaryPrimitives.Read{typeName.Substring(typeName.LastIndexOf('.') + 1)}{endianness}({source})"; return true; } @@ -379,12 +388,12 @@ protected bool TryGetDirectStorageWriteExpression(string valueExpression, out st string value = $"unchecked(({typeName})({valueExpression}))"; if (width == 8) { - expression = $"{source}[0] = unchecked((Byte){value})"; + expression = $"{source}[0] = unchecked((global::System.Byte){value})"; return true; } string endianness = BitOrder == BitOrder.MostSignificant ? "BigEndian" : "LittleEndian"; - expression = $"BinaryPrimitives.Write{typeName}{endianness}({source}, {value})"; + expression = $"global::System.Buffers.Binary.BinaryPrimitives.Write{typeName.Substring(typeName.LastIndexOf('.') + 1)}{endianness}({source}, {value})"; return true; } @@ -398,7 +407,7 @@ private bool TryGetDirectStorageInfo(bool writable, out int width, out string ty BitFieldType.Int64 or BitFieldType.UInt64 => 64, _ => 0 }; - typeName = FieldType?.ToString() ?? string.Empty; + typeName = FieldType?.ToTypeName() ?? string.Empty; if (!TryGetByteStorageSource(writable, out source)) return false; @@ -417,11 +426,11 @@ private bool TryGetByteStorageSource(bool writable, out string source) source = BackingFieldType switch { BackingFieldType.Memory => "{4}.Span", - BackingFieldType.Span when BackingField.TypeString == "byte[]" => - writable ? "((Span){4})" : "((ReadOnlySpan){4})", + BackingFieldType.Span when BackingField.IsByteArray => + writable ? "((global::System.Span){4})" : "((global::System.ReadOnlySpan){4})", BackingFieldType.Span => "{4}", BackingFieldType.InlineArray when BackingField.TypeString == "byte" => - writable ? "((Span)this)" : "((ReadOnlySpan)this)", + writable ? "((global::System.Span)this)" : "((global::System.ReadOnlySpan)this)", _ => string.Empty }; @@ -478,8 +487,8 @@ protected bool TryGetDirectStorageBooleanWriteTemplate(out string template) template = "{0} {1}\n" + "{{\n" + - $" Span source = {source};\n" + - $" source[{byteOffset}] = unchecked((Byte)((source[{byteOffset}] & 0x{255 ^ mask:X2}) | " + + $" global::System.Span source = {source};\n" + + $" source[{byteOffset}] = unchecked((global::System.Byte)((source[{byteOffset}] & 0x{255 ^ mask:X2}) | " + $"(value ? 0x{mask:X2} : 0)));\n" + "}}"; return true; @@ -503,10 +512,10 @@ protected bool TryGetDirectFixedWidthReadTemplate(out string template) int loadWidth = byteCount <= 4 ? 4 : 8; string fastWindow = - $"unchecked((UInt64)System.Runtime.CompilerServices.Unsafe.ReadUnaligned(" + - "ref MemoryMarshal.GetReference(source)))"; + $"unchecked((global::System.UInt64)global::System.Runtime.CompilerServices.Unsafe.ReadUnaligned(" + + "ref global::System.Runtime.InteropServices.MemoryMarshal.GetReference(source)))"; if (BitOrder == BitOrder.MostSignificant) - fastWindow = $"BinaryPrimitives.ReverseEndianness({fastWindow}) >> {64 - loadWidth * 8}"; + fastWindow = $"global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness({fastWindow}) >> {64 - loadWidth * 8}"; string returnType = ReturnType ?? typeName; int fastShift = BitOrder == BitOrder.MostSignificant ? @@ -519,17 +528,17 @@ protected bool TryGetDirectFixedWidthReadTemplate(out string template) template = "{0} {1}\n" + "{{\n" + - $" ReadOnlySpan source = {source};\n" + + $" global::System.ReadOnlySpan source = {source};\n" + $" if (source.Length < {loadWidth})\n" + " return ReadExact(source);\n" + - $" UInt64 current = {fastWindow};\n" + + $" global::System.UInt64 current = {fastWindow};\n" + $" return {fastResult};\n" + "\n" + - " [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]\n" + - $" static {returnType} ReadExact(ReadOnlySpan source)\n" + + " [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]\n" + + $" static {returnType} ReadExact(global::System.ReadOnlySpan source)\n" + " {{\n" + $" source = source.Slice(0, {byteCount});\n" + - $" UInt64 current = {exactWindow};\n" + + $" global::System.UInt64 current = {exactWindow};\n" + $" return {exactResult};\n" + " }}\n" + "}}"; @@ -560,41 +569,41 @@ protected bool TryGetDirectFixedWidthWriteTemplate(string valueExpression, out s ulong fastFieldMask = valueMask << fastShift; ulong exactFieldMask = valueMask << shift; string fastRead = - $"unchecked((UInt64)System.Runtime.CompilerServices.Unsafe.ReadUnaligned(" + - "ref MemoryMarshal.GetReference(source)))"; + $"unchecked((global::System.UInt64)global::System.Runtime.CompilerServices.Unsafe.ReadUnaligned(" + + "ref global::System.Runtime.InteropServices.MemoryMarshal.GetReference(source)))"; string fastWriteValue = "current"; if (BitOrder == BitOrder.MostSignificant) { - fastRead = $"BinaryPrimitives.ReverseEndianness({fastRead}) >> {64 - loadWidth * 8}"; + fastRead = $"global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness({fastRead}) >> {64 - loadWidth * 8}"; fastWriteValue = - $"BinaryPrimitives.ReverseEndianness(current << {64 - loadWidth * 8})"; + $"global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(current << {64 - loadWidth * 8})"; } - string typeName = FieldType!.Value.ToString(); + string typeName = FieldType!.Value.ToTypeName(); string exactRead = GetWindowReadExpression("source", byteCount, BitOrder); string exactWrites = GetWindowWriteStatements("source", "current", byteCount, BitOrder); template = "{0} {1}\n" + "{{\n" + - $" Span source = {source};\n" + + $" global::System.Span source = {source};\n" + $" if (source.Length < {loadWidth})\n" + " {{\n" + $" WriteExact(source, unchecked(({typeName})({valueExpression})));\n" + " return;\n" + " }}\n" + - $" UInt64 current = {fastRead};\n" + + $" global::System.UInt64 current = {fastRead};\n" + $" current = (current & ~0x{fastFieldMask:X}UL) | " + - $"((unchecked((UInt64)({valueExpression})) << {fastShift}) & 0x{fastFieldMask:X}UL);\n" + - $" System.Runtime.CompilerServices.Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(source), unchecked((UInt{loadWidth * 8})({fastWriteValue})));\n" + + $"((unchecked((global::System.UInt64)({valueExpression})) << {fastShift}) & 0x{fastFieldMask:X}UL);\n" + + $" global::System.Runtime.CompilerServices.Unsafe.WriteUnaligned(ref global::System.Runtime.InteropServices.MemoryMarshal.GetReference(source), unchecked((global::System.UInt{loadWidth * 8})({fastWriteValue})));\n" + "\n" + - " [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]\n" + - $" static void WriteExact(Span source, {typeName} value)\n" + + " [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]\n" + + $" static void WriteExact(global::System.Span source, {typeName} value)\n" + " {{\n" + $" source = source.Slice(0, {byteCount});\n" + - $" UInt64 current = {exactRead};\n" + + $" global::System.UInt64 current = {exactRead};\n" + $" current = (current & ~0x{exactFieldMask:X}UL) | " + - $"((unchecked((UInt64)(value)) << {shift}) & 0x{exactFieldMask:X}UL);\n" + + $"((unchecked((global::System.UInt64)(value)) << {shift}) & 0x{exactFieldMask:X}UL);\n" + $" {exactWrites}\n" + " }}\n" + "}}"; @@ -612,7 +621,7 @@ BitFieldType.Int32 or BitFieldType.Int64) { int signShift = 64 - BitCount; - return $"unchecked(({returnType})(unchecked((Int64)({extracted} << {signShift})) >> {signShift}))"; + return $"unchecked(({returnType})(unchecked((global::System.Int64)({extracted} << {signShift})) >> {signShift}))"; } return $"unchecked(({returnType}){extracted})"; @@ -628,7 +637,7 @@ private bool TryGetFixedWidthStorageInfo( source = string.Empty; byteCount = 0; shift = 0; - typeName = FieldType?.ToString() ?? string.Empty; + typeName = FieldType?.ToTypeName() ?? string.Empty; if (BitCount is not (11 or 12 or 24 or 48) || FieldType is not (BitFieldType.SByte or @@ -657,26 +666,27 @@ BitFieldType.Int64 or private static string GetWindowReadExpression(string source, int byteCount, BitOrder bitOrder) { + const string Binary = "global::System.Buffers.Binary.BinaryPrimitives"; if (bitOrder == BitOrder.LeastSignificant) { return byteCount switch { - 2 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}))", - 3 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}) | ((UInt64){source}[2] << 16))", - 4 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32LittleEndian({source}))", - 6 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32LittleEndian({source}) | ((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}.Slice(4)) << 32))", - 7 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32LittleEndian({source}) | ((UInt64)BinaryPrimitives.ReadUInt16LittleEndian({source}.Slice(4)) << 32) | ((UInt64){source}[6] << 48))", + 2 => $"unchecked((global::System.UInt64){Binary}.ReadUInt16LittleEndian({source}))", + 3 => $"unchecked((global::System.UInt64){Binary}.ReadUInt16LittleEndian({source}) | ((global::System.UInt64){source}[2] << 16))", + 4 => $"unchecked((global::System.UInt64){Binary}.ReadUInt32LittleEndian({source}))", + 6 => $"unchecked((global::System.UInt64){Binary}.ReadUInt32LittleEndian({source}) | ((global::System.UInt64){Binary}.ReadUInt16LittleEndian({source}.Slice(4)) << 32))", + 7 => $"unchecked((global::System.UInt64){Binary}.ReadUInt32LittleEndian({source}) | ((global::System.UInt64){Binary}.ReadUInt16LittleEndian({source}.Slice(4)) << 32) | ((global::System.UInt64){source}[6] << 48))", _ => throw new NotSupportedException() }; } return byteCount switch { - 2 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt16BigEndian({source}))", - 3 => $"unchecked(((UInt64)BinaryPrimitives.ReadUInt16BigEndian({source}) << 8) | {source}[2])", - 4 => $"unchecked((UInt64)BinaryPrimitives.ReadUInt32BigEndian({source}))", - 6 => $"unchecked(((UInt64)BinaryPrimitives.ReadUInt32BigEndian({source}) << 16) | BinaryPrimitives.ReadUInt16BigEndian({source}.Slice(4)))", - 7 => $"unchecked(((UInt64)BinaryPrimitives.ReadUInt32BigEndian({source}) << 24) | ((UInt64)BinaryPrimitives.ReadUInt16BigEndian({source}.Slice(4)) << 8) | {source}[6])", + 2 => $"unchecked((global::System.UInt64){Binary}.ReadUInt16BigEndian({source}))", + 3 => $"unchecked(((global::System.UInt64){Binary}.ReadUInt16BigEndian({source}) << 8) | {source}[2])", + 4 => $"unchecked((global::System.UInt64){Binary}.ReadUInt32BigEndian({source}))", + 6 => $"unchecked(((global::System.UInt64){Binary}.ReadUInt32BigEndian({source}) << 16) | {Binary}.ReadUInt16BigEndian({source}.Slice(4)))", + 7 => $"unchecked(((global::System.UInt64){Binary}.ReadUInt32BigEndian({source}) << 24) | ((global::System.UInt64){Binary}.ReadUInt16BigEndian({source}.Slice(4)) << 8) | {source}[6])", _ => throw new NotSupportedException() }; } @@ -687,26 +697,27 @@ private static string GetWindowWriteStatements( int byteCount, BitOrder bitOrder) { + const string Binary = "global::System.Buffers.Binary.BinaryPrimitives"; if (bitOrder == BitOrder.LeastSignificant) { return byteCount switch { - 2 => $"BinaryPrimitives.WriteUInt16LittleEndian({source}, unchecked((UInt16){value}));", - 3 => $"BinaryPrimitives.WriteUInt16LittleEndian({source}, unchecked((UInt16){value})); {source}[2] = unchecked((Byte)({value} >> 16));", - 4 => $"BinaryPrimitives.WriteUInt32LittleEndian({source}, unchecked((UInt32){value}));", - 6 => $"BinaryPrimitives.WriteUInt32LittleEndian({source}, unchecked((UInt32){value})); BinaryPrimitives.WriteUInt16LittleEndian({source}.Slice(4), unchecked((UInt16)({value} >> 32)));", - 7 => $"BinaryPrimitives.WriteUInt32LittleEndian({source}, unchecked((UInt32){value})); BinaryPrimitives.WriteUInt16LittleEndian({source}.Slice(4), unchecked((UInt16)({value} >> 32))); {source}[6] = unchecked((Byte)({value} >> 48));", + 2 => $"{Binary}.WriteUInt16LittleEndian({source}, unchecked((global::System.UInt16){value}));", + 3 => $"{Binary}.WriteUInt16LittleEndian({source}, unchecked((global::System.UInt16){value})); {source}[2] = unchecked((global::System.Byte)({value} >> 16));", + 4 => $"{Binary}.WriteUInt32LittleEndian({source}, unchecked((global::System.UInt32){value}));", + 6 => $"{Binary}.WriteUInt32LittleEndian({source}, unchecked((global::System.UInt32){value})); {Binary}.WriteUInt16LittleEndian({source}.Slice(4), unchecked((global::System.UInt16)({value} >> 32)));", + 7 => $"{Binary}.WriteUInt32LittleEndian({source}, unchecked((global::System.UInt32){value})); {Binary}.WriteUInt16LittleEndian({source}.Slice(4), unchecked((global::System.UInt16)({value} >> 32))); {source}[6] = unchecked((global::System.Byte)({value} >> 48));", _ => throw new NotSupportedException() }; } return byteCount switch { - 2 => $"BinaryPrimitives.WriteUInt16BigEndian({source}, unchecked((UInt16){value}));", - 3 => $"BinaryPrimitives.WriteUInt16BigEndian({source}, unchecked((UInt16)({value} >> 8))); {source}[2] = unchecked((Byte){value});", - 4 => $"BinaryPrimitives.WriteUInt32BigEndian({source}, unchecked((UInt32){value}));", - 6 => $"BinaryPrimitives.WriteUInt32BigEndian({source}, unchecked((UInt32)({value} >> 16))); BinaryPrimitives.WriteUInt16BigEndian({source}.Slice(4), unchecked((UInt16){value}));", - 7 => $"BinaryPrimitives.WriteUInt32BigEndian({source}, unchecked((UInt32)({value} >> 24))); BinaryPrimitives.WriteUInt16BigEndian({source}.Slice(4), unchecked((UInt16)({value} >> 8))); {source}[6] = unchecked((Byte){value});", + 2 => $"{Binary}.WriteUInt16BigEndian({source}, unchecked((global::System.UInt16){value}));", + 3 => $"{Binary}.WriteUInt16BigEndian({source}, unchecked((global::System.UInt16)({value} >> 8))); {source}[2] = unchecked((global::System.Byte){value});", + 4 => $"{Binary}.WriteUInt32BigEndian({source}, unchecked((global::System.UInt32){value}));", + 6 => $"{Binary}.WriteUInt32BigEndian({source}, unchecked((global::System.UInt32)({value} >> 16))); {Binary}.WriteUInt16BigEndian({source}.Slice(4), unchecked((global::System.UInt16){value}));", + 7 => $"{Binary}.WriteUInt32BigEndian({source}, unchecked((global::System.UInt32)({value} >> 24))); {Binary}.WriteUInt16BigEndian({source}.Slice(4), unchecked((global::System.UInt16)({value} >> 8))); {source}[6] = unchecked((global::System.Byte){value});", _ => throw new NotSupportedException() }; } @@ -726,19 +737,19 @@ protected bool TryGetDirectIntegralReadExpression(out string expression) return false; } - string backingType = FieldType!.Value.ToString(); + string backingType = FieldType!.Value.ToTypeName(); string unsignedSource = $"unchecked(({unsignedType}){{4}})"; if (BitOrder == BitOrder.MostSignificant) { string extracted = - $"(BinaryPrimitives.ReverseEndianness({unsignedSource}) << {BitOffset}) >> {workingWidth - BitCount}"; + $"(global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness({unsignedSource}) << {BitOffset}) >> {workingWidth - BitCount}"; if (FieldType is BitFieldType.SByte or BitFieldType.Int16 or BitFieldType.Int32 or BitFieldType.Int64) { - string signedType = workingWidth == 64 ? "Int64" : "Int32"; + string signedType = workingWidth == 64 ? "global::System.Int64" : "global::System.Int32"; int signShift = workingWidth - BitCount; expression = $"unchecked(({backingType})((unchecked(({signedType})({extracted})) << {signShift}) >> {signShift}))"; @@ -756,7 +767,7 @@ BitFieldType.Int16 or BitFieldType.Int32 or BitFieldType.Int64) { - string signedType = workingWidth == 64 ? "Int64" : "Int32"; + string signedType = workingWidth == 64 ? "global::System.Int64" : "global::System.Int32"; int leftShift = workingWidth - BitOffset - BitCount; int rightShift = workingWidth - BitCount; expression = @@ -791,7 +802,7 @@ protected bool TryGetDirectIntegralBooleanReadExpression(out string expression) if (BitOrder == BitOrder.MostSignificant) { expression = - $"((BinaryPrimitives.ReverseEndianness(unchecked(({unsignedType}){{4}})) << {BitOffset}) >> {workingWidth - 1}) != 0"; + $"((global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(unchecked(({unsignedType}){{4}})) << {BitOffset}) >> {workingWidth - 1}) != 0"; } else { @@ -819,7 +830,7 @@ protected bool TryGetDirectIntegralWriteExpression(string valueExpression, out s ulong valueMask = BitCount == 64 ? ulong.MaxValue : (1UL << BitCount) - 1; string valueMaskLiteral = FormatMask(valueMask, workingWidth); - string backingType = FieldType!.Value.ToString(); + string backingType = FieldType!.Value.ToTypeName(); if (BitOrder == BitOrder.MostSignificant) { @@ -837,9 +848,9 @@ protected bool TryGetDirectIntegralWriteExpression(string valueExpression, out s string alignedValue = backingWidth switch { 8 => $"({value}) << {shift}", - 16 => $"unchecked((UInt32)BinaryPrimitives.ReverseEndianness(unchecked((UInt16)(({value}) << {shift}))))", - 32 => $"BinaryPrimitives.ReverseEndianness(({value}) << {shift})", - 64 => $"BinaryPrimitives.ReverseEndianness(({value}) << {shift})", + 16 => $"unchecked((global::System.UInt32)global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(unchecked((global::System.UInt16)(({value}) << {shift}))))", + 32 => $"global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(({value}) << {shift})", + 64 => $"global::System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(({value}) << {shift})", _ => throw new NotSupportedException() }; @@ -872,7 +883,7 @@ private bool TryGetDirectIntegralInfo( _ => 0 }; workingWidth = backingWidth == 64 ? 64 : 32; - unsignedType = workingWidth == 64 ? "UInt64" : "UInt32"; + unsignedType = workingWidth == 64 ? "global::System.UInt64" : "global::System.UInt32"; return BackingFieldType == BackingFieldType.Integral && backingWidth != 0 && @@ -889,11 +900,8 @@ private static string FormatMask(ulong mask, int width) => /// protected bool IsReadOnly() { - string backingType = BackingField.TypeString; - return BackingField.IsReadOnly || - backingType == "System.ReadOnlySpan" || - backingType == "System.ReadOnlyMemory" || + BackingField.IsReadOnlyStorage || Modifiers.HasFlag(BitFieldModifiers.ReadOnly); } diff --git a/BitsKit.Generator/Models/BooleanFieldModel.cs b/BitsKit.Generator/Models/BooleanFieldModel.cs index 0a6b8a2..1d0a198 100644 --- a/BitsKit.Generator/Models/BooleanFieldModel.cs +++ b/BitsKit.Generator/Models/BooleanFieldModel.cs @@ -12,7 +12,7 @@ public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeS switch (attributeData.ConstructorArguments.Length) { case 1: // boolean constructor - Name = (string)attributeData.ConstructorArguments[0].Value!; + Name = attributeData.ConstructorArguments[0].Value as string ?? string.Empty; break; case 0: // padding constructor break; @@ -22,7 +22,7 @@ public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeS BitCount = 1; FieldType = BitFieldType.Boolean; - ReturnType = typeof(bool).FullName; + ReturnType = "global::System.Boolean"; if (string.IsNullOrEmpty(Name)) FieldType = BitFieldType.Padding; diff --git a/BitsKit.Generator/Models/EnumFieldModel.cs b/BitsKit.Generator/Models/EnumFieldModel.cs index dbe74c8..9c9f655 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -17,15 +17,15 @@ public EnumFieldAttributeModel(AttributeData attributeData) switch (attributeData.ConstructorArguments.Length) { case 1: // padding constructor - BitCount = (byte)attributeData.ConstructorArguments[0].Value!; + if (attributeData.ConstructorArguments[0].Value is byte paddingSize) + BitCount = paddingSize; break; case 3: // enum constructor - Name = (string)attributeData.ConstructorArguments[0].Value!; - BitCount = (byte)attributeData.ConstructorArguments[1].Value!; + Name = attributeData.ConstructorArguments[0].Value as string; + if (attributeData.ConstructorArguments[1].Value is byte enumSize) + BitCount = enumSize; EnumType = attributeData.ConstructorArguments[2].Value as INamedTypeSymbol; break; - default: - throw new InvalidDataException($"unknown number of enum attribute constructor arguments: {attributeData.ConstructorArguments.Length}"); } } } @@ -41,7 +41,9 @@ public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymb Name = attributeModel.Name!; // todo: the nullability on this is well.. wrong. padding fields have no name BitCount = attributeModel.BitCount; - ReturnType = attributeModel.EnumType?.ToDisplayString(); + ReturnType = attributeModel.EnumType is null + ? null + : SymbolFormatting.GetTypeName(attributeModel.EnumType); FieldType = attributeModel.EnumType?.EnumUnderlyingType?.SpecialType.ToBitFieldType(); if (string.IsNullOrEmpty(Name)) diff --git a/BitsKit.Generator/Models/IntegralFieldModel.cs b/BitsKit.Generator/Models/IntegralFieldModel.cs index 40ebb93..8ce68dd 100644 --- a/BitsKit.Generator/Models/IntegralFieldModel.cs +++ b/BitsKit.Generator/Models/IntegralFieldModel.cs @@ -18,23 +18,27 @@ public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor? type switch (attributeData.ConstructorArguments.Length) { case 1: // Padding constructor - BitCount = (byte)attributeData.ConstructorArguments[0].Value!; + if (attributeData.ConstructorArguments[0].Value is byte paddingSize) + BitCount = paddingSize; break; case 2: // Integral backed constructor - Name = (string)attributeData.ConstructorArguments[0].Value!; - BitCount = (byte)attributeData.ConstructorArguments[1].Value!; + Name = attributeData.ConstructorArguments[0].Value as string ?? string.Empty; + if (attributeData.ConstructorArguments[1].Value is byte integralSize) + BitCount = integralSize; break; case 3: // Memory backed OR Type Cast constructor - Name = (string)attributeData.ConstructorArguments[0].Value!; - BitCount = (byte)attributeData.ConstructorArguments[1].Value!; - FieldType = (BitFieldType)attributeData.ConstructorArguments[2].Value!; + Name = attributeData.ConstructorArguments[0].Value as string ?? string.Empty; + if (attributeData.ConstructorArguments[1].Value is byte explicitSize) + BitCount = explicitSize; + if (attributeData.ConstructorArguments[2].Value is int fieldType) + FieldType = (BitFieldType)fieldType; break; default: return; } - if (FieldType is not null) - ReturnType = FieldType.ToString(); + if (FieldType is not null && FieldType.Value.GetBitWidth() > 0) + ReturnType = FieldType.Value.ToTypeName(); if (string.IsNullOrEmpty(Name)) FieldType = BitFieldType.Padding; diff --git a/BitsKit.Generator/StringConstants.cs b/BitsKit.Generator/StringConstants.cs index 3255dc3..04aeacd 100644 --- a/BitsKit.Generator/StringConstants.cs +++ b/BitsKit.Generator/StringConstants.cs @@ -53,7 +53,7 @@ internal static class StringConstants /// {0} = Source /// /// - public const string IntegralGetterTemplate = "{{0}} {{1}} => BitPrimitives.Read{{2}}{{3}}({0}, {{5}}, {{6}});"; + public const string IntegralGetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.Read{{2}}{{3}}({0}, {{5}}, {{6}});"; /// /// Getter template for reading explicitly cast field bits /// @@ -61,21 +61,21 @@ internal static class StringConstants /// {1} = /// /// - public const string ExplicitGetterTemplate = "{{0}} {{1}} => ({1})BitPrimitives.Read{{2}}{{3}}({0}, {{5}}, {{6}});"; + public const string ExplicitGetterTemplate = "{{0}} {{1}} => ({1})global::BitsKit.Primitives.BitPrimitives.Read{{2}}{{3}}({0}, {{5}}, {{6}});"; /// /// Getter template for reading a boolean from an integral /// /// {0} = Source /// /// - public const string BooleanGetterTemplate = "{{0}} {{1}} => BitPrimitives.Read{{2}}{{3}}({0}, {{5}}, 1) == 1;"; + public const string BooleanGetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.Read{{2}}{{3}}({0}, {{5}}, 1) == 1;"; /// /// Getter template for reading a boolean from a span /// /// {0} = Source /// /// - public const string BooleanSpanGetterTemplate = "{{0}} {{1}} => BitPrimitives.ReadBit{{3}}({0}, {{5}});"; + public const string BooleanSpanGetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.ReadBit{{3}}({0}, {{5}});"; /// /// Setter template for writing integral bits @@ -83,7 +83,7 @@ internal static class StringConstants /// {0} = Source /// /// - public const string IntegralSetterTemplate = "{{0}} {{1}} => BitPrimitives.Write{{2}}{{3}}({0}, {{5}}, value, {{6}});"; + public const string IntegralSetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.Write{{2}}{{3}}({0}, {{5}}, value, {{6}});"; /// /// Setter template for writing explicitly cast field bits /// @@ -91,7 +91,7 @@ internal static class StringConstants /// {1} = /// /// - public const string ExplicitSetterTemplate = "{{0}} {{1}} => BitPrimitives.Write{{2}}{{3}}({0}, {{5}}, ({1})value, {{6}});"; + public const string ExplicitSetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.Write{{2}}{{3}}({0}, {{5}}, ({1})value, {{6}});"; /// /// Setter template for writing a boolean to an integral /// @@ -99,12 +99,12 @@ internal static class StringConstants /// {1} = /// /// - public const string BooleanSetterTemplate = "{{0}} {{1}} => BitPrimitives.Write{{2}}{{3}}({0}, {{5}}, ({1})(value ? 1 : 0), 1);"; + public const string BooleanSetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.Write{{2}}{{3}}({0}, {{5}}, ({1})(value ? 1 : 0), 1);"; /// /// Setter template for writing a boolean to a span /// /// {0} = Source /// /// - public const string BooleanSpanSetterTemplate = "{{0}} {{1}} => BitPrimitives.WriteBit{{3}}({0}, {{5}}, value);"; + public const string BooleanSpanSetterTemplate = "{{0}} {{1}} => global::BitsKit.Primitives.BitPrimitives.WriteBit{{3}}({0}, {{5}}, value);"; } diff --git a/BitsKit.Generator/SymbolFormatting.cs b/BitsKit.Generator/SymbolFormatting.cs new file mode 100644 index 0000000..504cf16 --- /dev/null +++ b/BitsKit.Generator/SymbolFormatting.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace BitsKit.Generator; + +internal static class SymbolFormatting +{ + private static readonly SymbolDisplayFormat TypeFormat = + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + public static string EscapeIdentifier(string identifier) + { + string value = identifier.StartsWith("@") ? identifier.Substring(1) : identifier; + return SyntaxFacts.GetKeywordKind(value) != SyntaxKind.None || + SyntaxFacts.GetContextualKeywordKind(value) != SyntaxKind.None + ? "@" + value + : value; + } + + public static bool IsValidGeneratedName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + return false; + + string value = name![0] == '@' ? name.Substring(1) : name; + return SyntaxFacts.IsValidIdentifier(value) || + SyntaxFacts.GetKeywordKind(value) != SyntaxKind.None || + SyntaxFacts.GetContextualKeywordKind(value) != SyntaxKind.None; + } + + public static string GetNamespace(INamespaceSymbol namespaceSymbol) + { + var segments = new Stack(); + for (INamespaceSymbol? current = namespaceSymbol; + current is { IsGlobalNamespace: false }; + current = current.ContainingNamespace) + { + segments.Push(EscapeIdentifier(current.Name)); + } + + return string.Join(".", segments); + } + + public static string GetTypeDeclarationIdentifier(INamedTypeSymbol typeSymbol) + { + string identifier = EscapeIdentifier(typeSymbol.Name); + if (typeSymbol.TypeParameters.Length == 0) + return identifier; + + return identifier + "<" + string.Join(", ", + typeSymbol.TypeParameters.Select(parameter => EscapeIdentifier(parameter.Name))) + ">"; + } + + public static string GetTypeName(ITypeSymbol typeSymbol) => typeSymbol.ToDisplayString(TypeFormat); +} diff --git a/BitsKit.Generator/TypeSymbolProcessor.cs b/BitsKit.Generator/TypeSymbolProcessor.cs index 52690e9..9183a77 100644 --- a/BitsKit.Generator/TypeSymbolProcessor.cs +++ b/BitsKit.Generator/TypeSymbolProcessor.cs @@ -16,6 +16,8 @@ internal sealed record TypeSymbolProcessor public bool GenerateBatchAccessors { get; } public bool IsStruct { get; } public bool IsInlineArray { get; } + public bool IsValid { get; } + public string HintName { get; } private readonly string _syntaxKeyword; private readonly string _syntaxIdentifier; @@ -30,29 +32,56 @@ public TypeSymbolProcessor(INamedTypeSymbol typeSymbol, AttributeData attribute) TypeKind.Class when typeSymbol.IsRecord => "record", _ => "class" }; - _syntaxIdentifier = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + _syntaxIdentifier = SymbolFormatting.GetTypeDeclarationIdentifier(typeSymbol); - Namespace = typeSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)); + Namespace = SymbolFormatting.GetNamespace(typeSymbol.ContainingNamespace); if (string.IsNullOrWhiteSpace(Namespace)) Namespace = null; - DefaultBitOrder = (BitOrder)attribute.ConstructorArguments[0].Value!; - AccessMode = GetAccessMode(attribute); + bool hasValidBitOrder = TryGetBitOrder(attribute, out BitOrder bitOrder); + bool hasValidAccessMode = TryGetAccessMode(attribute, out BitObjectAccessMode accessMode); + IsValid = hasValidBitOrder && hasValidAccessMode; + DefaultBitOrder = bitOrder; + AccessMode = accessMode; GenerateBatchAccessors = GetGenerateBatchAccessors(attribute); IsStruct = typeSymbol.TypeKind == TypeKind.Struct; IsInlineArray = HasInlineArrayAttribute(typeSymbol); + HintName = CreateHintName(typeSymbol); - Fields = EnumerateFields(typeSymbol); + Fields = IsValid + ? EnumerateFields(typeSymbol) + : new List().ToEquatableReadOnlyList(); } - private static BitObjectAccessMode GetAccessMode(AttributeData attribute) + internal static bool TryGetBitOrder(AttributeData attribute, out BitOrder bitOrder) { + bitOrder = BitOrder.LeastSignificant; + if (attribute.ConstructorArguments.Length == 0 || + attribute.ConstructorArguments[0].Value is not int value || + value is < (int)BitOrder.LeastSignificant or > (int)BitOrder.MostSignificant) + { + return false; + } + + bitOrder = (BitOrder)value; + return true; + } + + internal static bool TryGetAccessMode(AttributeData attribute, out BitObjectAccessMode accessMode) + { + accessMode = BitObjectAccessMode.Checked; foreach (KeyValuePair argument in attribute.NamedArguments) { if (argument.Key == "AccessMode" && argument.Value.Value is int value) - return (BitObjectAccessMode)value; + { + if (value is < (int)BitObjectAccessMode.Checked or > (int)BitObjectAccessMode.Unsafe) + return false; + + accessMode = (BitObjectAccessMode)value; + return true; + } } - return BitObjectAccessMode.Checked; + return true; } private static bool GetGenerateBatchAccessors(AttributeData attribute) @@ -91,39 +120,36 @@ public void GenerateCSharpSource(StringBuilder sb) private EquatableReadOnlyList EnumerateFields(ITypeSymbol typeSymbol) { var output = new List(); + var reservedNames = new HashSet(typeSymbol.GetMembers().Select(member => member.Name)); + reservedNames.Add(typeSymbol.Name); foreach (IFieldSymbol field in typeSymbol.GetMembers().OfType()) { if (!IsValidFieldSymbol(field)) continue; - BackingFieldType backingType = field.Type.ToDisplayString() switch - { - "System.Memory" => BackingFieldType.Memory, - "System.ReadOnlyMemory" => BackingFieldType.Memory, - "System.Span" => BackingFieldType.Span, - "System.ReadOnlySpan" => BackingFieldType.Span, - "byte[]" => BackingFieldType.Span, - "byte*" => BackingFieldType.Pointer, - - _ when field.Type.IsSupportedIntegralType() => IsInlineArray ? - BackingFieldType.InlineArray : - BackingFieldType.Integral, - - _ => BackingFieldType.Invalid - }; + BackingFieldType backingType = BackingFieldModel.Classify(field); + if (backingType == BackingFieldType.Integral && IsInlineArray) + backingType = BackingFieldType.InlineArray; if (backingType == BackingFieldType.Invalid) continue; var backingModel = new BackingFieldModel(field, backingType); - CreateBitFieldModels(output, field, backingModel); + if (backingModel.IsRawPointer && AccessMode != BitObjectAccessMode.Unsafe) + continue; + + CreateBitFieldModels(output, field, backingModel, reservedNames); } return output.ToEquatableReadOnlyList(); } - private void CreateBitFieldModels(List output, IFieldSymbol backingField, BackingFieldModel backingModel) + private void CreateBitFieldModels( + List output, + IFieldSymbol backingField, + BackingFieldModel backingModel, + HashSet reservedNames) { int offset = 0; @@ -140,6 +166,7 @@ private void CreateBitFieldModels(List output, IFieldSymbol backi // padding fields are not generated if (bitField is not { FieldType: BitFieldType.Padding }) { + BitFieldType? declaredFieldType = bitField.FieldType; // invert the bit order if necessary if (bitField.ReverseBitOrder) bitField.BitOrder ^= BitOrder.MostSignificant; @@ -155,7 +182,36 @@ private void CreateBitFieldModels(List output, IFieldSymbol backi // Diagnostics are reported by analyzers. Do not generate an invalid // property when its primitive type could not be resolved. if (bitField.FieldType is null) + { + offset += bitField.BitCount; + continue; + } + + if (!IsSupportedFieldType(bitField.FieldType.Value) || + (bitField is EnumFieldModel && + (declaredFieldType is null || + bitField.BitCount > declaredFieldType.Value.GetBitWidth())) || + !SymbolFormatting.IsValidGeneratedName(bitField.Name) || + !HasValidModifiers(bitField, backingModel)) + { + offset += bitField.BitCount; + continue; + } + + string memberName = bitField.Name.StartsWith("@") + ? bitField.Name.Substring(1) + : bitField.Name; + if (!reservedNames.Add(memberName)) + { + offset += bitField.BitCount; + continue; + } + + if (!IsValidLayout(bitField, backingField, backingModel, offset)) + { + offset += bitField.BitCount; continue; + } // add to list of fields to generate output.Add(bitField); @@ -169,13 +225,26 @@ private void CreateBitFieldModels(List output, IFieldSymbol backi { string? attributeType = attribute.AttributeClass?.ToDisplayString(); - return attributeType switch + BitFieldModel? model = attributeType switch { StringConstants.BitFieldAttributeFullName => new IntegralFieldModel(attribute, processor), StringConstants.BooleanFieldAttributeFullName => new BooleanFieldModel(attribute, processor), StringConstants.EnumFieldAttributeFullName => new EnumFieldModel(attribute, processor), _ => null }; + + if (model is not null) + return model; + + for (INamedTypeSymbol? current = attribute.AttributeClass?.BaseType; + current is not null; + current = current.BaseType) + { + if (current.ToDisplayString() == StringConstants.BitFieldAttributeFullName) + return new IntegralFieldModel(attribute, processor); + } + + return null; } private static bool HasInlineArrayAttribute(ITypeSymbol typeSymbol) @@ -186,6 +255,80 @@ private static bool HasInlineArrayAttribute(ITypeSymbol typeSymbol) .ConstructorArguments[0].Value > 0; } + private static bool IsSupportedFieldType(BitFieldType fieldType) => fieldType is + BitFieldType.SByte or BitFieldType.Byte or + BitFieldType.Int16 or BitFieldType.UInt16 or + BitFieldType.Int32 or BitFieldType.UInt32 or + BitFieldType.Int64 or BitFieldType.UInt64 or + BitFieldType.IntPtr or BitFieldType.UIntPtr or + BitFieldType.Boolean; + + private bool HasValidModifiers(BitFieldModel bitField, BackingFieldModel backing) + { + const BitFieldModifiers knownModifiers = + BitFieldModifiers.AccessorMask | + BitFieldModifiers.ReadOnly | + BitFieldModifiers.InitOnly | + BitFieldModifiers.Required; + if ((bitField.Modifiers & ~knownModifiers) != 0) + return false; + + if (IsStruct && + (bitField.Modifiers & BitFieldModifiers.AccessorMask) is + BitFieldModifiers.Protected or + BitFieldModifiers.ProtectedInternal or + BitFieldModifiers.PrivateProtected) + { + return false; + } + + return !bitField.Modifiers.HasFlag(BitFieldModifiers.Required) || + (!bitField.Modifiers.HasFlag(BitFieldModifiers.ReadOnly) && + !backing.IsReadOnly && + !backing.IsReadOnlyStorage); + } + + private bool IsValidLayout( + BitFieldModel bitField, + IFieldSymbol backingField, + BackingFieldModel backingModel, + int offset) + { + if (bitField.BitCount <= 0 || bitField.BitCount > bitField.FieldType!.Value.GetBitWidth()) + return false; + + int capacity = backingModel.Type switch + { + BackingFieldType.Integral => backingField.Type.SpecialType.GetBitWidth(), + BackingFieldType.Pointer when backingModel.FixedSize > 0 => backingModel.FixedSize * 8, + BackingFieldType.InlineArray => GetInlineArrayLength(backingField.ContainingType) * backingField.Type.SpecialType.GetBitWidth(), + _ => int.MaxValue + }; + + return offset <= capacity - bitField.BitCount; + } + + private static int GetInlineArrayLength(ITypeSymbol typeSymbol) => + (int?)typeSymbol.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == StringConstants.InlineArrayAttributeFullName)? + .ConstructorArguments[0].Value ?? 0; + + private static string CreateHintName(INamedTypeSymbol typeSymbol) + { + string identity = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + uint hash = 2166136261; + foreach (char value in identity) + { + hash ^= value; + hash *= 16777619; + } + + string safeName = new(typeSymbol.MetadataName + .Select(character => char.IsLetterOrDigit(character) ? character : '_') + .ToArray()); + return $"BitsKit.{safeName}.{hash:X8}.g.cs"; + } + private static bool IsValidFieldSymbol(IFieldSymbol member) => member is { CanBeReferencedByName: true, diff --git a/BitsKit.Tests/AnalyzerTests.cs b/BitsKit.Tests/AnalyzerTests.cs index 26364b3..d943dae 100644 --- a/BitsKit.Tests/AnalyzerTests.cs +++ b/BitsKit.Tests/AnalyzerTests.cs @@ -72,6 +72,116 @@ public partial struct InferredFieldType Assert.IsFalse(diagnostics.Any(d => d.Id == "BITSKIT003")); } + [TestMethod] + [DataRow("[BitObject((BitOrder)123)]", "BitOrder")] + [DataRow("[BitObject(BitOrder.LeastSignificant, AccessMode = (BitObjectAccessMode)123)]", "AccessMode")] + public async Task InvalidBitObjectOptionsReportBitsKit007(string attribute, string option) + { + string source = $$""" + {{attribute}} + public partial struct InvalidOptions + { + [BitField("Value", 1)] + public byte Backing; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Diagnostic diagnostic = diagnostics.Single(d => d.Id == "BITSKIT007"); + StringAssert.Contains(diagnostic.GetMessage(), option); + } + + [TestMethod] + public async Task UnsupportedBackingReportsBitsKit008() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public partial struct UnsupportedBacking + { + [BitField("Value", 1)] + public string Backing; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Assert.AreEqual(1, diagnostics.Count(d => d.Id == "BITSKIT008")); + } + + [TestMethod] + public async Task CheckedRawPointerReportsBitsKit009() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public unsafe partial struct CheckedPointer + { + [BitField("Value", 8, BitFieldType.Byte)] + public byte* Backing; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Assert.AreEqual(1, diagnostics.Count(d => d.Id == "BITSKIT009")); + } + + [TestMethod] + public async Task InvalidAndDuplicateNamesReportDiagnostics() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public partial struct InvalidNames + { + [BitField("not a name", 1)] + [BitField("Value", 1)] + [BitField("Value", 1)] + public byte Backing; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Assert.AreEqual(1, diagnostics.Count(d => d.Id == "BITSKIT010")); + Assert.AreEqual(1, diagnostics.Count(d => d.Id == "BITSKIT011")); + } + + [TestMethod] + public async Task InvalidWidthsAndLayoutsReportDiagnostics() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public partial struct InvalidLayouts + { + [BitField("TooWideForType", 9, BitFieldType.Byte)] + public byte[] DynamicBacking; + + [BitField("UnknownType", 1, (BitFieldType)123)] + public byte[] UnknownTypeBacking; + + [BitField(7)] + [BitField("TooWideForBacking", 2)] + public byte FixedBacking; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Assert.AreEqual(2, diagnostics.Count(d => d.Id == "BITSKIT012")); + Assert.AreEqual(1, diagnostics.Count(d => d.Id == "BITSKIT013")); + } + + [TestMethod] + public async Task InvalidRequiredReadonlyCombinationReportsBitsKit014() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public partial struct InvalidModifiers + { + [BitField("Value", 1, Modifiers = BitFieldModifiers.Required | BitFieldModifiers.ReadOnly)] + public byte Backing; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Assert.AreEqual(1, diagnostics.Count(d => d.Id == "BITSKIT014")); + } + private static async Task> GetDiagnosticsAsync(string source) { var references = AppDomain.CurrentDomain.GetAssemblies() @@ -87,7 +197,7 @@ private static async Task> GetDiagnosticsAsync(string allowUnsafe: true)); return await compilation - .WithAnalyzers([new BitFieldAnalyser()]) + .WithAnalyzers([new BitFieldAnalyser(), new BitObjectAnalyser()]) .GetAnalyzerDiagnosticsAsync(); } } diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index 14274cf..805beb3 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -387,6 +387,22 @@ public unsafe partial struct UnsafeFixedAccessorStruct public fixed byte Backing[16]; } +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +public unsafe partial struct UnsafePointerAccessorStruct +{ + [BitField(11)] + [BitField("Value", 20, BitFieldType.UInt32)] + public byte* Backing; +} + +[BitObject(BitOrder.LeastSignificant)] +public partial struct NullableArrayAccessorStruct +{ + [BitField(8)] + [BitField("Value", 16, BitFieldType.UInt16)] + public byte[]? Backing; +} + [BitObject(BitOrder.LeastSignificant)] public ref partial struct SpecializedSpanAccessorStruct { diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index fd72756..0b85f19 100644 --- a/BitsKit.Tests/GeneratorTests.cs +++ b/BitsKit.Tests/GeneratorTests.cs @@ -620,6 +620,18 @@ public void OffsetArrayAccessorsMatchReferenceBits() } } + [TestMethod] + public void NullableArrayBackingGeneratesWorkingAccessors() + { + byte[] bytes = new byte[3]; + var model = new NullableArrayAccessorStruct { Backing = bytes }; + + model.Value = 0xA55A; + + Assert.AreEqual((ushort)0xA55A, model.Value); + Assert.AreEqual((ushort)0xA55A, BitPrimitives.ReadUInt16LSB(bytes, 8, 16)); + } + [TestMethod] public void ReadOnlyMemberTest() { @@ -660,7 +672,7 @@ public Int32 Generated20 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } [TestMethod] @@ -703,7 +715,7 @@ public Int32 Generated20 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } [TestMethod] @@ -860,7 +872,7 @@ public UIntPtr Generated19 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } [TestMethod] @@ -952,7 +964,7 @@ private protected Int32 Generated0A string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); #endif } @@ -1013,7 +1025,7 @@ public unsafe System.Boolean Generated30 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } [TestMethod] @@ -1094,7 +1106,7 @@ public unsafe BitsKit.Tests.TestEnum Generated31 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } [TestMethod] @@ -1131,7 +1143,7 @@ public Int32 Generated10 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } #if NET8_0_OR_GREATER @@ -1183,12 +1195,146 @@ public System.Boolean Generated03 string? sourceOutput = GenerateSourceAndTest(source); - Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); + Assert.IsTrue(Helpers.StrEqualGeneratedSource(sourceOutput, expected)); } #endif - private static string? GenerateSourceAndTest(string source) + [TestMethod] + public void EscapedIdentifiersAndQualifiedFrameworkNamesCompile() + { + const string source = """ + namespace @event + { + public sealed class Byte { } + public static class BitPrimitives { } + public static class BinaryPrimitives { } + public static class MemoryMarshal { } + public ref struct Span { } + + [BitObject(BitOrder.LeastSignificant, GenerateBatchAccessors = true)] + public partial struct @class + { + [BitField("@event", 16, BitFieldType.UInt16)] + public byte[] @namespace; + } + } + """; + + string? output = GenerateSourceAndTest(source); + + StringAssert.Contains(output, "namespace @event"); + StringAssert.Contains(output, "partial struct @class"); + StringAssert.Contains(output, "global::System.UInt16 @event"); + StringAssert.Contains(output, "global::System.Buffers.Binary.BinaryPrimitives"); + StringAssert.Contains(output, "ReadeventBatch"); + StringAssert.Contains(output, "@namespace"); + } + + [TestMethod] + public void InvalidBitObjectDoesNotSuppressValidGeneration() + { + const string source = """ + [BitObject((BitOrder)123)] + public partial struct InvalidObject + { + [BitField("Missing", 1)] + public byte Backing; + } + + [BitObject(BitOrder.LeastSignificant)] + public partial struct ValidObject + { + [BitField("Value", 1)] + public byte Backing; + } + """; + + string? output = GenerateSourceAndTest(source); + + StringAssert.Contains(output, "partial struct ValidObject"); + StringAssert.Contains(output, "Value"); + Assert.IsFalse(output.Contains("partial struct InvalidObject")); + } + + [TestMethod] + public void TypesWithTheSameNameGenerateIndependentSources() + { + const string source = """ + namespace First + { + [BitObject(BitOrder.LeastSignificant)] + public partial struct Packet + { + [BitField("FirstValue", 1)] + public byte Backing; + } + } + + namespace Second + { + [BitObject(BitOrder.MostSignificant)] + public partial struct Packet + { + [BitField("SecondValue", 1)] + public byte Backing; + } + } + """; + + string? output = GenerateSourceAndTest(source, expectedSourceCount: 2); + + StringAssert.Contains(output, "namespace First"); + StringAssert.Contains(output, "FirstValue"); + StringAssert.Contains(output, "namespace Second"); + StringAssert.Contains(output, "SecondValue"); + } + + [TestMethod] + public void InvalidFieldDoesNotSuppressOtherAccessors() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public partial struct PartiallyValidObject + { + [BitField("not a member name", 1)] + public byte InvalidBacking; + + [BitField("Value", 8)] + public byte ValidBacking; + } + """; + + string? output = GenerateSourceAndTest(source); + + StringAssert.Contains(output, "Value"); + Assert.IsFalse(output.Contains("not a member name")); + } + + [TestMethod] + public void DerivedBitFieldAttributeGeneratesAccessor() + { + const string source = """ + public sealed class CustomFieldAttribute : BitFieldAttribute + { + public CustomFieldAttribute(string name, byte size, BitFieldType type) + : base(name, size, type) { } + } + + [BitObject(BitOrder.LeastSignificant)] + public partial struct CustomAttributeObject + { + [CustomField("Value", 8, BitFieldType.Byte)] + public byte[] Backing; + } + """; + + string? output = GenerateSourceAndTest(source); + + StringAssert.Contains(output, "global::System.Byte Value"); + } + + private static string? GenerateSourceAndTest(string source, int expectedSourceCount = 1) { var references = AppDomain.CurrentDomain.GetAssemblies() .Where(assembly => !assembly.IsDynamic) @@ -1222,17 +1368,19 @@ public System.Boolean Generated03 } AssertGeneratorDidntRun(run2Result.Results[0].TrackedSteps["Main"]); - Assert.AreEqual(1, run1Result.GeneratedTrees.Length); + Assert.AreEqual(expectedSourceCount, run1Result.GeneratedTrees.Length); Assert.IsTrue(run1Result.Diagnostics.IsEmpty); GeneratorRunResult generatorResult = run1Result.Results[0]; Assert.AreEqual(typeof(BitObjectGenerator), generatorResult.Generator.GetGeneratorType()); Assert.IsTrue(generatorResult.Diagnostics.IsEmpty); - Assert.AreEqual(1, generatorResult.GeneratedSources.Length); + Assert.AreEqual(expectedSourceCount, generatorResult.GeneratedSources.Length); Assert.IsTrue(generatorResult.Exception is null); - string sourceOutput = generatorResult.GeneratedSources[0].SourceText.ToString(); - return TruncateUsings(sourceOutput); + return string.Join( + "\n", + generatorResult.GeneratedSources.Select(sourceOutput => + TruncateUsings(sourceOutput.SourceText.ToString()))); } private static GeneratorDriverRunResult RunGenerator( diff --git a/BitsKit.Tests/Helpers.cs b/BitsKit.Tests/Helpers.cs index 9401150..c533d10 100644 --- a/BitsKit.Tests/Helpers.cs +++ b/BitsKit.Tests/Helpers.cs @@ -118,4 +118,21 @@ public static bool StrEqualExWhiteSpace(string? s1, string? s2) return string.Equals(normalisedS1, normalisedS2); } + + public static bool StrEqualGeneratedSource(string? actual, string? expected) + { + if (actual is not null) + { + actual = actual + .Replace("global::System.Boolean", "System.Boolean") + .Replace("global::System.Runtime.InteropServices.", string.Empty) + .Replace("global::System.Runtime.CompilerServices.", string.Empty) + .Replace("global::System.Buffers.Binary.", string.Empty) + .Replace("global::System.", string.Empty) + .Replace("global::BitsKit.Primitives.", string.Empty) + .Replace("global::", string.Empty); + } + + return StrEqualExWhiteSpace(actual, expected); + } } diff --git a/BitsKit.Tests/UnsafeAccessTests.cs b/BitsKit.Tests/UnsafeAccessTests.cs index c36c5e2..ee8652e 100644 --- a/BitsKit.Tests/UnsafeAccessTests.cs +++ b/BitsKit.Tests/UnsafeAccessTests.cs @@ -135,6 +135,11 @@ public unsafe void UnsafeGenerationSupportsEveryByteAddressableBackingKind() fixedModel.Value = Expected; Assert.AreEqual(Expected, fixedModel.Value); + byte* pointerBuffer = stackalloc byte[16]; + var pointerModel = new UnsafePointerAccessorStruct { Backing = pointerBuffer }; + pointerModel.Value = Expected; + Assert.AreEqual(Expected, pointerModel.Value); + var inlineModel = new UnsafeInlineArrayAccessorStruct(); inlineModel.Value = Expected; Assert.AreEqual(Expected, inlineModel.Value); diff --git a/CHANGELOG.md b/CHANGELOG.md index 272b0b4..f843fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,18 @@ Notable changes to the community-maintained fork are documented here. This proje - `UnsafeBitPrimitives` exposes the raw-reference operations used by unsafe generated accessors. - `BitBatchPrimitives` provides allocation-free contiguous and record-strided reads and writes for every supported integral type, Boolean values, and both bit orders. - `BitObjectAttribute.GenerateBatchAccessors` generates strongly typed packed and strided helpers for integral, Boolean, and enum fields. +- Generator diagnostics `BITSKIT007` through `BITSKIT014` validate options, backing types, raw pointers, generated names, member conflicts, widths, fixed layouts, and modifiers. + +### Changed + +- The source generator emits one isolated file per bit object, recognizes backing storage from Roslyn symbols instead of display strings, supports nullable byte-array declarations, and fully qualifies emitted framework and library references. +- Raw `byte*` backing fields require explicit `BitObjectAccessMode.Unsafe`; fixed buffers retain checked access. ### Fixed - Array-backed generated accessors compile when an optimized field begins at a nonzero byte offset. +- Malformed bit objects no longer suppress generation for unrelated valid types. +- Escaped C# identifiers, keyword namespaces, derived compatible bit-field attributes, and unsafe raw-pointer accessors generate valid source. ## 1.5.0 - 2026-07-21 diff --git a/README.md b/README.md index b4812a5..f36ac03 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,21 @@ Due to the nature of source generators, the user must generate the backing field `byte[], fixed byte[], byte*, Span, ReadOnlySpan, Memory, ReadOnlyMemory` +Fixed byte buffers have a compile-time capacity and support checked access. A raw `byte*` does not carry a length, so it is accepted only when the bit object explicitly selects unsafe access: + +```c# +[BitObject( + BitOrder.LeastSignificant, + AccessMode = BitObjectAccessMode.Unsafe)] +public unsafe partial struct NativePacket +{ + [BitField("Id", 12, BitFieldType.UInt16)] + public byte* Data; +} +``` + +The caller must keep `Data` non-null and ensure that it addresses enough accessible bytes for every generated field. Use a fixed buffer, array, span, or memory when checked bounds validation is required. + Bit fields are declared using the `[BitFieldAttribute]` attribute which describes their name, size, bit order and properties. Each attribute defines a new bit-field sequential from the previous. A backing field can have as many bit-fields as desired, limited only by field boundaries. **Notes:** @@ -363,6 +378,16 @@ BITSKIT003 | Error | Cannot infer FieldType BITSKIT004 | Warning | Conflicting accessibility modifiers BITSKIT005 | Warning | Conflicting setter modifiers BITSKIT006 | Error | Enum type argument expected +BITSKIT007 | Error | Invalid bit-object option +BITSKIT008 | Error | Unsupported backing-field type +BITSKIT009 | Error | Raw pointer backing requires unsafe access +BITSKIT010 | Error | Invalid generated member name +BITSKIT011 | Error | Generated member conflicts with an existing or generated member +BITSKIT012 | Error | Bit-field width exceeds its value type +BITSKIT013 | Error | Bit-field layout exceeds fixed backing storage +BITSKIT014 | Error | Invalid modifier combination + +Generation is isolated per bit object. An invalid declaration reports its own diagnostic without suppressing generated accessors for unrelated valid types. Generated type, namespace, field, enum, and framework references are escaped and fully qualified so legal C# keywords and consumer-defined type names do not corrupt generated source. ### IO Classes There are a number of IO types available under the `BitsKit.IO` namespace built to sequentially read/write regions of bit data. Each of these classes expose all the `BitPrimitives` methods whilst supporting seeking and writing in-place.