diff --git a/sources/SilkTouchX/Mods/AddApiProfiles.cs b/sources/SilkTouchX/Mods/AddApiProfiles.cs index 01172c17..41d4e676 100644 --- a/sources/SilkTouchX/Mods/AddApiProfiles.cs +++ b/sources/SilkTouchX/Mods/AddApiProfiles.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using SilkTouchX.Clang; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -16,8 +17,12 @@ namespace SilkTouchX.Mods; /// Adds SupportedApiProfile attributes to APIs in given source roots, optionally merging APIs into a single source set. /// /// The logger to use. +/// The configuration snapshot. [ModConfiguration] -public class AddApiProfiles(ILogger logger) : IMod +public class AddApiProfiles( + ILogger logger, + IOptionsSnapshot config +) : Mod { /// /// The mod configuration. @@ -38,7 +43,7 @@ public record ApiProfileDecl /// /// APIs declared in this relative source root are part of this profile. /// - public required string SourceRoot { get; init; } + public required string SourceSubdirectory { get; init; } /// /// The name of the API profile. @@ -61,10 +66,10 @@ public record ApiProfileDecl public Version? MaxVersion { get; init; } /// - /// If provided, merge and deduplicate ("bake") the APIs contained in the into this - /// root with any other profiles being baked into this root. + /// If provided, merge and deduplicate ("bake") the APIs contained in the into + /// this root with any other profiles being baked into this root. /// - public string? BakeToRoot { get; init; } + public string? BakedOutputSubdirectory { get; init; } internal IEnumerable GetSupportedApiProfileAttributeArgs() { @@ -119,6 +124,10 @@ class Rewriter : ModCSharpSyntaxRewriter public ApiProfileDecl? Profile { get; set; } + public ILogger? Logger { get; set; } + + public Dictionary Usings { get; } = new(); + // Allowable type members for baking (we need to override these): // - [x] FieldDeclarationSyntax (VariableDeclarator) // - [x] EventFieldDeclarationSyntax @@ -132,6 +141,7 @@ class Rewriter : ModCSharpSyntaxRewriter // - [x] DestructorDeclarationSyntax // - [x] MethodDeclarationSyntax // - [x] OperatorDeclarationSyntax + // - [x] EnumMemberDeclarationSyntax // // Additional allowed members (done for free by GetOrRegisterTypeBakeSet) // - [x] StructDeclarationSyntax @@ -140,6 +150,12 @@ class Rewriter : ModCSharpSyntaxRewriter // - [x] RecordDeclarationSyntax // - [x] InterfaceDeclarationSyntax + public override SyntaxNode? VisitUsingDirective(UsingDirectiveSyntax node) + { + Usings[node.ToString()] = node; + return base.VisitUsingDirective(node); + } + public override SyntaxNode? VisitDelegateDeclaration(DelegateDeclarationSyntax node) => Visit( node, @@ -158,6 +174,9 @@ class Rewriter : ModCSharpSyntaxRewriter public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) => Visit(node, node.Identifier.ToString(), base.VisitPropertyDeclaration); + public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => + Visit(node, node.Identifier.ToString(), base.VisitEnumMemberDeclaration); + public override SyntaxNode? VisitIndexerDeclaration(IndexerDeclarationSyntax node) => Visit( node, @@ -510,6 +529,54 @@ nodeToAdd is BaseMethodDeclarationSyntax meth ); } } + + // Check that constants and enums have the same value + if ( + (baked.Syntax, nodeToAdd) is + + (EnumMemberDeclarationSyntax lEnum, EnumMemberDeclarationSyntax rEnum) + ) + { + if (lEnum.EqualsValue?.Value.ToString() != rEnum.EqualsValue?.Value.ToString()) + { + Logger?.LogWarning( + "Enum member with discriminator \"{}\" differs between definitions. Left: {}, right: {}", + discrim, + lEnum.EqualsValue?.Value.ToString() ?? "auto-assigned", + rEnum.EqualsValue?.Value.ToString() ?? "auto-assigned" + ); + } + } + else if ( + (baked.Syntax, nodeToAdd) is + + (FieldDeclarationSyntax lConst, FieldDeclarationSyntax rConst) + ) + { + var isConst = lConst.Modifiers.Any(SyntaxKind.ConstKeyword); + if (isConst != rConst.Modifiers.Any(SyntaxKind.ConstKeyword)) + { + Logger?.LogWarning( + "Const with discriminator \"{}\" isn't const in its redefinition. Left: {}, right: {}", + discrim, + lConst.ToString(), + rConst.ToString() + ); + } + else if ( + isConst + && lConst.Declaration.Variables[0].Initializer?.Value.ToString() + != rConst.Declaration.Variables[0].Initializer?.Value.ToString() + ) + { + Logger?.LogWarning( + "Const value with discriminator \"{}\" differs between definitions. Left: {}, right: {}", + discrim, + lConst.Declaration.Variables[0].Initializer?.Value.ToString(), + rConst.Declaration.Variables[0].Initializer?.Value.ToString() + ); + } + } } // Update the bake set. This adds if we haven't seen the member before, otherwise we just update the @@ -715,6 +782,136 @@ public Dictionary< private Dictionary _baked = new(); /// - public Task AfterScrapeAsync(string key, GeneratedSyntax syntax) => - throw new NotImplementedException(); + public override Task AfterScrapeAsync(string key, GeneratedSyntax syntax) + { + var cfg = config.Get(key); + var rewriter = new Rewriter { Logger = logger }; + var bakery = new Dictionary(); + var baked = new List(); + foreach (var (path, root) in syntax.Files) + { + if (!path.StartsWith("sources/")) + { + continue; + } + + var profile = cfg.ApiProfiles + ?.Where( + x => + path[8..].StartsWith( + x.SourceSubdirectory, + StringComparison.OrdinalIgnoreCase + ) + ) + .MaxBy(x => x.SourceSubdirectory.Length); + if (profile is null) + { + continue; + } + + logger.LogDebug("Identified profile {} for {}", profile, path); + if (profile.BakedOutputSubdirectory is not null) + { + var discrim = $"sources/{profile.BakedOutputSubdirectory.Trim('/')}"; + if (!bakery.TryGetValue(discrim, out var bakeSet)) + { + bakeSet = bakery[discrim] = new BakeSet(); + } + + rewriter.Baked = bakeSet; + baked.Add(path); + } + + syntax.Files[path] = rewriter.Visit(root); + rewriter.Baked = null; + } + + foreach (var path in baked) + { + syntax.Files.Remove(path); + } + + foreach (var (subdir, bakeSet) in bakery) + { + foreach (var (fqTopLevelType, bakedMember) in bakeSet.Children) + { + var (iden, bakedSyntax) = Bake(bakedMember); + if (iden is not null) + { + throw new InvalidOperationException( + "Cannot output an unidentified syntax. Top-level syntax should be type declarations only." + ); + } + + var ns = fqTopLevelType.LastIndexOf('.') is not -1 and var idx + ? fqTopLevelType[..idx] + : null; + syntax.Files[$"sources/{subdir}/{PathForFullyQualified(fqTopLevelType)}"] = + CompilationUnit() + .WithMembers( + ns is null + ? SingletonList(bakedSyntax) + : SingletonList( + FileScopedNamespaceDeclaration( + ModUtils.NamespaceIntoIdentifierName(ns) + ) + .WithMembers(SingletonList(bakedSyntax)) + .WithUsings(List(rewriter.Usings.Values)) + ) + ) + .WithUsings(ns is null ? List(rewriter.Usings.Values) : default); + } + } + + return Task.FromResult(syntax); + } + + private static (string? Identifier, MemberDeclarationSyntax Syntax) Bake( + (MemberDeclarationSyntax Syntax, BakeSet? Inner, int Index) member + ) => + member.Syntax switch + { + TypeDeclarationSyntax ty + => ( + ty.Identifier + + ( + ty.TypeParameterList is { Parameters.Count: > 0 and var cnt } + ? $"`{cnt}" + : string.Empty + ), + ty.WithMembers( + List( + ty.Members.Concat( + member.Inner?.Children.Values.Select(x => Bake(x).Syntax) + ?? Enumerable.Empty() + ) + ) + ) + ), + EnumDeclarationSyntax enumDecl + => ( + enumDecl.Identifier.ToString(), + enumDecl.WithMembers( + SeparatedList( + enumDecl.Members.Concat( + member.Inner?.Children.Values + .Select(x => x.Syntax) + .OfType() + ?? Enumerable.Empty() + ) + ) + ) + ), + DelegateDeclarationSyntax del + => ( + del.Identifier + + ( + del.TypeParameterList is { Parameters.Count: > 0 and var cnt } + ? $"`{cnt}" + : string.Empty + ), + del + ), + var x => (null, x) + }; } diff --git a/sources/SilkTouchX/Mods/IMod.cs b/sources/SilkTouchX/Mods/Common/IMod.cs similarity index 100% rename from sources/SilkTouchX/Mods/IMod.cs rename to sources/SilkTouchX/Mods/Common/IMod.cs diff --git a/sources/SilkTouchX/Mods/IModConfigBinder.cs b/sources/SilkTouchX/Mods/Common/IModConfigBinder.cs similarity index 100% rename from sources/SilkTouchX/Mods/IModConfigBinder.cs rename to sources/SilkTouchX/Mods/Common/IModConfigBinder.cs diff --git a/sources/SilkTouchX/Mods/Common/Mod.cs b/sources/SilkTouchX/Mods/Common/Mod.cs new file mode 100644 index 00000000..fbf2df0b --- /dev/null +++ b/sources/SilkTouchX/Mods/Common/Mod.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SilkTouchX.Clang; +using SilkTouchX.Naming; + +namespace SilkTouchX.Mods; + +/// +/// Represents an with common functionality. +/// +public class Mod : IMod +{ + /// + /// Gets the common namespace determined from the default namespaces within the response file. + /// + public string? CommonNamespace { get; private set; } + + /// + /// Determines where to locate the given "fully qualified file name" i.e. a file name for a type prefixed with its + /// namespace e.g. Silk.NET.Core.SomeType`1.gen.cs. The extension is removed so that we don't mistake it as part of + /// the namespace. + /// + /// + /// The fully qualified file name without the extension e.g. Silk.NET.Core.SomeType`1 + /// + /// The file extension e.g. .gen.cs + /// The path within the sources directory to place this file. + public string PathForFullyQualified(string fullyQualified, string extension = ".gen.cs") + { + if (!fullyQualified.Contains('.') || CommonNamespace is null or { Length: 0 }) + { + return fullyQualified + extension; + } + + return fullyQualified.StartsWith(CommonNamespace) + ? fullyQualified[CommonNamespace.Length..].Trim('.').Replace('.', '/') + extension + : fullyQualified + extension; + } + + /// + public virtual Task BeforeJobAsync(string key, SilkTouchConfiguration config) => + Task.CompletedTask; + + /// + public virtual Task> BeforeScrapeAsync(string key, List rsps) + { + CommonNamespace = NameUtils.FindCommonPrefix( + rsps.Select(rsp => rsp.GeneratorConfiguration.DefaultNamespace).ToList(), + true, + int.MaxValue, + true + ); + return Task.FromResult(rsps); + } + + /// + public virtual Task AfterScrapeAsync(string key, GeneratedSyntax syntax) => + Task.FromResult(syntax); + + /// + public virtual Task BeforeOutputAsync( + string key, + GeneratorWorkspace workspace + ) => Task.FromResult(workspace); + + /// + public virtual Task AfterJobAsync(string key) => Task.CompletedTask; +} diff --git a/sources/SilkTouchX/Mods/ModCSharpSyntaxRewriter.cs b/sources/SilkTouchX/Mods/Common/ModCSharpSyntaxRewriter.cs similarity index 100% rename from sources/SilkTouchX/Mods/ModCSharpSyntaxRewriter.cs rename to sources/SilkTouchX/Mods/Common/ModCSharpSyntaxRewriter.cs diff --git a/sources/SilkTouchX/Mods/ModConfigurationAttribute.cs b/sources/SilkTouchX/Mods/Common/ModConfigurationAttribute.cs similarity index 100% rename from sources/SilkTouchX/Mods/ModConfigurationAttribute.cs rename to sources/SilkTouchX/Mods/Common/ModConfigurationAttribute.cs diff --git a/sources/SilkTouchX/Mods/ModLoader.cs b/sources/SilkTouchX/Mods/Common/ModLoader.cs similarity index 100% rename from sources/SilkTouchX/Mods/ModLoader.cs rename to sources/SilkTouchX/Mods/Common/ModLoader.cs diff --git a/sources/SilkTouchX/Mods/ModUtils.cs b/sources/SilkTouchX/Mods/Common/ModUtils.cs similarity index 100% rename from sources/SilkTouchX/Mods/ModUtils.cs rename to sources/SilkTouchX/Mods/Common/ModUtils.cs diff --git a/sources/SilkTouchX/SilkTouchX.csproj b/sources/SilkTouchX/SilkTouchX.csproj index 64902f63..39d8a2c4 100644 --- a/sources/SilkTouchX/SilkTouchX.csproj +++ b/sources/SilkTouchX/SilkTouchX.csproj @@ -31,6 +31,7 @@ + diff --git a/sources/SilkTouchX/SilkTouchX.csproj.DotSettings b/sources/SilkTouchX/SilkTouchX.csproj.DotSettings new file mode 100644 index 00000000..a60056ae --- /dev/null +++ b/sources/SilkTouchX/SilkTouchX.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file