From 9272b88e6c20bc5f140667c2f229c06d8957220c Mon Sep 17 00:00:00 2001 From: floribe2000 Date: Fri, 6 Oct 2023 17:58:22 +0200 Subject: [PATCH] initial draft for improved source gen --- .editorconfig | 11 +- .../DataElementGeneratorTest.cs | 55 +++ .../DataElementGeneratorTest/GenerateCode.cs | 137 +++++++ .../SourceGeneratorResultTest.cs | 86 ++--- .../SourceGeneratorTest.cs | 361 +++++++++--------- .../TestStructures/TestDataUi1.cs | 78 ++-- ...WoWsShipBuilder.Data.Generator.Test.csproj | 4 +- .../Attributes/AttributeGenerator.cs | 62 --- .../Attributes/AttributeHelper.cs | 146 +++++++ .../DataElementGenerator.cs | 221 +++++++++++ .../DataElementSourceGenerator.cs | 21 +- .../Internals/EquatableArray.cs | 196 ++++++++++ .../Internals/HashCode.cs | 180 +++++++++ .../Internals/SourceBuilder.cs | 201 ++++++++++ .../Internals/SymbolExtensions.cs | 38 ++ .../WoWsShipBuilder.Data.Generator.csproj | 6 +- .../DataElementFilteringAttribute.cs | 17 - .../DataElementTypeAttribute.cs | 72 ---- .../DataElementAttributes/DataElementTypes.cs | 14 - nuget.config | 9 + 20 files changed, 1474 insertions(+), 441 deletions(-) create mode 100644 WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/DataElementGeneratorTest.cs create mode 100644 WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/GenerateCode.cs delete mode 100644 WoWsShipBuilder.Data.Generator/Attributes/AttributeGenerator.cs create mode 100644 WoWsShipBuilder.Data.Generator/Attributes/AttributeHelper.cs create mode 100644 WoWsShipBuilder.Data.Generator/DataElementGenerator.cs create mode 100644 WoWsShipBuilder.Data.Generator/Internals/EquatableArray.cs create mode 100644 WoWsShipBuilder.Data.Generator/Internals/HashCode.cs create mode 100644 WoWsShipBuilder.Data.Generator/Internals/SourceBuilder.cs create mode 100644 WoWsShipBuilder.Data.Generator/Internals/SymbolExtensions.cs delete mode 100644 WoWsShipBuilder.DataElements/DataElementAttributes/DataElementFilteringAttribute.cs delete mode 100644 WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypeAttribute.cs delete mode 100644 WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypes.cs create mode 100644 nuget.config diff --git a/.editorconfig b/.editorconfig index 1faf532e4..c707c9c26 100644 --- a/.editorconfig +++ b/.editorconfig @@ -51,10 +51,10 @@ dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggesti dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion -dotnet_style_qualification_for_event = false:suggestion -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_event = true:suggestion +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_property = true:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion # ReSharper properties @@ -112,9 +112,8 @@ dotnet_diagnostic.rs2008.severity = none # Ignore analyzer release tracking dotnet_diagnostic.sa0001.severity = none # Ignore disabled xml documentation dotnet_diagnostic.sa1000.severity = none # Ignore missing space after "new" dotnet_diagnostic.sa1009.severity = none # Ignore missing space after closing parenthesis -dotnet_diagnostic.sa1028.severity = none # Ignore trailing whitespace dotnet_diagnostic.sa1100.severity = none # Ignore base. prefix if there is no local override -dotnet_diagnostic.sa1101.severity = none # Ignore missing this prefix for local calls +dotnet_diagnostic.sa1101.severity = suggestion # Ignore missing this prefix for local calls dotnet_diagnostic.sa1122.severity = none # Don't force the use of string.Empty dotnet_diagnostic.sa1124.severity = none # Allow the use of regions dotnet_diagnostic.sa1127.severity = none # Generic constraints may share a line with other declarations diff --git a/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/DataElementGeneratorTest.cs b/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/DataElementGeneratorTest.cs new file mode 100644 index 000000000..19a50c519 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/DataElementGeneratorTest.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using NUnit.Framework; +using WoWsShipBuilder.Data.Generator.Attributes; +using WoWsShipBuilder.DataElements.DataElements; + +namespace WoWsShipBuilder.Data.Generator.Test.DataElementGeneratorTest; + +[TestFixture] +public partial class DataElementGeneratorTest +{ + private static CSharpSourceGeneratorTest CreateTest(string source, string expected) + { + const string baseClass = """ + namespace WoWsShipBuilder.DataElements.DataElements; + + public abstract record DataContainerBase + { + public global::System.Collections.Generic.List DataElements { get; } = new(); + + protected static bool ShouldAdd(object? value) + { + return value switch + { + string strValue => !string.IsNullOrEmpty(strValue), + decimal decValue => decValue != 0, + (decimal min, decimal max) => min > 0 || max > 0, + int intValue => intValue != 0, + _ => false, + }; + } + } + """; + return new() + { + TestState = + { + Sources = { baseClass, source }, + GeneratedSources = + { + (typeof(DataElementGenerator), "DataElementTypes.g.cs", AttributeHelper.DataElementTypesEnum), + (typeof(DataElementGenerator), "DataContainerAttribute.g.cs", AttributeHelper.DataContainerAttribute), + (typeof(DataElementGenerator), "DataElementTypeAttribute.g.cs", AttributeHelper.DataElementTypeAttribute), + (typeof(DataElementGenerator), "DataElementFilteringAttribute.g.cs", AttributeHelper.DataElementFilteringAttribute), + (typeof(DataElementGenerator), "TestRecord.g.cs", expected), + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net70, + AdditionalReferences = { MetadataReference.CreateFromFile(typeof(IDataElement).GetTypeInfo().Assembly.Location) }, + }, + }; + } +} diff --git a/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/GenerateCode.cs b/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/GenerateCode.cs new file mode 100644 index 000000000..0efe4caa6 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTest/GenerateCode.cs @@ -0,0 +1,137 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace WoWsShipBuilder.Data.Generator.Test.DataElementGeneratorTest; + +[SuppressMessage("Maintainability", "S2699", Justification = "false-positive since sonarlint does not recognize custom CreateTest method")] +public partial class DataElementGeneratorTest +{ + [Test] + public async Task GenerateCode_EmptyRecord_EmptyMethod() + { + var source = """ + using WoWsShipBuilder.DataElements.DataElementAttributes; + using WoWsShipBuilder.DataElements.DataElements; + + namespace Test; + + [DataContainer] + public partial record TestRecord : DataContainerBase + { + } + """; + + var expected = """ + // + #nullable enable + namespace Test + { + public partial record TestRecord + { + private void UpdateDataElements() + { + this.DataElements.Clear(); + } + } + } + + """; + + await CreateTest(source, expected).RunAsync(); + } + + [Test] + public async Task GenerateCode_SingleKeyValueUnit_Success() + { + var source = """ + using WoWsShipBuilder.DataElements.DataElementAttributes; + using WoWsShipBuilder.DataElements.DataElements; + + namespace Test; + + [DataContainer] + public partial record TestRecord : DataContainerBase + { + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots")] + public decimal ManeuverabilityMaxSpeed { get; set; } + } + """; + + var expected = """ + // + #nullable enable + namespace Test + { + public partial record TestRecord + { + private void UpdateDataElements() + { + this.DataElements.Clear(); + if (global::WoWsShipBuilder.DataElements.DataElements.DataContainerBase.ShouldAdd(this.ManeuverabilityMaxSpeed)) + { + this.DataElements.Add(new global::WoWsShipBuilder.DataElements.DataElements.KeyValueUnitDataElement("ShipStats_", this.ManeuverabilityMaxSpeed.ToString(), "Unit_Knots")); + } + } + } + } + + """; + + await CreateTest(source, expected).RunAsync(); + } + + [Test] + public async Task GenerateCode_OneGroupTwoElements_Success() + { + var source = """ + using WoWsShipBuilder.DataElements.DataElementAttributes; + using WoWsShipBuilder.DataElements.DataElements; + + namespace Test; + + [DataContainer] + public partial record TestRecord : DataContainerBase + { + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValue, GroupKey = "Loaders")] + public string BowLoaders { get; set; } = default!; + + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValue, GroupKey = "Loaders")] + public string SternLoaders { get; set; } = default!; + } + """; + + var expected = """ + // + #nullable enable + namespace Test + { + public partial record TestRecord + { + private void UpdateDataElements() + { + this.DataElements.Clear(); + var LoadersList = new global::System.Collections.Generic.List(); + if (global::WoWsShipBuilder.DataElements.DataElements.DataContainerBase.ShouldAdd(this.BowLoaders)) + { + LoadersList.Add(new global::WoWsShipBuilder.DataElements.DataElements.KeyValueDataElement("ShipStats_", this.BowLoaders, false, false)); + } + + if (global::WoWsShipBuilder.DataElements.DataElements.DataContainerBase.ShouldAdd(this.SternLoaders)) + { + LoadersList.Add(new global::WoWsShipBuilder.DataElements.DataElements.KeyValueDataElement("ShipStats_", this.SternLoaders, false, false)); + } + + if (LoadersList.Count > 0) + { + this.DataElements.Add(new global::WoWsShipBuilder.DataElements.DataElements.GroupedDataElement("ShipStats_Loaders", LoadersList)); + } + } + } + } + + """; + + await CreateTest(source, expected).RunAsync(); + } +} diff --git a/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorResultTest.cs b/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorResultTest.cs index 6a2aa998f..da316003d 100644 --- a/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorResultTest.cs +++ b/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorResultTest.cs @@ -1,43 +1,43 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using WoWsShipBuilder.Data.Generator.Test.TestStructures; -using WoWsShipBuilder.DataElements.DataElements; - -namespace WoWsShipBuilder.Data.Generator.Test; - -public class SourceGeneratorResultTest -{ - [Test] - public void SingleDataValue_DataElementsNotEmpty() - { - const string testString = "1234test"; - var testRecord = new TestDataUi1 - { - TestValue = testString, - }; - - testRecord.UpdateData(); - - testRecord.DataElements.Should().NotBeEmpty(); - } - - [Test] - public void GroupedValuesSet_DataElementHasGroup() - { - const string testString = "1234test"; - var testRecord = new TestDataUi1 - { - TestGroup1 = testString, - Test2Group1 = testString, - }; - - testRecord.UpdateData(); - - testRecord.DataElements.Should().NotBeEmpty(); - testRecord.DataElements.OfType().Should().HaveCount(1); - var groupedData = testRecord.DataElements.OfType().Single(); - groupedData.Key.Should().BeEquivalentTo("ShipStats_test1"); - groupedData.Children.Should().HaveCount(2); - } -} +// using System.Linq; +// using FluentAssertions; +// using NUnit.Framework; +// using WoWsShipBuilder.Data.Generator.Test.TestStructures; +// using WoWsShipBuilder.DataElements.DataElements; +// +// namespace WoWsShipBuilder.Data.Generator.Test; +// +// public class SourceGeneratorResultTest +// { +// [Test] +// public void SingleDataValue_DataElementsNotEmpty() +// { +// const string testString = "1234test"; +// var testRecord = new TestDataUi1 +// { +// TestValue = testString, +// }; +// +// testRecord.UpdateData(); +// +// testRecord.DataElements.Should().NotBeEmpty(); +// } +// +// [Test] +// public void GroupedValuesSet_DataElementHasGroup() +// { +// const string testString = "1234test"; +// var testRecord = new TestDataUi1 +// { +// TestGroup1 = testString, +// Test2Group1 = testString, +// }; +// +// testRecord.UpdateData(); +// +// testRecord.DataElements.Should().NotBeEmpty(); +// testRecord.DataElements.OfType().Should().HaveCount(1); +// var groupedData = testRecord.DataElements.OfType().Single(); +// groupedData.Key.Should().BeEquivalentTo("ShipStats_test1"); +// groupedData.Children.Should().HaveCount(2); +// } +// } diff --git a/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorTest.cs b/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorTest.cs index dc4e36000..7211737c6 100644 --- a/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorTest.cs +++ b/WoWsShipBuilder.Data.Generator.Test/SourceGeneratorTest.cs @@ -3,9 +3,12 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; using NUnit.Framework; using WoWsShipBuilder.DataElements.DataElementAttributes; using Binder = Microsoft.CSharp.RuntimeBinder.Binder; @@ -61,176 +64,185 @@ public bool TestVisibility(object value) public void ManeuverabilityDataContainer_NoErrors() { var code = """ -using WoWsShipBuilder.DataElements.DataElementAttributes; -using WoWsShipBuilder.DataElements.DataElements; -using WoWsShipBuilder.DataStructures; -using WoWsShipBuilder.DataStructures.Ship; -using WoWsShipBuilder.Infrastructure.Utility; + using WoWsShipBuilder.DataElements.DataElementAttributes; + using WoWsShipBuilder.DataElements.DataElements; + using WoWsShipBuilder.DataStructures; + using WoWsShipBuilder.DataStructures.Ship; + using WoWsShipBuilder.Infrastructure.Utility; -namespace WoWsShipBuilder.DataContainers; + namespace WoWsShipBuilder.DataContainers; -public partial record ManeuverabilityDataContainer : DataContainerBase -{ - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots")] - public decimal ManeuverabilityMaxSpeed { get; set; } + public partial record ManeuverabilityDataContainer : DataContainerBase + { + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots")] + public decimal ManeuverabilityMaxSpeed { get; set; } - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots", NameLocalizationKey = "MaxReverseSpeed")] - public decimal ManeuverabilityMaxReverseSpeed { get; set; } + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots", NameLocalizationKey = "MaxReverseSpeed")] + public decimal ManeuverabilityMaxReverseSpeed { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeed", UnitKey = "Knots")] - public decimal ManeuverabilitySubsMaxSpeedOnSurface { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeed", UnitKey = "Knots")] + public decimal ManeuverabilitySubsMaxSpeedOnSurface { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeed", UnitKey = "Knots")] - public decimal ManeuverabilitySubsMaxSpeedAtPeriscope { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeed", UnitKey = "Knots")] + public decimal ManeuverabilitySubsMaxSpeedAtPeriscope { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeed", UnitKey = "Knots")] - public decimal ManeuverabilitySubsMaxSpeedAtMaxDepth { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeed", UnitKey = "Knots")] + public decimal ManeuverabilitySubsMaxSpeedAtMaxDepth { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxReverseSpeed", UnitKey = "Knots", NameLocalizationKey = "ManeuverabilitySubsMaxSpeedOnSurface")] - public decimal ManeuverabilitySubsMaxReverseSpeedOnSurface { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxReverseSpeed", UnitKey = "Knots", NameLocalizationKey = "ManeuverabilitySubsMaxSpeedOnSurface")] + public decimal ManeuverabilitySubsMaxReverseSpeedOnSurface { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxReverseSpeed", UnitKey = "Knots", NameLocalizationKey = "ManeuverabilitySubsMaxSpeedAtPeriscope")] - public decimal ManeuverabilitySubsMaxReverseSpeedAtPeriscope { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxReverseSpeed", UnitKey = "Knots", NameLocalizationKey = "ManeuverabilitySubsMaxSpeedAtPeriscope")] + public decimal ManeuverabilitySubsMaxReverseSpeedAtPeriscope { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxReverseSpeed", UnitKey = "Knots", NameLocalizationKey = "ManeuverabilitySubsMaxSpeedAtMaxDepth")] - public decimal ManeuverabilitySubsMaxReverseSpeedAtMaxDepth { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxReverseSpeed", UnitKey = "Knots", NameLocalizationKey = "ManeuverabilitySubsMaxSpeedAtMaxDepth")] + public decimal ManeuverabilitySubsMaxReverseSpeedAtMaxDepth { get; set; } - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "MPS")] - public decimal ManeuverabilitySubsMaxDiveSpeed { get; set; } + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "MPS")] + public decimal ManeuverabilitySubsMaxDiveSpeed { get; set; } - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "S")] - public decimal ManeuverabilitySubsDivingPlaneShiftTime { get; set; } + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "S")] + public decimal ManeuverabilitySubsDivingPlaneShiftTime { get; set; } - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "S")] - public decimal ManeuverabilityRudderShiftTime { get; set; } + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "S")] + public decimal ManeuverabilityRudderShiftTime { get; set; } - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "M")] - public decimal ManeuverabilityTurningCircle { get; set; } + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "M")] + public decimal ManeuverabilityTurningCircle { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeedTime", UnitKey = "S")] - public decimal ForwardMaxSpeedTime { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeedTime", UnitKey = "S")] + public decimal ForwardMaxSpeedTime { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeedTime", UnitKey = "S")] - public decimal ReverseMaxSpeedTime { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "MaxSpeedTime", UnitKey = "S")] + public decimal ReverseMaxSpeedTime { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.Tooltip, GroupKey = "BlastProtection", TooltipKey = "BlastExplanation")] - [DataElementFiltering(false)] - public decimal RudderBlastProtection { get; set; } + [DataElementType(DataElementTypes.Grouped | DataElementTypes.Tooltip, GroupKey = "BlastProtection", TooltipKey = "BlastExplanation")] + [DataElementFiltering(false)] + public decimal RudderBlastProtection { get; set; } - [DataElementType(DataElementTypes.Grouped | DataElementTypes.Tooltip, GroupKey = "BlastProtection", TooltipKey = "BlastExplanation")] - [DataElementFiltering(false)] - public decimal EngineBlastProtection { get; set; } -} -"""; + [DataElementType(DataElementTypes.Grouped | DataElementTypes.Tooltip, GroupKey = "BlastProtection", TooltipKey = "BlastExplanation")] + [DataElementFiltering(false)] + public decimal EngineBlastProtection { get; set; } + } + """; var expected = """ -using System; -using System.Collections.Generic; -using WoWsShipBuilder.DataElements.DataElements; - -namespace WoWsShipBuilder.DataContainers; - -#nullable enable -public partial record ManeuverabilityDataContainer -{ - private void UpdateDataElements() - { - DataElements.Clear(); - if (DataContainerBase.ShouldAdd(ManeuverabilityMaxSpeed)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilityMaxSpeed", ManeuverabilityMaxSpeed.ToString(), "Unit_Knots")); - - if (DataContainerBase.ShouldAdd(ManeuverabilityMaxReverseSpeed)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_MaxReverseSpeed", ManeuverabilityMaxReverseSpeed.ToString(), "Unit_Knots")); - - var MaxSpeedList = new List(); - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxSpeedOnSurface)) - MaxSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedOnSurface", ManeuverabilitySubsMaxSpeedOnSurface.ToString(), "Unit_Knots")); - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxSpeedAtPeriscope)) - MaxSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtPeriscope", ManeuverabilitySubsMaxSpeedAtPeriscope.ToString(), "Unit_Knots")); - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxSpeedAtMaxDepth)) - MaxSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtMaxDepth", ManeuverabilitySubsMaxSpeedAtMaxDepth.ToString(), "Unit_Knots")); - if (MaxSpeedList.Count > 0) - DataElements.Add(new GroupedDataElement("ShipStats_MaxSpeed", MaxSpeedList)); - - var MaxReverseSpeedList = new List(); - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxReverseSpeedOnSurface)) - MaxReverseSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedOnSurface", ManeuverabilitySubsMaxReverseSpeedOnSurface.ToString(), "Unit_Knots")); - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxReverseSpeedAtPeriscope)) - MaxReverseSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtPeriscope", ManeuverabilitySubsMaxReverseSpeedAtPeriscope.ToString(), "Unit_Knots")); - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxReverseSpeedAtMaxDepth)) - MaxReverseSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtMaxDepth", ManeuverabilitySubsMaxReverseSpeedAtMaxDepth.ToString(), "Unit_Knots")); - if (MaxReverseSpeedList.Count > 0) - DataElements.Add(new GroupedDataElement("ShipStats_MaxReverseSpeed", MaxReverseSpeedList)); - - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxDiveSpeed)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxDiveSpeed", ManeuverabilitySubsMaxDiveSpeed.ToString(), "Unit_MPS")); - - if (DataContainerBase.ShouldAdd(ManeuverabilitySubsDivingPlaneShiftTime)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsDivingPlaneShiftTime", ManeuverabilitySubsDivingPlaneShiftTime.ToString(), "Unit_S")); - - if (DataContainerBase.ShouldAdd(ManeuverabilityRudderShiftTime)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilityRudderShiftTime", ManeuverabilityRudderShiftTime.ToString(), "Unit_S")); - - if (DataContainerBase.ShouldAdd(ManeuverabilityTurningCircle)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilityTurningCircle", ManeuverabilityTurningCircle.ToString(), "Unit_M")); - - var MaxSpeedTimeList = new List(); - if (DataContainerBase.ShouldAdd(ForwardMaxSpeedTime)) - MaxSpeedTimeList.Add(new KeyValueUnitDataElement("ShipStats_ForwardMaxSpeedTime", ForwardMaxSpeedTime.ToString(), "Unit_S")); - if (DataContainerBase.ShouldAdd(ReverseMaxSpeedTime)) - MaxSpeedTimeList.Add(new KeyValueUnitDataElement("ShipStats_ReverseMaxSpeedTime", ReverseMaxSpeedTime.ToString(), "Unit_S")); - if (MaxSpeedTimeList.Count > 0) - DataElements.Add(new GroupedDataElement("ShipStats_MaxSpeedTime", MaxSpeedTimeList)); - - var BlastProtectionList = new List(); - - BlastProtectionList.Add(new TooltipDataElement("ShipStats_RudderBlastProtection", RudderBlastProtection.ToString(), "ShipStats_BlastExplanation", "")); - - BlastProtectionList.Add(new TooltipDataElement("ShipStats_EngineBlastProtection", EngineBlastProtection.ToString(), "ShipStats_BlastExplanation", "")); - if (BlastProtectionList.Count > 0) - DataElements.Add(new GroupedDataElement("ShipStats_BlastProtection", BlastProtectionList)); - } -} -#nullable restore -"""; + using System; + using System.Collections.Generic; + using WoWsShipBuilder.DataElements.DataElements; + + namespace WoWsShipBuilder.DataContainers; + + #nullable enable + public partial record ManeuverabilityDataContainer + { + private void UpdateDataElements() + { + DataElements.Clear(); + if (DataContainerBase.ShouldAdd(ManeuverabilityMaxSpeed)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilityMaxSpeed", ManeuverabilityMaxSpeed.ToString(), "Unit_Knots")); + + if (DataContainerBase.ShouldAdd(ManeuverabilityMaxReverseSpeed)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_MaxReverseSpeed", ManeuverabilityMaxReverseSpeed.ToString(), "Unit_Knots")); + + var MaxSpeedList = new List(); + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxSpeedOnSurface)) + MaxSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedOnSurface", ManeuverabilitySubsMaxSpeedOnSurface.ToString(), "Unit_Knots")); + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxSpeedAtPeriscope)) + MaxSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtPeriscope", ManeuverabilitySubsMaxSpeedAtPeriscope.ToString(), "Unit_Knots")); + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxSpeedAtMaxDepth)) + MaxSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtMaxDepth", ManeuverabilitySubsMaxSpeedAtMaxDepth.ToString(), "Unit_Knots")); + if (MaxSpeedList.Count > 0) + DataElements.Add(new GroupedDataElement("ShipStats_MaxSpeed", MaxSpeedList)); + + var MaxReverseSpeedList = new List(); + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxReverseSpeedOnSurface)) + MaxReverseSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedOnSurface", ManeuverabilitySubsMaxReverseSpeedOnSurface.ToString(), "Unit_Knots")); + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxReverseSpeedAtPeriscope)) + MaxReverseSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtPeriscope", ManeuverabilitySubsMaxReverseSpeedAtPeriscope.ToString(), "Unit_Knots")); + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxReverseSpeedAtMaxDepth)) + MaxReverseSpeedList.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxSpeedAtMaxDepth", ManeuverabilitySubsMaxReverseSpeedAtMaxDepth.ToString(), "Unit_Knots")); + if (MaxReverseSpeedList.Count > 0) + DataElements.Add(new GroupedDataElement("ShipStats_MaxReverseSpeed", MaxReverseSpeedList)); + + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsMaxDiveSpeed)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsMaxDiveSpeed", ManeuverabilitySubsMaxDiveSpeed.ToString(), "Unit_MPS")); + + if (DataContainerBase.ShouldAdd(ManeuverabilitySubsDivingPlaneShiftTime)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilitySubsDivingPlaneShiftTime", ManeuverabilitySubsDivingPlaneShiftTime.ToString(), "Unit_S")); + + if (DataContainerBase.ShouldAdd(ManeuverabilityRudderShiftTime)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilityRudderShiftTime", ManeuverabilityRudderShiftTime.ToString(), "Unit_S")); + + if (DataContainerBase.ShouldAdd(ManeuverabilityTurningCircle)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_ManeuverabilityTurningCircle", ManeuverabilityTurningCircle.ToString(), "Unit_M")); + + var MaxSpeedTimeList = new List(); + if (DataContainerBase.ShouldAdd(ForwardMaxSpeedTime)) + MaxSpeedTimeList.Add(new KeyValueUnitDataElement("ShipStats_ForwardMaxSpeedTime", ForwardMaxSpeedTime.ToString(), "Unit_S")); + if (DataContainerBase.ShouldAdd(ReverseMaxSpeedTime)) + MaxSpeedTimeList.Add(new KeyValueUnitDataElement("ShipStats_ReverseMaxSpeedTime", ReverseMaxSpeedTime.ToString(), "Unit_S")); + if (MaxSpeedTimeList.Count > 0) + DataElements.Add(new GroupedDataElement("ShipStats_MaxSpeedTime", MaxSpeedTimeList)); + + var BlastProtectionList = new List(); + + BlastProtectionList.Add(new TooltipDataElement("ShipStats_RudderBlastProtection", RudderBlastProtection.ToString(), "ShipStats_BlastExplanation", "")); + + BlastProtectionList.Add(new TooltipDataElement("ShipStats_EngineBlastProtection", EngineBlastProtection.ToString(), "ShipStats_BlastExplanation", "")); + if (BlastProtectionList.Count > 0) + DataElements.Add(new GroupedDataElement("ShipStats_BlastProtection", BlastProtectionList)); + } + } + #nullable restore + """; _ = VerifyGenerator(code, expected); } [Test] - public void SingleKeyValueUnitElement_NoErrors() + public async Task SingleKeyValueUnitElement_NoErrors() { var code = """ -using WoWsShipBuilder.DataElements.DataElementAttributes; -using WoWsShipBuilder.DataElements.DataElements; + using WoWsShipBuilder.DataElements.DataElementAttributes; + using WoWsShipBuilder.DataElements.DataElements; -namespace WoWsShipBuilder.DataContainers; + namespace WoWsShipBuilder.DataContainers; -public partial record TestContainer : DataContainerBase -{ - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots")] - public decimal TestProperty { get; set; } -} -"""; + public partial record TestContainer : DataContainerBase + { + [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "Knots")] + public decimal TestProperty { get; set; } + } + """; var expected = """ -using System; -using System.Collections.Generic; -using WoWsShipBuilder.DataElements.DataElements; - -namespace WoWsShipBuilder.DataContainers; - -#nullable enable -public partial record TestContainer -{ - private void UpdateDataElements() - { - DataElements.Clear(); - if (DataContainerBase.ShouldAdd(TestProperty)) - DataElements.Add(new KeyValueUnitDataElement("ShipStats_TestProperty", TestProperty.ToString(), "Unit_Knots")); - } -} -#nullable restore -"""; - + using System; + using System.Collections.Generic; + using WoWsShipBuilder.DataElements.DataElements; + + namespace WoWsShipBuilder.DataContainers; + + #nullable enable + public partial record TestContainer + { + private void UpdateDataElements() + { + DataElements.Clear(); + if (DataContainerBase.ShouldAdd(TestProperty)) + DataElements.Add(new KeyValueUnitDataElement("ShipStats_TestProperty", TestProperty.ToString(), "Unit_Knots")); + } + } + #nullable restore + """; + + await new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { code }, + GeneratedSources = { (typeof(DataElementSourceGenerator), "TestContainer.g.cs", expected) }, + AdditionalReferences = { MetadataReference.CreateFromFile(typeof(DataElementTypeAttribute).GetTypeInfo().Assembly.Location) }, + }, + }.RunAsync(); _ = VerifyGenerator(code, expected); } @@ -238,42 +250,42 @@ private void UpdateDataElements() public void GroupedKeyValueUnitElement_NoErrors() { var code = """ -using WoWsShipBuilder.DataElements.DataElementAttributes; -using WoWsShipBuilder.DataElements.DataElements; + using WoWsShipBuilder.DataElements.DataElementAttributes; + using WoWsShipBuilder.DataElements.DataElements; -namespace WoWsShipBuilder.DataContainers; + namespace WoWsShipBuilder.DataContainers; -public partial record TestContainer : DataContainerBase -{ - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "TestGroup", UnitKey = "Knots")] - public decimal TestProperty { get; set; } -} -"""; + public partial record TestContainer : DataContainerBase + { + [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValueUnit, GroupKey = "TestGroup", UnitKey = "Knots")] + public decimal TestProperty { get; set; } + } + """; var expected = """ -using System; -using System.Collections.Generic; -using WoWsShipBuilder.DataElements.DataElements; + using System; + using System.Collections.Generic; + using WoWsShipBuilder.DataElements.DataElements; -namespace WoWsShipBuilder.DataContainers; + namespace WoWsShipBuilder.DataContainers; -#nullable enable -public partial record TestContainer -{ - private void UpdateDataElements() - { - DataElements.Clear(); + #nullable enable + public partial record TestContainer + { + private void UpdateDataElements() + { + DataElements.Clear(); - var TestGroupList = new List(); - if (DataContainerBase.ShouldAdd(TestProperty)) - TestGroupList.Add(new KeyValueUnitDataElement("ShipStats_TestProperty", TestProperty.ToString(), "Unit_Knots")); - if (TestGroupList.Count > 0) - DataElements.Add(new GroupedDataElement("ShipStats_TestGroup", TestGroupList)); + var TestGroupList = new List(); + if (DataContainerBase.ShouldAdd(TestProperty)) + TestGroupList.Add(new KeyValueUnitDataElement("ShipStats_TestProperty", TestProperty.ToString(), "Unit_Knots")); + if (TestGroupList.Count > 0) + DataElements.Add(new GroupedDataElement("ShipStats_TestGroup", TestGroupList)); - } -} -#nullable restore -"""; + } + } + #nullable restore + """; _ = VerifyGenerator(code, expected); } @@ -281,12 +293,12 @@ private void UpdateDataElements() private static GeneratorDriverRunResult VerifyGenerator(string source, string generated = "") { var baseInput = """ -using WoWsShipBuilder.DataElements.DataElements; + using WoWsShipBuilder.DataElements.DataElements; -namespace WoWsShipBuilder.Data.Generator.Test.TestStructures; + namespace WoWsShipBuilder.Data.Generator.Test.TestStructures; -public record ProjectileDataContainer : DataContainerBase; -"""; + public record ProjectileDataContainer : DataContainerBase; + """; var compilation = CreateCompilation(baseInput, source); GeneratorDriver driver = CSharpGeneratorDriver.Create(new DataElementSourceGenerator()); @@ -323,5 +335,4 @@ private static Compilation CreateCompilation(params string[] source) }, new(OutputKind.ConsoleApplication)); } - } diff --git a/WoWsShipBuilder.Data.Generator.Test/TestStructures/TestDataUi1.cs b/WoWsShipBuilder.Data.Generator.Test/TestStructures/TestDataUi1.cs index 636b17a96..852062746 100644 --- a/WoWsShipBuilder.Data.Generator.Test/TestStructures/TestDataUi1.cs +++ b/WoWsShipBuilder.Data.Generator.Test/TestStructures/TestDataUi1.cs @@ -1,39 +1,39 @@ -using WoWsShipBuilder.DataElements.DataElementAttributes; - -namespace WoWsShipBuilder.Data.Generator.Test.TestStructures; - -public partial record TestDataUi1 : ProjectileDataContainer -{ - [DataElementType(DataElementTypes.Value)] - public string TestValue { get; init; } = default!; - - [DataElementType(DataElementTypes.KeyValue)] - [DataElementFiltering(false)] - public decimal TestKeyValue { get; init; } - - [DataElementType(DataElementTypes.KeyValue)] - [DataElementFiltering(true, "TestVisibility")] - public decimal TestVisibilityCustom { get; init; } - - [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "mm")] - public string TestKeyUnitValue { get; init; } = default!; - - [DataElementType(DataElementTypes.Tooltip, TooltipKey = "testTooltip")] - public decimal TestTooltipValue { get; init; } - - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValue, GroupKey = "test1")] - public string TestGroup1 { get; init; } = default!; - - [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValue, GroupKey = "test1")] - public string Test2Group1 { get; init; } = default!; - - public void UpdateData() - { - UpdateDataElements(); - } - - public bool TestVisibility(object value) - { - return true; - } -} +// using WoWsShipBuilder.DataElements.DataElementAttributes; +// +// namespace WoWsShipBuilder.Data.Generator.Test.TestStructures; +// +// public partial record TestDataUi1 : ProjectileDataContainer +// { +// [DataElementType(DataElementTypes.Value)] +// public string TestValue { get; init; } = default!; +// +// [DataElementType(DataElementTypes.KeyValue)] +// [DataElementFiltering(false)] +// public decimal TestKeyValue { get; init; } +// +// [DataElementType(DataElementTypes.KeyValue)] +// [DataElementFiltering(true, "TestVisibility")] +// public decimal TestVisibilityCustom { get; init; } +// +// [DataElementType(DataElementTypes.KeyValueUnit, UnitKey = "mm")] +// public string TestKeyUnitValue { get; init; } = default!; +// +// [DataElementType(DataElementTypes.Tooltip, TooltipKey = "testTooltip")] +// public decimal TestTooltipValue { get; init; } +// +// [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValue, GroupKey = "test1")] +// public string TestGroup1 { get; init; } = default!; +// +// [DataElementType(DataElementTypes.Grouped | DataElementTypes.KeyValue, GroupKey = "test1")] +// public string Test2Group1 { get; init; } = default!; +// +// public void UpdateData() +// { +// UpdateDataElements(); +// } +// +// public bool TestVisibility(object value) +// { +// return true; +// } +// } diff --git a/WoWsShipBuilder.Data.Generator.Test/WoWsShipBuilder.Data.Generator.Test.csproj b/WoWsShipBuilder.Data.Generator.Test/WoWsShipBuilder.Data.Generator.Test.csproj index 3c170182e..c87a9aa9d 100644 --- a/WoWsShipBuilder.Data.Generator.Test/WoWsShipBuilder.Data.Generator.Test.csproj +++ b/WoWsShipBuilder.Data.Generator.Test/WoWsShipBuilder.Data.Generator.Test.csproj @@ -11,7 +11,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/WoWsShipBuilder.Data.Generator/Attributes/AttributeGenerator.cs b/WoWsShipBuilder.Data.Generator/Attributes/AttributeGenerator.cs deleted file mode 100644 index a608d75a3..000000000 --- a/WoWsShipBuilder.Data.Generator/Attributes/AttributeGenerator.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace WoWsShipBuilder.Data.Generator.Attributes; - -public static class AttributeGenerator -{ - - public const string DataElementTypesEnum = @" -namespace WoWsShipBuilder.DataElements.DataElementAttributes; - -[Flags] -public enum DataElementTypes -{ - KeyValue = 1, - KeyValueUnit = 2, - Value = 4, - Grouped = 8, - Tooltip = 16, -} -"; - - public const string DataElementTypeAttribute = @" -using System; - -namespace WoWsShipBuilder.DataElements.DataElementAttributes; - -[AttributeUsage(AttributeTargets.Property)] -public class DataElementTypeAttribute : Attribute -{ - public DataElementTypeAttribute(DataElementTypes type) - { - Type = type; - } - - public DataElementTypes Type { get; } - - public string? UnitKey { get; set; } - - public string? TooltipKey { get; set; } - - public string? GroupKey { get; set; } - - public string[] LocalizationArguments { get; set; } = Array.Empty(); -} -"; - - public const string DataElementVisibilityAttribute = @"using System; - -namespace WoWsShipBuilder.DataElements.DataElementAttributes; - -[AttributeUsage(AttributeTargets.Property)] -public class DataElementVisibilityAttribute : Attribute -{ - public DataElementVisibilityAttribute(bool enableFilterVisibility, string filterMethodName = "") - { - EnableFilterVisibility = enableFilterVisibility; - FilterMethodName = filterMethodName; - } - - public bool EnableFilterVisibility { get; } - - public string FilterMethodName { get; } -}"; -} diff --git a/WoWsShipBuilder.Data.Generator/Attributes/AttributeHelper.cs b/WoWsShipBuilder.Data.Generator/Attributes/AttributeHelper.cs new file mode 100644 index 000000000..93bd1d447 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator/Attributes/AttributeHelper.cs @@ -0,0 +1,146 @@ +using Microsoft.CodeAnalysis; + +namespace WoWsShipBuilder.Data.Generator.Attributes; + +public static class AttributeHelper +{ + public const string AttributeNamespace = "WoWsShipBuilder.DataElements.DataElementAttributes"; + + public const string DataElementTypesEnumName = "DataElementTypes"; + + public const string DataElementTypesEnum = $$""" + // + #nullable enable + namespace {{AttributeNamespace}}; + + [global::System.Flags] + internal enum {{DataElementTypesEnumName}} + { + KeyValue = 1, + KeyValueUnit = 2, + Value = 4, + Grouped = 8, + Tooltip = 16, + FormattedText = 32, + } + """; + + public const string DataContainerAttributeName = "DataContainerAttribute"; + + public const string DataContainerAttribute = $$""" + // + #nullable enable + namespace {{AttributeNamespace}}; + + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false)] + internal class {{DataContainerAttributeName}} : global::System.Attribute + { + } + """; + + public const string DataElementTypeAttributeName = "DataElementTypeAttribute"; + + public const string DataElementTypeAttribute = $$""" + // + #nullable enable + namespace {{AttributeNamespace}}; + + [global::System.AttributeUsage(global::System.AttributeTargets.Property)] + internal class {{DataElementTypeAttributeName}} : global::System.Attribute + { + public {{DataElementTypeAttributeName}}(global::{{AttributeNamespace}}.DataElementTypes type) + { + this.Type = type; + } + + /// + /// Gets the type of the DataElement for the property marked by this attribute. /> + /// + public global::{{AttributeNamespace}}.DataElementTypes Type { get; } + + /// + /// Gets or sets the unit localization key for the property marked by this attribute.
+ /// Only valid for and . + ///
+ public string? UnitKey { get; set; } + + /// + /// Gets or sets the property name localization key for the property marked by this attribute.
+ /// Only valid for , , and . + ///
+ public string? NameLocalizationKey { get; set; } + + /// + /// Gets or sets the tooltip localization key for the property marked by this attribute.
+ /// Only valid for . + ///
+ public string? TooltipKey { get; set; } + + /// + /// Gets or sets the group localization key and identifier for the property marked by this attribute.
+ /// Only valid for . + ///
+ public string? GroupKey { get; set; } + + /// + /// Gets or set the name of the property containing the list of values that will replace the placeholder. Requires the value of the property marked by this attribute to follow the specifications.
+ /// Only valid for . + ///
+ public string? ValuesPropertyName { get; set; } + + /// + /// Gets or sets if the value of the property marked by this attribute is a localization key.
+ /// Only valid for , and + ///
+ public bool IsValueLocalizationKey { get; set; } + + /// + /// Gets or sets if the values indicated by are localization keys.
+ /// Only valid for + ///
+ public bool ArePropertyNameValuesKeys { get; set; } + + /// + /// Gets or sets if the value of the property marked by this attribute is an app localization key.
+ /// Only valid for , and + ///
+ public bool IsValueAppLocalization { get; set; } + + /// + /// Gets or sets if the values indicated by are app localization keys.
+ /// Only valid for + ///
+ public bool IsPropertyNameValuesAppLocalization { get; set; } + } + """; + + public const string DataElementFilteringAttributeName = "DataElementFilteringAttribute"; + + public const string DataElementFilteringAttribute = $$""" + // + #nullable enable + namespace {{AttributeNamespace}}; + + [global::System.AttributeUsage(global::System.AttributeTargets.Property)] + internal class {{DataElementFilteringAttributeName}} : global::System.Attribute + { + public {{DataElementFilteringAttributeName}}(bool enableFilterVisibility, string filterMethodName = "") + { + this.EnableFilterVisibility = enableFilterVisibility; + this.FilterMethodName = filterMethodName; + } + + public bool EnableFilterVisibility { get; } + + public string FilterMethodName { get; } + } + """; + + public static void GenerateAttributes(IncrementalGeneratorPostInitializationContext context) + { + context.AddSource("DataElementTypes.g.cs", DataElementTypesEnum); + context.AddSource("DataContainerAttribute.g.cs", DataContainerAttribute); + context.AddSource("DataElementTypeAttribute.g.cs", DataElementTypeAttribute); + context.AddSource("DataElementFilteringAttribute.g.cs", DataElementFilteringAttribute); + } +} diff --git a/WoWsShipBuilder.Data.Generator/DataElementGenerator.cs b/WoWsShipBuilder.Data.Generator/DataElementGenerator.cs new file mode 100644 index 000000000..6a122aac1 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator/DataElementGenerator.cs @@ -0,0 +1,221 @@ +using System; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using WoWsShipBuilder.Data.Generator.Attributes; +using WoWsShipBuilder.Data.Generator.Internals; + +namespace WoWsShipBuilder.Data.Generator; + +[Generator(LanguageNames.CSharp)] +public class DataElementGenerator : IIncrementalGenerator +{ + private const string DataContainerBaseName = "DataContainerBase"; + + private const string DataContainerAttributeFullName = $"{AttributeHelper.AttributeNamespace}.{AttributeHelper.DataContainerAttributeName}"; + private const string DataElementAttributeFullName = $"{AttributeHelper.AttributeNamespace}.{AttributeHelper.DataElementTypeAttributeName}"; + private const string DataElementNamespace = "global::WoWsShipBuilder.DataElements.DataElements"; + private const string DataElementsCollectionName = "this.DataElements"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(AttributeHelper.GenerateAttributes); + var model = context.SyntaxProvider + .ForAttributeWithMetadataName(DataContainerAttributeFullName, CouldBeDataContainer, GetModel) + .Select(ExtractPropertyGroups); + + context.RegisterSourceOutput(model, GenerateSourceCode); + } + + private static bool CouldBeDataContainer(SyntaxNode syntaxNode, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return syntaxNode is RecordDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); + } + + private static ContainerData GetModel(GeneratorAttributeSyntaxContext context, CancellationToken token) + { + var recordSymbol = (INamedTypeSymbol)context.TargetSymbol; + token.ThrowIfCancellationRequested(); + var name = recordSymbol.Name; + var dataNamespace = recordSymbol.ContainingNamespace.ToDisplayString(); + var properties = recordSymbol.GetMembers() + .OfType() + .Where(prop => prop.HasAttributeWithFullName(DataElementAttributeFullName)) + .Select(RefineProperty) + .ToEquatableArray(); + + token.ThrowIfCancellationRequested(); + return new(name, dataNamespace, properties, null); + } + + private static ContainerData ExtractPropertyGroups(ContainerData rawContainerData, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + var propertyGroups = rawContainerData.Properties + .Where(prop => (prop.DataElementType & DataElementTypes.Grouped) == DataElementTypes.Grouped) + .GroupBy(prop => prop.DisplayOptions.GroupKey!) + .Select(grouping => new PropertyGroup(grouping.Key, grouping.Select(prop => prop with { DataElementType = prop.DataElementType & ~DataElementTypes.Grouped }).ToEquatableArray())) + .ToEquatableArray(); + token.ThrowIfCancellationRequested(); + var singleProperties = rawContainerData.Properties.Where(prop => prop.DisplayOptions.GroupKey is null).ToEquatableArray(); + + token.ThrowIfCancellationRequested(); + return rawContainerData with { Properties = singleProperties, GroupedProperties = propertyGroups }; + } + + private sealed record ContainerData(string ContainerName, string Namespace, EquatableArray Properties, EquatableArray? GroupedProperties); + + private sealed record PropertyGroup(string GroupName, EquatableArray Properties); + + private sealed record PropertyData(string Name, bool IsString, bool IsNullable, DataElementTypes DataElementType, PropertyDisplayOptions DisplayOptions, PropertyFilter PropertyFilter, FormattedTextData FormattedTextData); + + private sealed record PropertyDisplayOptions(string? UnitKey, string? LocalizationKey, string? TooltipKey, string? GroupKey, bool TreatValueAsLocalizationKey, bool TreatValueAsAppLocalizationKey); + + private sealed record FormattedTextData(string? ArgumentsCollectionName, bool TreatArgumentsAsLocalizationKeys, bool TreatArgumentsAsAppLocalizationKeys); + + private sealed record PropertyFilter(bool IsEnabled, string FilterMethodName); + + private static PropertyData RefineProperty(IPropertySymbol propertySymbol) + { + var dataElementAttribute = propertySymbol.FindAttribute(DataElementAttributeFullName); + var dataElementType = (DataElementTypes)dataElementAttribute.ConstructorArguments[0].Value!; + + return new(propertySymbol.Name, propertySymbol.Type.SpecialType == SpecialType.System_String, propertySymbol.NullableAnnotation == NullableAnnotation.Annotated, dataElementType, ExtractDisplayOptions(dataElementAttribute), ExtractFilterOptions(propertySymbol), ExtractFormattedTextOptions(dataElementAttribute)); + } + + private static FormattedTextData ExtractFormattedTextOptions(AttributeData dataElementAttribute) + { + return new( + dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ValuesPropertyName").Value.Value?.ToString(), + (bool?)dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ArePropertyNameValuesKeys").Value.Value ?? false, + (bool?)dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsPropertyNameValuesAppLocalization").Value.Value ?? false); + } + + private static PropertyFilter ExtractFilterOptions(IPropertySymbol propertySymbol) + { + var filterAttribute = propertySymbol.FindAttributeOrDefault("WoWsShipBuilder.DataElements.DataElementAttributes.DataElementFilteringAttribute"); + if (filterAttribute is null) + { + return new(true, $"{DataElementNamespace}.DataContainerBase.ShouldAdd"); + } + + var isEnabled = (bool)filterAttribute.ConstructorArguments[0].Value!; + var filterMethodName = filterAttribute.ConstructorArguments[1].Value?.ToString() ?? $"{DataElementNamespace}.DataContainerBase.ShouldAdd"; + return new(isEnabled, filterMethodName); + } + + private static PropertyDisplayOptions ExtractDisplayOptions(AttributeData dataElementAttribute) + { + return new( + dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "UnitKey").Value.Value?.ToString(), + dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "NameLocalizationKey").Value.Value?.ToString(), + dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "TooltipKey").Value.Value?.ToString(), + dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "GroupKey").Value.Value?.ToString(), + (bool?)dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueLocalizationKey").Value.Value ?? false, + (bool?)dataElementAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false); + } + + private static void GenerateSourceCode(SourceProductionContext context, ContainerData containerData) + { + var builder = new SourceBuilder(); + builder.Line("// "); + builder.Line("#nullable enable"); + using (builder.Namespace(containerData.Namespace)) + { + using (builder.Record(containerData.ContainerName)) + { + using (builder.Block("private void UpdateDataElements()")) + { + GenerateUpdateMethodContent(builder, containerData); + } + } + } + + context.AddSource($"{containerData.ContainerName}.g.cs", builder.ToString()); + } + + private static void GenerateUpdateMethodContent(SourceBuilder builder, ContainerData containerData) + { + builder.Line("this.DataElements.Clear();"); + foreach (var propertyGroup in containerData.GroupedProperties ?? EquatableArray.Empty) + { + var listName = $"{propertyGroup.GroupName}List"; + builder.Line($"var {listName} = new global::System.Collections.Generic.List<{DataElementNamespace}.IDataElement>();"); + foreach (var property in propertyGroup.Properties) + { + GenerateGroupedPropertyCode(builder, property, listName); + } + + using (builder.Block($"if ({listName}.Count > 0)")) + { + builder.Line($"{DataElementsCollectionName}.Add(new {DataElementNamespace}.GroupedDataElement(\"ShipStats_{propertyGroup.GroupName}\", {listName}));"); + } + } + + foreach (var property in containerData.Properties) + { + GeneratePropertyCode(builder, property); + } + } + + private static void GenerateGroupedPropertyCode(SourceBuilder builder, PropertyData property, string listName) + { + if (property.PropertyFilter.IsEnabled) + { + var filterMethodName = property.PropertyFilter.FilterMethodName; + using (builder.Block($"if ({filterMethodName}(this.{property.Name}))")) + { + builder.Line($"{listName}.Add({GenerateDataElementCreationCode(property)});"); + } + } + else + { + builder.Line($"{listName}.Add({GenerateDataElementCreationCode(property)});"); + } + } + + private static void GeneratePropertyCode(SourceBuilder builder, PropertyData property) + { + if (property.PropertyFilter.IsEnabled) + { + var filterMethodName = property.PropertyFilter.FilterMethodName; + using (builder.Block($"if ({filterMethodName}(this.{property.Name}))")) + { + builder.Line($"{DataElementsCollectionName}.Add({GenerateDataElementCreationCode(property)});"); + } + } + else + { + builder.Line($"{DataElementsCollectionName}.Add({GenerateDataElementCreationCode(property)});"); + } + } + + private static string GenerateDataElementCreationCode(PropertyData propertyData) + { + return propertyData.DataElementType switch + { + DataElementTypes.Value => $"new {DataElementNamespace}.ValueDataElement({GeneratePropertyAccess(propertyData)}, {propertyData.DisplayOptions.TreatValueAsLocalizationKey.ToLowerString()}, {propertyData.DisplayOptions.TreatValueAsAppLocalizationKey.ToLowerString()})", + DataElementTypes.KeyValue => $"""new {DataElementNamespace}.KeyValueDataElement("ShipStats_{propertyData.DisplayOptions.LocalizationKey}", {GeneratePropertyAccess(propertyData)}, {propertyData.DisplayOptions.TreatValueAsLocalizationKey.ToLowerString()}, {propertyData.DisplayOptions.TreatValueAsAppLocalizationKey.ToLowerString()})""", + DataElementTypes.KeyValueUnit => $"""new {DataElementNamespace}.KeyValueUnitDataElement("ShipStats_{propertyData.DisplayOptions.LocalizationKey}", {GeneratePropertyAccess(propertyData)}, "Unit_{propertyData.DisplayOptions.UnitKey}")""", + DataElementTypes.FormattedText => $"new {DataElementNamespace}.KeyValueUnitDataElement({GeneratePropertyAccess(propertyData)}, this.{propertyData.FormattedTextData.ArgumentsCollectionName}, {propertyData.DisplayOptions.TreatValueAsLocalizationKey.ToLowerString()}, {propertyData.DisplayOptions.TreatValueAsAppLocalizationKey.ToLowerString()}, {propertyData.FormattedTextData.TreatArgumentsAsLocalizationKeys.ToLowerString()}, {propertyData.FormattedTextData.TreatArgumentsAsAppLocalizationKeys.ToLowerString()})", + DataElementTypes.Tooltip => $"""new {DataElementNamespace}.TooltipDataElement("ShipStats_{propertyData.DisplayOptions.LocalizationKey}", {GeneratePropertyAccess(propertyData)}, "ShipStats_{propertyData.DisplayOptions.TooltipKey}", "{ComputeNullableUnitValue(propertyData.DisplayOptions)}")""", + _ => throw new InvalidOperationException($"Invalid DataElementType: {propertyData.DataElementType}") + }; + } + + private static string ComputeNullableUnitValue(PropertyDisplayOptions displayOptions) => displayOptions.UnitKey is not null ? $"Unit_{displayOptions.UnitKey}" : string.Empty; + + private static string GeneratePropertyAccess(PropertyData propertyData) + { + return propertyData switch + { + { IsString: true, IsNullable: false } => $"this.{propertyData.Name}", + { IsString: true, IsNullable: true } => $"this.{propertyData.Name} ?? \"null\"", + { IsString: false, IsNullable: false } => $"this.{propertyData.Name}.ToString()", + { IsString: false, IsNullable: true } => $"this.{propertyData.Name}?.ToString() ?? \"null\"", + }; + } +} diff --git a/WoWsShipBuilder.Data.Generator/DataElementSourceGenerator.cs b/WoWsShipBuilder.Data.Generator/DataElementSourceGenerator.cs index fbbed4346..8e3776674 100644 --- a/WoWsShipBuilder.Data.Generator/DataElementSourceGenerator.cs +++ b/WoWsShipBuilder.Data.Generator/DataElementSourceGenerator.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using WoWsShipBuilder.Data.Generator.Attributes; using WoWsShipBuilder.Data.Generator.Internals; namespace WoWsShipBuilder.Data.Generator; @@ -94,7 +93,7 @@ private static (string className, string classNamespace, List p cancellationToken.ThrowIfCancellationRequested(); var name = symbol!.Name; var dataNamespace = symbol.ContainingNamespace.ToDisplayString(); - var properties = symbol.GetMembers().OfType().Where(prop => prop.GetAttributes().Any(attr => attr.AttributeClass!.Name == nameof(AttributeGenerator.DataElementTypeAttribute))).ToList(); + var properties = symbol.GetMembers().OfType().Where(prop => prop.GetAttributes().Any(attr => attr.AttributeClass!.Name == "DataElementTypeAttribute")).ToList(); return (name, dataNamespace, properties); } @@ -170,7 +169,7 @@ private static (string code, List additionalIndexes) GenerateCode(SourcePro { context.ReportDiagnostic(Diagnostic.Create(TooManyIterationsError, typeAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation())); additionalPropIndexes.Add(0); - return("", additionalPropIndexes); + return ("", additionalPropIndexes); } var builder = new StringBuilder(); @@ -180,12 +179,13 @@ private static (string code, List additionalIndexes) GenerateCode(SourcePro if (isGroup) { type &= ~DataElementTypes.Grouped; + // Grouped element is missing the element own type. Return and add error diagnostic if (type == 0) { context.ReportDiagnostic(Diagnostic.Create(GroupedMissingDefinitionError, typeAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), currentProp.Name)); additionalPropIndexes.Add(0); - return("", additionalPropIndexes); + return ("", additionalPropIndexes); } } @@ -232,7 +232,6 @@ private static (string code, List additionalIndexes) GenerateCode(SourcePro private static (string code, List additionalIndexes) GenerateGroupedRecord(SourceProductionContext context, AttributeData typeAttr, List properties, string collectionName, int iterationCounter) { - var groupName = (string?)typeAttr.NamedArguments.First(arg => arg.Key == "GroupKey").Value.Value; if (string.IsNullOrWhiteSpace(groupName)) @@ -291,12 +290,12 @@ private static string GenerateFormattedTextRecord(SourceProductionContext contex context.ReportDiagnostic(Diagnostic.Create(MissingAttributeError, typeAttribute.ApplicationSyntaxReference!.GetSyntax().GetLocation(), "ValuesPropertyName", "TooltipDataElement")); return string.Empty; } + var isKeyLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueLocalizationKey").Value.Value ?? false; var isListLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "ArePropertyNameValuesKeys").Value.Value ?? false; - var isKeyAppLocalization = (bool?) typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false; - var isListAppLocalization = (bool?) typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsPropertyNameValuesAppLocalization").Value.Value ?? false; - + var isKeyAppLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false; + var isListAppLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsPropertyNameValuesAppLocalization").Value.Value ?? false; var filter = GetFilterAttributeData(property.Name, propertyAttributes); var builder = new StringBuilder(); @@ -365,9 +364,9 @@ private static string GenerateKeyValueRecord(IPropertySymbol property, Attribute var filter = GetFilterAttributeData(property.Name, propertyAttributes); var isKeyLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueLocalizationKey").Value.Value ?? false; - var isKeyAppLocalization = (bool?) typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false; + var isKeyAppLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false; - var localizationKey = (string?) typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "NameLocalizationKey").Value.Value ?? name; + var localizationKey = (string?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "NameLocalizationKey").Value.Value ?? name; var builder = new StringBuilder(); builder.Append(filter); @@ -382,7 +381,7 @@ private static string GenerateValueRecord(IPropertySymbol property, AttributeDat var filter = GetFilterAttributeData(property.Name, propertyAttributes); var isKeyLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueLocalizationKey").Value.Value ?? false; - var isKeyAppLocalization = (bool?) typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false; + var isKeyAppLocalization = (bool?)typeAttribute.NamedArguments.FirstOrDefault(arg => arg.Key == "IsValueAppLocalization").Value.Value ?? false; var builder = new StringBuilder(); builder.Append(filter); diff --git a/WoWsShipBuilder.Data.Generator/Internals/EquatableArray.cs b/WoWsShipBuilder.Data.Generator/Internals/EquatableArray.cs new file mode 100644 index 000000000..80daa3801 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator/Internals/EquatableArray.cs @@ -0,0 +1,196 @@ +// Based on the EquatableArray{T} implementation from CommunityToolkit/dotnet +// see https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs for the original implementation + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace WoWsShipBuilder.Data.Generator.Internals; + +/// +/// Extensions for . +/// +internal static class EquatableArray +{ + public static EquatableArray ToEquatableArray(this ImmutableArray array) + where T : IEquatable + { + return new(array); + } + + public static EquatableArray ToEquatableArray(this IEnumerable enumerable) + where T : IEquatable + { + return new(enumerable.ToImmutableArray()); + } +} + +/// +/// An immutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? array; + + public EquatableArray(ImmutableArray array) + { + this.array = Unsafe.As, T[]?>(ref array); + } + + public static readonly EquatableArray Empty = new(); + + /// + /// Gets a reference to an item at a specified position within the array. + /// + /// The index of the item to retrieve a reference to. + /// A reference to an item at a specified position within the array. + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this.AsImmutableArray().ItemRef(index); + } + + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.AsImmutableArray().IsEmpty; + } + + /// + public bool Equals(EquatableArray array) + { + return this.AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override int GetHashCode() + { + if (this.array is not { } array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (var item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + /// + /// Gets an instance from the current . + /// + /// The from the current . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return Unsafe.As>(ref Unsafe.AsRef(in this.array)); + } + + /// + /// Creates an instance from a given . + /// + /// The input instance. + /// An instance from a given . + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return this.AsImmutableArray().AsSpan(); + } + + /// + /// Copies the contents of this instance to a mutable array. + /// + /// The newly instantiated array. + public T[] ToArray() + { + return this.AsImmutableArray().ToArray(); + } + + /// + /// Gets an value to traverse items in the current array. + /// + /// An value to traverse items in the current array. + public ImmutableArray.Enumerator GetEnumerator() + { + return this.AsImmutableArray().GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this.AsImmutableArray()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this.AsImmutableArray()).GetEnumerator(); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator EquatableArray(ImmutableArray array) + { + return FromImmutableArray(array); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator ImmutableArray(EquatableArray array) + { + return array.AsImmutableArray(); + } + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} diff --git a/WoWsShipBuilder.Data.Generator/Internals/HashCode.cs b/WoWsShipBuilder.Data.Generator/Internals/HashCode.cs new file mode 100644 index 000000000..6333ea576 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator/Internals/HashCode.cs @@ -0,0 +1,180 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +#pragma warning disable CS0809 + +namespace WoWsShipBuilder.Data.Generator.Internals; + +/// +/// A polyfill for the HashCode struct from .NET 6 with the methods required for . +/// +internal struct HashCode +{ + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static readonly uint Seed = GenerateGlobalSeed(); + + private uint v1, v2, v3, v4; + private uint queue1, queue2, queue3; + private uint length; + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + public void Add(T value) + { + this.Add(value?.GetHashCode() ?? 0); + } + + /// + /// Gets the resulting hashcode from the current instance. + /// + /// The resulting hashcode from the current instance. + public int ToHashCode() + { + uint length = this.length; + uint position = length % 4; + uint hash = length < 4 ? MixEmptyState() : MixState(this.v1, this.v2, this.v3, this.v4); + + hash += length * 4; + + if (position > 0) + { + hash = QueueRound(hash, this.queue1); + + if (position > 1) + { + hash = QueueRound(hash, this.queue2); + + if (position > 2) + { + hash = QueueRound(hash, this.queue3); + } + } + } + + hash = MixFinal(hash); + + return (int)hash; + } + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException(); + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => throw new NotSupportedException(); + + private static uint GenerateGlobalSeed() + { + var bytes = new byte[4]; + using (var generator = RandomNumberGenerator.Create()) + { + generator.GetBytes(bytes); + } + + return BitConverter.ToUInt32(bytes, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = Seed + Prime1 + Prime2; + v2 = Seed + Prime2; + v3 = Seed; + v4 = Seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + (input * Prime2), 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixEmptyState() + { + return Seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + + return hash; + } + + private void Add(int value) + { + uint val = (uint)value; + uint previousLength = this.length++; + uint position = previousLength % 4; + + if (position == 0) + { + this.queue1 = val; + } + else if (position == 1) + { + this.queue2 = val; + } + else if (position == 2) + { + this.queue3 = val; + } + else + { + if (previousLength == 3) + { + Initialize(out this.v1, out this.v2, out this.v3, out this.v4); + } + + this.v1 = Round(this.v1, this.queue1); + this.v2 = Round(this.v2, this.queue2); + this.v3 = Round(this.v3, this.queue3); + this.v4 = Round(this.v4, val); + } + } + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } +} diff --git a/WoWsShipBuilder.Data.Generator/Internals/SourceBuilder.cs b/WoWsShipBuilder.Data.Generator/Internals/SourceBuilder.cs new file mode 100644 index 000000000..1bcc6ec3b --- /dev/null +++ b/WoWsShipBuilder.Data.Generator/Internals/SourceBuilder.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WoWsShipBuilder.Data.Generator.Internals; + +internal sealed class SourceBuilder +{ + private const int IndentSize = 4; + + private readonly StringBuilder sb; + + private int indent; + + private LastAction lastAction; + + public SourceBuilder(StringBuilder? sb = null) + { + this.sb = sb ?? new StringBuilder(); + } + + private enum LastAction + { + None, + + BlockClose, + } + + public IDisposable Namespace(string ns) + { + return this.Block($"namespace {ns}"); + } + + public IDisposable Class(string className) + { + return this.Block($"public partial class {className}"); + } + + public IDisposable Record(string recordName) + { + return this.Block($"public partial record {recordName}"); + } + + public IDisposable Block(string? line = null) + { + this.BlockOpen(line); + return this.BlockCloseAction(); + } + + public IDisposable PragmaWarning(string warning) + { + this.AddBlankLineIfNeeded(); + this.Append($"#pragma warning disable {warning}").AppendNewLine(); + return new DisposableAction(() => this.Append($"#pragma warning restore {warning}").AppendNewLine()); + } + + public SourceBuilder DelimitedLines(string delimiter, params string[] lines) => this.DelimitedLines(delimiter, lines as IReadOnlyList); + + public SourceBuilder DelimitedLines(string delimiter, IEnumerable lines) => this.DelimitedLines(delimiter, lines.ToList()); + + public SourceBuilder DelimitedLines(string delimiter, IReadOnlyList lines) + { + _ = lines ?? throw new ArgumentNullException(nameof(lines)); + + this.AddBlankLineIfNeeded(); + + for (var i = 0; i < lines.Count; ++i) + { + this.AppendIndented(lines[i]); + + if (i < lines.Count - 1) + { + this.Append(delimiter); + } + + this.AppendNewLine(); + } + + return this; + } + + public SourceBuilder Line() + { + this.AddBlankLineIfNeeded(); + return this.AppendNewLine(); + } + + public SourceBuilder Line(string line) + { + this.AddBlankLineIfNeeded(); + return this.AppendIndentedLine(line); + } + + public SourceBuilder SpacedLine(string line) + { + this.AddBlankLineIfNeeded(); + this.AppendIndentedLine(line); + this.lastAction = LastAction.BlockClose; + return this; + } + + public SourceBuilder Lines(params string[] lines) + { + this.AddBlankLineIfNeeded(); + return this.DelimitedLines("", lines); + } + + public SourceBuilder Lines(IEnumerable lines) + { + this.AddBlankLineIfNeeded(); + return this.DelimitedLines("", lines.ToList()); + } + + public IDisposable Parens(string line, string? postfix = null) + { + this.AddBlankLineIfNeeded(); + this.AppendIndented(line).Append("(").AppendNewLine().IncreaseIndent(); + return new DisposableAction(() => this.DecreaseIndent().AppendIndented(")").Append(postfix).AppendNewLine()); + } + + public override string ToString() => this.sb.ToString(); + + private void BlockClose() + { + this.DecreaseIndent().AppendIndentedLine("}"); + this.lastAction = LastAction.BlockClose; + } + + private void BlockOpen(string? line = null) + { + this.AddBlankLineIfNeeded(); + + if (line is not null) + { + this.AppendIndentedLine(line); + } + + this.AppendIndentedLine("{").IncreaseIndent(); + } + + private SourceBuilder DecreaseIndent() + { + --this.indent; + return this; + } + + private SourceBuilder IncreaseIndent() + { + ++this.indent; + return this; + } + + private void AddBlankLineIfNeeded() + { + if (this.lastAction != LastAction.BlockClose) + { + return; + } + + this.lastAction = LastAction.None; + this.AppendNewLine(); + } + + private SourceBuilder Append(string? text) + { + if (text is not null) + { + this.sb.Append(text); + } + + return this; + } + + private SourceBuilder AppendIndent() + { + this.sb.Append(' ', this.indent * IndentSize); + return this; + } + + private SourceBuilder AppendIndented(string text) => this.AppendIndent().Append(text); + + private SourceBuilder AppendIndentedLine(string text) => this.AppendIndent().Append(text).AppendNewLine(); + + private SourceBuilder AppendNewLine() + { + this.sb.AppendLine(); + return this; + } + + private IDisposable BlockCloseAction() => new DisposableAction(this.BlockClose); + + private sealed class DisposableAction : IDisposable + { + private readonly Action action; + + public DisposableAction(Action action) => this.action = action; + + public void Dispose() => this.action(); + } +} diff --git a/WoWsShipBuilder.Data.Generator/Internals/SymbolExtensions.cs b/WoWsShipBuilder.Data.Generator/Internals/SymbolExtensions.cs new file mode 100644 index 000000000..654132a25 --- /dev/null +++ b/WoWsShipBuilder.Data.Generator/Internals/SymbolExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.CodeAnalysis; + +namespace WoWsShipBuilder.Data.Generator.Internals; + +internal static class SymbolExtensions +{ + public static AttributeData? FindAttributeOrDefault(this ISymbol symbol, string fullAttributeName) + { + return symbol.GetAttributes().FirstOrDefault(attribute => attribute.AttributeClass?.ToDisplayString().Equals(fullAttributeName, StringComparison.Ordinal) == true); + } + + public static AttributeData FindAttribute(this ISymbol symbol, string fullAttributeName) + { + return symbol.FindAttributeOrDefault(fullAttributeName) ?? throw new KeyNotFoundException($"No attribute found with name {fullAttributeName}."); + } + + public static bool HasAttributeWithFullName(this ISymbol symbol, string fullAttributeName) + { + return symbol.FindAttributeOrDefault(fullAttributeName) != null; + } + + public static bool HasInterface(this INamedTypeSymbol symbol, INamedTypeSymbol interfaceType) + { + return symbol.AllInterfaces.Contains(interfaceType, SymbolEqualityComparer.Default); + } + + public static bool NamespaceContains(this INamedTypeSymbol symbol, string search) + { + return symbol.ContainingNamespace.ToDisplayString().IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static string ToLowerString(this bool value) => value.ToString().ToLowerInvariant(); + +} diff --git a/WoWsShipBuilder.Data.Generator/WoWsShipBuilder.Data.Generator.csproj b/WoWsShipBuilder.Data.Generator/WoWsShipBuilder.Data.Generator.csproj index 339dbe19a..5d0f90414 100644 --- a/WoWsShipBuilder.Data.Generator/WoWsShipBuilder.Data.Generator.csproj +++ b/WoWsShipBuilder.Data.Generator/WoWsShipBuilder.Data.Generator.csproj @@ -7,11 +7,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementFilteringAttribute.cs b/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementFilteringAttribute.cs deleted file mode 100644 index a2aefd790..000000000 --- a/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementFilteringAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace WoWsShipBuilder.DataElements.DataElementAttributes; - -[AttributeUsage(AttributeTargets.Property)] -public class DataElementFilteringAttribute : Attribute -{ - public DataElementFilteringAttribute(bool enableFilterVisibility, string filterMethodName = "") - { - EnableFilterVisibility = enableFilterVisibility; - FilterMethodName = filterMethodName; - } - - public bool EnableFilterVisibility { get; } - - public string FilterMethodName { get; } -} diff --git a/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypeAttribute.cs b/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypeAttribute.cs deleted file mode 100644 index b7a8b5f6c..000000000 --- a/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypeAttribute.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; - -namespace WoWsShipBuilder.DataElements.DataElementAttributes; - -[AttributeUsage(AttributeTargets.Property)] -public class DataElementTypeAttribute : Attribute -{ - public DataElementTypeAttribute(DataElementTypes type) - { - Type = type; - } - - /// - /// Gets the type of the DataElement for the property marked by this attribute. /> - /// - public DataElementTypes Type { get; } - - /// - /// Gets or sets the unit localization key for the property marked by this attribute.
- /// Only valid for and . - ///
- public string? UnitKey { get; set; } - - /// - /// Gets or sets the property name localization key for the property marked by this attribute.
- /// Only valid for , , and . - ///
- public string? NameLocalizationKey { get; set; } - - /// - /// Gets or sets the tooltip localization key for the property marked by this attribute.
- /// Only valid for . - ///
- public string? TooltipKey { get; set; } - - /// - /// Gets or sets the group localization key and identifier for the property marked by this attribute.
- /// Only valid for . - ///
- public string? GroupKey { get; set; } - - /// - /// Gets or set the name of the property containing the list of values that will replace the placeholder. Requires the value of the property marked by this attribute to follow the specifications.
- /// Only valid for . - ///
- public string? ValuesPropertyName { get; set; } - - /// - /// Gets or sets if the value of the property marked by this attribute is a localization key.
- /// Only valid for , and - ///
- public bool IsValueLocalizationKey { get; set; } - - /// - /// Gets or sets if the values indicated by are localization keys.
- /// Only valid for - ///
- public bool ArePropertyNameValuesKeys { get; set; } - - /// - /// Gets or sets if the value of the property marked by this attribute is an app localization key.
- /// Only valid for , and - ///
- public bool IsValueAppLocalization { get; set; } - - /// - /// Gets or sets if the values indicated by are app localization keys.
- /// Only valid for - ///
- public bool IsPropertyNameValuesAppLocalization { get; set; } - -} diff --git a/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypes.cs b/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypes.cs deleted file mode 100644 index a7b83d491..000000000 --- a/WoWsShipBuilder.DataElements/DataElementAttributes/DataElementTypes.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace WoWsShipBuilder.DataElements.DataElementAttributes; - -[Flags] -public enum DataElementTypes -{ - KeyValue = 1, - KeyValueUnit = 2, - Value = 4, - Grouped = 8, - Tooltip = 16, - FormattedText = 32, -} diff --git a/nuget.config b/nuget.config new file mode 100644 index 000000000..b52765b30 --- /dev/null +++ b/nuget.config @@ -0,0 +1,9 @@ + + + + + + + + +