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