diff --git a/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj b/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj index efb4597..d4b0263 100644 --- a/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj +++ b/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj @@ -4,9 +4,7 @@ Exe net6.0;net7.0;net8.0 enable - enable BitsKit.Benchmarks.Program - AnyCPU;x86;x64 diff --git a/BitsKit.Generator/Analysers/BitFieldAnalyser.cs b/BitsKit.Generator/Analysers/BitFieldAnalyser.cs new file mode 100644 index 0000000..42693c9 --- /dev/null +++ b/BitsKit.Generator/Analysers/BitFieldAnalyser.cs @@ -0,0 +1,79 @@ +using System.Collections.Immutable; +using BitsKit.Generator.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace BitsKit.Generator.Analysers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class BitFieldAnalyser : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.ConflictingAccessors, + DiagnosticDescriptors.ConflictingSetters, + DiagnosticDescriptors.EnumTypeExpected + ]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + // todo: reintroduce FieldTypeNotDefined + // but i'm not sure how it was triggered + + context.RegisterCompilationStartAction(context => + { + var bitFieldAttribute = context.Compilation.GetTypeByMetadataName(StringConstants.BitFieldAttributeFullName); + if (bitFieldAttribute == null) return; + + context.RegisterSymbolAction(context => + { + var fieldSymbol = (IFieldSymbol)context.Symbol; + if (!fieldSymbol.TryGetAttributesWithBaseType(bitFieldAttribute, out var thisAttributes)) + { + return; + } + + foreach (var thisAttribute in thisAttributes) + { + // todo: code doesn't read the processor, just stores. so we don't need to give one + var bitField = TypeSymbolProcessor.CreateBitFieldFromAttribute(thisAttribute, null!); + if (bitField == null) continue; + + var accessorModifiers = bitField.Modifiers & BitFieldModifiers.AccessorMask; + if ((accessorModifiers & (accessorModifiers - 1)) != 0 && + accessorModifiers != BitFieldModifiers.ProtectedInternal && + accessorModifiers != BitFieldModifiers.PrivateProtected) + { + // "protected internal" and "private protected" combos are allowed + + context.ReportDiagnostic( + Diagnostic.Create(DiagnosticDescriptors.ConflictingAccessors, thisAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), fieldSymbol.ContainingType.Name, bitField.Name) + ); + } + + var setterModifiers = bitField.Modifiers & BitFieldModifiers.SetterMask; + if ((setterModifiers & (setterModifiers - 1)) != 0) + { + context.ReportDiagnostic( + Diagnostic.Create(DiagnosticDescriptors.ConflictingSetters, thisAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), fieldSymbol.ContainingType.Name, bitField.Name) + ); + } + + if (bitField is EnumFieldModel) + { + var enumFieldAttrModel = new EnumFieldAttributeModel(thisAttribute); + if (enumFieldAttrModel.EnumType != null && enumFieldAttrModel.EnumType.EnumUnderlyingType == null) + { + context.ReportDiagnostic( + Diagnostic.Create(DiagnosticDescriptors.EnumTypeExpected, thisAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), fieldSymbol.ContainingType.Name, bitField.Name) + ); + } + } + } + }, SymbolKind.Field); + }); + } + } +} \ No newline at end of file diff --git a/BitsKit.Generator/Analysers/BitObjectAnalyser.cs b/BitsKit.Generator/Analysers/BitObjectAnalyser.cs new file mode 100644 index 0000000..9827efd --- /dev/null +++ b/BitsKit.Generator/Analysers/BitObjectAnalyser.cs @@ -0,0 +1,59 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace BitsKit.Generator.Analysers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class BitObjectAnalyser : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.MustBePartial, + DiagnosticDescriptors.NestedNotAllowed + ]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + var bitObjectAttribute = context.Compilation.GetTypeByMetadataName(StringConstants.BitObjectAttributeFullName); + if (bitObjectAttribute == null) return; + + context.RegisterSymbolAction(context => + { + 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); + }); + } + } +} \ No newline at end of file diff --git a/BitsKit.Generator/BitObjectGenerator.cs b/BitsKit.Generator/BitObjectGenerator.cs index c6e2f71..f3827ee 100644 --- a/BitsKit.Generator/BitObjectGenerator.cs +++ b/BitsKit.Generator/BitObjectGenerator.cs @@ -19,13 +19,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) StringConstants.BitObjectAttributeFullName, predicate: IsValidTypeDeclaration, transform: ProcessSyntaxNode) - .WithComparer(TypeSymbolProcessorComparer.Default) - .Where(x => x is not null)!; - - IncrementalValueProvider<(Compilation, ImmutableArray)> model = context - .CompilationProvider - .Combine(typeDeclarations.Collect()); + .Where(x => x is not null) + .WithTrackingName("Main")!; + var model = typeDeclarations.Collect(); context.RegisterSourceOutput(model, GenerateSourceCode); } @@ -43,18 +40,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .GetAttributes() .Single(a => a.AttributeClass?.ToDisplayString() == StringConstants.BitObjectAttributeFullName); - return new(typeSymbol, typeDeclaration, attribute); + return new(typeSymbol, attribute); } - private static void GenerateSourceCode(SourceProductionContext context, (Compilation _, ImmutableArray Processors) result) + private static void GenerateSourceCode(SourceProductionContext context, ImmutableArray processors) { - if (result.Processors.Length == 0) + if (processors.Length == 0) return; StringBuilder stringBuilder = new(StringConstants.Header); // group the objects by their respective namespace - var namespaceGroups = result.Processors.GroupBy(x => x.Namespace); + var namespaceGroups = processors.GroupBy(x => x.Namespace); foreach (var namespaceGroup in namespaceGroups) { @@ -63,20 +60,11 @@ private static void GenerateSourceCode(SourceProductionContext context, (Compila // print the current namespace if (namespaceGroup.Key is not null) stringBuilder - .AppendLine($"namespace {namespaceGroup.Key.Name.ToFullString()}") + .AppendLine($"namespace {namespaceGroup.Key}") .AppendLine("{"); foreach (TypeSymbolProcessor processor in namespaceGroup) { - // evaluate if there are actually any valid fields - if (processor.EnumerateFields() == 0) - continue; - - // check and report any compilation issues and prevent - // code generation for this type if there are - if (processor.ReportCompilationIssues(context)) - continue; - processor.GenerateCSharpSource(stringBuilder); } @@ -94,14 +82,3 @@ private static void GenerateSourceCode(SourceProductionContext context, (Compila private static bool IsValidTypeDeclaration(SyntaxNode node, CancellationToken _) => node is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax; } - -file class TypeSymbolProcessorComparer : IEqualityComparer -{ - public static TypeSymbolProcessorComparer Default { get; } = new(); - - public bool Equals(TypeSymbolProcessor? x, TypeSymbolProcessor? y) => - SymbolEqualityComparer.Default.Equals(x?.TypeSymbol, y?.TypeSymbol); - - public int GetHashCode(TypeSymbolProcessor? obj) => - SymbolEqualityComparer.Default.GetHashCode(obj?.TypeSymbol); -} diff --git a/BitsKit.Generator/BitsKit.Generator.csproj b/BitsKit.Generator/BitsKit.Generator.csproj index 7872e93..dc5f7b5 100644 --- a/BitsKit.Generator/BitsKit.Generator.csproj +++ b/BitsKit.Generator/BitsKit.Generator.csproj @@ -3,12 +3,7 @@ netstandard2.0 false - preview - enable - true - Generated true - AnyCPU;x86;x64 True True @@ -24,8 +19,13 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/BitsKit.Generator/DiagnosticValidator.cs b/BitsKit.Generator/DiagnosticValidator.cs index ac1d9ed..c03d03b 100644 --- a/BitsKit.Generator/DiagnosticValidator.cs +++ b/BitsKit.Generator/DiagnosticValidator.cs @@ -1,7 +1,4 @@ -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis; using BitsKit.Generator.Models; namespace BitsKit.Generator; @@ -19,27 +16,7 @@ public static bool ReportDiagnostic(SourceProductionContext context, DiagnosticD return descriptor is { DefaultSeverity: DiagnosticSeverity.Error }; } - public static bool IsNotPartial(SourceProductionContext context, TypeDeclarationSyntax typeDeclaration, string typeName) - { - return !typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) - && ReportDiagnostic( - context, - DiagnosticDescriptors.MustBePartial, - typeDeclaration.GetLocation(), - typeName); - } - - public static bool IsNested(SourceProductionContext context, TypeDeclarationSyntax typeDeclaration, string typeName) - { - return typeDeclaration.Parent is TypeDeclarationSyntax - && ReportDiagnostic( - context, - DiagnosticDescriptors.NestedNotAllowed, - typeDeclaration.GetLocation(), - typeName); - } - - public static bool HasMissingFieldType(SourceProductionContext context, BitFieldModel bitField, string typeName) + /*public static bool HasMissingFieldType(SourceProductionContext context, BitFieldModel bitField, string typeName) { return bitField.FieldType is null && ReportDiagnostic( @@ -48,46 +25,5 @@ public static bool HasMissingFieldType(SourceProductionContext context, BitField bitField.BackingField.Locations[0], typeName, bitField.Name); - } - - public static bool HasConflictingAccessors(SourceProductionContext context, BitFieldModel bitField, string typeName) - { - BitFieldModifiers modifiers = bitField.Modifiers & BitFieldModifiers.AccessorMask; - - // "protected internal" and "private protected" combos are allowed - if (modifiers is BitFieldModifiers.ProtectedInternal or BitFieldModifiers.PrivateProtected) - return false; - - return (modifiers & (modifiers - 1)) != 0 - && ReportDiagnostic( - context, - DiagnosticDescriptors.ConflictingAccessors, - bitField.BackingField.Locations[0], - typeName, - bitField.Name); - } - - public static bool HasConflictingSetters(SourceProductionContext context, BitFieldModel bitField, string typeName) - { - BitFieldModifiers modifiers = bitField.Modifiers & BitFieldModifiers.SetterMask; - - return (modifiers & (modifiers - 1)) != 0 - && ReportDiagnostic( - context, - DiagnosticDescriptors.ConflictingSetters, - bitField.BackingField.Locations[0], - typeName, - bitField.Name); - } - - public static bool IsNotEnumType(SourceProductionContext context, EnumFieldModel enumField, string typeName) - { - return enumField.EnumType is not { EnumUnderlyingType: { } } - && ReportDiagnostic( - context, - DiagnosticDescriptors.EnumTypeExpected, - enumField.BackingField.Locations[0], - typeName, - enumField.Name); - } + }*/ } diff --git a/BitsKit.Generator/EquatableReadOnlyList.cs b/BitsKit.Generator/EquatableReadOnlyList.cs new file mode 100644 index 0000000..29684b9 --- /dev/null +++ b/BitsKit.Generator/EquatableReadOnlyList.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace BitsKit.Generator +{ + [ExcludeFromCodeCoverage] + public static class EquatableReadOnlyList + { + public static EquatableReadOnlyList ToEquatableReadOnlyList(this IEnumerable enumerable) + => new(enumerable is IReadOnlyList l ? l : [.. enumerable]); + } + + /// + /// A wrapper for IReadOnlyList that provides value equality support for the wrapped list. + /// + [ExcludeFromCodeCoverage] + public readonly struct EquatableReadOnlyList( + IReadOnlyList? collection + ) : IEquatable>, IReadOnlyList + { + private IReadOnlyList Collection => collection ?? []; + + public bool Equals(EquatableReadOnlyList other) + => this.SequenceEqual(other); + + public override bool Equals(object? obj) + => obj is EquatableReadOnlyList other && Equals(other); + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var item in Collection) + hashCode.Add(item); + + return hashCode.ToHashCode(); + } + + IEnumerator IEnumerable.GetEnumerator() + => Collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => Collection.GetEnumerator(); + + public int Count => Collection.Count; + public T this[int index] => Collection[index]; + + public static bool operator ==(EquatableReadOnlyList left, EquatableReadOnlyList right) + => left.Equals(right); + + public static bool operator !=(EquatableReadOnlyList left, EquatableReadOnlyList right) + => !left.Equals(right); + } +} \ No newline at end of file diff --git a/BitsKit.Generator/Models/BackingFieldModel.cs b/BitsKit.Generator/Models/BackingFieldModel.cs new file mode 100644 index 0000000..2b43ade --- /dev/null +++ b/BitsKit.Generator/Models/BackingFieldModel.cs @@ -0,0 +1,24 @@ +using Microsoft.CodeAnalysis; + +namespace BitsKit.Generator.Models +{ + internal record BackingFieldModel + { + public readonly string Name; + public readonly string TypeString; + public readonly int FixedSize; + public readonly bool IsReadOnly; + + public readonly BackingFieldType Type; + + public BackingFieldModel(IFieldSymbol fieldSymbol, BackingFieldType type) + { + Name = fieldSymbol.Name; + TypeString = fieldSymbol.Type.ToDisplayString(); + FixedSize = fieldSymbol.FixedSize; + IsReadOnly = fieldSymbol.IsReadOnly; + + Type = type; + } + } +} \ No newline at end of file diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index abe97ef..b96f2f1 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -4,23 +4,31 @@ namespace BitsKit.Generator.Models; -internal abstract class BitFieldModel +internal abstract record BitFieldModel { public string Name { get; set; } = null!; public BitFieldType? FieldType { get; set; } public string? ReturnType { get; set; } - public IFieldSymbol BackingField { get; set; } = null!; - public BackingFieldType BackingFieldType { get; set; } + public BackingFieldModel BackingField { get; set; } = null!; + public BackingFieldType BackingFieldType => BackingField.Type; public int BitOffset { get; set; } public int BitCount { get; set; } public BitOrder BitOrder { get; set; } public bool ReverseBitOrder { get; } public BitFieldModifiers Modifiers { get; } - public TypeSymbolProcessor TypeSymbol { get; } + + private readonly bool _containingTypeIsStruct; - public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbol) + public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbol) { - TypeSymbol = typeSymbol; + if (typeSymbol != null) + { + // todo: for now, analyser passes null + // these fields don't matter for it + + _containingTypeIsStruct = typeSymbol.IsStruct; + BitOrder = typeSymbol.DefaultBitOrder; + } for (int i = 0; i < attributeData.NamedArguments.Length; i++) { @@ -54,7 +62,7 @@ public void GenerateCSharpSource(StringBuilder sb) GetPropertyTemplate(), accessor, Modifiers.HasFlag(BitFieldModifiers.Required) ? "required" : "", - ReturnType ?? FieldType.ToString(), + ReturnType ?? FieldType?.ToString(), Name) .AppendIndentedLine(2, "{"); @@ -64,13 +72,13 @@ public void GenerateCSharpSource(StringBuilder sb) GetGetterTemplate(), SupportsReadOnlyGetter() ? "readonly" : "", "get", - FieldType!.Value.ToIntegralName(), + FieldType?.ToIntegralName(), BitOrder.ToShortName(), BackingField.Name, BitOffset, BitCount, BackingField.FixedSize, - BackingField.Type); + BackingField.TypeString); } // setter @@ -80,29 +88,19 @@ public void GenerateCSharpSource(StringBuilder sb) GetSetterTemplate(), "", Modifiers.HasFlag(BitFieldModifiers.InitOnly) ? "init" : "set", - FieldType!.Value.ToIntegralName(), + FieldType?.ToIntegralName(), BitOrder.ToShortName(), BackingField.Name, BitOffset, BitCount, BackingField.FixedSize, - BackingField.Type); + BackingField.TypeString); } sb.AppendIndentedLine(2, "}") .AppendLine(); } - /// - /// Diagnoses if the field will produce non-compilable or erroneous code - /// - public virtual bool HasCompilationIssues(SourceProductionContext context, TypeSymbolProcessor processor) - { - return DiagnosticValidator.HasMissingFieldType(context, this, processor.TypeSymbol.Name) | - DiagnosticValidator.HasConflictingAccessors(context, this, processor.TypeSymbol.Name) | - DiagnosticValidator.HasConflictingSetters(context, this, processor.TypeSymbol.Name); - } - /// /// Generates a template for the property accessors, type and name /// @@ -188,7 +186,7 @@ BackingFieldType.Span or /// protected bool IsReadOnly() { - string backingType = BackingField.Type.ToDisplayString(); + string backingType = BackingField.TypeString; return BackingField.IsReadOnly || backingType == "System.ReadOnlySpan" || @@ -202,7 +200,7 @@ protected bool IsReadOnly() /// private bool SupportsReadOnlyGetter() { - return TypeSymbol.TypeDeclaration.IsStruct() && + return _containingTypeIsStruct && BackingFieldType != BackingFieldType.Pointer && BackingFieldType != BackingFieldType.InlineArray && !IsReadOnly(); diff --git a/BitsKit.Generator/Models/BooleanFieldModel.cs b/BitsKit.Generator/Models/BooleanFieldModel.cs index f09ba99..beda46d 100644 --- a/BitsKit.Generator/Models/BooleanFieldModel.cs +++ b/BitsKit.Generator/Models/BooleanFieldModel.cs @@ -5,9 +5,9 @@ namespace BitsKit.Generator.Models; /// /// A model representing a boolean bit-field /// -internal sealed class BooleanFieldModel : BitFieldModel +internal sealed record BooleanFieldModel : BitFieldModel { - public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbol) : base(attributeData, typeSymbol) + public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbol) : base(attributeData, typeSymbol) { switch (attributeData.ConstructorArguments.Length) { diff --git a/BitsKit.Generator/Models/EnumFieldModel.cs b/BitsKit.Generator/Models/EnumFieldModel.cs index 8aea47c..f8f3238 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -1,15 +1,18 @@ -using Microsoft.CodeAnalysis; +using System.IO; +using Microsoft.CodeAnalysis; namespace BitsKit.Generator.Models; /// -/// A model representing an enum bit-field +/// Parsed data from EnumFieldAttribute. Intermediate data only (don't store in incremental pipeline) /// -internal sealed class EnumFieldModel : BitFieldModel +internal class EnumFieldAttributeModel { + public string? Name { get; } public INamedTypeSymbol? EnumType { get; } - - public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbol) : base(attributeData, typeSymbol) + public int BitCount { get; set; } + + public EnumFieldAttributeModel(AttributeData attributeData) { switch(attributeData.ConstructorArguments.Length) { @@ -22,22 +25,29 @@ public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbo EnumType = attributeData.ConstructorArguments[2].Value as INamedTypeSymbol; break; default: - return; + throw new InvalidDataException($"unknown number of enum attribute constructor arguments: {attributeData.ConstructorArguments.Length}"); } + } +} - ReturnType = EnumType?.ToDisplayString(); - FieldType = EnumType?.EnumUnderlyingType?.SpecialType.ToBitFieldType(); +/// +/// A model representing an enum bit-field +/// +internal sealed record EnumFieldModel : BitFieldModel +{ + public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbol) : base(attributeData, typeSymbol) + { + var attributeModel = new EnumFieldAttributeModel(attributeData); + Name = attributeModel.Name!; // todo: the nullability on this is well.. wrong. padding fields have no name + BitCount = attributeModel.BitCount; + + ReturnType = attributeModel.EnumType?.ToDisplayString(); + FieldType = attributeModel.EnumType?.EnumUnderlyingType?.SpecialType.ToBitFieldType(); if (string.IsNullOrEmpty(Name)) FieldType = BitFieldType.Padding; } - public override bool HasCompilationIssues(SourceProductionContext context, TypeSymbolProcessor processor) - { - return DiagnosticValidator.IsNotEnumType(context, this, processor.TypeSymbol.Name) | - base.HasCompilationIssues(context, processor); - } - protected override string GetGetterTemplate() { return string.Format(StringConstants.ExplicitGetterTemplate, GetterSource(), ReturnType); diff --git a/BitsKit.Generator/Models/IntegralFieldModel.cs b/BitsKit.Generator/Models/IntegralFieldModel.cs index 98f4e90..2358555 100644 --- a/BitsKit.Generator/Models/IntegralFieldModel.cs +++ b/BitsKit.Generator/Models/IntegralFieldModel.cs @@ -5,7 +5,7 @@ namespace BitsKit.Generator.Models; /// /// A model representing an integral bit-field /// -internal sealed class IntegralFieldModel : BitFieldModel +internal sealed record IntegralFieldModel : BitFieldModel { private bool IsTypeCast => this is { @@ -13,7 +13,7 @@ internal sealed class IntegralFieldModel : BitFieldModel ReturnType.Length: > 0 }; - public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbol) : base(attributeData, typeSymbol) + public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbol) : base(attributeData, typeSymbol) { switch (attributeData.ConstructorArguments.Length) { diff --git a/BitsKit.Generator/Properties/launchSettings.json b/BitsKit.Generator/Properties/launchSettings.json index 517c167..3fb17af 100644 --- a/BitsKit.Generator/Properties/launchSettings.json +++ b/BitsKit.Generator/Properties/launchSettings.json @@ -1,8 +1,8 @@ { "profiles": { - "Profile 1": { + "Debug Source Generator - on BitsKit.Tests": { "commandName": "DebugRoslynComponent", - "targetProject": "..\\BitsKit.Generator.Tests\\BitsKit.Generator.Tests.csproj" + "targetProject": "..\\BitsKit.Tests\\BitsKit.Tests.csproj" } } } \ No newline at end of file diff --git a/BitsKit.Generator/StringConstants.cs b/BitsKit.Generator/StringConstants.cs index 1d77a98..96dc573 100644 --- a/BitsKit.Generator/StringConstants.cs +++ b/BitsKit.Generator/StringConstants.cs @@ -31,13 +31,11 @@ internal static class StringConstants /// /// Template for the type declaration /// - /// {0} = Modifiers
- /// {1} = Keyword
- /// {2} = Record ClassOrStructKeyword
- /// {3} = Identifier + /// {0} = Keyword
+ /// {1} = Identifier ///
///
- public const string TypeDeclarationTemplate = "{0} {1} {2} {3}"; + public const string TypeDeclarationTemplate = "partial {0} {1}"; /// /// Template for a property declaration diff --git a/BitsKit.Generator/SymbolExtensions.cs b/BitsKit.Generator/SymbolExtensions.cs new file mode 100644 index 0000000..1cfe880 --- /dev/null +++ b/BitsKit.Generator/SymbolExtensions.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace BitsKit.Generator +{ + public static class SymbolExtensions + { + public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + return false; + } + + public static bool TryGetAttributesWithBaseType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out List? result) + { + result = null; + + foreach (AttributeData attribute in symbol.GetAttributes()) + { + var attributeClass = attribute.AttributeClass!; + do + { + if (SymbolEqualityComparer.Default.Equals(attributeClass, typeSymbol)) + { + result ??= []; + result.Add(attribute); + break; + } + + attributeClass = attributeClass.BaseType; + } while (attributeClass != null); + + } + + return result != null; + } + } +} \ No newline at end of file diff --git a/BitsKit.Generator/TypeSymbolProcessor.cs b/BitsKit.Generator/TypeSymbolProcessor.cs index 9ca09be..d5ec8a9 100644 --- a/BitsKit.Generator/TypeSymbolProcessor.cs +++ b/BitsKit.Generator/TypeSymbolProcessor.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis; using System.Text; using BitsKit.Generator.Models; @@ -7,38 +6,49 @@ namespace BitsKit.Generator; -internal sealed class TypeSymbolProcessor +internal sealed record TypeSymbolProcessor { - public INamedTypeSymbol TypeSymbol { get; } - public TypeDeclarationSyntax TypeDeclaration { get; } - public IReadOnlyList Fields => _fields; - public BaseNamespaceDeclarationSyntax? Namespace { get; } + public EquatableReadOnlyList Fields { get; } + public string? Namespace { get; } + + public BitOrder DefaultBitOrder { get; } + public bool IsStruct { get; } public bool IsInlineArray { get; } + + private readonly string _syntaxKeyword; + private readonly string _syntaxIdentifier; - private readonly BitOrder _defaultBitOrder; - private readonly List _fields = []; - - public TypeSymbolProcessor(INamedTypeSymbol typeSymbol, TypeDeclarationSyntax typeDeclaration, AttributeData attribute) + public TypeSymbolProcessor(INamedTypeSymbol typeSymbol, AttributeData attribute) { - TypeSymbol = typeSymbol; - TypeDeclaration = typeDeclaration; - Namespace = TypeDeclaration.Parent as BaseNamespaceDeclarationSyntax; - IsInlineArray = HasInlineArrayAttribute(); - - _defaultBitOrder = (BitOrder)attribute.ConstructorArguments[0].Value!; + _syntaxKeyword = typeSymbol.TypeKind switch + { + TypeKind.Struct when typeSymbol.IsRecord => "record struct", + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + TypeKind.Class when typeSymbol.IsRecord => "record", + _ => "class" + }; + _syntaxIdentifier = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + + Namespace = typeSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)); + if (string.IsNullOrWhiteSpace(Namespace)) Namespace = null; + + DefaultBitOrder = (BitOrder)attribute.ConstructorArguments[0].Value!; + IsStruct = typeSymbol.TypeKind == TypeKind.Struct; + IsInlineArray = HasInlineArrayAttribute(typeSymbol); + + Fields = EnumerateFields(typeSymbol); } public void GenerateCSharpSource(StringBuilder sb) { sb.AppendIndentedLine(1, StringConstants.TypeDeclarationTemplate, - TypeDeclaration.Modifiers, - TypeDeclaration.Keyword.Text, - (TypeDeclaration as RecordDeclarationSyntax)?.ClassOrStructKeyword.Text, - TypeDeclaration.Identifier.Text) + _syntaxKeyword, + _syntaxIdentifier) .AppendIndentedLine(1, "{"); - foreach (BitFieldModel field in _fields) + foreach (BitFieldModel field in Fields) field.GenerateCSharpSource(sb); sb.RemoveLastLine() @@ -46,11 +56,11 @@ public void GenerateCSharpSource(StringBuilder sb) .AppendLine(); } - public int EnumerateFields() + private EquatableReadOnlyList EnumerateFields(ITypeSymbol typeSymbol) { - _fields.Clear(); + var output = new List(); - foreach (IFieldSymbol field in TypeSymbol.GetMembers().OfType()) + foreach (IFieldSymbol field in typeSymbol.GetMembers().OfType()) { if (!IsValidFieldSymbol(field)) continue; @@ -74,52 +84,26 @@ _ when field.Type.IsSupportedIntegralType() => IsInlineArray ? if (backingType == BackingFieldType.Invalid) continue; - CreateBitFieldModels(field, backingType); + var backingModel = new BackingFieldModel(field, backingType); + CreateBitFieldModels(output, field, backingModel); } - return _fields.Count; + return output.ToEquatableReadOnlyList(); } - public bool ReportCompilationIssues(SourceProductionContext context) - { - bool hasCompilationIssues = false; - - if (DiagnosticValidator.IsNotPartial(context, TypeDeclaration, TypeSymbol.Name) | - DiagnosticValidator.IsNested(context, TypeDeclaration, TypeSymbol.Name)) - hasCompilationIssues = true; - - foreach (BitFieldModel field in _fields) - { - if (field.HasCompilationIssues(context, this)) - hasCompilationIssues = true; - } - - return hasCompilationIssues; - } - - private void CreateBitFieldModels(IFieldSymbol backingField, BackingFieldType backingType) + private void CreateBitFieldModels(List output, IFieldSymbol backingField, BackingFieldModel backingModel) { int offset = 0; foreach (AttributeData attribute in backingField.GetAttributes()) { - string? attributeType = attribute.AttributeClass?.ToDisplayString(); - - BitFieldModel? bitField = attributeType switch - { - StringConstants.BitFieldAttributeFullName => new IntegralFieldModel(attribute, this), - StringConstants.BooleanFieldAttributeFullName => new BooleanFieldModel(attribute, this), - StringConstants.EnumFieldAttributeFullName => new EnumFieldModel(attribute, this), - _ => null - }; + BitFieldModel? bitField = CreateBitFieldFromAttribute(attribute, this); if (bitField == null) continue; - bitField.BackingField = backingField; - bitField.BackingFieldType = backingType; + bitField.BackingField = backingModel; bitField.BitOffset = offset; - bitField.BitOrder = _defaultBitOrder; // padding fields are not generated if (bitField is not { FieldType: BitFieldType.Padding }) @@ -129,24 +113,37 @@ private void CreateBitFieldModels(IFieldSymbol backingField, BackingFieldType ba bitField.BitOrder ^= BitOrder.MostSignificant; // integrals inherit their field type from their backing field - if (backingType == BackingFieldType.Integral) + if (backingModel.Type == BackingFieldType.Integral) bitField.FieldType = backingField.Type.SpecialType.ToBitFieldType(); // allow inline arrays to infer their type - if (backingType == BackingFieldType.InlineArray) + if (backingModel.Type == BackingFieldType.InlineArray) bitField.FieldType ??= backingField.Type.SpecialType.ToBitFieldType(); // add to list of fields to generate - _fields.Add(bitField); + output.Add(bitField); } offset += bitField.BitCount; } } + + public static BitFieldModel? CreateBitFieldFromAttribute(AttributeData attribute, TypeSymbolProcessor processor) + { + string? attributeType = attribute.AttributeClass?.ToDisplayString(); + + return attributeType switch + { + StringConstants.BitFieldAttributeFullName => new IntegralFieldModel(attribute, processor), + StringConstants.BooleanFieldAttributeFullName => new BooleanFieldModel(attribute, processor), + StringConstants.EnumFieldAttributeFullName => new EnumFieldModel(attribute, processor), + _ => null + }; + } - private bool HasInlineArrayAttribute() + private static bool HasInlineArrayAttribute(ITypeSymbol typeSymbol) { - return (int?)TypeSymbol + return (int?)typeSymbol .GetAttributes() .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == StringConstants.InlineArrayAttributeFullName)? .ConstructorArguments[0].Value > 0; diff --git a/BitsKit.Tests/BitsKit.Tests.csproj b/BitsKit.Tests/BitsKit.Tests.csproj index ca31b2e..da39c34 100644 --- a/BitsKit.Tests/BitsKit.Tests.csproj +++ b/BitsKit.Tests/BitsKit.Tests.csproj @@ -2,12 +2,9 @@ net6.0;net7.0;net8.0 - enable false - AnyCPU;x86;x64 true True - preview diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index 3af0e70..62aa4d4 100644 --- a/BitsKit.Tests/GeneratorTests.cs +++ b/BitsKit.Tests/GeneratorTests.cs @@ -242,7 +242,7 @@ public ref partial struct BitFieldReadOnly "; string expected = @" - public ref partial struct BitFieldReadOnly + partial struct BitFieldReadOnly { public Int32 Generated00 { @@ -261,7 +261,7 @@ public Int32 Generated20 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } @@ -285,7 +285,7 @@ public readonly ref partial struct BitFieldReadOnly "; string expected = @" - public readonly ref partial struct BitFieldReadOnly + partial struct BitFieldReadOnly { public Int32 Generated00 { @@ -304,7 +304,7 @@ public Int32 Generated20 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } @@ -344,7 +344,7 @@ public unsafe partial class BitFieldGeneratorTest "; string expected = @" - public unsafe partial class BitFieldGeneratorTest + partial class BitFieldGeneratorTest { public Int32 Generated01 { @@ -461,7 +461,7 @@ public UIntPtr Generated19 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } @@ -490,7 +490,7 @@ public unsafe partial class BitFieldGeneratorTest "; string expected = @" - public unsafe partial class BitFieldGeneratorTest + partial class BitFieldGeneratorTest { public Int32 Generated01 { @@ -553,7 +553,7 @@ private protected Int32 Generated0A } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); #endif @@ -581,7 +581,7 @@ public unsafe ref partial struct BooleanGeneratorTest "; string expected = @" - public unsafe ref partial struct BooleanGeneratorTest + partial struct BooleanGeneratorTest { public System.Boolean Generated01 { @@ -608,7 +608,7 @@ public unsafe System.Boolean Generated30 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } @@ -639,7 +639,7 @@ public unsafe ref partial struct EnumGeneratorTest "; string expected = @" - public unsafe ref partial struct EnumGeneratorTest + partial struct EnumGeneratorTest { public BitsKit.Tests.TestEnum Generated00 { @@ -689,7 +689,7 @@ public unsafe BitsKit.Tests.TestEnum Generated31 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } @@ -710,7 +710,7 @@ public ref partial struct BitFieldIntegerConversion "; string expected = @" - public ref partial struct BitFieldIntegerConversion + partial struct BitFieldIntegerConversion { public Byte Generated00 { @@ -726,7 +726,7 @@ public Int32 Generated10 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } @@ -750,7 +750,7 @@ public partial struct BitFieldInlineArray "; string expected = @" - public partial struct BitFieldInlineArray + partial struct BitFieldInlineArray { public Int32 Generated00 { @@ -778,51 +778,93 @@ public System.Boolean Generated03 } "; - string? sourceOutput = GenerateSourceAndTest(source, new BitObjectGenerator()); + string? sourceOutput = GenerateSourceAndTest(source); Assert.IsTrue(Helpers.StrEqualExWhiteSpace(sourceOutput, expected)); } #endif - private static string? GenerateSourceAndTest(string source, IIncrementalGenerator generator) + private static string? GenerateSourceAndTest(string source) { var references = AppDomain.CurrentDomain.GetAssemblies() .Where(assembly => !assembly.IsDynamic) .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) .Cast(); - CSharpCompilation compilation = CSharpCompilation.Create("compilation", - [CSharpSyntaxTree.ParseText(Helpers.GeneratorTestHeader + source)], - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); - - GeneratorDriver driver = CSharpGeneratorDriver - .Create(generator) - .RunGeneratorsAndUpdateCompilation(compilation, out Compilation? outputCompilation, out ImmutableArray diagnostics); - - var diag = outputCompilation.GetDiagnostics(); - - Assert.IsTrue(diagnostics.IsEmpty); // there were no diagnostics created by the generators - Assert.AreEqual(outputCompilation.SyntaxTrees.Count(), 2); // we have two syntax trees, the original 'user' provided one, and the one added by the generator - Assert.IsTrue(outputCompilation.GetDiagnostics().IsEmpty); // verify the compilation with the added source has no diagnostics - - GeneratorDriverRunResult runResult = driver.GetRunResult(); - - Assert.AreEqual(runResult.GeneratedTrees.Length, 1); - Assert.IsTrue(runResult.Diagnostics.IsEmpty); + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "BitsKit.Tests.InMemory", + syntaxTrees: [CSharpSyntaxTree.ParseText(Helpers.GeneratorTestHeader + source)], + references: references, + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true + ) + ); + + var insignificantEditComp = compilation.Clone() + .AddSyntaxTrees(CSharpSyntaxTree.ParseText("// dummy")); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [new BitObjectGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)); + + var run1Result = RunGenerator(ref driver, compilation); + var run2Result = RunGenerator(ref driver, insignificantEditComp); + + foreach (var outputStep in run2Result.Results[0].TrackedOutputSteps) + { + AssertGeneratorDidntRun(outputStep.Value); + } + AssertGeneratorDidntRun(run2Result.Results[0].TrackedSteps["Main"]); + + Assert.AreEqual(run1Result.GeneratedTrees.Length, 1); + Assert.IsTrue(run1Result.Diagnostics.IsEmpty); - GeneratorRunResult generatorResult = runResult.Results[0]; + GeneratorRunResult generatorResult = run1Result.Results[0]; Assert.AreEqual(generatorResult.Generator.GetGeneratorType(), typeof(BitObjectGenerator)); Assert.IsTrue(generatorResult.Diagnostics.IsEmpty); Assert.AreEqual(generatorResult.GeneratedSources.Length, 1); Assert.IsTrue(generatorResult.Exception is null); string sourceOutput = generatorResult.GeneratedSources[0].SourceText.ToString(); - return TruncateUsings(sourceOutput); } - + + private static GeneratorDriverRunResult RunGenerator( + ref GeneratorDriver driver, + Compilation compilation + ) + { + driver = driver + .RunGeneratorsAndUpdateCompilation( + compilation, + out var outputCompilation, + out var diagnostics + ); + + // verify the compilation with the added source has no diagnostics + Assert.IsFalse( + outputCompilation + .GetDiagnostics() + .Any(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning) + ); + + // there were no diagnostics created by the generators + Assert.IsTrue(diagnostics.IsEmpty); + + return driver.GetRunResult(); + } + + private static void AssertGeneratorDidntRun(ImmutableArray steps) + { + var outputs = steps.SelectMany(o => o.Outputs); + foreach (var output in outputs) + { + Assert.IsTrue(output.Reason == IncrementalStepRunReason.Unchanged || + output.Reason == IncrementalStepRunReason.Cached); + } + } + private static string? TruncateUsings(string? source) { if (string.IsNullOrEmpty(source)) @@ -833,8 +875,8 @@ public System.Boolean Generated03 return source[eol..].TrimStart(); } - - /// Loads the BitsKit.Generator assembly into the current AppDomain - [BitObject(BitOrder.LeastSignificant)] - private readonly partial struct BitsKitGeneratorStub { } } + +/// Loads the BitsKit.Generator assembly into the current AppDomain +[BitObject(BitOrder.LeastSignificant)] +internal readonly partial struct BitsKitGeneratorStub { } \ No newline at end of file diff --git a/BitsKit/BitsKit.csproj b/BitsKit/BitsKit.csproj index 5a78919..d41796d 100644 --- a/BitsKit/BitsKit.csproj +++ b/BitsKit/BitsKit.csproj @@ -3,10 +3,7 @@ netstandard2.1;net6.0;net7.0;net8.0 enable - enable - AnyCPU;x86;x64 True - preview true diff --git a/Directory.Build.props b/Directory.Build.props index 2ab06a2..4e725a0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,7 +22,7 @@ enable - preview + 12