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)
+ {
+ }
+}