From 73d2f69a864c3296ad24aab951403ef9ea246492 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Feb 2024 00:05:23 +0100 Subject: [PATCH 01/31] WIP for testing cacheability of the source generator --- Directory.Packages.props | 1 + .../Helpers/TestHelper.cs | 214 ++++++++++++++++++ ...heetah.SourceGenerator.SnapshotTest.csproj | 1 + .../Tests/WorksheetRowGeneratorTests.cs | 33 +++ .../Helpers/TrackingNames.cs | 7 + .../WorksheetRowGenerator.cs | 9 +- 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1b724c73..1edb2d69 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ + diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs index 3708f38a..d4f6cad6 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs @@ -1,6 +1,10 @@ +using FluentAssertions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using SpreadCheetah.SourceGeneration; +using System.Collections; +using System.Collections.Immutable; +using System.Reflection; namespace SpreadCheetah.SourceGenerator.SnapshotTest.Helpers; @@ -41,4 +45,214 @@ internal static class TestHelper ? task.UseParameters(parameters) : task; } + + public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees( + string[] sources, // C# source code + string[] stages, // The tracking stages we expect + bool assertOutputs = true) // You can disable cacheability checking during dev + where T : IIncrementalGenerator, new() // T is your generator + { + // Convert the source files to SyntaxTrees + IEnumerable syntaxTrees = sources.Select(static x => CSharpSyntaxTree.ParseText(x)); + + // Configure the assembly references you need + // This will vary depending on your generator and requirements + //var references = AppDomain.CurrentDomain.GetAssemblies() + // .Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location)) + // .Select(_ => MetadataReference.CreateFromFile(_.Location)) + // .Concat(new[] { MetadataReference.CreateFromFile(typeof(T).Assembly.Location) }); + + var dotNetAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location) ?? throw new InvalidOperationException(); + + var references = new[] +{ + MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "mscorlib.dll")), + MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "netstandard.dll")), + MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.dll")), + MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Core.dll")), + MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Private.CoreLib.dll")), + MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Runtime.dll")), + MetadataReference.CreateFromFile(typeof(WorksheetRowAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(TestHelper).Assembly.Location) + }; + + // Create a Compilation object + // You may want to specify other results here + CSharpCompilation compilation = CSharpCompilation.Create( + "SpreadCheetah.Generated", + syntaxTrees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + // Run the generator, get the results, and assert cacheability if applicable + GeneratorDriverRunResult runResult = RunGeneratorAndAssertOutput( + compilation, stages, assertOutputs); + + // Return the generator diagnostics and generated sources + return (runResult.Diagnostics, runResult.GeneratedTrees.Select(x => x.ToString()).ToArray()); + } + + private static GeneratorDriverRunResult RunGeneratorAndAssertOutput(CSharpCompilation compilation, string[] trackingNames, bool assertOutput = true) + where T : IIncrementalGenerator, new() + { + ISourceGenerator generator = new T().AsSourceGenerator(); + + // โš  Tell the driver to track all the incremental generator outputs + // without this, you'll have no tracked outputs! + var opts = new GeneratorDriverOptions( + disabledOutputs: IncrementalGeneratorOutputKind.None, + trackIncrementalGeneratorSteps: true); + + GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], driverOptions: opts); + + // Create a clone of the compilation that we will use later + var clone = compilation.Clone(); + + // Do the initial run + // Note that we store the returned driver value, as it contains cached previous outputs + driver = driver.RunGenerators(compilation); + GeneratorDriverRunResult runResult = driver.GetRunResult(); + + if (assertOutput) + { + // Run again, using the same driver, with a clone of the compilation + GeneratorDriverRunResult runResult2 = driver + .RunGenerators(clone) + .GetRunResult(); + + // Compare all the tracked outputs, throw if there's a failure + AssertRunsEqual(runResult, runResult2, trackingNames); + + // verify the second run only generated cached source outputs + runResult2.Results[0] + .TrackedOutputSteps + .SelectMany(x => x.Value) // step executions + .SelectMany(x => x.Outputs) // execution results + .Should() + .OnlyContain(x => x.Reason == IncrementalStepRunReason.Cached); + } + + return runResult; + } + + private static void AssertRunsEqual( + GeneratorDriverRunResult runResult1, + GeneratorDriverRunResult runResult2, + string[] trackingNames) + { + // We're given all the tracking names, but not all the + // stages will necessarily execute, so extract all the + // output steps, and filter to ones we know about + var trackedSteps1 = GetTrackedSteps(runResult1, trackingNames); + var trackedSteps2 = GetTrackedSteps(runResult2, trackingNames); + + // Both runs should have the same tracked steps + trackedSteps1.Should() + .NotBeEmpty() + .And.HaveSameCount(trackedSteps2) + .And.ContainKeys(trackedSteps2.Keys); + + // Get the IncrementalGeneratorRunStep collection for each run + foreach (var (trackingName, runSteps1) in trackedSteps1) + { + // Assert that both runs produced the same outputs + var runSteps2 = trackedSteps2[trackingName]; + AssertEqual(runSteps1, runSteps2, trackingName); + } + + // Local function that extracts the tracked steps + static Dictionary> GetTrackedSteps( + GeneratorDriverRunResult runResult, string[] trackingNames) + => runResult + .Results[0] // We're only running a single generator, so this is safe + .TrackedSteps // Get the pipeline outputs + .Where(step => trackingNames.Contains(step.Key)) // filter to known steps + .ToDictionary(x => x.Key, x => x.Value); // Convert to a dictionary + } + + private static void AssertEqual( + ImmutableArray runSteps1, + ImmutableArray runSteps2, + string stepName) + { + runSteps1.Should().HaveSameCount(runSteps2); + + for (var i = 0; i < runSteps1.Length; i++) + { + var runStep1 = runSteps1[i]; + var runStep2 = runSteps2[i]; + + // The outputs should be equal between different runs + IEnumerable outputs1 = runStep1.Outputs.Select(x => x.Value); + IEnumerable outputs2 = runStep2.Outputs.Select(x => x.Value); + + outputs1.Should() + .Equal(outputs2, $"because {stepName} should produce cacheable outputs"); + + // Therefore, on the second run the results should always be cached or unchanged! + // - Unchanged is when the _input_ has changed, but the output hasn't + // - Cached is when the the input has not changed, so the cached output is used + runStep2.Outputs.Should() + .OnlyContain( + x => x.Reason == IncrementalStepRunReason.Cached || x.Reason == IncrementalStepRunReason.Unchanged, + $"{stepName} expected to have reason {IncrementalStepRunReason.Cached} or {IncrementalStepRunReason.Unchanged}"); + + // Make sure we're not using anything we shouldn't + AssertObjectGraph(runStep1, stepName); + } + } + + private static void AssertObjectGraph(IncrementalGeneratorRunStep runStep, string stepName) + { + // Including the stepName in error messages to make it easy to isolate issues + var because = $"{stepName} shouldn't contain banned symbols"; + var visited = new HashSet(); + + // Check all of the outputs - probably overkill, but why not + foreach (var (obj, _) in runStep.Outputs) + { + Visit(obj); + } + + void Visit(object node) + { + // If we've already seen this object, or it's null, stop. + if (node is null || !visited.Add(node)) + { + return; + } + + // Make sure it's not a banned type + node.Should() + .NotBeOfType(because) + .And.NotBeOfType(because) + .And.NotBeOfType(because); + + // Examine the object + Type type = node.GetType(); + if (type.IsPrimitive || type.IsEnum || type == typeof(string)) + { + return; + } + + // If the object is a collection, check each of the values + if (node is IEnumerable collection and not string) + { + foreach (object element in collection) + { + // recursively check each element in the collection + Visit(element); + } + + return; + } + + // Recursively check each field in the object + foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + object fieldValue = field.GetValue(node); + Visit(fieldValue); + } + } + } } \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj b/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj index 5b468508..1711f95b 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj @@ -10,6 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs index 2bd7d85b..2f8da1e8 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs @@ -1,3 +1,5 @@ +using FluentAssertions; +using FluentAssertions.Execution; using SpreadCheetah.SourceGenerator.SnapshotTest.Helpers; using SpreadCheetah.SourceGenerators; @@ -5,6 +7,37 @@ namespace SpreadCheetah.SourceGenerator.SnapshotTest.Tests; public class WorksheetRowGeneratorTests { + private static readonly string[] AllTrackingNames = ["InitialExtraction", "Transform"]; + + [Fact] + public void WorksheetRowGenerator_Generate_CachingCorrectly() + { + // Arrange + const string source = """ + using SpreadCheetah.SourceGeneration; + using SpreadCheetah.SourceGenerator.SnapshotTest.Models; + using System; + + namespace MyNamespace + { + [WorksheetRow(typeof(ClassWithSingleProperty))] + public partial class MyGenRowContext : WorksheetRowContext + { + } + } + """; + + const string expected = """... not shown for brevity..."""; + + // Act + var (diagnostics, output) = TestHelper.GetGeneratedTrees([source], AllTrackingNames); + + // Assert the output + using var s = new AssertionScope(); + diagnostics.Should().BeEmpty(); + output.Should().OnlyContain(x => x == expected); + } + [Fact] public Task WorksheetRowGenerator_Generate_ClassWithSingleProperty() { diff --git a/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs b/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs new file mode 100644 index 00000000..c00fbc02 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs @@ -0,0 +1,7 @@ +namespace SpreadCheetah.SourceGenerator.Helpers; + +internal static class TrackingNames +{ + public const string InitialExtraction = nameof(InitialExtraction); + public const string Transform = nameof(Transform); +} diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 51b090d4..adf7af32 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -23,9 +23,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration) .Where(static x => x is not null) - .Collect(); + .Collect() + .WithTrackingName(TrackingNames.InitialExtraction); - var source = context.CompilationProvider.Combine(filtered); + var source = context.CompilationProvider + .Combine(filtered) + .WithTrackingName(TrackingNames.Transform); context.RegisterSourceOutput(source, static (spc, source) => Execute(source.Left, source.Right, spc)); } @@ -540,7 +543,7 @@ private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, INamedTy sb.AppendLine(); } - for (var i = 0; i < properties.Count; i++) + for (var i = 0; i < properties.Count; i++) { sb.AppendIndentation(indent + 1) .Append("cells[") From ebcd0b37a10bc8d1fb1bcc9c427e15cc48cc2fdb Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Feb 2024 00:19:48 +0100 Subject: [PATCH 02/31] Replacing use of ITypeSymbol in src gen ContextClass --- .../Helpers/ContextClass.cs | 6 ++-- .../WorksheetRowGenerator.cs | 31 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs index 51629ef7..15ed2aed 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs @@ -4,7 +4,9 @@ namespace SpreadCheetah.SourceGenerator.Helpers; internal sealed record ContextClass( - ITypeSymbol ContextClassType, - Dictionary RowTypes, + string Name, + Accessibility DeclaredAccessibility, + string? Namespace, + Dictionary RowTypes, // TODO: Don't use INamedTypeSymbol CompilationTypes CompilationTypes, GeneratorOptions? Options); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index adf7af32..ccc31c7e 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -77,7 +77,13 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat generatorOptions = options; } - return new ContextClass(classSymbol, rowTypes, compilationTypes, generatorOptions); + return new ContextClass( + DeclaredAccessibility: classSymbol.DeclaredAccessibility, + Namespace: classSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToString() : null, + Name: classSymbol.Name, + RowTypes: rowTypes, + CompilationTypes: compilationTypes, + Options: generatorOptions); } private static bool TryParseWorksheetRowAttribute( @@ -292,7 +298,12 @@ private static void Execute(Compilation compilation, ImmutableArray _default ??= new {contextType.Name}();"); + sb.AppendLine($" private static {contextClass.Name}? _default;"); + sb.AppendLine($" public static {contextClass.Name} Default => _default ??= new {contextClass.Name}();"); sb.AppendLine(); - sb.AppendLine($" public {contextType.Name}()"); + sb.AppendLine($" public {contextClass.Name}()"); sb.AppendLine(" {"); sb.AppendLine(" }"); From f159b3a5d86b0d44204b62bb64812d60ed31d940 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Feb 2024 12:02:53 +0100 Subject: [PATCH 03/31] Src gen: Remove CompilationTypes from ContextClass --- .../Helpers/ContextClass.cs | 2 -- .../WorksheetRowGenerator.cs | 14 ++++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs index 15ed2aed..efc153eb 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using SpreadCheetah.SourceGenerator.Models; namespace SpreadCheetah.SourceGenerator.Helpers; @@ -8,5 +7,4 @@ internal sealed record ContextClass( Accessibility DeclaredAccessibility, string? Namespace, Dictionary RowTypes, // TODO: Don't use INamedTypeSymbol - CompilationTypes CompilationTypes, GeneratorOptions? Options); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index ccc31c7e..b0599429 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -82,7 +82,6 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat Namespace: classSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToString() : null, Name: classSymbol.Name, RowTypes: rowTypes, - CompilationTypes: compilationTypes, Options: generatorOptions); } @@ -288,6 +287,9 @@ private static void Execute(Compilation compilation, ImmutableArray Date: Sat, 10 Feb 2024 12:30:32 +0100 Subject: [PATCH 04/31] Add EquatableArray to source generator project --- .../Helpers/EquatableArray.cs | 61 ++++++ .../Helpers/HashCode.cs | 189 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs create mode 100644 SpreadCheetah.SourceGenerator/Helpers/HashCode.cs diff --git a/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs b/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs new file mode 100644 index 00000000..51f1aa42 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs @@ -0,0 +1,61 @@ +using System.Collections; + +namespace SpreadCheetah.SourceGenerator.Helpers; + +/// +/// Based on the implementation from: +/// https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs +/// +internal readonly struct EquatableArray(T[] underlyingArray) + : IEquatable>, IEnumerable + where T : IEquatable +{ + public static readonly EquatableArray Empty = new([]); + + private readonly T[]? _array = underlyingArray; + + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + public override int GetHashCode() + { + if (_array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + public ReadOnlySpan AsSpan() => _array.AsSpan(); + public T[]? GetArray() => _array; + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? [])).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? [])).GetEnumerator(); + } + + public int Count => _array?.Length ?? 0; + + public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); + public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); +} \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs b/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs new file mode 100644 index 00000000..356fbc60 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs @@ -0,0 +1,189 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; + +namespace SpreadCheetah.SourceGenerator.Helpers; + +/// +/// Based on the implementation from: +/// https://github.com/CommunityToolkit/dotnet/blob/7b53ae23dfc6a7fb12d0fc058b89b6e948f48448/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs +/// +[StructLayout(LayoutKind.Auto)] +#pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals +internal struct HashCode +#pragma warning restore CA1066 // Implement IEquatable when overriding Object.Equals +{ + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static readonly uint seed = GenerateGlobalSeed(); + + private uint v1, v2, v3, v4; + private uint queue1, queue2, queue3; + private uint length; + + /// + /// Initializes the default seed. + /// + /// A random seed. + private static uint GenerateGlobalSeed() + { + byte[] bytes = new byte[4]; + + using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) + { + generator.GetBytes(bytes); + } + + return BitConverter.ToUInt32(bytes, 0); + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = seed + Prime1 + Prime2; + v2 = seed + Prime2; + v3 = seed; + v4 = seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixEmptyState() + { + return seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + + return hash; + } + + private void Add(int value) + { + uint val = (uint)value; + uint previousLength = length++; + uint position = previousLength % 4; + + if (position == 0) + { + queue1 = val; + } + else if (position == 1) + { + queue2 = val; + } + else if (position == 2) + { + queue3 = val; + } + else + { + if (previousLength == 3) + { + Initialize(out v1, out v2, out v3, out v4); + } + + v1 = Round(v1, queue1); + v2 = Round(v2, queue2); + v3 = Round(v3, queue3); + v4 = Round(v4, val); + } + } + + /// + /// Gets the resulting hashcode from the current instance. + /// + /// The resulting hashcode from the current instance. + public readonly int ToHashCode() + { + uint len = this.length; + uint position = len % 4; + uint hash = len < 4 ? MixEmptyState() : MixState(v1, v2, v3, v4); + + hash += len * 4; + + if (position > 0) + { + hash = QueueRound(hash, queue1); + + if (position > 1) + { + hash = QueueRound(hash, queue2); + + if (position > 2) + { + hash = QueueRound(hash, queue3); + } + } + } + + hash = MixFinal(hash); + + return (int)hash; + } + +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member +#pragma warning disable S3877 // Exceptions should not be thrown from unexpected methods + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override readonly int GetHashCode() => throw new NotSupportedException(); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override readonly bool Equals(object? obj) => throw new NotSupportedException(); +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member +#pragma warning restore S3877 // Exceptions should not be thrown from unexpected methods + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } +} \ No newline at end of file From b27c55fa5c04008849733facba07b6f08ece4e45 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 11 Feb 2024 23:49:33 +0100 Subject: [PATCH 05/31] Src gen: Replace a ISymbol comparison with a string comparison to check for correct attribute --- SpreadCheetah.SourceGenerator/Helpers/Attributes.cs | 6 ++++++ SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs | 9 ++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Helpers/Attributes.cs diff --git a/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs b/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs new file mode 100644 index 00000000..6be81035 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs @@ -0,0 +1,6 @@ +namespace SpreadCheetah.SourceGenerator.Helpers; + +internal static class Attributes +{ + public const string ColumnOrder = "SpreadCheetah.SourceGeneration.ColumnOrderAttribute"; +} diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index b0599429..508dd823 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -158,12 +158,11 @@ private static bool TryParseColumnHeaderAttribute( private static bool TryParseColumnOrderAttribute( AttributeData attribute, - INamedTypeSymbol expectedAttribute, out int order) { order = 0; - if (!SymbolEqualityComparer.Default.Equals(expectedAttribute, attribute.AttributeClass)) + if (!string.Equals(attribute.AttributeClass?.ToDisplayString(), Attributes.ColumnOrder, StringComparison.Ordinal)) return false; var args = attribute.ConstructorArguments; @@ -202,7 +201,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, var columnHeader = GetColumnHeader(p, compilationTypes.ColumnHeaderAttribute); var columnProperty = new ColumnProperty(p.Name, columnHeader); - if (!TryGetExplicitColumnOrder(p, compilationTypes.ColumnOrderAttribute, context.CancellationToken, out var columnOrder, out var location)) + if (!TryGetExplicitColumnOrder(p, context.CancellationToken, out var columnOrder, out var location)) implicitOrderProperties.Add(columnProperty); else if (!explicitOrderProperties.ContainsKey(columnOrder)) explicitOrderProperties.Add(columnOrder, columnProperty); @@ -226,7 +225,7 @@ private static string GetColumnHeader(IPropertySymbol property, INamedTypeSymbol return @$"""{property.Name}"""; } - private static bool TryGetExplicitColumnOrder(IPropertySymbol property, INamedTypeSymbol columnOrderAttribute, + private static bool TryGetExplicitColumnOrder(IPropertySymbol property, CancellationToken token, out int columnOrder, out Location? location) { columnOrder = 0; @@ -234,7 +233,7 @@ private static bool TryGetExplicitColumnOrder(IPropertySymbol property, INamedTy foreach (var attribute in property.GetAttributes()) { - if (!TryParseColumnOrderAttribute(attribute, columnOrderAttribute, out columnOrder)) + if (!TryParseColumnOrderAttribute(attribute, out columnOrder)) continue; location = attribute.ApplicationSyntaxReference?.GetSyntax(token).GetLocation(); From 5db193100fb63fce74bc8efd0a7b074af2e20f91 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 11 Feb 2024 23:54:24 +0100 Subject: [PATCH 06/31] Src gen: Replace another ISymbol comparison with a string comparison to check for correct attribute --- .../Helpers/Attributes.cs | 1 + .../WorksheetRowGenerator.cs | 26 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs b/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs index 6be81035..17da7682 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs @@ -2,5 +2,6 @@ namespace SpreadCheetah.SourceGenerator.Helpers; internal static class Attributes { + public const string ColumnHeader = "SpreadCheetah.SourceGeneration.ColumnHeaderAttribute"; public const string ColumnOrder = "SpreadCheetah.SourceGeneration.ColumnOrderAttribute"; } diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 508dd823..8ee38c93 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -140,12 +140,11 @@ private static bool TryParseOptionsAttribute( private static bool TryParseColumnHeaderAttribute( AttributeData attribute, - INamedTypeSymbol expectedAttribute, out TypedConstant attributeArg) { attributeArg = default; - if (!SymbolEqualityComparer.Default.Equals(expectedAttribute, attribute.AttributeClass)) + if (!string.Equals(Attributes.ColumnHeader, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) return false; var args = attribute.ConstructorArguments; @@ -162,7 +161,7 @@ private static bool TryParseColumnOrderAttribute( { order = 0; - if (!string.Equals(attribute.AttributeClass?.ToDisplayString(), Attributes.ColumnOrder, StringComparison.Ordinal)) + if (!string.Equals(Attributes.ColumnOrder, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) return false; var args = attribute.ConstructorArguments; @@ -173,7 +172,7 @@ private static bool TryParseColumnOrderAttribute( return true; } - private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, CompilationTypes compilationTypes, + private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, ITypeSymbol classType, SourceProductionContext context) { var implicitOrderProperties = new List(); @@ -198,7 +197,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, continue; } - var columnHeader = GetColumnHeader(p, compilationTypes.ColumnHeaderAttribute); + var columnHeader = GetColumnHeader(p); var columnProperty = new ColumnProperty(p.Name, columnHeader); if (!TryGetExplicitColumnOrder(p, context.CancellationToken, out var columnOrder, out var location)) @@ -214,11 +213,11 @@ private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, return new TypePropertiesInfo(explicitOrderProperties, unsupportedPropertyNames); } - private static string GetColumnHeader(IPropertySymbol property, INamedTypeSymbol columnHeaderAttribute) + private static string GetColumnHeader(IPropertySymbol property) { foreach (var attribute in property.GetAttributes()) { - if (TryParseColumnHeaderAttribute(attribute, columnHeaderAttribute, out var arg)) + if (TryParseColumnHeaderAttribute(attribute, out var arg)) return arg.ToCSharpString(); } @@ -286,9 +285,6 @@ private static void Execute(Compilation compilation, ImmutableArray Date: Mon, 12 Feb 2024 00:08:22 +0100 Subject: [PATCH 07/31] Replace the use of ISymbol comparisons with string comparisons --- .../Extensions/CompilationExtensions.cs | 43 ------------------- .../Helpers/Attributes.cs | 1 + .../Models/CompilationTypes.cs | 9 ---- .../WorksheetRowGenerator.cs | 10 ++--- 4 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 SpreadCheetah.SourceGenerator/Extensions/CompilationExtensions.cs delete mode 100644 SpreadCheetah.SourceGenerator/Models/CompilationTypes.cs diff --git a/SpreadCheetah.SourceGenerator/Extensions/CompilationExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/CompilationExtensions.cs deleted file mode 100644 index 17d3acad..00000000 --- a/SpreadCheetah.SourceGenerator/Extensions/CompilationExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.CodeAnalysis; -using SpreadCheetah.SourceGenerator.Models; -using System.Diagnostics.CodeAnalysis; - -namespace SpreadCheetah.SourceGenerator.Extensions; - -internal static class CompilationExtensions -{ - [ExcludeFromCodeCoverage] - public static bool TryGetCompilationTypes( - this Compilation compilation, - [NotNullWhen(true)] out CompilationTypes? result) - { - result = null; - const string ns = "SpreadCheetah.SourceGeneration"; - - if (!compilation.TryGetType($"{ns}.ColumnHeaderAttribute", out var columnHeader)) - return false; - if (!compilation.TryGetType($"{ns}.ColumnOrderAttribute", out var columnOrder)) - return false; - if (!compilation.TryGetType($"{ns}.WorksheetRowContext", out var context)) - return false; - if (!compilation.TryGetType($"{ns}.WorksheetRowGenerationOptionsAttribute", out var options)) - return false; - - result = new CompilationTypes( - ColumnHeaderAttribute: columnHeader, - ColumnOrderAttribute: columnOrder, - WorksheetRowContext: context, - WorksheetRowGenerationOptionsAttribute: options); - - return true; - } - - private static bool TryGetType( - this Compilation compilation, - string fullyQualifiedMetadataName, - [NotNullWhen(true)] out INamedTypeSymbol? result) - { - result = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); - return result is not null; - } -} diff --git a/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs b/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs index 17da7682..3d7e9669 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/Attributes.cs @@ -4,4 +4,5 @@ internal static class Attributes { public const string ColumnHeader = "SpreadCheetah.SourceGeneration.ColumnHeaderAttribute"; public const string ColumnOrder = "SpreadCheetah.SourceGeneration.ColumnOrderAttribute"; + public const string GenerationOptions = "SpreadCheetah.SourceGeneration.WorksheetRowGenerationOptionsAttribute"; } diff --git a/SpreadCheetah.SourceGenerator/Models/CompilationTypes.cs b/SpreadCheetah.SourceGenerator/Models/CompilationTypes.cs deleted file mode 100644 index 8b76253b..00000000 --- a/SpreadCheetah.SourceGenerator/Models/CompilationTypes.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace SpreadCheetah.SourceGenerator.Models; - -internal sealed record CompilationTypes( - INamedTypeSymbol WorksheetRowGenerationOptionsAttribute, - INamedTypeSymbol ColumnHeaderAttribute, - INamedTypeSymbol ColumnOrderAttribute, - INamedTypeSymbol WorksheetRowContext); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 8ee38c93..65b19eb2 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -50,10 +50,7 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat if (classSymbol is not { IsStatic: false, BaseType: { } baseType }) return null; - if (!context.SemanticModel.Compilation.TryGetCompilationTypes(out var compilationTypes)) - return null; - - if (!SymbolEqualityComparer.Default.Equals(compilationTypes.WorksheetRowContext, baseType)) + if (!string.Equals("SpreadCheetah.SourceGeneration.WorksheetRowContext", baseType.ToDisplayString(), StringComparison.Ordinal)) return null; var rowTypes = new Dictionary(SymbolEqualityComparer.Default); @@ -73,7 +70,7 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat foreach (var attribute in classSymbol.GetAttributes()) { - if (TryParseOptionsAttribute(attribute, compilationTypes.WorksheetRowGenerationOptionsAttribute, out var options)) + if (TryParseOptionsAttribute(attribute, out var options)) generatorOptions = options; } @@ -112,12 +109,11 @@ private static bool TryParseWorksheetRowAttribute( private static bool TryParseOptionsAttribute( AttributeData attribute, - INamedTypeSymbol expectedAttribute, [NotNullWhen(true)] out GeneratorOptions? options) { options = null; - if (!SymbolEqualityComparer.Default.Equals(expectedAttribute, attribute.AttributeClass)) + if (!string.Equals(Attributes.GenerationOptions, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) return false; if (attribute.NamedArguments.IsDefaultOrEmpty) From 9b1fd690f6c1a1b87e1bbbf01fe7a92092a4527a Mon Sep 17 00:00:00 2001 From: sveinungf Date: Mon, 12 Feb 2024 00:35:47 +0100 Subject: [PATCH 08/31] Src gen: Avoid combining with the compilation --- .../WorksheetRowGenerator.cs | 83 +++++++++---------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 65b19eb2..822e91cf 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -17,20 +17,22 @@ public class WorksheetRowGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - var filtered = context.SyntaxProvider + // TODO: Add tracking name (if it can't be hardcoded) + var supportedNullableTypes = context.CompilationProvider.Select(static (c, _) => GetSupportedNullableTypes(c)); + + var contextClasses = context.SyntaxProvider .ForAttributeWithMetadataName( "SpreadCheetah.SourceGeneration.WorksheetRowAttribute", IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration) .Where(static x => x is not null) - .Collect() .WithTrackingName(TrackingNames.InitialExtraction); - var source = context.CompilationProvider - .Combine(filtered) + var combined = contextClasses + .Combine(supportedNullableTypes) .WithTrackingName(TrackingNames.Transform); - context.RegisterSourceOutput(source, static (spc, source) => Execute(source.Left, source.Right, spc)); + context.RegisterSourceOutput(combined, static (spc, source) => Execute(source.Left, source.Right, spc)); } private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, CancellationToken _) => syntaxNode is ClassDeclarationSyntax @@ -168,7 +170,7 @@ private static bool TryParseColumnOrderAttribute( return true; } - private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, + private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray supportedNullableTypes, ITypeSymbol classType, SourceProductionContext context) { var implicitOrderProperties = new List(); @@ -187,7 +189,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(Compilation compilation, continue; } - if (!IsSupportedType(p.Type, compilation)) + if (!IsSupportedType(p.Type, supportedNullableTypes)) { unsupportedPropertyNames.Add(p); continue; @@ -238,31 +240,33 @@ private static bool TryGetExplicitColumnOrder(IPropertySymbol property, return false; } - private static bool IsSupportedType(ITypeSymbol type, Compilation compilation) + private static EquatableArray GetSupportedNullableTypes(Compilation compilation) { - return type.SpecialType == SpecialType.System_String - || SupportedPrimitiveTypes.Contains(type.SpecialType) - || IsSupportedNullableType(compilation, type); - } - - private static bool IsSupportedNullableType(Compilation compilation, ITypeSymbol type) - { - if (type.NullableAnnotation != NullableAnnotation.Annotated) - return false; - + var result = new List(); var nullableT = compilation.GetTypeByMetadataName("System.Nullable`1"); + // TODO: Could be hardcoded? foreach (var primitiveType in SupportedPrimitiveTypes) { var nullableType = nullableT?.Construct(compilation.GetSpecialType(primitiveType)); - if (nullableType is null) - continue; - - if (nullableType.Equals(type, SymbolEqualityComparer.Default)) - return true; + if (nullableType is not null) + result.Add(nullableType.ToDisplayString()); } - return false; + return new EquatableArray(result.ToArray()); + } + + private static bool IsSupportedType(ITypeSymbol type, EquatableArray supportedNullableTypes) + { + return type.SpecialType == SpecialType.System_String + || SupportedPrimitiveTypes.Contains(type.SpecialType) + || IsSupportedNullableType(type, supportedNullableTypes); + } + + private static bool IsSupportedNullableType(ITypeSymbol type, EquatableArray supportedNullableTypes) + { + return type.NullableAnnotation == NullableAnnotation.Annotated + && supportedNullableTypes.Contains(type.ToDisplayString(), StringComparer.Ordinal); } private static readonly SpecialType[] SupportedPrimitiveTypes = @@ -276,28 +280,19 @@ private static bool IsSupportedNullableType(Compilation compilation, ITypeSymbol SpecialType.System_Single ]; - private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) + private static void Execute(ContextClass? contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) { - if (classes.IsDefaultOrEmpty) + if (contextClass is null) return; var sb = new StringBuilder(); + GenerateCode(sb, contextClass, supportedNullableTypes, context); - foreach (var item in classes) - { - if (item is null) continue; - - context.CancellationToken.ThrowIfCancellationRequested(); + var hintName = contextClass.Namespace is { } ns + ? $"{ns}.{contextClass.Name}.g.cs" + : $"{contextClass.Name}.g.cs"; - sb.Clear(); - GenerateCode(sb, item, compilation, context); - - var hintName = item.Namespace is { } ns - ? $"{ns}.{item.Name}.g.cs" - : $"{item.Name}.g.cs"; - - context.AddSource(hintName, sb.ToString()); - } + context.AddSource(hintName, sb.ToString()); } private static void GenerateHeader(StringBuilder sb) @@ -314,7 +309,7 @@ private static void GenerateHeader(StringBuilder sb) sb.AppendLine(); } - private static void GenerateCode(StringBuilder sb, ContextClass contextClass, Compilation compilation, SourceProductionContext context) + private static void GenerateCode(StringBuilder sb, ContextClass contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) { GenerateHeader(sb); @@ -342,7 +337,7 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, Co if (!rowTypeNames.Add(rowTypeName)) continue; - GenerateCodeForType(sb, typeIndex, rowType, location, contextClass, compilation, context); + GenerateCodeForType(sb, typeIndex, rowType, location, contextClass, supportedNullableTypes, context); ++typeIndex; } @@ -351,12 +346,12 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, Co } private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedTypeSymbol rowType, Location location, - ContextClass contextClass, Compilation compilation, SourceProductionContext context) + ContextClass contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) { var rowTypeName = rowType.Name; var rowTypeFullName = rowType.ToString(); - var info = AnalyzeTypeProperties(compilation, rowType, context); + var info = AnalyzeTypeProperties(supportedNullableTypes, rowType, context); ReportDiagnostics(info, rowType, location, contextClass.Options, context); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" From 43a7128ca1db902d996ad367250a227bc78b70ff Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 16 Feb 2024 19:41:50 +0100 Subject: [PATCH 09/31] Normalize line endings in special character column header test --- .../Helpers/Backporting/StringExtensions.cs | 29 +++++++++++++++++++ .../Tests/WorksheetRowGeneratorTests.cs | 3 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 SpreadCheetah.SourceGenerator.Test/Helpers/Backporting/StringExtensions.cs diff --git a/SpreadCheetah.SourceGenerator.Test/Helpers/Backporting/StringExtensions.cs b/SpreadCheetah.SourceGenerator.Test/Helpers/Backporting/StringExtensions.cs new file mode 100644 index 00000000..dda9a1f5 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Helpers/Backporting/StringExtensions.cs @@ -0,0 +1,29 @@ +using System.Text; + +namespace SpreadCheetah.SourceGenerator.Test.Helpers.Backporting; + +internal static class StringExtensions +{ + public static string ReplaceLineEndings(this string value) + { + if (string.Equals(Environment.NewLine, "\n", StringComparison.Ordinal)) + { +#pragma warning disable CA1307 // Specify StringComparison for clarity + return value.Replace("\r\n", Environment.NewLine); +#pragma warning restore CA1307 // Specify StringComparison for clarity + } + + var parts = value.Split('\n'); + var sb = new StringBuilder(); + + foreach (var part in parts) + { + if (part.Length > 1 && part[^1] == '\r') + sb.Append(part.AsSpan(0, part.Length - 1)); + else + sb.Append(part); + } + + return sb.ToString(); + } +} diff --git a/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs b/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs index 76a566d7..346d4d03 100644 --- a/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs +++ b/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs @@ -2,6 +2,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using SpreadCheetah.SourceGeneration; using SpreadCheetah.SourceGenerator.Test.Helpers; +using SpreadCheetah.SourceGenerator.Test.Helpers.Backporting; using SpreadCheetah.SourceGenerator.Test.Models; using SpreadCheetah.SourceGenerator.Test.Models.Accessibility; using SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader; @@ -596,7 +597,7 @@ public async Task Spreadsheet_AddHeaderRow_SpecialCharacterColumnHeaders() // Assert using var sheet = SpreadsheetAssert.SingleSheet(stream); - Assert.Equal(expectedValues, sheet.Row(1).Select(x => x.StringValue)); + Assert.Equal(expectedValues.Select(x => x.ReplaceLineEndings()), sheet.Row(1).Select(x => x.StringValue?.ReplaceLineEndings())); } [Fact] From 9de2eb64d48bf2faecdd17b20e853bf1bc2efcc1 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 16 Feb 2024 19:43:27 +0100 Subject: [PATCH 10/31] Add cachable models for use by the source generator --- SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs | 5 +++++ SpreadCheetah.SourceGenerator/Models/LocationInfo.cs | 5 +++++ SpreadCheetah.SourceGenerator/Models/RowType.cs | 7 +++++++ SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs | 7 +++++++ 4 files changed, 24 insertions(+) create mode 100644 SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs create mode 100644 SpreadCheetah.SourceGenerator/Models/LocationInfo.cs create mode 100644 SpreadCheetah.SourceGenerator/Models/RowType.cs create mode 100644 SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs diff --git a/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs b/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs new file mode 100644 index 00000000..13b81196 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis; + +namespace SpreadCheetah.SourceGenerator.Models; + +internal sealed record DiagnosticInfo(DiagnosticDescriptor Descriptor, LocationInfo? Location); diff --git a/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs b/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs new file mode 100644 index 00000000..206e7395 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis.Text; + +namespace SpreadCheetah.SourceGenerator.Models; + +internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/RowType.cs b/SpreadCheetah.SourceGenerator/Models/RowType.cs new file mode 100644 index 00000000..7796fa9a --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/RowType.cs @@ -0,0 +1,7 @@ +namespace SpreadCheetah.SourceGenerator.Models; + +internal sealed record RowType( + string Name, + string FullName, + string FullNameWithNullableAnnotation, + bool IsReferenceType); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs new file mode 100644 index 00000000..cc2188bb --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs @@ -0,0 +1,7 @@ +using Microsoft.CodeAnalysis; + +namespace SpreadCheetah.SourceGenerator.Models; + +internal sealed record RowTypeProperty( + string Name, + TypedConstant? ColumnHeaderAttributeValue); \ No newline at end of file From f53a96b5e943c575e2b2d00561176516d2f7ebfb Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 16 Feb 2024 20:09:06 +0100 Subject: [PATCH 11/31] WIP for making the src gen cachable --- .../Helpers/ContextClass.cs | 3 +- .../Helpers/DiagnosticInfoMap.cs | 25 ++++ .../Helpers/StringBuilderExtensions.cs | 7 - .../Helpers/SymbolExtensions.cs | 12 -- .../Helpers/TypePropertiesInfo.cs | 3 +- .../WorksheetRowGenerator.cs | 121 +++++++++--------- 6 files changed, 91 insertions(+), 80 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs delete mode 100644 SpreadCheetah.SourceGenerator/Helpers/SymbolExtensions.cs diff --git a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs index efc153eb..181cc28c 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using SpreadCheetah.SourceGenerator.Models; namespace SpreadCheetah.SourceGenerator.Helpers; @@ -6,5 +7,5 @@ internal sealed record ContextClass( string Name, Accessibility DeclaredAccessibility, string? Namespace, - Dictionary RowTypes, // TODO: Don't use INamedTypeSymbol + Dictionary RowTypes, // TODO: Don't use INamedTypeSymbol GeneratorOptions? Options); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs b/SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs new file mode 100644 index 00000000..e31b964a --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; +using SpreadCheetah.SourceGenerator.Models; + +namespace SpreadCheetah.SourceGenerator.Helpers; + +internal static class DiagnosticInfoMap +{ + public static Location ToLocation(this LocationInfo info) + { + return Location.Create(info.FilePath, info.TextSpan, info.LineSpan); + } + + public static LocationInfo? ToLocationInfo(this SyntaxNode node) + { + return node.GetLocation().ToLocationInfo(); + } + + public static LocationInfo? ToLocationInfo(this Location location) + { + if (location.SourceTree is null) + return null; + + return new LocationInfo(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); + } +} diff --git a/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs b/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs index 46c70784..4b8a135d 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs @@ -1,4 +1,3 @@ -using Microsoft.CodeAnalysis; using System.Text; namespace SpreadCheetah.SourceGenerator.Helpers; @@ -21,10 +20,4 @@ public static StringBuilder AppendLine(this StringBuilder sb, int indentationLev { return sb.AppendIndentation(indentationLevel).AppendLine(value); } - - public static StringBuilder AppendType(this StringBuilder sb, INamedTypeSymbol symbol) - { - sb.Append(symbol); - return symbol.IsReferenceType ? sb.Append('?') : sb; - } } diff --git a/SpreadCheetah.SourceGenerator/Helpers/SymbolExtensions.cs b/SpreadCheetah.SourceGenerator/Helpers/SymbolExtensions.cs deleted file mode 100644 index 21cd0704..00000000 --- a/SpreadCheetah.SourceGenerator/Helpers/SymbolExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace SpreadCheetah.SourceGenerator.Helpers; - -internal static class SymbolExtensions -{ - public static string ToTypeString(this INamedTypeSymbol symbol) - { - var result = symbol.ToString(); - return symbol.IsReferenceType ? result + '?' : result; - } -} diff --git a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs index 45c00034..f120248b 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs @@ -1,7 +1,6 @@ -using Microsoft.CodeAnalysis; using SpreadCheetah.SourceGenerator.Models; namespace SpreadCheetah.SourceGenerator.Helpers; internal sealed record TypePropertiesInfo( SortedDictionary Properties, - List UnsupportedProperties); \ No newline at end of file + EquatableArray UnsupportedPropertyTypeNames); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 822e91cf..64d16a62 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -55,7 +55,7 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat if (!string.Equals("SpreadCheetah.SourceGeneration.WorksheetRowContext", baseType.ToDisplayString(), StringComparison.Ordinal)) return null; - var rowTypes = new Dictionary(SymbolEqualityComparer.Default); + var rowTypes = new Dictionary(SymbolEqualityComparer.Default); GeneratorOptions? generatorOptions = null; foreach (var worksheetRowAttribute in context.Attributes) @@ -63,7 +63,9 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat if (TryParseWorksheetRowAttribute(worksheetRowAttribute, token, out var typeSymbol, out var location) && !rowTypes.ContainsKey(typeSymbol)) { - rowTypes[typeSymbol] = location; + var locationInfo = location.ToLocationInfo(); + if (locationInfo is not null) + rowTypes[typeSymbol] = locationInfo; } } @@ -175,7 +177,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s { var implicitOrderProperties = new List(); var explicitOrderProperties = new SortedDictionary(); - var unsupportedPropertyNames = new List(); + var unsupportedPropertyTypeNames = new HashSet(StringComparer.Ordinal); foreach (var member in classType.GetMembers()) { @@ -189,55 +191,55 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s continue; } + TypedConstant? columnHeaderAttributeValue = null; + int? columnOrderValue = null; + Location? columnOrderAttributeLocation = null; + + foreach (var attribute in p.GetAttributes()) + { + if (columnHeaderAttributeValue is null && TryParseColumnHeaderAttribute(attribute, out var arg)) + { + columnHeaderAttributeValue = arg; + } + + if (columnOrderValue is null && TryParseColumnOrderAttribute(attribute, out var order)) + { + columnOrderValue = order; + columnOrderAttributeLocation = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(); + } + } + + var rowTypeProperty = new RowTypeProperty( + Name: p.Name, + ColumnHeaderAttributeValue: columnHeaderAttributeValue); + if (!IsSupportedType(p.Type, supportedNullableTypes)) { - unsupportedPropertyNames.Add(p); + unsupportedPropertyTypeNames.Add(p.Type.Name); continue; } - var columnHeader = GetColumnHeader(p); - var columnProperty = new ColumnProperty(p.Name, columnHeader); + var columnHeader = GetColumnHeader(rowTypeProperty); + var columnProperty = new ColumnProperty(rowTypeProperty.Name, columnHeader); - if (!TryGetExplicitColumnOrder(p, context.CancellationToken, out var columnOrder, out var location)) + if (columnOrderValue is not { } columnOrder) implicitOrderProperties.Add(columnProperty); else if (!explicitOrderProperties.ContainsKey(columnOrder)) explicitOrderProperties.Add(columnOrder, columnProperty); else - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.DuplicateColumnOrder, location, classType.Name)); + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.DuplicateColumnOrder, columnOrderAttributeLocation, classType.Name)); } explicitOrderProperties.AddWithImplicitKeys(implicitOrderProperties); - return new TypePropertiesInfo(explicitOrderProperties, unsupportedPropertyNames); - } - - private static string GetColumnHeader(IPropertySymbol property) - { - foreach (var attribute in property.GetAttributes()) - { - if (TryParseColumnHeaderAttribute(attribute, out var arg)) - return arg.ToCSharpString(); - } - - return @$"""{property.Name}"""; + return new TypePropertiesInfo(explicitOrderProperties, new EquatableArray(unsupportedPropertyTypeNames.ToArray())); } - private static bool TryGetExplicitColumnOrder(IPropertySymbol property, - CancellationToken token, out int columnOrder, out Location? location) + private static string GetColumnHeader(RowTypeProperty property) { - columnOrder = 0; - location = null; - - foreach (var attribute in property.GetAttributes()) - { - if (!TryParseColumnOrderAttribute(attribute, out columnOrder)) - continue; - - location = attribute.ApplicationSyntaxReference?.GetSyntax(token).GetLocation(); - return true; - } - - return false; + return property.ColumnHeaderAttributeValue is { } attributeValue + ? attributeValue.ToCSharpString() + : @$"""{property.Name}"""; } private static EquatableArray GetSupportedNullableTypes(Compilation compilation) @@ -345,52 +347,55 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, Eq sb.AppendLine("}"); } - private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedTypeSymbol rowType, Location location, + private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedTypeSymbol rowTypeOld, LocationInfo location, ContextClass contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) { - var rowTypeName = rowType.Name; - var rowTypeFullName = rowType.ToString(); + var rowType = new RowType( + Name: rowTypeOld.Name, + FullName: rowTypeOld.ToString(), + FullNameWithNullableAnnotation: rowTypeOld.IsReferenceType ? $"{rowTypeOld}?" : rowTypeOld.ToString(), + IsReferenceType: rowTypeOld.IsReferenceType); - var info = AnalyzeTypeProperties(supportedNullableTypes, rowType, context); + var info = AnalyzeTypeProperties(supportedNullableTypes, rowTypeOld, context); ReportDiagnostics(info, rowType, location, contextClass.Options, context); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" - private WorksheetRowTypeInfo<{{rowTypeFullName}}>? _{{rowTypeName}}; - public WorksheetRowTypeInfo<{{rowTypeFullName}}> {{rowTypeName}} => _{{rowTypeName}} + private WorksheetRowTypeInfo<{{rowType.FullName}}>? _{{rowType.Name}}; + public WorksheetRowTypeInfo<{{rowType.FullName}}> {{rowType.Name}} => _{{rowType.Name}} """)); if (info.Properties.Count == 0) { sb.AppendLine($$""" - ??= EmptyWorksheetRowContext.CreateTypeInfo<{{rowTypeFullName}}>(); + ??= EmptyWorksheetRowContext.CreateTypeInfo<{{rowType.FullName}}>(); """); return; } sb.AppendLine(FormattableString.Invariant($$""" - ??= WorksheetRowMetadataServices.CreateObjectInfo<{{rowTypeFullName}}>(AddHeaderRow{{typeIndex}}Async, AddAsRowAsync, AddRangeAsRowsAsync); + ??= WorksheetRowMetadataServices.CreateObjectInfo<{{rowType.FullName}}>(AddHeaderRow{{typeIndex}}Async, AddAsRowAsync, AddRangeAsRowsAsync); """)); var properties = info.Properties.Values.ToList(); GenerateAddHeaderRow(sb, typeIndex, properties); GenerateAddAsRow(sb, 2, rowType); GenerateAddRangeAsRows(sb, 2, rowType); - GenerateAddAsRowInternal(sb, 2, rowTypeFullName, properties); + GenerateAddAsRowInternal(sb, 2, rowType.FullName, properties); GenerateAddRangeAsRowsInternal(sb, rowType, properties); GenerateAddEnumerableAsRows(sb, 2, rowType); GenerateAddCellsAsRow(sb, 2, rowType, properties); } - private static void ReportDiagnostics(TypePropertiesInfo info, INamedTypeSymbol rowType, Location location, GeneratorOptions? options, SourceProductionContext context) + private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, LocationInfo location, GeneratorOptions? options, SourceProductionContext context) { if (options?.SuppressWarnings ?? false) return; if (info.Properties.Count == 0) - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoPropertiesFound, location, rowType.Name)); + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoPropertiesFound, location.ToLocation(), rowType.Name)); - if (info.UnsupportedProperties.FirstOrDefault() is { } unsupportedProperty) - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location, rowType.Name, unsupportedProperty.Type.Name)); + if (info.UnsupportedPropertyTypeNames.FirstOrDefault() is { } unsupportedPropertyTypeName) + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); } private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyList properties) @@ -424,12 +429,12 @@ private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadO """); } - private static void GenerateAddAsRow(StringBuilder sb, int indent, INamedTypeSymbol rowType) + private static void GenerateAddAsRow(StringBuilder sb, int indent, RowType rowType) { sb.AppendLine() .AppendIndentation(indent) .Append("private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, ") - .AppendType(rowType) + .Append(rowType.FullNameWithNullableAnnotation) .AppendLine(" obj, CancellationToken token)"); sb.AppendLine(indent, "{"); @@ -465,12 +470,12 @@ private static void GenerateAddAsRowInternal(StringBuilder sb, int indent, strin sb.AppendLine(indent, "}"); } - private static void GenerateAddRangeAsRows(StringBuilder sb, int indent, INamedTypeSymbol rowType) + private static void GenerateAddRangeAsRows(StringBuilder sb, int indent, RowType rowType) { sb.AppendLine() .AppendIndentation(indent) .Append("private static ValueTask AddRangeAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<") - .AppendType(rowType) + .Append(rowType.FullNameWithNullableAnnotation) .AppendLine("> objs, CancellationToken token)"); sb.AppendLine(indent, "{"); @@ -482,11 +487,11 @@ private static void GenerateAddRangeAsRows(StringBuilder sb, int indent, INamedT sb.AppendLine(indent, "}"); } - private static void GenerateAddRangeAsRowsInternal(StringBuilder sb, INamedTypeSymbol rowType, IReadOnlyCollection properties) + private static void GenerateAddRangeAsRowsInternal(StringBuilder sb, RowType rowType, IReadOnlyCollection properties) { Debug.Assert(properties.Count > 0); - var typeString = rowType.ToTypeString(); + var typeString = rowType.FullNameWithNullableAnnotation; sb.Append($$""" private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<{{typeString}}> objs, CancellationToken token) @@ -505,12 +510,12 @@ private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreads """); } - private static void GenerateAddEnumerableAsRows(StringBuilder sb, int indent, INamedTypeSymbol rowType) + private static void GenerateAddEnumerableAsRows(StringBuilder sb, int indent, RowType rowType) { sb.AppendLine() .AppendIndentation(indent) .Append("private static async ValueTask AddEnumerableAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<") - .AppendType(rowType) + .Append(rowType.FullNameWithNullableAnnotation) .AppendLine("> objs, DataCell[] cells, CancellationToken token)"); sb.AppendLine(indent, "{"); @@ -521,14 +526,14 @@ private static void GenerateAddEnumerableAsRows(StringBuilder sb, int indent, IN sb.AppendLine(indent, "}"); } - private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, INamedTypeSymbol rowType, IReadOnlyList properties) + private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType rowType, IReadOnlyList properties) { Debug.Assert(properties.Count > 0); sb.AppendLine() .AppendIndentation(indent) .Append("private static ValueTask AddCellsAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, ") - .AppendType(rowType) + .Append(rowType.FullNameWithNullableAnnotation) .AppendLine(" obj, DataCell[] cells, CancellationToken token)"); sb.AppendLine(indent, "{"); From 36bd64cda001bf6180f1fecfaff8478620c2691c Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 16 Feb 2024 22:58:21 +0100 Subject: [PATCH 12/31] Move property type information to RowTypeProperty --- .../Models/RowTypeProperty.cs | 4 ++++ .../WorksheetRowGenerator.cs | 22 +++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs index cc2188bb..ff29d37e 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs @@ -4,4 +4,8 @@ namespace SpreadCheetah.SourceGenerator.Models; internal sealed record RowTypeProperty( string Name, + string TypeName, + string TypeFullName, + NullableAnnotation TypeNullableAnnotation, + SpecialType TypeSpecialType, TypedConstant? ColumnHeaderAttributeValue); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 64d16a62..f724da03 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -211,11 +211,15 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s var rowTypeProperty = new RowTypeProperty( Name: p.Name, + TypeName: p.Type.Name, + TypeFullName: p.Type.ToDisplayString(), + TypeNullableAnnotation: p.NullableAnnotation, + TypeSpecialType: p.Type.SpecialType, ColumnHeaderAttributeValue: columnHeaderAttributeValue); - if (!IsSupportedType(p.Type, supportedNullableTypes)) + if (!IsSupportedType(rowTypeProperty, supportedNullableTypes)) { - unsupportedPropertyTypeNames.Add(p.Type.Name); + unsupportedPropertyTypeNames.Add(rowTypeProperty.TypeName); continue; } @@ -258,17 +262,17 @@ private static EquatableArray GetSupportedNullableTypes(Compilation comp return new EquatableArray(result.ToArray()); } - private static bool IsSupportedType(ITypeSymbol type, EquatableArray supportedNullableTypes) + private static bool IsSupportedType(RowTypeProperty typeProperty, EquatableArray supportedNullableTypes) { - return type.SpecialType == SpecialType.System_String - || SupportedPrimitiveTypes.Contains(type.SpecialType) - || IsSupportedNullableType(type, supportedNullableTypes); + return typeProperty.TypeSpecialType == SpecialType.System_String + || SupportedPrimitiveTypes.Contains(typeProperty.TypeSpecialType) + || IsSupportedNullableType(typeProperty, supportedNullableTypes); } - private static bool IsSupportedNullableType(ITypeSymbol type, EquatableArray supportedNullableTypes) + private static bool IsSupportedNullableType(RowTypeProperty typeProperty, EquatableArray supportedNullableTypes) { - return type.NullableAnnotation == NullableAnnotation.Annotated - && supportedNullableTypes.Contains(type.ToDisplayString(), StringComparer.Ordinal); + return typeProperty.TypeNullableAnnotation == NullableAnnotation.Annotated + && supportedNullableTypes.Contains(typeProperty.TypeFullName, StringComparer.Ordinal); } private static readonly SpecialType[] SupportedPrimitiveTypes = From 96993c00b70684bacec82fc4385937c46e1909ef Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 16 Feb 2024 23:18:19 +0100 Subject: [PATCH 13/31] Replace ColumnProperty with RowTypeProperty --- .../Helpers/TypePropertiesInfo.cs | 2 +- .../Models/ColumnProperty.cs | 3 -- .../Models/RowTypeProperty.cs | 2 +- .../WorksheetRowGenerator.cs | 34 ++++++++----------- 4 files changed, 16 insertions(+), 25 deletions(-) delete mode 100644 SpreadCheetah.SourceGenerator/Models/ColumnProperty.cs diff --git a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs index f120248b..9007bd3d 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs @@ -2,5 +2,5 @@ namespace SpreadCheetah.SourceGenerator.Helpers; internal sealed record TypePropertiesInfo( - SortedDictionary Properties, + SortedDictionary Properties, EquatableArray UnsupportedPropertyTypeNames); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/ColumnProperty.cs b/SpreadCheetah.SourceGenerator/Models/ColumnProperty.cs deleted file mode 100644 index f918cb08..00000000 --- a/SpreadCheetah.SourceGenerator/Models/ColumnProperty.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SpreadCheetah.SourceGenerator.Models; - -internal sealed record ColumnProperty(string PropertyName, string ColumnHeader); diff --git a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs index ff29d37e..9b4321eb 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs @@ -8,4 +8,4 @@ internal sealed record RowTypeProperty( string TypeFullName, NullableAnnotation TypeNullableAnnotation, SpecialType TypeSpecialType, - TypedConstant? ColumnHeaderAttributeValue); \ No newline at end of file + string ColumnHeader); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index f724da03..130243ee 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -175,8 +175,8 @@ private static bool TryParseColumnOrderAttribute( private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray supportedNullableTypes, ITypeSymbol classType, SourceProductionContext context) { - var implicitOrderProperties = new List(); - var explicitOrderProperties = new SortedDictionary(); + var implicitOrderProperties = new List(); + var explicitOrderProperties = new SortedDictionary(); var unsupportedPropertyTypeNames = new HashSet(StringComparer.Ordinal); foreach (var member in classType.GetMembers()) @@ -209,13 +209,17 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s } } + var columnHeader = columnHeaderAttributeValue is { } value + ? value.ToCSharpString() + : @$"""{p.Name}"""; + var rowTypeProperty = new RowTypeProperty( Name: p.Name, TypeName: p.Type.Name, TypeFullName: p.Type.ToDisplayString(), TypeNullableAnnotation: p.NullableAnnotation, TypeSpecialType: p.Type.SpecialType, - ColumnHeaderAttributeValue: columnHeaderAttributeValue); + ColumnHeader: columnHeader); if (!IsSupportedType(rowTypeProperty, supportedNullableTypes)) { @@ -223,13 +227,10 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s continue; } - var columnHeader = GetColumnHeader(rowTypeProperty); - var columnProperty = new ColumnProperty(rowTypeProperty.Name, columnHeader); - if (columnOrderValue is not { } columnOrder) - implicitOrderProperties.Add(columnProperty); + implicitOrderProperties.Add(rowTypeProperty); else if (!explicitOrderProperties.ContainsKey(columnOrder)) - explicitOrderProperties.Add(columnOrder, columnProperty); + explicitOrderProperties.Add(columnOrder, rowTypeProperty); else context.ReportDiagnostic(Diagnostic.Create(Diagnostics.DuplicateColumnOrder, columnOrderAttributeLocation, classType.Name)); } @@ -239,13 +240,6 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s return new TypePropertiesInfo(explicitOrderProperties, new EquatableArray(unsupportedPropertyTypeNames.ToArray())); } - private static string GetColumnHeader(RowTypeProperty property) - { - return property.ColumnHeaderAttributeValue is { } attributeValue - ? attributeValue.ToCSharpString() - : @$"""{property.Name}"""; - } - private static EquatableArray GetSupportedNullableTypes(Compilation compilation) { var result = new List(); @@ -402,7 +396,7 @@ private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); } - private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyList properties) + private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyList properties) { Debug.Assert(properties.Count > 0); @@ -455,7 +449,7 @@ private static void GenerateAddAsRow(StringBuilder sb, int indent, RowType rowTy sb.AppendLine(indent, "}"); } - private static void GenerateAddAsRowInternal(StringBuilder sb, int indent, string rowTypeFullname, IReadOnlyCollection properties) + private static void GenerateAddAsRowInternal(StringBuilder sb, int indent, string rowTypeFullname, IReadOnlyCollection properties) { Debug.Assert(properties.Count > 0); @@ -491,7 +485,7 @@ private static void GenerateAddRangeAsRows(StringBuilder sb, int indent, RowType sb.AppendLine(indent, "}"); } - private static void GenerateAddRangeAsRowsInternal(StringBuilder sb, RowType rowType, IReadOnlyCollection properties) + private static void GenerateAddRangeAsRowsInternal(StringBuilder sb, RowType rowType, IReadOnlyCollection properties) { Debug.Assert(properties.Count > 0); @@ -530,7 +524,7 @@ private static void GenerateAddEnumerableAsRows(StringBuilder sb, int indent, Ro sb.AppendLine(indent, "}"); } - private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType rowType, IReadOnlyList properties) + private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType rowType, IReadOnlyList properties) { Debug.Assert(properties.Count > 0); @@ -555,7 +549,7 @@ private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType .Append("cells[") .Append(i) .Append("] = new DataCell(obj.") - .Append(properties[i].PropertyName) + .Append(properties[i].Name) .AppendLine(");"); } From f91d73422c8cbfc88f087a229baae1a04942e71c Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 16 Feb 2024 23:24:33 +0100 Subject: [PATCH 14/31] Src gen: Add Index() extension method for IEnumerable --- .../Extensions/EnumerableExtensions.cs | 9 +++++++++ .../WorksheetRowGenerator.cs | 13 ++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs diff --git a/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000..f00a01af --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs @@ -0,0 +1,9 @@ +namespace SpreadCheetah.SourceGenerator.Extensions; + +internal static class EnumerableExtensions +{ + public static IEnumerable<(int Index, T Element)> Index(this IEnumerable elements) + { + return elements.Select((x, i) => (i, x)); + } +} diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 130243ee..cecb0e9a 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -375,7 +375,7 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT ??= WorksheetRowMetadataServices.CreateObjectInfo<{{rowType.FullName}}>(AddHeaderRow{{typeIndex}}Async, AddAsRowAsync, AddRangeAsRowsAsync); """)); - var properties = info.Properties.Values.ToList(); + var properties = info.Properties.Values; GenerateAddHeaderRow(sb, typeIndex, properties); GenerateAddAsRow(sb, 2, rowType); GenerateAddRangeAsRows(sb, 2, rowType); @@ -396,7 +396,7 @@ private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); } - private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyList properties) + private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyCollection properties) { Debug.Assert(properties.Count > 0); @@ -408,9 +408,8 @@ private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadO { """)); - for (var i = 0; i < properties.Count; i++) + foreach (var (i, property) in properties.Index()) { - var property = properties[i]; sb.AppendLine(FormattableString.Invariant($""" cells[{i}] = new StyledCell({property.ColumnHeader}, styleId); """)); @@ -524,7 +523,7 @@ private static void GenerateAddEnumerableAsRows(StringBuilder sb, int indent, Ro sb.AppendLine(indent, "}"); } - private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType rowType, IReadOnlyList properties) + private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType rowType, IReadOnlyCollection properties) { Debug.Assert(properties.Count > 0); @@ -543,13 +542,13 @@ private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType sb.AppendLine(); } - for (var i = 0; i < properties.Count; i++) + foreach (var (i, property) in properties.Index()) { sb.AppendIndentation(indent + 1) .Append("cells[") .Append(i) .Append("] = new DataCell(obj.") - .Append(properties[i].Name) + .Append(property.Name) .AppendLine(");"); } From 281b1bb066470d47c44de2ce5a30d9e4de497146 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 15:38:00 +0100 Subject: [PATCH 15/31] Src gen: ToEquatableArray extension --- .../Extensions/EnumerableExtensions.cs | 9 +++++++++ SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs index f00a01af..e9bfe177 100644 --- a/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Extensions/EnumerableExtensions.cs @@ -1,3 +1,5 @@ +using SpreadCheetah.SourceGenerator.Helpers; + namespace SpreadCheetah.SourceGenerator.Extensions; internal static class EnumerableExtensions @@ -6,4 +8,11 @@ internal static class EnumerableExtensions { return elements.Select((x, i) => (i, x)); } + + public static EquatableArray ToEquatableArray(this IEnumerable elements) + where T : IEquatable + { + var array = elements is T[] arr ? arr : elements.ToArray(); + return new EquatableArray(array); + } } diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index cecb0e9a..5ceadd51 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -237,7 +237,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s explicitOrderProperties.AddWithImplicitKeys(implicitOrderProperties); - return new TypePropertiesInfo(explicitOrderProperties, new EquatableArray(unsupportedPropertyTypeNames.ToArray())); + return new TypePropertiesInfo(explicitOrderProperties, unsupportedPropertyTypeNames.ToEquatableArray()); } private static EquatableArray GetSupportedNullableTypes(Compilation compilation) @@ -253,7 +253,7 @@ private static EquatableArray GetSupportedNullableTypes(Compilation comp result.Add(nullableType.ToDisplayString()); } - return new EquatableArray(result.ToArray()); + return result.ToEquatableArray(); } private static bool IsSupportedType(RowTypeProperty typeProperty, EquatableArray supportedNullableTypes) From f173b579083964bcc69a03c0b7169ce0f7602bc7 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 19:32:26 +0100 Subject: [PATCH 16/31] Remove dependency to SourceProductionContext in AnalyzeTypeProperties --- .../Helpers/DiagnosticMap.cs | 12 +++++++++++ .../{DiagnosticInfoMap.cs => LocationMap.cs} | 2 +- .../Helpers/TypePropertiesInfo.cs | 3 ++- .../Models/DiagnosticInfo.cs | 6 +++++- .../WorksheetRowGenerator.cs | 21 +++++++++++++------ 5 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Helpers/DiagnosticMap.cs rename SpreadCheetah.SourceGenerator/Helpers/{DiagnosticInfoMap.cs => LocationMap.cs} (94%) diff --git a/SpreadCheetah.SourceGenerator/Helpers/DiagnosticMap.cs b/SpreadCheetah.SourceGenerator/Helpers/DiagnosticMap.cs new file mode 100644 index 00000000..25d1abf2 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/DiagnosticMap.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis; +using SpreadCheetah.SourceGenerator.Models; + +namespace SpreadCheetah.SourceGenerator.Helpers; + +internal static class DiagnosticMap +{ + public static Diagnostic ToDiagnostic(this DiagnosticInfo info) + { + return Diagnostic.Create(info.Descriptor, info.Location?.ToLocation(), info.MessageArgs.GetArray()); + } +} diff --git a/SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs b/SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs similarity index 94% rename from SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs rename to SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs index e31b964a..ad2f4d2e 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/DiagnosticInfoMap.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs @@ -3,7 +3,7 @@ namespace SpreadCheetah.SourceGenerator.Helpers; -internal static class DiagnosticInfoMap +internal static class LocationMap { public static Location ToLocation(this LocationInfo info) { diff --git a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs index 9007bd3d..359a3385 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs @@ -3,4 +3,5 @@ namespace SpreadCheetah.SourceGenerator.Helpers; internal sealed record TypePropertiesInfo( SortedDictionary Properties, - EquatableArray UnsupportedPropertyTypeNames); \ No newline at end of file + EquatableArray UnsupportedPropertyTypeNames, + EquatableArray DiagnosticInfos); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs b/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs index 13b81196..624f514d 100644 --- a/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs +++ b/SpreadCheetah.SourceGenerator/Models/DiagnosticInfo.cs @@ -1,5 +1,9 @@ using Microsoft.CodeAnalysis; +using SpreadCheetah.SourceGenerator.Helpers; namespace SpreadCheetah.SourceGenerator.Models; -internal sealed record DiagnosticInfo(DiagnosticDescriptor Descriptor, LocationInfo? Location); +internal sealed record DiagnosticInfo( + DiagnosticDescriptor Descriptor, + LocationInfo? Location, + EquatableArray MessageArgs); diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 5ceadd51..d01de9b1 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -173,11 +173,12 @@ private static bool TryParseColumnOrderAttribute( } private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray supportedNullableTypes, - ITypeSymbol classType, SourceProductionContext context) + ITypeSymbol classType, CancellationToken token) { var implicitOrderProperties = new List(); var explicitOrderProperties = new SortedDictionary(); var unsupportedPropertyTypeNames = new HashSet(StringComparer.Ordinal); + var diagnosticInfos = new List(); foreach (var member in classType.GetMembers()) { @@ -193,7 +194,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s TypedConstant? columnHeaderAttributeValue = null; int? columnOrderValue = null; - Location? columnOrderAttributeLocation = null; + LocationInfo? columnOrderAttributeLocation = null; foreach (var attribute in p.GetAttributes()) { @@ -205,7 +206,10 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s if (columnOrderValue is null && TryParseColumnOrderAttribute(attribute, out var order)) { columnOrderValue = order; - columnOrderAttributeLocation = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(); + columnOrderAttributeLocation = attribute.ApplicationSyntaxReference? + .GetSyntax(token) + .GetLocation() + .ToLocationInfo(); } } @@ -232,12 +236,12 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s else if (!explicitOrderProperties.ContainsKey(columnOrder)) explicitOrderProperties.Add(columnOrder, rowTypeProperty); else - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.DuplicateColumnOrder, columnOrderAttributeLocation, classType.Name)); + diagnosticInfos.Add(new DiagnosticInfo(Diagnostics.DuplicateColumnOrder, columnOrderAttributeLocation, new([classType.Name]))); } explicitOrderProperties.AddWithImplicitKeys(implicitOrderProperties); - return new TypePropertiesInfo(explicitOrderProperties, unsupportedPropertyTypeNames.ToEquatableArray()); + return new TypePropertiesInfo(explicitOrderProperties, unsupportedPropertyTypeNames.ToEquatableArray(), diagnosticInfos.ToEquatableArray()); } private static EquatableArray GetSupportedNullableTypes(Compilation compilation) @@ -354,7 +358,7 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT FullNameWithNullableAnnotation: rowTypeOld.IsReferenceType ? $"{rowTypeOld}?" : rowTypeOld.ToString(), IsReferenceType: rowTypeOld.IsReferenceType); - var info = AnalyzeTypeProperties(supportedNullableTypes, rowTypeOld, context); + var info = AnalyzeTypeProperties(supportedNullableTypes, rowTypeOld, context.CancellationToken); ReportDiagnostics(info, rowType, location, contextClass.Options, context); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" @@ -394,6 +398,11 @@ private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, if (info.UnsupportedPropertyTypeNames.FirstOrDefault() is { } unsupportedPropertyTypeName) context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); + + foreach (var diagnosticInfo in info.DiagnosticInfos) + { + context.ReportDiagnostic(diagnosticInfo.ToDiagnostic()); + } } private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyCollection properties) From a2f1b8b3ac987d0dcebac5399e2ff2bcbf733749 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 19:37:20 +0100 Subject: [PATCH 17/31] Handle SuppressWarnings in ReportDiagnostics --- .../WorksheetRowGenerator.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index d01de9b1..f4d3f247 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -391,18 +391,24 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, LocationInfo location, GeneratorOptions? options, SourceProductionContext context) { - if (options?.SuppressWarnings ?? false) return; + var suppressWarnings = options?.SuppressWarnings ?? false; + + foreach (var diagnosticInfo in info.DiagnosticInfos) + { + var isWarning = diagnosticInfo.Descriptor.DefaultSeverity == DiagnosticSeverity.Warning; + if (isWarning && suppressWarnings) + continue; + + context.ReportDiagnostic(diagnosticInfo.ToDiagnostic()); + } + + if (suppressWarnings) return; if (info.Properties.Count == 0) context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoPropertiesFound, location.ToLocation(), rowType.Name)); if (info.UnsupportedPropertyTypeNames.FirstOrDefault() is { } unsupportedPropertyTypeName) context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); - - foreach (var diagnosticInfo in info.DiagnosticInfos) - { - context.ReportDiagnostic(diagnosticInfo.ToDiagnostic()); - } } private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyCollection properties) From 19971e80d6304f0bd7c3a9a22b716253cfc6a796 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 19:58:07 +0100 Subject: [PATCH 18/31] Replace SortedDictionary with EquatableArray in TypePropertiesInfo --- .../Helpers/EquatableArray.cs | 2 +- .../Helpers/TypePropertiesInfo.cs | 2 +- .../WorksheetRowGenerator.cs | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs b/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs index 51f1aa42..e0a737a8 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs @@ -7,7 +7,7 @@ namespace SpreadCheetah.SourceGenerator.Helpers; /// https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs /// internal readonly struct EquatableArray(T[] underlyingArray) - : IEquatable>, IEnumerable + : IEquatable>, IReadOnlyCollection where T : IEquatable { public static readonly EquatableArray Empty = new([]); diff --git a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs index 359a3385..862716e5 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs @@ -2,6 +2,6 @@ namespace SpreadCheetah.SourceGenerator.Helpers; internal sealed record TypePropertiesInfo( - SortedDictionary Properties, + EquatableArray Properties, EquatableArray UnsupportedPropertyTypeNames, EquatableArray DiagnosticInfos); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index f4d3f247..d7fd58cf 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -241,7 +241,10 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s explicitOrderProperties.AddWithImplicitKeys(implicitOrderProperties); - return new TypePropertiesInfo(explicitOrderProperties, unsupportedPropertyTypeNames.ToEquatableArray(), diagnosticInfos.ToEquatableArray()); + return new TypePropertiesInfo( + Properties: explicitOrderProperties.Values.ToEquatableArray(), + UnsupportedPropertyTypeNames: unsupportedPropertyTypeNames.ToEquatableArray(), + DiagnosticInfos: diagnosticInfos.ToEquatableArray()); } private static EquatableArray GetSupportedNullableTypes(Compilation compilation) @@ -379,14 +382,13 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT ??= WorksheetRowMetadataServices.CreateObjectInfo<{{rowType.FullName}}>(AddHeaderRow{{typeIndex}}Async, AddAsRowAsync, AddRangeAsRowsAsync); """)); - var properties = info.Properties.Values; - GenerateAddHeaderRow(sb, typeIndex, properties); + GenerateAddHeaderRow(sb, typeIndex, info.Properties); GenerateAddAsRow(sb, 2, rowType); GenerateAddRangeAsRows(sb, 2, rowType); - GenerateAddAsRowInternal(sb, 2, rowType.FullName, properties); - GenerateAddRangeAsRowsInternal(sb, rowType, properties); + GenerateAddAsRowInternal(sb, 2, rowType.FullName, info.Properties); + GenerateAddRangeAsRowsInternal(sb, rowType, info.Properties); GenerateAddEnumerableAsRows(sb, 2, rowType); - GenerateAddCellsAsRow(sb, 2, rowType, properties); + GenerateAddCellsAsRow(sb, 2, rowType, info.Properties); } private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, LocationInfo location, GeneratorOptions? options, SourceProductionContext context) From b21356930d022dc7b7397d81c6cfe297968a3175 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 22:42:59 +0100 Subject: [PATCH 19/31] Hardcoded array of supported nullable types --- .../WorksheetRowGenerator.cs | 61 ++++++++----------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index d7fd58cf..de7188cb 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -17,9 +17,6 @@ public class WorksheetRowGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - // TODO: Add tracking name (if it can't be hardcoded) - var supportedNullableTypes = context.CompilationProvider.Select(static (c, _) => GetSupportedNullableTypes(c)); - var contextClasses = context.SyntaxProvider .ForAttributeWithMetadataName( "SpreadCheetah.SourceGeneration.WorksheetRowAttribute", @@ -28,11 +25,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(static x => x is not null) .WithTrackingName(TrackingNames.InitialExtraction); - var combined = contextClasses - .Combine(supportedNullableTypes) - .WithTrackingName(TrackingNames.Transform); - - context.RegisterSourceOutput(combined, static (spc, source) => Execute(source.Left, source.Right, spc)); + context.RegisterSourceOutput(contextClasses, static (spc, source) => Execute(source, spc)); } private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, CancellationToken _) => syntaxNode is ClassDeclarationSyntax @@ -172,8 +165,7 @@ private static bool TryParseColumnOrderAttribute( return true; } - private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray supportedNullableTypes, - ITypeSymbol classType, CancellationToken token) + private static TypePropertiesInfo AnalyzeTypeProperties(ITypeSymbol classType, CancellationToken token) { var implicitOrderProperties = new List(); var explicitOrderProperties = new SortedDictionary(); @@ -225,7 +217,7 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s TypeSpecialType: p.Type.SpecialType, ColumnHeader: columnHeader); - if (!IsSupportedType(rowTypeProperty, supportedNullableTypes)) + if (!IsSupportedType(rowTypeProperty)) { unsupportedPropertyTypeNames.Add(rowTypeProperty.TypeName); continue; @@ -247,33 +239,17 @@ private static TypePropertiesInfo AnalyzeTypeProperties(EquatableArray s DiagnosticInfos: diagnosticInfos.ToEquatableArray()); } - private static EquatableArray GetSupportedNullableTypes(Compilation compilation) - { - var result = new List(); - var nullableT = compilation.GetTypeByMetadataName("System.Nullable`1"); - - // TODO: Could be hardcoded? - foreach (var primitiveType in SupportedPrimitiveTypes) - { - var nullableType = nullableT?.Construct(compilation.GetSpecialType(primitiveType)); - if (nullableType is not null) - result.Add(nullableType.ToDisplayString()); - } - - return result.ToEquatableArray(); - } - - private static bool IsSupportedType(RowTypeProperty typeProperty, EquatableArray supportedNullableTypes) + private static bool IsSupportedType(RowTypeProperty typeProperty) { return typeProperty.TypeSpecialType == SpecialType.System_String || SupportedPrimitiveTypes.Contains(typeProperty.TypeSpecialType) - || IsSupportedNullableType(typeProperty, supportedNullableTypes); + || IsSupportedNullableType(typeProperty); } - private static bool IsSupportedNullableType(RowTypeProperty typeProperty, EquatableArray supportedNullableTypes) + private static bool IsSupportedNullableType(RowTypeProperty typeProperty) { return typeProperty.TypeNullableAnnotation == NullableAnnotation.Annotated - && supportedNullableTypes.Contains(typeProperty.TypeFullName, StringComparer.Ordinal); + && SupportedNullableTypes.Contains(typeProperty.TypeFullName, StringComparer.Ordinal); } private static readonly SpecialType[] SupportedPrimitiveTypes = @@ -287,13 +263,24 @@ private static bool IsSupportedNullableType(RowTypeProperty typeProperty, Equata SpecialType.System_Single ]; - private static void Execute(ContextClass? contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) + private static readonly string[] SupportedNullableTypes = + [ + "bool?", + "decimal?", + "double?", + "float?", + "int?", + "long?", + "System.DateTime?" + ]; + + private static void Execute(ContextClass? contextClass, SourceProductionContext context) { if (contextClass is null) return; var sb = new StringBuilder(); - GenerateCode(sb, contextClass, supportedNullableTypes, context); + GenerateCode(sb, contextClass, context); var hintName = contextClass.Namespace is { } ns ? $"{ns}.{contextClass.Name}.g.cs" @@ -316,7 +303,7 @@ private static void GenerateHeader(StringBuilder sb) sb.AppendLine(); } - private static void GenerateCode(StringBuilder sb, ContextClass contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) + private static void GenerateCode(StringBuilder sb, ContextClass contextClass, SourceProductionContext context) { GenerateHeader(sb); @@ -344,7 +331,7 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, Eq if (!rowTypeNames.Add(rowTypeName)) continue; - GenerateCodeForType(sb, typeIndex, rowType, location, contextClass, supportedNullableTypes, context); + GenerateCodeForType(sb, typeIndex, rowType, location, contextClass, context); ++typeIndex; } @@ -353,7 +340,7 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, Eq } private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedTypeSymbol rowTypeOld, LocationInfo location, - ContextClass contextClass, EquatableArray supportedNullableTypes, SourceProductionContext context) + ContextClass contextClass, SourceProductionContext context) { var rowType = new RowType( Name: rowTypeOld.Name, @@ -361,7 +348,7 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT FullNameWithNullableAnnotation: rowTypeOld.IsReferenceType ? $"{rowTypeOld}?" : rowTypeOld.ToString(), IsReferenceType: rowTypeOld.IsReferenceType); - var info = AnalyzeTypeProperties(supportedNullableTypes, rowTypeOld, context.CancellationToken); + var info = AnalyzeTypeProperties(rowTypeOld, context.CancellationToken); ReportDiagnostics(info, rowType, location, contextClass.Options, context); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" From 2b1f96f95f1b88e0eac299cf89b7981f6af5a66f Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 22:54:03 +0100 Subject: [PATCH 20/31] Merge TypePropertiesInfo into RowType --- .../Helpers/TypePropertiesInfo.cs | 7 ---- .../Models/RowType.cs | 8 +++- .../WorksheetRowGenerator.cs | 37 +++++++++---------- 3 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs diff --git a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs b/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs deleted file mode 100644 index 862716e5..00000000 --- a/SpreadCheetah.SourceGenerator/Helpers/TypePropertiesInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using SpreadCheetah.SourceGenerator.Models; - -namespace SpreadCheetah.SourceGenerator.Helpers; -internal sealed record TypePropertiesInfo( - EquatableArray Properties, - EquatableArray UnsupportedPropertyTypeNames, - EquatableArray DiagnosticInfos); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/RowType.cs b/SpreadCheetah.SourceGenerator/Models/RowType.cs index 7796fa9a..ab88a5bf 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowType.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowType.cs @@ -1,7 +1,13 @@ +using SpreadCheetah.SourceGenerator.Helpers; + namespace SpreadCheetah.SourceGenerator.Models; internal sealed record RowType( string Name, string FullName, string FullNameWithNullableAnnotation, - bool IsReferenceType); \ No newline at end of file + bool IsReferenceType, + LocationInfo WorksheetRowAttributeLocation, + EquatableArray Properties, + EquatableArray UnsupportedPropertyTypeNames, + EquatableArray DiagnosticInfos); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index de7188cb..4d7cd0aa 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -165,7 +165,7 @@ private static bool TryParseColumnOrderAttribute( return true; } - private static TypePropertiesInfo AnalyzeTypeProperties(ITypeSymbol classType, CancellationToken token) + private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo worksheetRowAttributeLocation, CancellationToken token) { var implicitOrderProperties = new List(); var explicitOrderProperties = new SortedDictionary(); @@ -233,7 +233,12 @@ private static TypePropertiesInfo AnalyzeTypeProperties(ITypeSymbol classType, C explicitOrderProperties.AddWithImplicitKeys(implicitOrderProperties); - return new TypePropertiesInfo( + return new RowType( + Name: classType.Name, + FullName: classType.ToString(), + FullNameWithNullableAnnotation: classType.IsReferenceType ? $"{classType}?" : classType.ToString(), + IsReferenceType: classType.IsReferenceType, + WorksheetRowAttributeLocation: worksheetRowAttributeLocation, Properties: explicitOrderProperties.Values.ToEquatableArray(), UnsupportedPropertyTypeNames: unsupportedPropertyTypeNames.ToEquatableArray(), DiagnosticInfos: diagnosticInfos.ToEquatableArray()); @@ -342,21 +347,15 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, So private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedTypeSymbol rowTypeOld, LocationInfo location, ContextClass contextClass, SourceProductionContext context) { - var rowType = new RowType( - Name: rowTypeOld.Name, - FullName: rowTypeOld.ToString(), - FullNameWithNullableAnnotation: rowTypeOld.IsReferenceType ? $"{rowTypeOld}?" : rowTypeOld.ToString(), - IsReferenceType: rowTypeOld.IsReferenceType); - - var info = AnalyzeTypeProperties(rowTypeOld, context.CancellationToken); - ReportDiagnostics(info, rowType, location, contextClass.Options, context); + var rowType = AnalyzeTypeProperties(rowTypeOld, location, context.CancellationToken); + ReportDiagnostics(rowType, location, contextClass.Options, context); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" private WorksheetRowTypeInfo<{{rowType.FullName}}>? _{{rowType.Name}}; public WorksheetRowTypeInfo<{{rowType.FullName}}> {{rowType.Name}} => _{{rowType.Name}} """)); - if (info.Properties.Count == 0) + if (rowType.Properties.Count == 0) { sb.AppendLine($$""" ??= EmptyWorksheetRowContext.CreateTypeInfo<{{rowType.FullName}}>(); @@ -369,20 +368,20 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT ??= WorksheetRowMetadataServices.CreateObjectInfo<{{rowType.FullName}}>(AddHeaderRow{{typeIndex}}Async, AddAsRowAsync, AddRangeAsRowsAsync); """)); - GenerateAddHeaderRow(sb, typeIndex, info.Properties); + GenerateAddHeaderRow(sb, typeIndex, rowType.Properties); GenerateAddAsRow(sb, 2, rowType); GenerateAddRangeAsRows(sb, 2, rowType); - GenerateAddAsRowInternal(sb, 2, rowType.FullName, info.Properties); - GenerateAddRangeAsRowsInternal(sb, rowType, info.Properties); + GenerateAddAsRowInternal(sb, 2, rowType.FullName, rowType.Properties); + GenerateAddRangeAsRowsInternal(sb, rowType, rowType.Properties); GenerateAddEnumerableAsRows(sb, 2, rowType); - GenerateAddCellsAsRow(sb, 2, rowType, info.Properties); + GenerateAddCellsAsRow(sb, 2, rowType, rowType.Properties); } - private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, LocationInfo location, GeneratorOptions? options, SourceProductionContext context) + private static void ReportDiagnostics(RowType rowType, LocationInfo location, GeneratorOptions? options, SourceProductionContext context) { var suppressWarnings = options?.SuppressWarnings ?? false; - foreach (var diagnosticInfo in info.DiagnosticInfos) + foreach (var diagnosticInfo in rowType.DiagnosticInfos) { var isWarning = diagnosticInfo.Descriptor.DefaultSeverity == DiagnosticSeverity.Warning; if (isWarning && suppressWarnings) @@ -393,10 +392,10 @@ private static void ReportDiagnostics(TypePropertiesInfo info, RowType rowType, if (suppressWarnings) return; - if (info.Properties.Count == 0) + if (rowType.Properties.Count == 0) context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoPropertiesFound, location.ToLocation(), rowType.Name)); - if (info.UnsupportedPropertyTypeNames.FirstOrDefault() is { } unsupportedPropertyTypeName) + if (rowType.UnsupportedPropertyTypeNames.FirstOrDefault() is { } unsupportedPropertyTypeName) context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); } From 882d7db81d3c194d4a93e451c028522c5ecee3a7 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 23:15:56 +0100 Subject: [PATCH 21/31] Src gen: Analyze type properties earlier in the pipeline to make it cacheable --- .../Helpers/ContextClass.cs | 2 +- .../Models/RowType.cs | 2 +- .../WorksheetRowGenerator.cs | 39 ++++++++++--------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs index 181cc28c..0110059f 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/ContextClass.cs @@ -7,5 +7,5 @@ internal sealed record ContextClass( string Name, Accessibility DeclaredAccessibility, string? Namespace, - Dictionary RowTypes, // TODO: Don't use INamedTypeSymbol + EquatableArray RowTypes, GeneratorOptions? Options); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/RowType.cs b/SpreadCheetah.SourceGenerator/Models/RowType.cs index ab88a5bf..ce07317c 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowType.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowType.cs @@ -7,7 +7,7 @@ internal sealed record RowType( string FullName, string FullNameWithNullableAnnotation, bool IsReferenceType, - LocationInfo WorksheetRowAttributeLocation, + LocationInfo? WorksheetRowAttributeLocation, EquatableArray Properties, EquatableArray UnsupportedPropertyTypeNames, EquatableArray DiagnosticInfos); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 4d7cd0aa..32a264b7 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -48,23 +48,23 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat if (!string.Equals("SpreadCheetah.SourceGeneration.WorksheetRowContext", baseType.ToDisplayString(), StringComparison.Ordinal)) return null; - var rowTypes = new Dictionary(SymbolEqualityComparer.Default); - GeneratorOptions? generatorOptions = null; + var rowTypes = new List(); foreach (var worksheetRowAttribute in context.Attributes) { - if (TryParseWorksheetRowAttribute(worksheetRowAttribute, token, out var typeSymbol, out var location) - && !rowTypes.ContainsKey(typeSymbol)) - { - var locationInfo = location.ToLocationInfo(); - if (locationInfo is not null) - rowTypes[typeSymbol] = locationInfo; - } + if (!TryParseWorksheetRowAttribute(worksheetRowAttribute, token, out var typeSymbol, out var location)) + continue; + + var rowType = AnalyzeTypeProperties(typeSymbol, location.ToLocationInfo(), token); + if (!rowTypes.Exists(x => string.Equals(x.FullName, rowType.FullName, StringComparison.Ordinal))) + rowTypes.Add(rowType); } if (rowTypes.Count == 0) return null; + GeneratorOptions? generatorOptions = null; + foreach (var attribute in classSymbol.GetAttributes()) { if (TryParseOptionsAttribute(attribute, out var options)) @@ -75,7 +75,7 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat DeclaredAccessibility: classSymbol.DeclaredAccessibility, Namespace: classSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToString() : null, Name: classSymbol.Name, - RowTypes: rowTypes, + RowTypes: rowTypes.ToEquatableArray(), Options: generatorOptions); } @@ -165,7 +165,7 @@ private static bool TryParseColumnOrderAttribute( return true; } - private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo worksheetRowAttributeLocation, CancellationToken token) + private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo? worksheetRowAttributeLocation, CancellationToken token) { var implicitOrderProperties = new List(); var explicitOrderProperties = new SortedDictionary(); @@ -330,13 +330,13 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, So var rowTypeNames = new HashSet(StringComparer.Ordinal); var typeIndex = 0; - foreach (var (rowType, location) in contextClass.RowTypes) + foreach (var rowType in contextClass.RowTypes) { var rowTypeName = rowType.Name; if (!rowTypeNames.Add(rowTypeName)) continue; - GenerateCodeForType(sb, typeIndex, rowType, location, contextClass, context); + GenerateCodeForType(sb, typeIndex, rowType, contextClass, context); ++typeIndex; } @@ -344,11 +344,10 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, So sb.AppendLine("}"); } - private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedTypeSymbol rowTypeOld, LocationInfo location, + private static void GenerateCodeForType(StringBuilder sb, int typeIndex, RowType rowType, ContextClass contextClass, SourceProductionContext context) { - var rowType = AnalyzeTypeProperties(rowTypeOld, location, context.CancellationToken); - ReportDiagnostics(rowType, location, contextClass.Options, context); + ReportDiagnostics(rowType, rowType.WorksheetRowAttributeLocation, contextClass.Options, context); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" private WorksheetRowTypeInfo<{{rowType.FullName}}>? _{{rowType.Name}}; @@ -377,7 +376,7 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, INamedT GenerateAddCellsAsRow(sb, 2, rowType, rowType.Properties); } - private static void ReportDiagnostics(RowType rowType, LocationInfo location, GeneratorOptions? options, SourceProductionContext context) + private static void ReportDiagnostics(RowType rowType, LocationInfo? locationInfo, GeneratorOptions? options, SourceProductionContext context) { var suppressWarnings = options?.SuppressWarnings ?? false; @@ -392,11 +391,13 @@ private static void ReportDiagnostics(RowType rowType, LocationInfo location, Ge if (suppressWarnings) return; + var location = locationInfo?.ToLocation(); + if (rowType.Properties.Count == 0) - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoPropertiesFound, location.ToLocation(), rowType.Name)); + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoPropertiesFound, location, rowType.Name)); if (rowType.UnsupportedPropertyTypeNames.FirstOrDefault() is { } unsupportedPropertyTypeName) - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location.ToLocation(), rowType.Name, unsupportedPropertyTypeName)); + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedTypeForCellValue, location, rowType.Name, unsupportedPropertyTypeName)); } private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadOnlyCollection properties) From 83f78856987aa5bf2df9635cdbb877da726a5335 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 23:24:36 +0100 Subject: [PATCH 22/31] Src gen: Update test that verifies cacheability --- ...#MyNamespace.MyGenRowContext.g.verified.cs | 4 +- ...tor_Generate_CachingCorrectly.verified.txt | 101 ++++++++++++++++++ .../Tests/WorksheetRowGeneratorTests.cs | 17 ++- 3 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorTests.WorksheetRowGenerator_Generate_CachingCorrectly.verified.txt diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs index efe144d1..f32661da 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs @@ -33,8 +33,8 @@ private static async ValueTask AddHeaderRow0Async(SpreadCheetah.Spreadsheet spre cells[1] = new StyledCell("", styleId); cells[2] = new StyledCell("Nationality (escaped characters \", ', \\)", styleId); cells[3] = new StyledCell("Address line 1 (escaped characters \r\n, \t)", styleId); - cells[4] = new StyledCell("Address line 2 (verbatim\r\nstring: \", \\)", styleId); - cells[5] = new StyledCell(" Age (\r\n raw\r\n string\r\n literal\r\n )", styleId); + cells[4] = new StyledCell("Address line 2 (verbatim\nstring: \", \\)", styleId); + cells[5] = new StyledCell(" Age (\n raw\n string\n literal\n )", styleId); cells[6] = new StyledCell("Note (unicode escape sequence ๐ŸŒ‰, ๐Ÿ‘, รง)", styleId); cells[7] = new StyledCell("Note 2 (constant interpolated string: This is a constant)", styleId); await spreadsheet.AddRowAsync(cells.AsMemory(0, 8), token).ConfigureAwait(false); diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorTests.WorksheetRowGenerator_Generate_CachingCorrectly.verified.txt b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorTests.WorksheetRowGenerator_Generate_CachingCorrectly.verified.txt new file mode 100644 index 00000000..9e7ee9e0 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorTests.WorksheetRowGenerator_Generate_CachingCorrectly.verified.txt @@ -0,0 +1,101 @@ +๏ปฟ// +#nullable enable +using SpreadCheetah; +using SpreadCheetah.SourceGeneration; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MyNamespace +{ + public partial class MyGenRowContext + { + private static MyGenRowContext? _default; + public static MyGenRowContext Default => _default ??= new MyGenRowContext(); + + public MyGenRowContext() + { + } + + private WorksheetRowTypeInfo? _ClassWithSingleProperty; + public WorksheetRowTypeInfo ClassWithSingleProperty => _ClassWithSingleProperty + ??= WorksheetRowMetadataServices.CreateObjectInfo(AddHeaderRow0Async, AddAsRowAsync, AddRangeAsRowsAsync); + + private static async ValueTask AddHeaderRow0Async(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.Styling.StyleId? styleId, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(1); + try + { + cells[0] = new StyledCell("Name", styleId); + await spreadsheet.AddRowAsync(cells.AsMemory(0, 1), token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.SourceGenerator.SnapshotTest.Models.ClassWithSingleProperty? obj, CancellationToken token) + { + if (spreadsheet is null) + throw new ArgumentNullException(nameof(spreadsheet)); + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + return AddAsRowInternalAsync(spreadsheet, obj, token); + } + + private static ValueTask AddRangeAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable objs, CancellationToken token) + { + if (spreadsheet is null) + throw new ArgumentNullException(nameof(spreadsheet)); + if (objs is null) + throw new ArgumentNullException(nameof(objs)); + return AddRangeAsRowsInternalAsync(spreadsheet, objs, token); + } + + private static async ValueTask AddAsRowInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.SourceGenerator.SnapshotTest.Models.ClassWithSingleProperty obj, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(1); + try + { + await AddCellsAsRowAsync(spreadsheet, obj, cells, token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable objs, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(1); + try + { + await AddEnumerableAsRowsAsync(spreadsheet, objs, cells, token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static async ValueTask AddEnumerableAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable objs, DataCell[] cells, CancellationToken token) + { + foreach (var obj in objs) + { + await AddCellsAsRowAsync(spreadsheet, obj, cells, token).ConfigureAwait(false); + } + } + + private static ValueTask AddCellsAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.SourceGenerator.SnapshotTest.Models.ClassWithSingleProperty? obj, DataCell[] cells, CancellationToken token) + { + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + + cells[0] = new DataCell(obj.Name); + return spreadsheet.AddRowAsync(cells.AsMemory(0, 1), token); + } + } +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs index 2f8da1e8..0cc691e3 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs @@ -1,5 +1,3 @@ -using FluentAssertions; -using FluentAssertions.Execution; using SpreadCheetah.SourceGenerator.SnapshotTest.Helpers; using SpreadCheetah.SourceGenerators; @@ -10,7 +8,7 @@ public class WorksheetRowGeneratorTests private static readonly string[] AllTrackingNames = ["InitialExtraction", "Transform"]; [Fact] - public void WorksheetRowGenerator_Generate_CachingCorrectly() + public Task WorksheetRowGenerator_Generate_CachingCorrectly() { // Arrange const string source = """ @@ -27,15 +25,16 @@ public partial class MyGenRowContext : WorksheetRowContext } """; - const string expected = """... not shown for brevity..."""; - // Act var (diagnostics, output) = TestHelper.GetGeneratedTrees([source], AllTrackingNames); - // Assert the output - using var s = new AssertionScope(); - diagnostics.Should().BeEmpty(); - output.Should().OnlyContain(x => x == expected); + // Assert + Assert.Empty(diagnostics); + var outputSource = Assert.Single(output); + + var settings = new VerifySettings(); + settings.UseDirectory("../Snapshots"); + return Verify(outputSource, settings); } [Fact] From 57f3386e61d21f753720cf9af2c050818b5338e6 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 17 Feb 2024 23:57:51 +0100 Subject: [PATCH 23/31] Simplify TestHelper --- .../Helpers/TestHelper.cs | 119 +++++++----------- .../Tests/WorksheetRowGeneratorTests.cs | 2 +- 2 files changed, 44 insertions(+), 77 deletions(-) diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs index d4f6cad6..c0f68131 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs @@ -10,14 +10,12 @@ namespace SpreadCheetah.SourceGenerator.SnapshotTest.Helpers; internal static class TestHelper { - public static SettingsTask CompileAndVerify(string source, params object?[] parameters) where T : IIncrementalGenerator, new() + private static PortableExecutableReference[] GetAssemblyReferences() { - var syntaxTree = CSharpSyntaxTree.ParseText(source); - var dotNetAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location) ?? throw new InvalidOperationException(); - var references = new[] - { + return + [ MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "mscorlib.dll")), MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "netstandard.dll")), MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.dll")), @@ -26,8 +24,13 @@ internal static class TestHelper MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Runtime.dll")), MetadataReference.CreateFromFile(typeof(WorksheetRowAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(TestHelper).Assembly.Location) - }; + ]; + } + public static SettingsTask CompileAndVerify(string source, params object?[] parameters) where T : IIncrementalGenerator, new() + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = GetAssemblyReferences(); var compilation = CSharpCompilation.Create("Tests", [syntaxTree], references); var generator = new T(); @@ -47,46 +50,18 @@ internal static class TestHelper } public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees( - string[] sources, // C# source code - string[] stages, // The tracking stages we expect - bool assertOutputs = true) // You can disable cacheability checking during dev - where T : IIncrementalGenerator, new() // T is your generator + string source, + string[] trackingStages, + bool assertOutputs = true) + where T : IIncrementalGenerator, new() { - // Convert the source files to SyntaxTrees - IEnumerable syntaxTrees = sources.Select(static x => CSharpSyntaxTree.ParseText(x)); - - // Configure the assembly references you need - // This will vary depending on your generator and requirements - //var references = AppDomain.CurrentDomain.GetAssemblies() - // .Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location)) - // .Select(_ => MetadataReference.CreateFromFile(_.Location)) - // .Concat(new[] { MetadataReference.CreateFromFile(typeof(T).Assembly.Location) }); - - var dotNetAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location) ?? throw new InvalidOperationException(); - - var references = new[] -{ - MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "mscorlib.dll")), - MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "netstandard.dll")), - MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.dll")), - MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Core.dll")), - MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Private.CoreLib.dll")), - MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Runtime.dll")), - MetadataReference.CreateFromFile(typeof(WorksheetRowAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(TestHelper).Assembly.Location) - }; - - // Create a Compilation object - // You may want to specify other results here - CSharpCompilation compilation = CSharpCompilation.Create( - "SpreadCheetah.Generated", - syntaxTrees, - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = GetAssemblyReferences(); + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + var compilation = CSharpCompilation.Create("SpreadCheetah.Generated", [syntaxTree], references, options); // Run the generator, get the results, and assert cacheability if applicable - GeneratorDriverRunResult runResult = RunGeneratorAndAssertOutput( - compilation, stages, assertOutputs); + var runResult = RunGeneratorAndAssertOutput(compilation, trackingStages, assertOutputs); // Return the generator diagnostics and generated sources return (runResult.Diagnostics, runResult.GeneratedTrees.Select(x => x.ToString()).ToArray()); @@ -95,7 +70,7 @@ public static (ImmutableArray Diagnostics, string[] Output) GetGener private static GeneratorDriverRunResult RunGeneratorAndAssertOutput(CSharpCompilation compilation, string[] trackingNames, bool assertOutput = true) where T : IIncrementalGenerator, new() { - ISourceGenerator generator = new T().AsSourceGenerator(); + var generator = new T().AsSourceGenerator(); // โš  Tell the driver to track all the incremental generator outputs // without this, you'll have no tracked outputs! @@ -116,20 +91,20 @@ private static GeneratorDriverRunResult RunGeneratorAndAssertOutput(CSharpCom if (assertOutput) { // Run again, using the same driver, with a clone of the compilation - GeneratorDriverRunResult runResult2 = driver - .RunGenerators(clone) - .GetRunResult(); + var runResult2 = driver.RunGenerators(clone).GetRunResult(); // Compare all the tracked outputs, throw if there's a failure AssertRunsEqual(runResult, runResult2, trackingNames); // verify the second run only generated cached source outputs - runResult2.Results[0] - .TrackedOutputSteps - .SelectMany(x => x.Value) // step executions - .SelectMany(x => x.Outputs) // execution results - .Should() - .OnlyContain(x => x.Reason == IncrementalStepRunReason.Cached); + var outputs = runResult2 + .Results[0] + .TrackedOutputSteps + .SelectMany(x => x.Value) // step executions + .SelectMany(x => x.Outputs); // execution results + + var output = Assert.Single(outputs); + Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); } return runResult; @@ -147,10 +122,8 @@ private static void AssertRunsEqual( var trackedSteps2 = GetTrackedSteps(runResult2, trackingNames); // Both runs should have the same tracked steps - trackedSteps1.Should() - .NotBeEmpty() - .And.HaveSameCount(trackedSteps2) - .And.ContainKeys(trackedSteps2.Keys); + var trackedSteps1Keys = trackedSteps1.Keys.ToHashSet(StringComparer.Ordinal); + Assert.True(trackedSteps1Keys.SetEquals(trackedSteps2.Keys)); // Get the IncrementalGeneratorRunStep collection for each run foreach (var (trackingName, runSteps1) in trackedSteps1) @@ -160,14 +133,15 @@ private static void AssertRunsEqual( AssertEqual(runSteps1, runSteps2, trackingName); } - // Local function that extracts the tracked steps static Dictionary> GetTrackedSteps( GeneratorDriverRunResult runResult, string[] trackingNames) - => runResult - .Results[0] // We're only running a single generator, so this is safe - .TrackedSteps // Get the pipeline outputs - .Where(step => trackingNames.Contains(step.Key)) // filter to known steps - .ToDictionary(x => x.Key, x => x.Value); // Convert to a dictionary + { + return runResult + .Results[0] // We're only running a single generator, so this is safe + .TrackedSteps // Get the pipeline outputs + .Where(step => trackingNames.Contains(step.Key, StringComparer.Ordinal)) + .ToDictionary(x => x.Key, x => x.Value, StringComparer.Ordinal); + } } private static void AssertEqual( @@ -175,27 +149,20 @@ private static void AssertEqual( ImmutableArray runSteps2, string stepName) { - runSteps1.Should().HaveSameCount(runSteps2); + Assert.Equal(runSteps1.Length, runSteps2.Length); - for (var i = 0; i < runSteps1.Length; i++) + foreach (var (runStep1, runStep2) in runSteps1.Zip(runSteps2)) { - var runStep1 = runSteps1[i]; - var runStep2 = runSteps2[i]; - // The outputs should be equal between different runs - IEnumerable outputs1 = runStep1.Outputs.Select(x => x.Value); - IEnumerable outputs2 = runStep2.Outputs.Select(x => x.Value); + var outputs1 = runStep1.Outputs.Select(x => x.Value); + var outputs2 = runStep2.Outputs.Select(x => x.Value); - outputs1.Should() - .Equal(outputs2, $"because {stepName} should produce cacheable outputs"); + Assert.True(outputs1.SequenceEqual(outputs2), $"Step {stepName} did not produce cacheable outputs"); // Therefore, on the second run the results should always be cached or unchanged! // - Unchanged is when the _input_ has changed, but the output hasn't // - Cached is when the the input has not changed, so the cached output is used - runStep2.Outputs.Should() - .OnlyContain( - x => x.Reason == IncrementalStepRunReason.Cached || x.Reason == IncrementalStepRunReason.Unchanged, - $"{stepName} expected to have reason {IncrementalStepRunReason.Cached} or {IncrementalStepRunReason.Unchanged}"); + Assert.All(runStep2.Outputs, x => Assert.True(x.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged)); // Make sure we're not using anything we shouldn't AssertObjectGraph(runStep1, stepName); diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs index 0cc691e3..5b165a86 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs @@ -26,7 +26,7 @@ public partial class MyGenRowContext : WorksheetRowContext """; // Act - var (diagnostics, output) = TestHelper.GetGeneratedTrees([source], AllTrackingNames); + var (diagnostics, output) = TestHelper.GetGeneratedTrees(source, AllTrackingNames); // Assert Assert.Empty(diagnostics); From 303b9c75774755374e61e15948e506bb6b349ed0 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 00:21:10 +0100 Subject: [PATCH 24/31] Simplify TestHelper --- Directory.Packages.props | 1 - .../Helpers/TestHelper.cs | 26 +++++++------------ ...heetah.SourceGenerator.SnapshotTest.csproj | 1 - 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1edb2d69..1b724c73 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,6 @@ - diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs index c0f68131..421aecce 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs @@ -1,4 +1,3 @@ -using FluentAssertions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using SpreadCheetah.SourceGeneration; @@ -165,14 +164,12 @@ private static void AssertEqual( Assert.All(runStep2.Outputs, x => Assert.True(x.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged)); // Make sure we're not using anything we shouldn't - AssertObjectGraph(runStep1, stepName); + AssertObjectGraph(runStep1); } } - private static void AssertObjectGraph(IncrementalGeneratorRunStep runStep, string stepName) + private static void AssertObjectGraph(IncrementalGeneratorRunStep runStep) { - // Including the stepName in error messages to make it easy to isolate issues - var because = $"{stepName} shouldn't contain banned symbols"; var visited = new HashSet(); // Check all of the outputs - probably overkill, but why not @@ -181,26 +178,21 @@ private static void AssertObjectGraph(IncrementalGeneratorRunStep runStep, strin Visit(obj); } - void Visit(object node) + void Visit(object? node) { // If we've already seen this object, or it's null, stop. if (node is null || !visited.Add(node)) - { return; - } // Make sure it's not a banned type - node.Should() - .NotBeOfType(because) - .And.NotBeOfType(because) - .And.NotBeOfType(because); + Assert.IsNotAssignableFrom(node); + Assert.IsNotAssignableFrom(node); + Assert.IsNotAssignableFrom(node); // Examine the object - Type type = node.GetType(); + var type = node.GetType(); if (type.IsPrimitive || type.IsEnum || type == typeof(string)) - { return; - } // If the object is a collection, check each of the values if (node is IEnumerable collection and not string) @@ -215,9 +207,9 @@ void Visit(object node) } // Recursively check each field in the object - foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - object fieldValue = field.GetValue(node); + var fieldValue = field.GetValue(node); Visit(fieldValue); } } diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj b/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj index 1711f95b..5b468508 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj @@ -10,7 +10,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - From 3daa5e20283d5766fa1c9457ffc52ccf048e25f7 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 00:27:21 +0100 Subject: [PATCH 25/31] Src gen: Remove unused tracking name --- .../Tests/WorksheetRowGeneratorTests.cs | 4 +--- SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs | 1 - SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs index 5b165a86..a8142165 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorTests.cs @@ -5,8 +5,6 @@ namespace SpreadCheetah.SourceGenerator.SnapshotTest.Tests; public class WorksheetRowGeneratorTests { - private static readonly string[] AllTrackingNames = ["InitialExtraction", "Transform"]; - [Fact] public Task WorksheetRowGenerator_Generate_CachingCorrectly() { @@ -26,7 +24,7 @@ public partial class MyGenRowContext : WorksheetRowContext """; // Act - var (diagnostics, output) = TestHelper.GetGeneratedTrees(source, AllTrackingNames); + var (diagnostics, output) = TestHelper.GetGeneratedTrees(source, ["Transform"]); // Assert Assert.Empty(diagnostics); diff --git a/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs b/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs index c00fbc02..97d7c811 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/TrackingNames.cs @@ -2,6 +2,5 @@ namespace SpreadCheetah.SourceGenerator.Helpers; internal static class TrackingNames { - public const string InitialExtraction = nameof(InitialExtraction); public const string Transform = nameof(Transform); } diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 32a264b7..5bd8e432 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -23,7 +23,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration) .Where(static x => x is not null) - .WithTrackingName(TrackingNames.InitialExtraction); + .WithTrackingName(TrackingNames.Transform); context.RegisterSourceOutput(contextClasses, static (spc, source) => Execute(source, spc)); } From ddefd153bd6a66578db145c929c9705447958c4f Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 10:15:01 +0100 Subject: [PATCH 26/31] Src gen: Improve readability with extension methods --- .../Helpers/TestHelper.cs | 4 + .../Extensions/AttributeDataExtensions.cs | 94 +++++++++++ .../Extensions/SymbolExtensions.cs | 60 +++++++ .../WorksheetRowGenerator.cs | 150 ++---------------- 4 files changed, 168 insertions(+), 140 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs create mode 100644 SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs index 421aecce..103686e5 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs @@ -48,6 +48,10 @@ private static PortableExecutableReference[] GetAssemblyReferences() : task; } + /// + /// Based on the implementation from: + /// https://andrewlock.net/creating-a-source-generator-part-10-testing-your-incremental-generator-pipeline-outputs-are-cacheable/ + /// public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees( string source, string[] trackingStages, diff --git a/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs new file mode 100644 index 00000000..68a2e9d4 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -0,0 +1,94 @@ +using Microsoft.CodeAnalysis; +using SpreadCheetah.SourceGenerator.Helpers; +using System.Diagnostics.CodeAnalysis; + +namespace SpreadCheetah.SourceGenerator.Extensions; + +internal static class AttributeDataExtensions +{ + public static bool TryParseWorksheetRowAttribute( + this AttributeData attribute, + CancellationToken token, + [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol, + [NotNullWhen(true)] out Location? location) + { + typeSymbol = null; + location = null; + + var args = attribute.ConstructorArguments; + if (args is not [{ Value: INamedTypeSymbol symbol }]) + return false; + + if (symbol.Kind == SymbolKind.ErrorType) + return false; + + var syntaxReference = attribute.ApplicationSyntaxReference; + if (syntaxReference is null) + return false; + + location = syntaxReference.GetSyntax(token).GetLocation(); + typeSymbol = symbol; + return true; + } + + public static bool TryParseOptionsAttribute( + this AttributeData attribute, + [NotNullWhen(true)] out GeneratorOptions? options) + { + options = null; + + if (!string.Equals(Attributes.GenerationOptions, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) + return false; + + if (attribute.NamedArguments.IsDefaultOrEmpty) + return false; + + foreach (var (key, value) in attribute.NamedArguments) + { + if (!string.Equals(key, "SuppressWarnings", StringComparison.Ordinal)) + continue; + + if (value.Value is bool suppressWarnings) + { + options = new GeneratorOptions(suppressWarnings); + return true; + } + } + + return false; + } + + public static bool TryParseColumnHeaderAttribute( + this AttributeData attribute, + out TypedConstant attributeArg) + { + attributeArg = default; + + if (!string.Equals(Attributes.ColumnHeader, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) + return false; + + var args = attribute.ConstructorArguments; + if (args is not [{ Value: string } arg]) + return false; + + attributeArg = arg; + return true; + } + + public static bool TryParseColumnOrderAttribute( + this AttributeData attribute, + out int order) + { + order = 0; + + if (!string.Equals(Attributes.ColumnOrder, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) + return false; + + var args = attribute.ConstructorArguments; + if (args is not [{ Value: int attributeValue }]) + return false; + + order = attributeValue; + return true; + } +} diff --git a/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs new file mode 100644 index 00000000..37a5f272 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs @@ -0,0 +1,60 @@ +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +namespace SpreadCheetah.SourceGenerator.Extensions; + +internal static class SymbolExtensions +{ + public static bool IsSupportedType(this ITypeSymbol type) + { + return type.SpecialType switch + { + SpecialType.System_Boolean => true, + SpecialType.System_DateTime => true, + SpecialType.System_Decimal => true, + SpecialType.System_Double => true, + SpecialType.System_Int32 => true, + SpecialType.System_Int64 => true, + SpecialType.System_Single => true, + SpecialType.System_String => true, + _ => type.IsSupportedNullableType(), + }; + } + + private static bool IsSupportedNullableType(this ITypeSymbol type) + { + if (type.NullableAnnotation != NullableAnnotation.Annotated) + return false; + + return type.ToDisplayString() switch + { + "bool?" => true, + "decimal?" => true, + "double?" => true, + "float?" => true, + "int?" => true, + "long?" => true, + "System.DateTime?" => true, + _ => false, + }; + } + + public static bool IsPropertyWithPublicGetter( + this ISymbol symbol, + [NotNullWhen(true)] out IPropertySymbol? property) + { + if (symbol is IPropertySymbol + { + DeclaredAccessibility: Accessibility.Public, + IsStatic: false, + IsWriteOnly: false + } p) + { + property = p; + return true; + } + + property = null; + return false; + } +} diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 5bd8e432..f35473ff 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -5,9 +5,7 @@ using SpreadCheetah.SourceGenerator.Extensions; using SpreadCheetah.SourceGenerator.Helpers; using SpreadCheetah.SourceGenerator.Models; -using System.Collections.Immutable; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Text; namespace SpreadCheetah.SourceGenerators; @@ -50,9 +48,9 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat var rowTypes = new List(); - foreach (var worksheetRowAttribute in context.Attributes) + foreach (var attribute in context.Attributes) { - if (!TryParseWorksheetRowAttribute(worksheetRowAttribute, token, out var typeSymbol, out var location)) + if (!attribute.TryParseWorksheetRowAttribute(token, out var typeSymbol, out var location)) continue; var rowType = AnalyzeTypeProperties(typeSymbol, location.ToLocationInfo(), token); @@ -67,7 +65,7 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat foreach (var attribute in classSymbol.GetAttributes()) { - if (TryParseOptionsAttribute(attribute, out var options)) + if (attribute.TryParseOptionsAttribute(out var options)) generatorOptions = options; } @@ -79,92 +77,6 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat Options: generatorOptions); } - private static bool TryParseWorksheetRowAttribute( - AttributeData attribute, - CancellationToken token, - [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol, - [NotNullWhen(true)] out Location? location) - { - typeSymbol = null; - location = null; - - var args = attribute.ConstructorArguments; - if (args is not [{ Value: INamedTypeSymbol symbol }]) - return false; - - if (symbol.Kind == SymbolKind.ErrorType) - return false; - - var syntaxReference = attribute.ApplicationSyntaxReference; - if (syntaxReference is null) - return false; - - location = syntaxReference.GetSyntax(token).GetLocation(); - typeSymbol = symbol; - return true; - } - - private static bool TryParseOptionsAttribute( - AttributeData attribute, - [NotNullWhen(true)] out GeneratorOptions? options) - { - options = null; - - if (!string.Equals(Attributes.GenerationOptions, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) - return false; - - if (attribute.NamedArguments.IsDefaultOrEmpty) - return false; - - foreach (var (key, value) in attribute.NamedArguments) - { - if (!string.Equals(key, "SuppressWarnings", StringComparison.Ordinal)) - continue; - - if (value.Value is bool suppressWarnings) - { - options = new GeneratorOptions(suppressWarnings); - return true; - } - } - - return false; - } - - private static bool TryParseColumnHeaderAttribute( - AttributeData attribute, - out TypedConstant attributeArg) - { - attributeArg = default; - - if (!string.Equals(Attributes.ColumnHeader, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) - return false; - - var args = attribute.ConstructorArguments; - if (args is not [{ Value: string } arg]) - return false; - - attributeArg = arg; - return true; - } - - private static bool TryParseColumnOrderAttribute( - AttributeData attribute, - out int order) - { - order = 0; - - if (!string.Equals(Attributes.ColumnOrder, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) - return false; - - var args = attribute.ConstructorArguments; - if (args is not [{ Value: int attributeValue }]) - return false; - - order = attributeValue; - return true; - } - private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo? worksheetRowAttributeLocation, CancellationToken token) { var implicitOrderProperties = new List(); @@ -174,13 +86,12 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo foreach (var member in classType.GetMembers()) { - if (member is not IPropertySymbol - { - DeclaredAccessibility: Accessibility.Public, - IsStatic: false, - IsWriteOnly: false - } p) + if (!member.IsPropertyWithPublicGetter(out var p)) + continue; + + if (!p.Type.IsSupportedType()) { + unsupportedPropertyTypeNames.Add(p.Type.Name); continue; } @@ -190,12 +101,12 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo foreach (var attribute in p.GetAttributes()) { - if (columnHeaderAttributeValue is null && TryParseColumnHeaderAttribute(attribute, out var arg)) + if (columnHeaderAttributeValue is null && attribute.TryParseColumnHeaderAttribute(out var arg)) { columnHeaderAttributeValue = arg; } - if (columnOrderValue is null && TryParseColumnOrderAttribute(attribute, out var order)) + if (columnOrderValue is null && attribute.TryParseColumnOrderAttribute(out var order)) { columnOrderValue = order; columnOrderAttributeLocation = attribute.ApplicationSyntaxReference? @@ -217,12 +128,6 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo TypeSpecialType: p.Type.SpecialType, ColumnHeader: columnHeader); - if (!IsSupportedType(rowTypeProperty)) - { - unsupportedPropertyTypeNames.Add(rowTypeProperty.TypeName); - continue; - } - if (columnOrderValue is not { } columnOrder) implicitOrderProperties.Add(rowTypeProperty); else if (!explicitOrderProperties.ContainsKey(columnOrder)) @@ -244,41 +149,6 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo DiagnosticInfos: diagnosticInfos.ToEquatableArray()); } - private static bool IsSupportedType(RowTypeProperty typeProperty) - { - return typeProperty.TypeSpecialType == SpecialType.System_String - || SupportedPrimitiveTypes.Contains(typeProperty.TypeSpecialType) - || IsSupportedNullableType(typeProperty); - } - - private static bool IsSupportedNullableType(RowTypeProperty typeProperty) - { - return typeProperty.TypeNullableAnnotation == NullableAnnotation.Annotated - && SupportedNullableTypes.Contains(typeProperty.TypeFullName, StringComparer.Ordinal); - } - - private static readonly SpecialType[] SupportedPrimitiveTypes = - [ - SpecialType.System_Boolean, - SpecialType.System_DateTime, - SpecialType.System_Decimal, - SpecialType.System_Double, - SpecialType.System_Int32, - SpecialType.System_Int64, - SpecialType.System_Single - ]; - - private static readonly string[] SupportedNullableTypes = - [ - "bool?", - "decimal?", - "double?", - "float?", - "int?", - "long?", - "System.DateTime?" - ]; - private static void Execute(ContextClass? contextClass, SourceProductionContext context) { if (contextClass is null) From b92ca5f43f8405db6710c89e3e14a51fa70503d3 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 10:43:15 +0100 Subject: [PATCH 27/31] Src gen: Improve code readability --- .../Extensions/AttributeDataExtensions.cs | 14 ++++-- .../Extensions/SymbolExtensions.cs | 17 +++++++ .../Models/ColumnOrder.cs | 3 ++ .../Models/LocationInfo.cs | 5 ++- .../Models/RowType.cs | 6 ++- .../WorksheetRowGenerator.cs | 45 ++++++------------- 6 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 SpreadCheetah.SourceGenerator/Models/ColumnOrder.cs diff --git a/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs index 68a2e9d4..c59ecabd 100644 --- a/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using SpreadCheetah.SourceGenerator.Helpers; +using SpreadCheetah.SourceGenerator.Models; using System.Diagnostics.CodeAnalysis; namespace SpreadCheetah.SourceGenerator.Extensions; @@ -77,9 +78,10 @@ public static bool TryParseColumnHeaderAttribute( public static bool TryParseColumnOrderAttribute( this AttributeData attribute, - out int order) + CancellationToken token, + [NotNullWhen(true)] out ColumnOrder? order) { - order = 0; + order = null; if (!string.Equals(Attributes.ColumnOrder, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) return false; @@ -88,7 +90,13 @@ public static bool TryParseColumnOrderAttribute( if (args is not [{ Value: int attributeValue }]) return false; - order = attributeValue; + var location = attribute + .ApplicationSyntaxReference? + .GetSyntax(token) + .GetLocation() + .ToLocationInfo(); + + order = new ColumnOrder(attributeValue, location); return true; } } diff --git a/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs index 37a5f272..e48397e7 100644 --- a/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using SpreadCheetah.SourceGenerator.Models; using System.Diagnostics.CodeAnalysis; namespace SpreadCheetah.SourceGenerator.Extensions; @@ -57,4 +59,19 @@ public static bool IsPropertyWithPublicGetter( property = null; return false; } + + public static RowTypeProperty ToRowTypeProperty( + this IPropertySymbol p, + TypedConstant? columnHeaderAttributeValue) + { + var columnHeader = columnHeaderAttributeValue?.ToCSharpString() ?? @$"""{p.Name}"""; + + return new RowTypeProperty( + ColumnHeader: columnHeader, + Name: p.Name, + TypeFullName: p.Type.ToDisplayString(), + TypeName: p.Type.Name, + TypeNullableAnnotation: p.NullableAnnotation, + TypeSpecialType: p.Type.SpecialType); + } } diff --git a/SpreadCheetah.SourceGenerator/Models/ColumnOrder.cs b/SpreadCheetah.SourceGenerator/Models/ColumnOrder.cs new file mode 100644 index 00000000..7af07976 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/ColumnOrder.cs @@ -0,0 +1,3 @@ +namespace SpreadCheetah.SourceGenerator.Models; + +internal readonly record struct ColumnOrder(int Value, LocationInfo? Location); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs b/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs index 206e7395..d7e8b467 100644 --- a/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs +++ b/SpreadCheetah.SourceGenerator/Models/LocationInfo.cs @@ -2,4 +2,7 @@ namespace SpreadCheetah.SourceGenerator.Models; -internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan); \ No newline at end of file +internal sealed record LocationInfo( + string FilePath, + TextSpan TextSpan, + LinePositionSpan LineSpan); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/RowType.cs b/SpreadCheetah.SourceGenerator/Models/RowType.cs index ce07317c..aa7dac06 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowType.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowType.cs @@ -5,9 +5,11 @@ namespace SpreadCheetah.SourceGenerator.Models; internal sealed record RowType( string Name, string FullName, - string FullNameWithNullableAnnotation, bool IsReferenceType, LocationInfo? WorksheetRowAttributeLocation, EquatableArray Properties, EquatableArray UnsupportedPropertyTypeNames, - EquatableArray DiagnosticInfos); \ No newline at end of file + EquatableArray DiagnosticInfos) +{ + public string FullNameWithNullableAnnotation => IsReferenceType ? $"{FullName}?" : FullName; +} \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index f35473ff..2e905922 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -66,7 +66,10 @@ private static bool IsSyntaxTargetForGeneration(SyntaxNode syntaxNode, Cancellat foreach (var attribute in classSymbol.GetAttributes()) { if (attribute.TryParseOptionsAttribute(out var options)) + { generatorOptions = options; + break; + } } return new ContextClass( @@ -96,57 +99,37 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo } TypedConstant? columnHeaderAttributeValue = null; - int? columnOrderValue = null; - LocationInfo? columnOrderAttributeLocation = null; + ColumnOrder? columnOrder = null; foreach (var attribute in p.GetAttributes()) { if (columnHeaderAttributeValue is null && attribute.TryParseColumnHeaderAttribute(out var arg)) - { columnHeaderAttributeValue = arg; - } - if (columnOrderValue is null && attribute.TryParseColumnOrderAttribute(out var order)) - { - columnOrderValue = order; - columnOrderAttributeLocation = attribute.ApplicationSyntaxReference? - .GetSyntax(token) - .GetLocation() - .ToLocationInfo(); - } + if (columnOrder is null && attribute.TryParseColumnOrderAttribute(token, out var orderArg)) + columnOrder = orderArg; } - var columnHeader = columnHeaderAttributeValue is { } value - ? value.ToCSharpString() - : @$"""{p.Name}"""; - - var rowTypeProperty = new RowTypeProperty( - Name: p.Name, - TypeName: p.Type.Name, - TypeFullName: p.Type.ToDisplayString(), - TypeNullableAnnotation: p.NullableAnnotation, - TypeSpecialType: p.Type.SpecialType, - ColumnHeader: columnHeader); + var rowTypeProperty = p.ToRowTypeProperty(columnHeaderAttributeValue); - if (columnOrderValue is not { } columnOrder) + if (columnOrder is not { } order) implicitOrderProperties.Add(rowTypeProperty); - else if (!explicitOrderProperties.ContainsKey(columnOrder)) - explicitOrderProperties.Add(columnOrder, rowTypeProperty); + else if (!explicitOrderProperties.ContainsKey(order.Value)) + explicitOrderProperties.Add(order.Value, rowTypeProperty); else - diagnosticInfos.Add(new DiagnosticInfo(Diagnostics.DuplicateColumnOrder, columnOrderAttributeLocation, new([classType.Name]))); + diagnosticInfos.Add(new DiagnosticInfo(Diagnostics.DuplicateColumnOrder, order.Location, new([classType.Name]))); } explicitOrderProperties.AddWithImplicitKeys(implicitOrderProperties); return new RowType( - Name: classType.Name, + DiagnosticInfos: diagnosticInfos.ToEquatableArray(), FullName: classType.ToString(), - FullNameWithNullableAnnotation: classType.IsReferenceType ? $"{classType}?" : classType.ToString(), IsReferenceType: classType.IsReferenceType, - WorksheetRowAttributeLocation: worksheetRowAttributeLocation, + Name: classType.Name, Properties: explicitOrderProperties.Values.ToEquatableArray(), UnsupportedPropertyTypeNames: unsupportedPropertyTypeNames.ToEquatableArray(), - DiagnosticInfos: diagnosticInfos.ToEquatableArray()); + WorksheetRowAttributeLocation: worksheetRowAttributeLocation); } private static void Execute(ContextClass? contextClass, SourceProductionContext context) From 8a7b8d6d61052f7efb21cd3247b458da7b5f938b Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 11:18:14 +0100 Subject: [PATCH 28/31] Src gen: More use of raw string literals --- .../Helpers/StringBuilderExtensions.cs | 23 -- .../WorksheetRowGenerator.cs | 245 +++++++++--------- 2 files changed, 126 insertions(+), 142 deletions(-) delete mode 100644 SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs diff --git a/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs b/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs deleted file mode 100644 index 4b8a135d..00000000 --- a/SpreadCheetah.SourceGenerator/Helpers/StringBuilderExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text; - -namespace SpreadCheetah.SourceGenerator.Helpers; - -internal static class StringBuilderExtensions -{ - public static StringBuilder AppendIndentation(this StringBuilder sb, int indentationLevel) => indentationLevel switch - { - <= 0 => sb, - 1 => sb.Append(" "), - 2 => sb.Append(" "), - 3 => sb.Append(" "), - 4 => sb.Append(" "), - 5 => sb.Append(" "), - 6 => sb.Append(" "), - _ => sb.Append(new string(' ', 4 * indentationLevel)) - }; - - public static StringBuilder AppendLine(this StringBuilder sb, int indentationLevel, string value) - { - return sb.AppendIndentation(indentationLevel).AppendLine(value); - } -} diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 2e905922..1d05d95f 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -149,16 +149,18 @@ private static void Execute(ContextClass? contextClass, SourceProductionContext private static void GenerateHeader(StringBuilder sb) { - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine("using SpreadCheetah;"); - sb.AppendLine("using SpreadCheetah.SourceGeneration;"); - sb.AppendLine("using System;"); - sb.AppendLine("using System.Buffers;"); - sb.AppendLine("using System.Collections.Generic;"); - sb.AppendLine("using System.Threading;"); - sb.AppendLine("using System.Threading.Tasks;"); - sb.AppendLine(); + sb.AppendLine(""" + // + #nullable enable + using SpreadCheetah; + using SpreadCheetah.SourceGeneration; + using System; + using System.Buffers; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + """); } private static void GenerateCode(StringBuilder sb, ContextClass contextClass, SourceProductionContext context) @@ -166,19 +168,21 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, So GenerateHeader(sb); if (contextClass.Namespace is { } ns) - sb.AppendLine($"namespace {ns}"); + sb.Append("namespace ").AppendLine(ns); var accessibility = SyntaxFacts.GetText(contextClass.DeclaredAccessibility); - sb.AppendLine("{"); - sb.AppendLine($" {accessibility} partial class {contextClass.Name}"); - sb.AppendLine(" {"); - sb.AppendLine($" private static {contextClass.Name}? _default;"); - sb.AppendLine($" public static {contextClass.Name} Default => _default ??= new {contextClass.Name}();"); - sb.AppendLine(); - sb.AppendLine($" public {contextClass.Name}()"); - sb.AppendLine(" {"); - sb.AppendLine(" }"); + sb.AppendLine($$""" + { + {{accessibility}} partial class {{contextClass.Name}} + { + private static {{contextClass.Name}}? _default; + public static {{contextClass.Name}} Default => _default ??= new {{contextClass.Name}}(); + + public {{contextClass.Name}}() + { + } + """); var rowTypeNames = new HashSet(StringComparer.Ordinal); @@ -193,8 +197,10 @@ private static void GenerateCode(StringBuilder sb, ContextClass contextClass, So ++typeIndex; } - sb.AppendLine(" }"); - sb.AppendLine("}"); + sb.AppendLine(""" + } + } + """); } private static void GenerateCodeForType(StringBuilder sb, int typeIndex, RowType rowType, @@ -202,10 +208,10 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, RowType { ReportDiagnostics(rowType, rowType.WorksheetRowAttributeLocation, contextClass.Options, context); - sb.AppendLine().AppendLine(FormattableString.Invariant($$""" - private WorksheetRowTypeInfo<{{rowType.FullName}}>? _{{rowType.Name}}; - public WorksheetRowTypeInfo<{{rowType.FullName}}> {{rowType.Name}} => _{{rowType.Name}} - """)); + sb.AppendLine().AppendLine($$""" + private WorksheetRowTypeInfo<{{rowType.FullName}}>? _{{rowType.Name}}; + public WorksheetRowTypeInfo<{{rowType.FullName}}> {{rowType.Name}} => _{{rowType.Name}} + """); if (rowType.Properties.Count == 0) { @@ -221,12 +227,12 @@ private static void GenerateCodeForType(StringBuilder sb, int typeIndex, RowType """)); GenerateAddHeaderRow(sb, typeIndex, rowType.Properties); - GenerateAddAsRow(sb, 2, rowType); - GenerateAddRangeAsRows(sb, 2, rowType); - GenerateAddAsRowInternal(sb, 2, rowType.FullName, rowType.Properties); - GenerateAddRangeAsRowsInternal(sb, rowType, rowType.Properties); - GenerateAddEnumerableAsRows(sb, 2, rowType); - GenerateAddCellsAsRow(sb, 2, rowType, rowType.Properties); + GenerateAddAsRow(sb, rowType); + GenerateAddRangeAsRows(sb, rowType); + GenerateAddAsRowInternal(sb, rowType); + GenerateAddRangeAsRowsInternal(sb, rowType); + GenerateAddEnumerableAsRows(sb, rowType); + GenerateAddCellsAsRow(sb, rowType); } private static void ReportDiagnostics(RowType rowType, LocationInfo? locationInfo, GeneratorOptions? options, SourceProductionContext context) @@ -258,12 +264,12 @@ private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadO Debug.Assert(properties.Count > 0); sb.AppendLine().AppendLine(FormattableString.Invariant($$""" - private static async ValueTask AddHeaderRow{{typeIndex}}Async(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.Styling.StyleId? styleId, CancellationToken token) - { - var cells = ArrayPool.Shared.Rent({{properties.Count}}); - try + private static async ValueTask AddHeaderRow{{typeIndex}}Async(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.Styling.StyleId? styleId, CancellationToken token) { - """)); + var cells = ArrayPool.Shared.Rent({{properties.Count}}); + try + { + """)); foreach (var (i, property) in properties.Index()) { @@ -273,82 +279,85 @@ private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadO } sb.AppendLine($$""" - await spreadsheet.AddRowAsync(cells.AsMemory(0, {{properties.Count}}), token).ConfigureAwait(false); - } - finally - { - ArrayPool.Shared.Return(cells, true); + await spreadsheet.AddRowAsync(cells.AsMemory(0, {{properties.Count}}), token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } } - } - """); + """); } - private static void GenerateAddAsRow(StringBuilder sb, int indent, RowType rowType) + private static void GenerateAddAsRow(StringBuilder sb, RowType rowType) { - sb.AppendLine() - .AppendIndentation(indent) - .Append("private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, ") - .Append(rowType.FullNameWithNullableAnnotation) - .AppendLine(" obj, CancellationToken token)"); + sb.AppendLine($$""" - sb.AppendLine(indent, "{"); - sb.AppendLine(indent, " if (spreadsheet is null)"); - sb.AppendLine(indent, " throw new ArgumentNullException(nameof(spreadsheet));"); + private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, {{rowType.FullNameWithNullableAnnotation}} obj, CancellationToken token) + { + if (spreadsheet is null) + throw new ArgumentNullException(nameof(spreadsheet)); + """); if (rowType.IsReferenceType) { - sb.AppendLine(indent + 1, "if (obj is null)"); - sb.AppendLine(indent + 1, " return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token);"); + sb.AppendLine(""" + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + """); } - sb.AppendLine(indent, " return AddAsRowInternalAsync(spreadsheet, obj, token);"); - sb.AppendLine(indent, "}"); + sb.AppendLine(""" + return AddAsRowInternalAsync(spreadsheet, obj, token); + } + """); } - private static void GenerateAddAsRowInternal(StringBuilder sb, int indent, string rowTypeFullname, IReadOnlyCollection properties) + private static void GenerateAddAsRowInternal(StringBuilder sb, RowType rowType) { + var properties = rowType.Properties; Debug.Assert(properties.Count > 0); - sb.AppendLine(); - sb.AppendLine(indent, $"private static async ValueTask AddAsRowInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, {rowTypeFullname} obj, CancellationToken token)"); - sb.AppendLine(indent, "{"); - sb.AppendLine(indent, $" var cells = ArrayPool.Shared.Rent({properties.Count});"); - sb.AppendLine(indent, " try"); - sb.AppendLine(indent, " {"); - sb.AppendLine(indent, " await AddCellsAsRowAsync(spreadsheet, obj, cells, token).ConfigureAwait(false);"); - sb.AppendLine(indent, " }"); - sb.AppendLine(indent, " finally"); - sb.AppendLine(indent, " {"); - sb.AppendLine(indent, " ArrayPool.Shared.Return(cells, true);"); - sb.AppendLine(indent, " }"); - sb.AppendLine(indent, "}"); + sb.AppendLine($$""" + + private static async ValueTask AddAsRowInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, {{rowType.FullName}} obj, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent({{properties.Count}}); + try + { + await AddCellsAsRowAsync(spreadsheet, obj, cells, token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + """); } - private static void GenerateAddRangeAsRows(StringBuilder sb, int indent, RowType rowType) + private static void GenerateAddRangeAsRows(StringBuilder sb, RowType rowType) { - sb.AppendLine() - .AppendIndentation(indent) - .Append("private static ValueTask AddRangeAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<") - .Append(rowType.FullNameWithNullableAnnotation) - .AppendLine("> objs, CancellationToken token)"); - - sb.AppendLine(indent, "{"); - sb.AppendLine(indent, " if (spreadsheet is null)"); - sb.AppendLine(indent, " throw new ArgumentNullException(nameof(spreadsheet));"); - sb.AppendLine(indent, " if (objs is null)"); - sb.AppendLine(indent, " throw new ArgumentNullException(nameof(objs));"); - sb.AppendLine(indent, " return AddRangeAsRowsInternalAsync(spreadsheet, objs, token);"); - sb.AppendLine(indent, "}"); + sb.AppendLine($$""" + + private static ValueTask AddRangeAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<{{rowType.FullNameWithNullableAnnotation}}> objs, CancellationToken token) + { + if (spreadsheet is null) + throw new ArgumentNullException(nameof(spreadsheet)); + if (objs is null) + throw new ArgumentNullException(nameof(objs)); + return AddRangeAsRowsInternalAsync(spreadsheet, objs, token); + } + """); } - private static void GenerateAddRangeAsRowsInternal(StringBuilder sb, RowType rowType, IReadOnlyCollection properties) + private static void GenerateAddRangeAsRowsInternal(StringBuilder sb, RowType rowType) { + var properties = rowType.Properties; Debug.Assert(properties.Count > 0); - var typeString = rowType.FullNameWithNullableAnnotation; sb.Append($$""" - private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<{{typeString}}> objs, CancellationToken token) + private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<{{rowType.FullNameWithNullableAnnotation}}> objs, CancellationToken token) { var cells = ArrayPool.Shared.Rent({{properties.Count}}); try @@ -364,52 +373,50 @@ private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreads """); } - private static void GenerateAddEnumerableAsRows(StringBuilder sb, int indent, RowType rowType) + private static void GenerateAddEnumerableAsRows(StringBuilder sb, RowType rowType) { - sb.AppendLine() - .AppendIndentation(indent) - .Append("private static async ValueTask AddEnumerableAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<") - .Append(rowType.FullNameWithNullableAnnotation) - .AppendLine("> objs, DataCell[] cells, CancellationToken token)"); - - sb.AppendLine(indent, "{"); - sb.AppendLine(indent, " foreach (var obj in objs)"); - sb.AppendLine(indent, " {"); - sb.AppendLine(indent, " await AddCellsAsRowAsync(spreadsheet, obj, cells, token).ConfigureAwait(false);"); - sb.AppendLine(indent, " }"); - sb.AppendLine(indent, "}"); + sb.AppendLine($$""" + + private static async ValueTask AddEnumerableAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, IEnumerable<{{rowType.FullNameWithNullableAnnotation}}> objs, DataCell[] cells, CancellationToken token) + { + foreach (var obj in objs) + { + await AddCellsAsRowAsync(spreadsheet, obj, cells, token).ConfigureAwait(false); + } + } + """); } - private static void GenerateAddCellsAsRow(StringBuilder sb, int indent, RowType rowType, IReadOnlyCollection properties) + private static void GenerateAddCellsAsRow(StringBuilder sb, RowType rowType) { + var properties = rowType.Properties; Debug.Assert(properties.Count > 0); - sb.AppendLine() - .AppendIndentation(indent) - .Append("private static ValueTask AddCellsAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, ") - .Append(rowType.FullNameWithNullableAnnotation) - .AppendLine(" obj, DataCell[] cells, CancellationToken token)"); + sb.AppendLine($$""" - sb.AppendLine(indent, "{"); + private static ValueTask AddCellsAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, {{rowType.FullNameWithNullableAnnotation}} obj, DataCell[] cells, CancellationToken token) + { + """); if (rowType.IsReferenceType) { - sb.AppendLine(indent, " if (obj is null)"); - sb.AppendLine(indent, " return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token);"); - sb.AppendLine(); + sb.AppendLine(""" + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + + """); } foreach (var (i, property) in properties.Index()) { - sb.AppendIndentation(indent + 1) - .Append("cells[") - .Append(i) - .Append("] = new DataCell(obj.") - .Append(property.Name) - .AppendLine(");"); + sb.AppendLine(FormattableString.Invariant($$""" + cells[{{i}}] = new DataCell(obj.{{property.Name}}); + """)); } - sb.AppendLine(indent, $" return spreadsheet.AddRowAsync(cells.AsMemory(0, {properties.Count}), token);"); - sb.AppendLine(indent, "}"); + sb.AppendLine($$""" + return spreadsheet.AddRowAsync(cells.AsMemory(0, {{properties.Count}}), token); + } + """); } } From 21fb049a29047f9a0a8a8d9408532b10156753f2 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 11:36:56 +0100 Subject: [PATCH 29/31] Replace line endings in snapshot test --- .../Helpers/TestHelper.cs | 5 ++++- .../Tests/WorksheetRowGeneratorColumnHeaderTests.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs index 103686e5..a242c98b 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs @@ -26,7 +26,7 @@ private static PortableExecutableReference[] GetAssemblyReferences() ]; } - public static SettingsTask CompileAndVerify(string source, params object?[] parameters) where T : IIncrementalGenerator, new() + public static SettingsTask CompileAndVerify(string source, bool replaceLineEndings = false, params object?[] parameters) where T : IIncrementalGenerator, new() { var syntaxTree = CSharpSyntaxTree.ParseText(source); var references = GetAssemblyReferences(); @@ -41,6 +41,9 @@ private static PortableExecutableReference[] GetAssemblyReferences() var settings = new VerifySettings(); settings.UseDirectory("../Snapshots"); + if (replaceLineEndings) + settings.ScrubLinesWithReplace(x => x.Replace("\r\n", "\n", StringComparison.Ordinal)); + var task = Verify(target, settings); return parameters.Length > 0 diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs index cbcdf11b..019b4343 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs @@ -38,6 +38,6 @@ public partial class MyGenRowContext : WorksheetRowContext; """; // Act & Assert - return TestHelper.CompileAndVerify(source); + return TestHelper.CompileAndVerify(source, replaceLineEndings: true); } } From e075615da3428ab91a6d37cb473539cba20b165e Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 11:58:32 +0100 Subject: [PATCH 30/31] Replace escaped line endings in snapshot test --- .../Helpers/TestHelper.cs | 6 +++--- ...rColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs | 4 ++-- .../Tests/WorksheetRowGeneratorColumnHeaderTests.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs index a242c98b..f0bdae8c 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Helpers/TestHelper.cs @@ -26,7 +26,7 @@ private static PortableExecutableReference[] GetAssemblyReferences() ]; } - public static SettingsTask CompileAndVerify(string source, bool replaceLineEndings = false, params object?[] parameters) where T : IIncrementalGenerator, new() + public static SettingsTask CompileAndVerify(string source, bool replaceEscapedLineEndings = false, params object?[] parameters) where T : IIncrementalGenerator, new() { var syntaxTree = CSharpSyntaxTree.ParseText(source); var references = GetAssemblyReferences(); @@ -41,8 +41,8 @@ private static PortableExecutableReference[] GetAssemblyReferences() var settings = new VerifySettings(); settings.UseDirectory("../Snapshots"); - if (replaceLineEndings) - settings.ScrubLinesWithReplace(x => x.Replace("\r\n", "\n", StringComparison.Ordinal)); + if (replaceEscapedLineEndings) + settings.ScrubLinesWithReplace(x => x.Replace("\\r\\n", "\\n", StringComparison.Ordinal)); var task = Verify(target, settings); diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs index f32661da..faf28a3e 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithSpecialCharacterColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs @@ -1,4 +1,4 @@ -๏ปฟ//HintName: MyNamespace.MyGenRowContext.g.cs +//HintName: MyNamespace.MyGenRowContext.g.cs // #nullable enable using SpreadCheetah; @@ -32,7 +32,7 @@ private static async ValueTask AddHeaderRow0Async(SpreadCheetah.Spreadsheet spre cells[0] = new StyledCell("First name", styleId); cells[1] = new StyledCell("", styleId); cells[2] = new StyledCell("Nationality (escaped characters \", ', \\)", styleId); - cells[3] = new StyledCell("Address line 1 (escaped characters \r\n, \t)", styleId); + cells[3] = new StyledCell("Address line 1 (escaped characters \n, \t)", styleId); cells[4] = new StyledCell("Address line 2 (verbatim\nstring: \", \\)", styleId); cells[5] = new StyledCell(" Age (\n raw\n string\n literal\n )", styleId); cells[6] = new StyledCell("Note (unicode escape sequence ๐ŸŒ‰, ๐Ÿ‘, รง)", styleId); diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs index 019b4343..36e15e1a 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs @@ -38,6 +38,6 @@ public partial class MyGenRowContext : WorksheetRowContext; """; // Act & Assert - return TestHelper.CompileAndVerify(source, replaceLineEndings: true); + return TestHelper.CompileAndVerify(source, replaceEscapedLineEndings: true); } } From 093ee7c184c5a89e4fc2d464118559a2ed341a3d Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 18 Feb 2024 12:22:13 +0100 Subject: [PATCH 31/31] Src gen: Remove unused code --- .../Extensions/SymbolExtensions.cs | 6 +----- SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs | 2 ++ SpreadCheetah.SourceGenerator/Helpers/HashCode.cs | 2 ++ SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs | 5 ----- SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs | 6 ------ 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs index e48397e7..08fb0f71 100644 --- a/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs @@ -68,10 +68,6 @@ public static RowTypeProperty ToRowTypeProperty( return new RowTypeProperty( ColumnHeader: columnHeader, - Name: p.Name, - TypeFullName: p.Type.ToDisplayString(), - TypeName: p.Type.Name, - TypeNullableAnnotation: p.NullableAnnotation, - TypeSpecialType: p.Type.SpecialType); + Name: p.Name); } } diff --git a/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs b/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs index e0a737a8..4efa817a 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/EquatableArray.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; namespace SpreadCheetah.SourceGenerator.Helpers; @@ -6,6 +7,7 @@ namespace SpreadCheetah.SourceGenerator.Helpers; /// Based on the implementation from: /// https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs /// +[ExcludeFromCodeCoverage] internal readonly struct EquatableArray(T[] underlyingArray) : IEquatable>, IReadOnlyCollection where T : IEquatable diff --git a/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs b/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs index 356fbc60..5435febb 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/HashCode.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -10,6 +11,7 @@ namespace SpreadCheetah.SourceGenerator.Helpers; /// https://github.com/CommunityToolkit/dotnet/blob/7b53ae23dfc6a7fb12d0fc058b89b6e948f48448/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs /// [StructLayout(LayoutKind.Auto)] +[ExcludeFromCodeCoverage] #pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals internal struct HashCode #pragma warning restore CA1066 // Implement IEquatable when overriding Object.Equals diff --git a/SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs b/SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs index ad2f4d2e..a0aabce8 100644 --- a/SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs +++ b/SpreadCheetah.SourceGenerator/Helpers/LocationMap.cs @@ -10,11 +10,6 @@ public static Location ToLocation(this LocationInfo info) return Location.Create(info.FilePath, info.TextSpan, info.LineSpan); } - public static LocationInfo? ToLocationInfo(this SyntaxNode node) - { - return node.GetLocation().ToLocationInfo(); - } - public static LocationInfo? ToLocationInfo(this Location location) { if (location.SourceTree is null) diff --git a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs index 9b4321eb..ad54e740 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs @@ -1,11 +1,5 @@ -using Microsoft.CodeAnalysis; - namespace SpreadCheetah.SourceGenerator.Models; internal sealed record RowTypeProperty( string Name, - string TypeName, - string TypeFullName, - NullableAnnotation TypeNullableAnnotation, - SpecialType TypeSpecialType, string ColumnHeader); \ No newline at end of file