diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 5c35d64..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Dependency review - -on: - pull_request: - branches: [master] - -permissions: - contents: read - -jobs: - review: - name: Review dependency changes - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Review dependencies - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - with: - fail-on-severity: moderate diff --git a/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj b/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj index 30a5f59..6a8b837 100644 --- a/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj +++ b/BitsKit.Benchmarks/BitsKit.Benchmarks.csproj @@ -4,9 +4,7 @@ Exe net8.0;net10.0 enable - enable BitsKit.Benchmarks.Program - AnyCPU;x86;x64 diff --git a/BitsKit.Benchmarks/packages.lock.json b/BitsKit.Benchmarks/packages.lock.json index a8d5cbc..b14452d 100644 --- a/BitsKit.Benchmarks/packages.lock.json +++ b/BitsKit.Benchmarks/packages.lock.json @@ -278,12 +278,12 @@ "bitskit.tests": { "type": "Project", "dependencies": { - "BitsKit.Generator": "[1.2.0, )", + "BitsKit.Generator": "[1.3.0, )", "MSTest.TestAdapter": "[3.5.0, )", "MSTest.TestFramework": "[3.5.0, )", "Microsoft.CodeAnalysis.CSharp": "[4.10.0, )", "Microsoft.NET.Test.Sdk": "[17.10.0, )", - "RejectKid.BitsKit": "[1.2.0, )" + "RejectKid.BitsKit": "[1.3.0, )" } }, "RejectKid.BitsKit": { @@ -567,12 +567,12 @@ "bitskit.tests": { "type": "Project", "dependencies": { - "BitsKit.Generator": "[1.2.0, )", + "BitsKit.Generator": "[1.3.0, )", "MSTest.TestAdapter": "[3.5.0, )", "MSTest.TestFramework": "[3.5.0, )", "Microsoft.CodeAnalysis.CSharp": "[4.10.0, )", "Microsoft.NET.Test.Sdk": "[17.10.0, )", - "RejectKid.BitsKit": "[1.2.0, )" + "RejectKid.BitsKit": "[1.3.0, )" } }, "RejectKid.BitsKit": { diff --git a/BitsKit.Generator/Analysers/BitFieldAnalyser.cs b/BitsKit.Generator/Analysers/BitFieldAnalyser.cs new file mode 100644 index 0000000..3a243d7 --- /dev/null +++ b/BitsKit.Generator/Analysers/BitFieldAnalyser.cs @@ -0,0 +1,110 @@ +using System.Collections.Immutable; +using BitsKit.Generator.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace BitsKit.Generator.Analysers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class BitFieldAnalyser : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + [ + DiagnosticDescriptors.FieldTypeNotDefined, + DiagnosticDescriptors.ConflictingAccessors, + DiagnosticDescriptors.ConflictingSetters, + DiagnosticDescriptors.EnumTypeExpected + ]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(startContext => + { + INamedTypeSymbol? bitFieldAttribute = startContext.Compilation.GetTypeByMetadataName( + StringConstants.BitFieldAttributeFullName); + + if (bitFieldAttribute is null) + return; + + startContext.RegisterSymbolAction( + symbolContext => AnalyzeField(symbolContext, bitFieldAttribute), + SymbolKind.Field); + }); + } + + private static void AnalyzeField(SymbolAnalysisContext context, ITypeSymbol bitFieldAttribute) + { + if (context.Symbol is not IFieldSymbol fieldSymbol || + !fieldSymbol.TryGetAttributesWithBaseType(bitFieldAttribute, out var attributes)) + { + return; + } + + foreach (AttributeData attribute in attributes) + { + BitFieldModel? bitField = TypeSymbolProcessor.CreateBitFieldFromAttribute(attribute, null); + if (bitField is null) + continue; + + Location location = attribute.ApplicationSyntaxReference? + .GetSyntax(context.CancellationToken) + .GetLocation() ?? fieldSymbol.Locations[0]; + + if (bitField is IntegralFieldModel { FieldType: null } && RequiresExplicitFieldType(fieldSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.FieldTypeNotDefined, + location, + fieldSymbol.ContainingType.Name, + bitField.Name)); + } + + BitFieldModifiers accessorModifiers = bitField.Modifiers & BitFieldModifiers.AccessorMask; + if ((accessorModifiers & (accessorModifiers - 1)) != 0 && + accessorModifiers != BitFieldModifiers.ProtectedInternal && + accessorModifiers != BitFieldModifiers.PrivateProtected) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ConflictingAccessors, + location, + fieldSymbol.ContainingType.Name, + bitField.Name)); + } + + BitFieldModifiers setterModifiers = bitField.Modifiers & BitFieldModifiers.SetterMask; + if ((setterModifiers & (setterModifiers - 1)) != 0) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ConflictingSetters, + location, + fieldSymbol.ContainingType.Name, + bitField.Name)); + } + + if (bitField is EnumFieldModel) + { + var enumField = new EnumFieldAttributeModel(attribute); + if (enumField.EnumType is { EnumUnderlyingType: null }) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.EnumTypeExpected, + location, + fieldSymbol.ContainingType.Name, + bitField.Name)); + } + } + } + } + + 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*"; +} diff --git a/BitsKit.Generator/Analysers/BitObjectAnalyser.cs b/BitsKit.Generator/Analysers/BitObjectAnalyser.cs new file mode 100644 index 0000000..9eb3cb2 --- /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); + }); + } + } +} 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 d8e7e4d..ddfe8f1 100644 --- a/BitsKit.Generator/BitsKit.Generator.csproj +++ b/BitsKit.Generator/BitsKit.Generator.csproj @@ -3,12 +3,7 @@ netstandard2.0 false - 12.0 - enable - true - Generated true - AnyCPU;x86;x64 True True @@ -26,6 +21,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/BitsKit.Generator/DiagnosticValidator.cs b/BitsKit.Generator/DiagnosticValidator.cs deleted file mode 100644 index ac1d9ed..0000000 --- a/BitsKit.Generator/DiagnosticValidator.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp; -using BitsKit.Generator.Models; - -namespace BitsKit.Generator; - -internal static class DiagnosticValidator -{ - /// - /// Adds a diagnostic to the compilation and returns true - /// if compilation will fail because of it - /// - public static bool ReportDiagnostic(SourceProductionContext context, DiagnosticDescriptor descriptor, Location location, params object?[]? messageArgs) - { - context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs)); - - 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) - { - return bitField.FieldType is null - && ReportDiagnostic( - context, - DiagnosticDescriptors.FieldTypeNotDefined, - 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..1395ef7 --- /dev/null +++ b/BitsKit.Generator/EquatableReadOnlyList.cs @@ -0,0 +1,60 @@ +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() + { + unchecked + { + int hashCode = 17; + + foreach (T item in Collection) + hashCode = (hashCode * 31) + (item?.GetHashCode() ?? 0); + + return hashCode; + } + } + + 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); + } +} diff --git a/BitsKit.Generator/Models/BackingFieldModel.cs b/BitsKit.Generator/Models/BackingFieldModel.cs new file mode 100644 index 0000000..32e4ecd --- /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; + } + } +} diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index abe97ef..65616be 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; } - public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbol) + private readonly bool _containingTypeIsStruct; + + 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..a9b8995 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -1,17 +1,20 @@ -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 int BitCount { get; set; } - public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor typeSymbol) : base(attributeData, typeSymbol) + public EnumFieldAttributeModel(AttributeData attributeData) { - switch(attributeData.ConstructorArguments.Length) + switch (attributeData.ConstructorArguments.Length) { case 1: // padding constructor BitCount = (byte)attributeData.ConstructorArguments[0].Value!; @@ -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}"); } + } +} + +/// +/// 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 = EnumType?.ToDisplayString(); - FieldType = EnumType?.EnumUnderlyingType?.SpecialType.ToBitFieldType(); + 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..5836b77 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..26f4252 --- /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; + } + } +} diff --git a/BitsKit.Generator/TypeSymbolProcessor.cs b/BitsKit.Generator/TypeSymbolProcessor.cs index 9ca09be..c5032bd 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 BitOrder _defaultBitOrder; - private readonly List _fields = []; + private readonly string _syntaxKeyword; + private readonly string _syntaxIdentifier; - 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; @@ -64,8 +74,8 @@ public int EnumerateFields() "byte[]" => BackingFieldType.Span, "byte*" => BackingFieldType.Pointer, - _ when field.Type.IsSupportedIntegralType() => IsInlineArray ? - BackingFieldType.InlineArray : + _ when field.Type.IsSupportedIntegralType() => IsInlineArray ? + BackingFieldType.InlineArray : BackingFieldType.Integral, _ => BackingFieldType.Invalid @@ -74,52 +84,26 @@ _ when field.Type.IsSupportedIntegralType() => IsInlineArray ? if (backingType == BackingFieldType.Invalid) continue; - CreateBitFieldModels(field, backingType); - } - - return _fields.Count; - } - - 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; + var backingModel = new BackingFieldModel(field, backingType); + CreateBitFieldModels(output, field, backingModel); } - return hasCompilationIssues; + return output.ToEquatableReadOnlyList(); } - 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,42 @@ 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(); + // Diagnostics are reported by analyzers. Do not generate an invalid + // property when its primitive type could not be resolved. + if (bitField.FieldType is null) + continue; + // add to list of fields to generate - _fields.Add(bitField); + output.Add(bitField); } offset += bitField.BitCount; } } - private bool HasInlineArrayAttribute() + 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 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.Generator/packages.lock.json b/BitsKit.Generator/packages.lock.json index 0d66a90..ca8c71d 100644 --- a/BitsKit.Generator/packages.lock.json +++ b/BitsKit.Generator/packages.lock.json @@ -35,6 +35,12 @@ "Microsoft.NETCore.Platforms": "1.1.0" } }, + "PolySharp": { + "type": "Direct", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" + }, "Microsoft.CodeAnalysis.Common": { "type": "Transitive", "resolved": "4.10.0", diff --git a/BitsKit.Tests/AnalyzerTests.cs b/BitsKit.Tests/AnalyzerTests.cs new file mode 100644 index 0000000..387d91e --- /dev/null +++ b/BitsKit.Tests/AnalyzerTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using BitsKit.Generator.Analysers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BitsKit.Tests; + +[TestClass] +public class AnalyzerTests +{ + [DataTestMethod] + [DataRow("Span")] + [DataRow("ReadOnlySpan")] + [DataRow("Memory")] + [DataRow("ReadOnlyMemory")] + [DataRow("byte[]")] + [DataRow("byte*")] + public async Task MissingFieldTypeReportsBitsKit003(string backingType) + { + string source = $$""" + [BitObject(BitOrder.LeastSignificant)] + public unsafe ref partial struct MissingFieldType + { + [BitField("Value", 3)] + public {{backingType}} BackingField; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + Diagnostic diagnostic = diagnostics.Single(d => d.Id == "BITSKIT003"); + + Assert.AreEqual(DiagnosticSeverity.Error, diagnostic.Severity); + Assert.AreEqual("'MissingFieldType.Value' FieldType cannot be inferred", diagnostic.GetMessage()); + } + + [TestMethod] + public async Task ExplicitMemoryFieldTypeDoesNotReportBitsKit003() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public ref partial struct ExplicitFieldType + { + [BitField("Value", 3, BitFieldType.UInt16)] + public Span BackingField; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + + Assert.IsFalse(diagnostics.Any(d => d.Id == "BITSKIT003")); + } + + [TestMethod] + public async Task IntegralBackingFieldInfersType() + { + const string source = """ + [BitObject(BitOrder.LeastSignificant)] + public partial struct InferredFieldType + { + [BitField("Value", 3)] + public ushort BackingField; + } + """; + + ImmutableArray diagnostics = await GetDiagnosticsAsync(source); + + Assert.IsFalse(diagnostics.Any(d => d.Id == "BITSKIT003")); + } + + private static async Task> GetDiagnosticsAsync(string source) + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) + .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)); + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "BitsKit.Tests.AnalyzerInput", + syntaxTrees: [CSharpSyntaxTree.ParseText(Helpers.GeneratorTestHeader + source)], + references: references, + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + allowUnsafe: true)); + + return await compilation + .WithAnalyzers([new BitFieldAnalyser()]) + .GetAnalyzerDiagnosticsAsync(); + } +} diff --git a/BitsKit.Tests/BitsKit.Tests.csproj b/BitsKit.Tests/BitsKit.Tests.csproj index 3ec6196..c2e1636 100644 --- a/BitsKit.Tests/BitsKit.Tests.csproj +++ b/BitsKit.Tests/BitsKit.Tests.csproj @@ -2,12 +2,9 @@ net8.0;net10.0 - enable false - AnyCPU;x86;x64 true True - 12.0 diff --git a/BitsKit.Tests/GeneratorTests.cs b/BitsKit.Tests/GeneratorTests.cs index 3af0e70..b2195d6 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)); + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "BitsKit.Tests.InMemory", + syntaxTrees: [CSharpSyntaxTree.ParseText(Helpers.GeneratorTestHeader + source)], + references: references, + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true + ) + ); - GeneratorDriver driver = CSharpGeneratorDriver - .Create(generator) - .RunGeneratorsAndUpdateCompilation(compilation, out Compilation? outputCompilation, out ImmutableArray diagnostics); + var insignificantEditComp = compilation.Clone() + .AddSyntaxTrees(CSharpSyntaxTree.ParseText("// dummy")); - var diag = outputCompilation.GetDiagnostics(); + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [new BitObjectGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)); - 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 + var run1Result = RunGenerator(ref driver, compilation); + var run2Result = RunGenerator(ref driver, insignificantEditComp); - GeneratorDriverRunResult runResult = driver.GetRunResult(); + foreach (var outputStep in run2Result.Results[0].TrackedOutputSteps) + { + AssertGeneratorDidntRun(outputStep.Value); + } + AssertGeneratorDidntRun(run2Result.Results[0].TrackedSteps["Main"]); - Assert.AreEqual(runResult.GeneratedTrees.Length, 1); - Assert.IsTrue(runResult.Diagnostics.IsEmpty); + 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 { } diff --git a/BitsKit/BitsKit.csproj b/BitsKit/BitsKit.csproj index 60ae5c4..4abd393 100644 --- a/BitsKit/BitsKit.csproj +++ b/BitsKit/BitsKit.csproj @@ -3,10 +3,7 @@ netstandard2.1;net6.0;net7.0;net8.0;net10.0 enable - enable - AnyCPU;x86;x64 True - 12.0 false diff --git a/CHANGELOG.md b/CHANGELOG.md index 28eb790..e2abbd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,18 @@ Notable changes to the community-maintained fork are documented here. This proje - Cross-platform continuous integration for .NET 8 and .NET 10. - Validated, tag-driven NuGet and GitHub release automation. -- CodeQL, dependency review, Dependabot, package provenance attestations, and contribution guidance. +- CodeQL, Dependabot, package provenance attestations, and contribution guidance. +- Incremental source-generator regression coverage and analyzer-based diagnostics. ### Changed - Maintained packages use the `RejectKid.BitsKit` NuGet ID while retaining the `BitsKit` assembly and namespaces. - Builds use a pinned .NET SDK and C# 12 instead of an environment-dependent preview language version. +- The source generator no longer retains Roslyn symbols or syntax nodes between runs. + +### Fixed + +- Restored `BITSKIT003` when a memory-backed bit field omits its required `FieldType`. ## 1.2.0 - 2024-11-19 diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 1043991..7d58fe5 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -2,6 +2,7 @@ v1.3.0 - Establish community-maintained releases under the RejectKid.BitsKit package ID. - Add .NET 10 support while retaining existing library targets. - Add reproducible builds, dependency locking, and automated package validation. +- Fix incremental source-generator caching and restore the BITSKIT003 diagnostic. v.1.2.0 - Support InlineArrays.