diff --git a/Dalamud.sln b/Dalamud.sln index de91e7ceb..fa26a5d67 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -75,6 +75,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -173,6 +181,18 @@ Global {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -197,6 +217,9 @@ Global {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF} + {8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF} + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f5e75af63..bb8f5af7c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -88,6 +88,15 @@ + + + + + + + + + imgui-frag.hlsl.bytes diff --git a/Dalamud/EnumCloneMap.txt b/Dalamud/EnumCloneMap.txt new file mode 100644 index 000000000..bbc3c1eda --- /dev/null +++ b/Dalamud/EnumCloneMap.txt @@ -0,0 +1,3 @@ +# Format: Target.Full.TypeName = Source.Full.EnumTypeName +# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum +Dalamud.Game.Agent.AgentId = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj new file mode 100644 index 000000000..225ea5f94 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + Dalamud.EnumGenerator.Sample + + false + + + + + + + + + + + + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt new file mode 100644 index 000000000..a7db08bf3 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt @@ -0,0 +1,4 @@ +# Format: Target.Full.TypeName = Source.Full.EnumTypeName +# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum +Dalamud.EnumGenerator.Sample.Gen.MyGeneratedEnum = Dalamud.EnumGenerator.Sample.SourceEnums.SampleSourceEnum + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs new file mode 100644 index 000000000..407b4c151 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs @@ -0,0 +1,9 @@ +namespace Dalamud.EnumGenerator.Sample.SourceEnums +{ + public enum SampleSourceEnum : long + { + First = 1, + Second = 2, + Third = 10000000000L + } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj new file mode 100644 index 000000000..50de4a7c8 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + + false + + Dalamud.EnumGenerator.Tests + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs new file mode 100644 index 000000000..f14279c53 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Dalamud.EnumGenerator.Tests; + +public class EnumCloneMapTests +{ + [Fact] + public void ParseMappings_SimpleLines_ParsesCorrectly() + { + var text = @"# Comment line +My.Namespace.Target = Other.Namespace.Source + +Another.Target = Some.Source"; + + var results = Dalamud.EnumGenerator.EnumCloneGenerator.ParseMappings(text); + + Assert.Equal(2, results.Length); + Assert.Equal("My.Namespace.Target", results[0].TargetFullName); + Assert.Equal("Other.Namespace.Source", results[0].SourceFullName); + Assert.Equal("Another.Target", results[1].TargetFullName); + } + + [Fact] + public void Generator_ProducesFile_WhenSourceResolved() + { + // We'll create a compilation that contains a source enum type and add an AdditionalText mapping + var sourceEnum = @"namespace Foo.Bar { public enum SourceEnum { A = 1, B = 2 } }"; + + var mapText = "GeneratedNs.TargetEnum = Foo.Bar.SourceEnum"; + + var generator = new EnumCloneGenerator(); + var driver = CSharpGeneratorDriver.Create(generator) + .AddAdditionalTexts(ImmutableArray.Create(new Utils.TestAdditionalFile("EnumCloneMap.txt", mapText))); + + var compilation = CSharpCompilation.Create("TestGen", [CSharpSyntaxTree.ParseText(sourceEnum)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics); + + var generated = newCompilation.SyntaxTrees.Select(t => t.FilePath).Where(p => p.EndsWith("TargetEnum.CloneEnum.g.cs")).ToArray(); + Assert.Single(generated); + } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs new file mode 100644 index 000000000..e5c0df848 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs @@ -0,0 +1,21 @@ +using System.Threading; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Dalamud.EnumGenerator.Tests.Utils; + +public class TestAdditionalFile : AdditionalText +{ + private readonly SourceText text; + + public TestAdditionalFile(string path, string text) + { + Path = path; + this.text = SourceText.From(text); + } + + public override SourceText GetText(CancellationToken cancellationToken = new()) => this.text; + + public override string Path { get; } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..60b59dd99 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..e90084796 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,9 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +ENUMGEN001 | EnumGenerator | Warning | SourceGeneratorWithAttributes +ENUMGEN002 | EnumGenerator | Warning | SourceGeneratorWithAttributes \ No newline at end of file diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj new file mode 100644 index 000000000..106b036a8 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + + Dalamud.EnumGenerator + Dalamud.EnumGenerator + + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs new file mode 100644 index 000000000..95af4c38b --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Dalamud.EnumGenerator; + +[Generator] +public class EnumCloneGenerator : IIncrementalGenerator +{ + private const string NewLine = "\r\n"; + + private const string MappingFileName = "EnumCloneMap.txt"; + + private static readonly DiagnosticDescriptor MissingSourceDescriptor = new( + id: "ENUMGEN001", + title: "Source enum not found", + messageFormat: "Source enum '{0}' could not be resolved by the compilation", + category: "EnumGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor DuplicateTargetDescriptor = new( + id: "ENUMGEN002", + title: "Duplicate target mapping", + messageFormat: "Target enum '{0}' is mapped multiple times; generation skipped for this target", + category: "EnumGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Read mappings from additional files named EnumCloneMap.txt + var mappingEntries = context.AdditionalTextsProvider + .Where(at => Path.GetFileName(at.Path).Equals(MappingFileName, StringComparison.OrdinalIgnoreCase)) + .SelectMany((at, _) => ParseMappings(at.GetText()?.ToString() ?? string.Empty)); + + // Combine with compilation so we can resolve types + var compilationAndMaps = context.CompilationProvider.Combine(mappingEntries.Collect()); + + context.RegisterSourceOutput(compilationAndMaps, (spc, pair) => + { + var compilation = pair.Left; + var maps = pair.Right; + + // Detect duplicate targets first and report diagnostics + var duplicateTargets = maps.GroupBy(m => m.TargetFullName, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToImmutableArray(); + foreach (var dup in duplicateTargets) + { + var diag = Diagnostic.Create(DuplicateTargetDescriptor, Location.None, dup); + spc.ReportDiagnostic(diag); + } + + foreach (var (targetFullName, sourceFullName) in maps) + { + if (string.IsNullOrWhiteSpace(targetFullName) || string.IsNullOrWhiteSpace(sourceFullName)) + continue; + + if (duplicateTargets.Contains(targetFullName, StringComparer.OrdinalIgnoreCase)) + continue; + + // Resolve the source enum type by metadata name (namespace.type) + var sourceSymbol = compilation.GetTypeByMetadataName(sourceFullName); + if (sourceSymbol is null) + { + // Report diagnostic for missing source type + var diag = Diagnostic.Create(MissingSourceDescriptor, Location.None, sourceFullName); + spc.ReportDiagnostic(diag); + continue; + } + + if (sourceSymbol.TypeKind != TypeKind.Enum) + continue; + + var sourceNamed = sourceSymbol; // GetTypeByMetadataName already returns INamedTypeSymbol + + // Split target into namespace and type name + string? targetNamespace = null; + var targetName = targetFullName; + var lastDot = targetFullName.LastIndexOf('.'); + if (lastDot >= 0) + { + targetNamespace = targetFullName.Substring(0, lastDot); + targetName = targetFullName.Substring(lastDot + 1); + } + + var underlyingType = sourceNamed.EnumUnderlyingType; + var underlyingDisplay = underlyingType?.ToDisplayString() ?? "int"; + + var fields = sourceNamed.GetMembers() + .OfType() + .Where(f => f.IsStatic && f.HasConstantValue) + .ToArray(); + + var memberLines = fields.Select(f => + { + var name = f.Name; + var constValue = f.ConstantValue; + string literal; + + var st = underlyingType?.SpecialType ?? SpecialType.System_Int32; + + if (constValue is null) + { + literal = "0"; + } + else if (st == SpecialType.System_UInt64) + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "UL"; + } + else if (st == SpecialType.System_UInt32) + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "U"; + } + else if (st == SpecialType.System_Int64) + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "L"; + } + else + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) ?? throw new InvalidOperationException("Unable to convert enum constant value to string."); + } + + return $" {name} = {literal},"; + }); + + var membersText = string.Join(NewLine, memberLines); + + var nsPrefix = targetNamespace is null ? string.Empty : $"namespace {targetNamespace};" + NewLine + NewLine; + + var code = "// " + NewLine + NewLine + + nsPrefix + + $"public enum {targetName} : {underlyingDisplay}" + NewLine + + "{" + NewLine + + membersText + NewLine + + "}" + NewLine; + + var hintName = $"{targetName}.CloneEnum.g.cs"; + spc.AddSource(hintName, SourceText.From(code, Encoding.UTF8)); + } + }); + } + + internal static ImmutableArray<(string TargetFullName, string SourceFullName)> ParseMappings(string text) + { + var builder = ImmutableArray.CreateBuilder<(string, string)>(); + using var reader = new StringReader(text); + string? line; + while ((line = reader.ReadLine()) != null) + { + // Remove comments starting with # + var commentIndex = line.IndexOf('#'); + var content = commentIndex >= 0 ? line.Substring(0, commentIndex) : line; + content = content.Trim(); + if (string.IsNullOrEmpty(content)) + continue; + + // Expected format: Target.Full.Name = Source.Full.Name + var idx = content.IndexOf('='); + if (idx <= 0) + continue; + + var left = content.Substring(0, idx).Trim(); + var right = content.Substring(idx + 1).Trim(); + if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) + continue; + + builder.Add((left, right)); + } + + return builder.ToImmutable(); + } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6eac4d12e --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dalamud.EnumGenerator.Tests")] +