diff --git a/.editorconfig b/.editorconfig index ad55b2f8..6b402243 100644 --- a/.editorconfig +++ b/.editorconfig @@ -230,6 +230,10 @@ dotnet_diagnostic.MA0076.severity = none # RCS1163: Unused parameter dotnet_diagnostic.RCS1163.severity = warning +[{ColumnHeaderAttribute.cs,ColumnOrderAttribute.cs,WorksheetRowAttribute.cs}] +# RCS1163: Unused parameter +dotnet_diagnostic.RCS1163.severity = none + ############################### # SonarAnalyzer Options # ############################### diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Assembly.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Assembly.cs new file mode 100644 index 00000000..44628ddc --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Assembly.cs @@ -0,0 +1,3 @@ +using System.Resources; + +[assembly: NeutralResourcesLanguage("en-US")] \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ClassWithPropertyReferenceColumnHeaders.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ClassWithPropertyReferenceColumnHeaders.cs new file mode 100644 index 00000000..69ba1cfa --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ClassWithPropertyReferenceColumnHeaders.cs @@ -0,0 +1,24 @@ +using SpreadCheetah.SourceGeneration; + +namespace SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader; + +public class ClassWithPropertyReferenceColumnHeaders +{ + [ColumnHeader(typeof(ColumnHeaderResources), nameof(ColumnHeaderResources.Header_FirstName))] + public string? FirstName { get; set; } + + [ColumnHeader(propertyName: nameof(ColumnHeaderResources.Header_LastName), type: typeof(ColumnHeaderResources))] + public string? LastName { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderNationality))] + public string? Nationality { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderAddressLine1))] + public string? AddressLine1 { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderAddressLine2))] + public string? AddressLine2 { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderAge))] + public int Age { get; set; } +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaderResources.Designer.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaderResources.Designer.cs new file mode 100644 index 00000000..72e34281 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaderResources.Designer.cs @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ColumnHeaderResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ColumnHeaderResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaderResour" + + "ces", typeof(ColumnHeaderResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to First name. + /// + public static string Header_FirstName { + get { + return ResourceManager.GetString("Header_FirstName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last name. + /// + public static string Header_LastName { + get { + return ResourceManager.GetString("Header_LastName", resourceCulture); + } + } + } +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaderResources.resx b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaderResources.resx new file mode 100644 index 00000000..0d488609 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaderResources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + First name + + + Last name + + \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaders.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaders.cs new file mode 100644 index 00000000..23479dc2 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Models/ColumnHeader/ColumnHeaders.cs @@ -0,0 +1,9 @@ +namespace SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader; + +public static class ColumnHeaders +{ + public static string HeaderNationality { get; } = "The nationality"; + public static string HeaderAddressLine1 => "Address line 1"; + public static string? HeaderAddressLine2 => null; + public static string? HeaderAge => $"Age (in {DateTime.UtcNow.Year})"; +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs new file mode 100644 index 00000000..df5a1b8b --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs @@ -0,0 +1,114 @@ +//HintName: MyNamespace.MyGenRowContext.g.cs +// +#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? _ClassWithInvalidPropertyReferenceColumnHeaders; + public WorksheetRowTypeInfo ClassWithInvalidPropertyReferenceColumnHeaders => _ClassWithInvalidPropertyReferenceColumnHeaders + ??= WorksheetRowMetadataServices.CreateObjectInfo(AddHeaderRow0Async, AddAsRowAsync, AddRangeAsRowsAsync); + + private static async ValueTask AddHeaderRow0Async(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.Styling.StyleId? styleId, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(7); + try + { + cells[0] = new StyledCell("PropertyA", styleId); + cells[1] = new StyledCell("PropertyB", styleId); + cells[2] = new StyledCell("PropertyC", styleId); + cells[3] = new StyledCell("PropertyD", styleId); + cells[4] = new StyledCell("PropertyE", styleId); + cells[5] = new StyledCell("PropertyF", styleId); + cells[6] = new StyledCell("PropertyG", styleId); + await spreadsheet.AddRowAsync(cells.AsMemory(0, 7), token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, MyNamespace.ClassWithInvalidPropertyReferenceColumnHeaders? 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, MyNamespace.ClassWithInvalidPropertyReferenceColumnHeaders obj, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(7); + 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(7); + 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, MyNamespace.ClassWithInvalidPropertyReferenceColumnHeaders? obj, DataCell[] cells, CancellationToken token) + { + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + + cells[0] = new DataCell(obj.PropertyA); + cells[1] = new DataCell(obj.PropertyB); + cells[2] = new DataCell(obj.PropertyC); + cells[3] = new DataCell(obj.PropertyD); + cells[4] = new DataCell(obj.PropertyE); + cells[5] = new DataCell(obj.PropertyF); + cells[6] = new DataCell(obj.PropertyG); + return spreadsheet.AddRowAsync(cells.AsMemory(0, 7), token); + } + } +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders.verified.txt b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders.verified.txt new file mode 100644 index 00000000..209f96ca --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders.verified.txt @@ -0,0 +1,74 @@ +{ + Diagnostics: [ + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (16,5)-(16,63), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'NonExistingProperty' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + }, + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (18,5)-(18,48), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'name' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + }, + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (20,5)-(20,85), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'PrivateGetterProperty' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + }, + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (22,5)-(22,81), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'WriteOnlyProperty' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + }, + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (24,5)-(24,81), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'NonStringProperty' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + }, + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (26,5)-(26,80), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'InternalProperty' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + }, + { + Id: SPCH1004, + Title: Invalid ColumnHeader property reference, + Severity: Error, + WarningLevel: 0, + Location: : (28,5)-(28,81), + MessageFormat: '{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Message: 'NonStaticProperty' on type 'MyNamespace.ColumnHeaders' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?)., + Category: SpreadCheetah.SourceGenerator + } + ] +} \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithPropertyReferenceColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithPropertyReferenceColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs new file mode 100644 index 00000000..125f0fc8 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/WorksheetRowGeneratorColumnHeaderTests.WorksheetRowGenerator_Generate_ClassWithPropertyReferenceColumnHeaders#MyNamespace.MyGenRowContext.g.verified.cs @@ -0,0 +1,112 @@ +//HintName: MyNamespace.MyGenRowContext.g.cs +// +#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? _ClassWithPropertyReferenceColumnHeaders; + public WorksheetRowTypeInfo ClassWithPropertyReferenceColumnHeaders => _ClassWithPropertyReferenceColumnHeaders + ??= WorksheetRowMetadataServices.CreateObjectInfo(AddHeaderRow0Async, AddAsRowAsync, AddRangeAsRowsAsync); + + private static async ValueTask AddHeaderRow0Async(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.Styling.StyleId? styleId, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(6); + try + { + cells[0] = new StyledCell(SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaderResources.Header_FirstName, styleId); + cells[1] = new StyledCell(SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaderResources.Header_LastName, styleId); + cells[2] = new StyledCell(SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaders.HeaderNationality, styleId); + cells[3] = new StyledCell(SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaders.HeaderAddressLine1, styleId); + cells[4] = new StyledCell(SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaders.HeaderAddressLine2, styleId); + cells[5] = new StyledCell(SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ColumnHeaders.HeaderAge, styleId); + await spreadsheet.AddRowAsync(cells.AsMemory(0, 6), token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader.ClassWithPropertyReferenceColumnHeaders? 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.ColumnHeader.ClassWithPropertyReferenceColumnHeaders obj, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(6); + 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(6); + 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.ColumnHeader.ClassWithPropertyReferenceColumnHeaders? obj, DataCell[] cells, CancellationToken token) + { + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + + cells[0] = new DataCell(obj.FirstName); + cells[1] = new DataCell(obj.LastName); + cells[2] = new DataCell(obj.Nationality); + cells[3] = new DataCell(obj.AddressLine1); + cells[4] = new DataCell(obj.AddressLine2); + cells[5] = new DataCell(obj.Age); + return spreadsheet.AddRowAsync(cells.AsMemory(0, 6), token); + } + } +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj b/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj index 5b468508..7ac7351e 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/SpreadCheetah.SourceGenerator.SnapshotTest.csproj @@ -27,4 +27,19 @@ + + + True + True + ColumnHeaderResources.resx + + + + + + PublicResXFileCodeGenerator + ColumnHeaderResources.Designer.cs + + + diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs index 36e15e1a..35af3b48 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/WorksheetRowGeneratorColumnHeaderTests.cs @@ -40,4 +40,67 @@ public partial class MyGenRowContext : WorksheetRowContext; // Act & Assert return TestHelper.CompileAndVerify(source, replaceEscapedLineEndings: true); } + + [Fact] + public Task WorksheetRowGenerator_Generate_ClassWithPropertyReferenceColumnHeaders() + { + // Arrange + const string source = """ + using SpreadCheetah.SourceGeneration; + using SpreadCheetah.SourceGenerator.SnapshotTest.Models.ColumnHeader; + + namespace MyNamespace; + + [WorksheetRow(typeof(ClassWithPropertyReferenceColumnHeaders))] + public partial class MyGenRowContext : WorksheetRowContext; + """; + + // Act & Assert + return TestHelper.CompileAndVerify(source); + } + + [Fact] + public Task WorksheetRowGenerator_Generate_ClassWithInvalidPropertyReferenceColumnHeaders() + { + // Arrange + const string source = """ + using SpreadCheetah.SourceGeneration; + + namespace MyNamespace; + + public class ColumnHeaders + { + public static string Name => "The name"; + public static string PrivateGetterProperty { private get; set; } = "Private getter property"; + public static string WriteOnlyProperty { set => _ = value; } + public static int NonStringProperty => 2024; + internal static string InternalProperty => "Internal property"; + public string NonStaticProperty => "Non static property"; + } + + public class ClassWithInvalidPropertyReferenceColumnHeaders + { + [ColumnHeader(typeof(ColumnHeaders), "NonExistingProperty")] + public string PropertyA { get; set; } + [ColumnHeader(typeof(ColumnHeaders), "name")] + public string PropertyB { get; set; } + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.PrivateGetterProperty))] + public string PropertyC { get; set; } + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.WriteOnlyProperty))] + public string PropertyD { get; set; } + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.NonStringProperty))] + public string PropertyE { get; set; } + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.InternalProperty))] + public string PropertyF { get; set; } + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.NonStaticProperty))] + public string PropertyG { get; set; } + } + + [WorksheetRow(typeof(ClassWithInvalidPropertyReferenceColumnHeaders))] + public partial class MyGenRowContext : WorksheetRowContext; + """; + + // Act & Assert + return TestHelper.CompileAndVerify(source); + } } diff --git a/SpreadCheetah.SourceGenerator.Test/Assembly.cs b/SpreadCheetah.SourceGenerator.Test/Assembly.cs new file mode 100644 index 00000000..44628ddc --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Assembly.cs @@ -0,0 +1,3 @@ +using System.Resources; + +[assembly: NeutralResourcesLanguage("en-US")] \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ClassWithPropertyReferenceColumnHeaders.cs b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ClassWithPropertyReferenceColumnHeaders.cs new file mode 100644 index 00000000..40ee45d8 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ClassWithPropertyReferenceColumnHeaders.cs @@ -0,0 +1,24 @@ +using SpreadCheetah.SourceGeneration; + +namespace SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader; + +public class ClassWithPropertyReferenceColumnHeaders +{ + [ColumnHeader(typeof(ColumnHeaderResources), nameof(ColumnHeaderResources.Header_FirstName))] + public string? FirstName { get; set; } + + [ColumnHeader(propertyName: nameof(ColumnHeaderResources.Header_LastName), type: typeof(ColumnHeaderResources))] + public string? LastName { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderNationality))] + public string? Nationality { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderAddressLine1))] + public string? AddressLine1 { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderAddressLine2))] + public string? AddressLine2 { get; set; } + + [ColumnHeader(typeof(ColumnHeaders), nameof(ColumnHeaders.HeaderAge))] + public int Age { get; set; } +} diff --git a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderContext.cs b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderContext.cs index 2b4e569c..8b342ba4 100644 --- a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderContext.cs +++ b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderContext.cs @@ -2,5 +2,6 @@ namespace SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader; +[WorksheetRow(typeof(ClassWithPropertyReferenceColumnHeaders))] [WorksheetRow(typeof(ClassWithSpecialCharacterColumnHeaders))] public partial class ColumnHeaderContext : WorksheetRowContext; \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.Designer.cs b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.Designer.cs new file mode 100644 index 00000000..14afd31f --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ColumnHeaderResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ColumnHeaderResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader.ColumnHeaderResources", typeof(ColumnHeaderResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to First name. + /// + public static string Header_FirstName { + get { + return ResourceManager.GetString("Header_FirstName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Last name. + /// + public static string Header_LastName { + get { + return ResourceManager.GetString("Header_LastName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Length (in cm). + /// + public static string Header_Length { + get { + return ResourceManager.GetString("Header_Length", resourceCulture); + } + } + } +} diff --git a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.nb.resx b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.nb.resx new file mode 100644 index 00000000..0674979c --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.nb.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Fornavn + + \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.resx b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.resx new file mode 100644 index 00000000..e27c2b35 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaderResources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + First name + + + Last name + + + Length (in cm) + + \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaders.cs b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaders.cs new file mode 100644 index 00000000..c004174a --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/ColumnHeader/ColumnHeaders.cs @@ -0,0 +1,9 @@ +namespace SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader; + +public static class ColumnHeaders +{ + public static string HeaderNationality { get; } = "The nationality"; + public static string HeaderAddressLine1 => "Address line 1"; + public static string? HeaderAddressLine2 => null; + public static string? HeaderAge => $"Age (in {DateTime.UtcNow.Year})"; +} diff --git a/SpreadCheetah.SourceGenerator.Test/Models/Combinations/ClassWithColumnAttributes.cs b/SpreadCheetah.SourceGenerator.Test/Models/Combinations/ClassWithColumnAttributes.cs index 996d85a7..b498e47a 100644 --- a/SpreadCheetah.SourceGenerator.Test/Models/Combinations/ClassWithColumnAttributes.cs +++ b/SpreadCheetah.SourceGenerator.Test/Models/Combinations/ClassWithColumnAttributes.cs @@ -1,8 +1,9 @@ using SpreadCheetah.SourceGeneration; +using SpreadCheetah.SourceGenerator.Test.Models.ColumnHeader; namespace SpreadCheetah.SourceGenerator.Test.Models.Combinations; -public class ClassWithColumnAttributes(string model, string make, int year, decimal kW) +public class ClassWithColumnAttributes(string model, string make, int year, decimal kW, decimal length) { public string Model { get; } = model; @@ -16,4 +17,7 @@ public class ClassWithColumnAttributes(string model, string make, int year, deci #pragma warning disable IDE1006 // Naming Styles public decimal kW { get; } = kW; #pragma warning restore IDE1006 // Naming Styles + + [ColumnHeader(typeof(ColumnHeaderResources), nameof(ColumnHeaderResources.Header_Length))] + public decimal Length { get; } = length; } diff --git a/SpreadCheetah.SourceGenerator.Test/SpreadCheetah.SourceGenerator.Test.csproj b/SpreadCheetah.SourceGenerator.Test/SpreadCheetah.SourceGenerator.Test.csproj index 38f5b858..048a4387 100644 --- a/SpreadCheetah.SourceGenerator.Test/SpreadCheetah.SourceGenerator.Test.csproj +++ b/SpreadCheetah.SourceGenerator.Test/SpreadCheetah.SourceGenerator.Test.csproj @@ -31,4 +31,19 @@ + + + True + True + ColumnHeaderResources.resx + + + + + + PublicResXFileCodeGenerator + ColumnHeaderResources.Designer.cs + + + diff --git a/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs b/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs index 346d4d03..12ea2ea5 100644 --- a/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs +++ b/SpreadCheetah.SourceGenerator.Test/Tests/WorksheetRowGeneratorTests.cs @@ -2,7 +2,6 @@ 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; @@ -13,9 +12,14 @@ using SpreadCheetah.SourceGenerator.Test.Models.NoProperties; using SpreadCheetah.Styling; using SpreadCheetah.TestHelpers.Assertions; +using System.Globalization; using Xunit; using OpenXmlCell = DocumentFormat.OpenXml.Spreadsheet.Cell; +#if NET472 +using SpreadCheetah.SourceGenerator.Test.Helpers.Backporting; +#endif + namespace SpreadCheetah.SourceGenerator.Test.Tests; public class WorksheetRowGeneratorTests @@ -600,6 +604,40 @@ public async Task Spreadsheet_AddHeaderRow_SpecialCharacterColumnHeaders() Assert.Equal(expectedValues.Select(x => x.ReplaceLineEndings()), sheet.Row(1).Select(x => x.StringValue?.ReplaceLineEndings())); } + [Fact] + public async Task Spreadsheet_AddHeaderRow_PropertyReferenceColumnHeaders() + { + // Arrange + var ctx = ColumnHeaderContext.Default; + + using var stream = new MemoryStream(); + await using var s = await Spreadsheet.CreateNewAsync(stream); + await s.StartWorksheetAsync("Sheet"); + + IList expectedValues = + [ + "Fornavn", + "Last name", + "The nationality", + "Address line 1", + null, + $"Age (in {DateTime.UtcNow.Year})" + ]; + + var originalCulture = CultureInfo.CurrentUICulture; + + // Act + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("nb-NO"); + await s.AddHeaderRowAsync(ctx.ClassWithPropertyReferenceColumnHeaders); + CultureInfo.CurrentUICulture = originalCulture; + + await s.FinishAsync(); + + // Assert + using var sheet = SpreadsheetAssert.SingleSheet(stream); + Assert.Equal(expectedValues, sheet.Row(1).Select(x => x.StringValue)); + } + [Fact] public async Task Spreadsheet_AddHeaderRow_ObjectWithMultipleColumnAttributes() { @@ -615,7 +653,8 @@ public async Task Spreadsheet_AddHeaderRow_ObjectWithMultipleColumnAttributes() "Year", "The make", "Model", - "kW" + "kW", + "Length (in cm)" ]; // Act diff --git a/SpreadCheetah.SourceGenerator/AnalyzerReleases.Unshipped.md b/SpreadCheetah.SourceGenerator/AnalyzerReleases.Unshipped.md index e69de29b..896c7d1f 100644 --- a/SpreadCheetah.SourceGenerator/AnalyzerReleases.Unshipped.md +++ b/SpreadCheetah.SourceGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,5 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +SPCH1004 | SpreadCheetah.SourceGenerator | Error | InvalidColumnHeaderPropertyReference \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Diagnostics.cs b/SpreadCheetah.SourceGenerator/Diagnostics.cs index b28714a7..73c731ac 100644 --- a/SpreadCheetah.SourceGenerator/Diagnostics.cs +++ b/SpreadCheetah.SourceGenerator/Diagnostics.cs @@ -29,4 +29,12 @@ internal static class Diagnostics category: Category, DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidColumnHeaderPropertyReference = new( + id: "SPCH1004", + title: "Invalid ColumnHeader property reference", + messageFormat: "'{0}' on type '{1}' is not a valid property reference. It must be a static property, have a public getter, and the return type must be a string (or string?).", + category: Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs b/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs index c59ecabd..0629c7ff 100644 --- a/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using SpreadCheetah.SourceGenerator.Helpers; using SpreadCheetah.SourceGenerator.Models; using System.Diagnostics.CodeAnalysis; @@ -59,44 +60,67 @@ public static bool TryParseOptionsAttribute( return false; } - public static bool TryParseColumnHeaderAttribute( - this AttributeData attribute, - out TypedConstant attributeArg) + public static ColumnHeader? TryGetColumnHeaderAttribute(this AttributeData attribute, ICollection diagnosticInfos, CancellationToken token) { - attributeArg = default; - if (!string.Equals(Attributes.ColumnHeader, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) - return false; + return null; var args = attribute.ConstructorArguments; - if (args is not [{ Value: string } arg]) - return false; - attributeArg = arg; - return true; + if (args is [{ Value: string } arg]) + return new ColumnHeader(arg.ToCSharpString()); + + if (args is [{ Value: INamedTypeSymbol type }, { Value: string propertyName }]) + return TryGetColumnHeaderWithPropertyReference(type, propertyName, attribute, diagnosticInfos, token); + + return null; } - public static bool TryParseColumnOrderAttribute( - this AttributeData attribute, - CancellationToken token, - [NotNullWhen(true)] out ColumnOrder? order) + private static ColumnHeader? TryGetColumnHeaderWithPropertyReference( + INamedTypeSymbol type, string propertyName, AttributeData attribute, + ICollection diagnosticInfos, CancellationToken token) { - order = null; + var typeFullName = type.ToDisplayString(); + + foreach (var member in type.GetMembers()) + { + if (!string.Equals(member.Name, propertyName, StringComparison.Ordinal)) + continue; + + if (!member.IsStaticPropertyWithPublicGetter(out var p)) + break; + if (p.Type.SpecialType != SpecialType.System_String) + break; + + var propertyReference = new ColumnHeaderPropertyReference(typeFullName, propertyName); + return new ColumnHeader(propertyReference); + } + + var location = attribute.GetLocation(token); + diagnosticInfos.Add(new DiagnosticInfo(Diagnostics.InvalidColumnHeaderPropertyReference, location, new([propertyName, typeFullName]))); + return null; + } + + public static ColumnOrder? TryGetColumnOrderAttribute(this AttributeData attribute, CancellationToken token) + { if (!string.Equals(Attributes.ColumnOrder, attribute.AttributeClass?.ToDisplayString(), StringComparison.Ordinal)) - return false; + return null; var args = attribute.ConstructorArguments; if (args is not [{ Value: int attributeValue }]) - return false; + return null; + + var location = attribute.GetLocation(token); + return new ColumnOrder(attributeValue, location); + } - var location = attribute + private static LocationInfo? GetLocation(this AttributeData attribute, CancellationToken token) + { + return 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 08fb0f71..a3a38ed8 100644 --- a/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs +++ b/SpreadCheetah.SourceGenerator/Extensions/SymbolExtensions.cs @@ -1,6 +1,4 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using SpreadCheetah.SourceGenerator.Models; using System.Diagnostics.CodeAnalysis; namespace SpreadCheetah.SourceGenerator.Extensions; @@ -60,14 +58,23 @@ public static bool IsPropertyWithPublicGetter( return false; } - public static RowTypeProperty ToRowTypeProperty( - this IPropertySymbol p, - TypedConstant? columnHeaderAttributeValue) + public static bool IsStaticPropertyWithPublicGetter( + this ISymbol symbol, + [NotNullWhen(true)] out IPropertySymbol? property) { - var columnHeader = columnHeaderAttributeValue?.ToCSharpString() ?? @$"""{p.Name}"""; + if (symbol is IPropertySymbol + { + DeclaredAccessibility: Accessibility.Public, + GetMethod.DeclaredAccessibility: Accessibility.Public, + IsStatic: true, + IsWriteOnly: false + } p) + { + property = p; + return true; + } - return new RowTypeProperty( - ColumnHeader: columnHeader, - Name: p.Name); + property = null; + return false; } } diff --git a/SpreadCheetah.SourceGenerator/Helpers/ColumnHeaderMap.cs b/SpreadCheetah.SourceGenerator/Helpers/ColumnHeaderMap.cs new file mode 100644 index 00000000..1d169a89 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Helpers/ColumnHeaderMap.cs @@ -0,0 +1,17 @@ +using SpreadCheetah.SourceGenerator.Models; + +namespace SpreadCheetah.SourceGenerator.Helpers; + +internal static class ColumnHeaderMap +{ + public static ColumnHeaderInfo ToColumnHeaderInfo(this ColumnHeader columnHeader) + { + var fullPropertyReference = columnHeader.PropertyReference is { } reference + ? $"{reference.TypeFullName}.{reference.PropertyName}" + : null; + + return new ColumnHeaderInfo( + columnHeader.RawString, + fullPropertyReference); + } +} diff --git a/SpreadCheetah.SourceGenerator/Models/ColumnHeader.cs b/SpreadCheetah.SourceGenerator/Models/ColumnHeader.cs new file mode 100644 index 00000000..70803b76 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/ColumnHeader.cs @@ -0,0 +1,10 @@ +namespace SpreadCheetah.SourceGenerator.Models; + +internal readonly record struct ColumnHeader +{ + public string? RawString { get; } + public ColumnHeaderPropertyReference? PropertyReference { get; } + + public ColumnHeader(string rawString) => RawString = rawString; + public ColumnHeader(ColumnHeaderPropertyReference propertyReference) => PropertyReference = propertyReference; +} \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/ColumnHeaderInfo.cs b/SpreadCheetah.SourceGenerator/Models/ColumnHeaderInfo.cs new file mode 100644 index 00000000..ab79fea9 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/ColumnHeaderInfo.cs @@ -0,0 +1,5 @@ +namespace SpreadCheetah.SourceGenerator.Models; + +internal readonly record struct ColumnHeaderInfo( + string? RawString, + string? FullPropertyReference); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/ColumnHeaderPropertyReference.cs b/SpreadCheetah.SourceGenerator/Models/ColumnHeaderPropertyReference.cs new file mode 100644 index 00000000..3d14c3f4 --- /dev/null +++ b/SpreadCheetah.SourceGenerator/Models/ColumnHeaderPropertyReference.cs @@ -0,0 +1,5 @@ +namespace SpreadCheetah.SourceGenerator.Models; + +internal readonly record struct ColumnHeaderPropertyReference( + string TypeFullName, + string PropertyName); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs index ad54e740..d2871177 100644 --- a/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs +++ b/SpreadCheetah.SourceGenerator/Models/RowTypeProperty.cs @@ -2,4 +2,4 @@ namespace SpreadCheetah.SourceGenerator.Models; internal sealed record RowTypeProperty( string Name, - string ColumnHeader); \ No newline at end of file + ColumnHeaderInfo? ColumnHeader); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index 1d05d95f..1c6e71a1 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -98,19 +98,16 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo continue; } - TypedConstant? columnHeaderAttributeValue = null; + ColumnHeader? columnHeader = null; ColumnOrder? columnOrder = null; foreach (var attribute in p.GetAttributes()) { - if (columnHeaderAttributeValue is null && attribute.TryParseColumnHeaderAttribute(out var arg)) - columnHeaderAttributeValue = arg; - - if (columnOrder is null && attribute.TryParseColumnOrderAttribute(token, out var orderArg)) - columnOrder = orderArg; + columnHeader ??= attribute.TryGetColumnHeaderAttribute(diagnosticInfos, token); + columnOrder ??= attribute.TryGetColumnOrderAttribute(token); } - var rowTypeProperty = p.ToRowTypeProperty(columnHeaderAttributeValue); + var rowTypeProperty = new RowTypeProperty(p.Name, columnHeader?.ToColumnHeaderInfo()); if (columnOrder is not { } order) implicitOrderProperties.Add(rowTypeProperty); @@ -273,8 +270,12 @@ private static void GenerateAddHeaderRow(StringBuilder sb, int typeIndex, IReadO foreach (var (i, property) in properties.Index()) { + var header = property.ColumnHeader?.RawString + ?? property.ColumnHeader?.FullPropertyReference + ?? @$"""{property.Name}"""; + sb.AppendLine(FormattableString.Invariant($""" - cells[{i}] = new StyledCell({property.ColumnHeader}, styleId); + cells[{i}] = new StyledCell({header}, styleId); """)); } diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt index 174152c6..c2a7555b 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt @@ -178,6 +178,7 @@ namespace SpreadCheetah.SourceGeneration public sealed class ColumnHeaderAttribute : System.Attribute { public ColumnHeaderAttribute(string name) { } + public ColumnHeaderAttribute(System.Type type, string propertyName) { } } [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=false)] public sealed class ColumnOrderAttribute : System.Attribute diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt index 2e57a1f9..7d431f34 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt @@ -178,6 +178,7 @@ namespace SpreadCheetah.SourceGeneration public sealed class ColumnHeaderAttribute : System.Attribute { public ColumnHeaderAttribute(string name) { } + public ColumnHeaderAttribute(System.Type type, string propertyName) { } } [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=false)] public sealed class ColumnOrderAttribute : System.Attribute diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt index 0226936c..36f020c5 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt @@ -178,6 +178,7 @@ namespace SpreadCheetah.SourceGeneration public sealed class ColumnHeaderAttribute : System.Attribute { public ColumnHeaderAttribute(string name) { } + public ColumnHeaderAttribute(System.Type type, string propertyName) { } } [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=false)] public sealed class ColumnOrderAttribute : System.Attribute diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt index 812b94c7..3a715048 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt @@ -177,6 +177,7 @@ namespace SpreadCheetah.SourceGeneration public sealed class ColumnHeaderAttribute : System.Attribute { public ColumnHeaderAttribute(string name) { } + public ColumnHeaderAttribute(System.Type type, string propertyName) { } } [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=false)] public sealed class ColumnOrderAttribute : System.Attribute diff --git a/SpreadCheetah.TestHelpers/Assertions/ClosedXmlAssertCell.cs b/SpreadCheetah.TestHelpers/Assertions/ClosedXmlAssertCell.cs index bf719d6f..769faf2f 100644 --- a/SpreadCheetah.TestHelpers/Assertions/ClosedXmlAssertCell.cs +++ b/SpreadCheetah.TestHelpers/Assertions/ClosedXmlAssertCell.cs @@ -8,7 +8,7 @@ internal sealed class ClosedXmlAssertCell(IXLCell cell) : ISpreadsheetAssertCell public decimal? DecimalValue => cell.GetValue(); - public string? StringValue => cell.GetText(); + public string? StringValue => cell.Value.IsBlank ? null : cell.GetText(); public ISpreadsheetAssertStyle Style => new ClosedXmlAssertStyle(cell.Style); } diff --git a/SpreadCheetah/SourceGeneration/ColumnHeaderAttribute.cs b/SpreadCheetah/SourceGeneration/ColumnHeaderAttribute.cs index 47427dc5..a9684e32 100644 --- a/SpreadCheetah/SourceGeneration/ColumnHeaderAttribute.cs +++ b/SpreadCheetah/SourceGeneration/ColumnHeaderAttribute.cs @@ -6,4 +6,24 @@ namespace SpreadCheetah.SourceGeneration; /// Header names are written to a worksheet with . /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public sealed class ColumnHeaderAttribute(string name) : Attribute; +public sealed class ColumnHeaderAttribute : Attribute +{ + /// + /// Use the value of name as the header name for the column. + /// + public ColumnHeaderAttribute(string name) + { + } + + /// + /// Get the header name from a property. The property must: + /// + /// Be a property. + /// Have a public getter. + /// Have a return type of (or ). + /// + /// + public ColumnHeaderAttribute(Type type, string propertyName) + { + } +}