Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
Untested bakery impl
Browse files Browse the repository at this point in the history
  • Loading branch information
Perksey committed Nov 10, 2023
1 parent fc91c78 commit ecf114d
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 7 deletions.
211 changes: 204 additions & 7 deletions sources/SilkTouchX/Mods/AddApiProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,8 +17,12 @@ namespace SilkTouchX.Mods;
/// Adds SupportedApiProfile attributes to APIs in given source roots, optionally merging APIs into a single source set.
/// </summary>
/// <param name="logger">The logger to use.</param>
/// <param name="config">The configuration snapshot.</param>
[ModConfiguration<Configuration>]
public class AddApiProfiles(ILogger<AddApiProfiles> logger) : IMod
public class AddApiProfiles(
ILogger<AddApiProfiles> logger,
IOptionsSnapshot<AddApiProfiles.Configuration> config
) : Mod
{
/// <summary>
/// The mod configuration.
Expand All @@ -38,7 +43,7 @@ public record ApiProfileDecl
/// <summary>
/// APIs declared in this relative source root are part of this profile.
/// </summary>
public required string SourceRoot { get; init; }
public required string SourceSubdirectory { get; init; }

/// <summary>
/// The name of the API profile.
Expand All @@ -61,10 +66,10 @@ public record ApiProfileDecl
public Version? MaxVersion { get; init; }

/// <summary>
/// If provided, merge and deduplicate ("bake") the APIs contained in the <see cref="SourceRoot"/> into this
/// root with any other profiles being baked into this root.
/// If provided, merge and deduplicate ("bake") the APIs contained in the <see cref="SourceSubdirectory"/> into
/// this root with any other profiles being baked into this root.
/// </summary>
public string? BakeToRoot { get; init; }
public string? BakedOutputSubdirectory { get; init; }

internal IEnumerable<AttributeArgumentSyntax> GetSupportedApiProfileAttributeArgs()
{
Expand Down Expand Up @@ -119,6 +124,10 @@ class Rewriter : ModCSharpSyntaxRewriter

public ApiProfileDecl? Profile { get; set; }

public ILogger? Logger { get; set; }

public Dictionary<string, UsingDirectiveSyntax> Usings { get; } = new();

// Allowable type members for baking (we need to override these):
// - [x] FieldDeclarationSyntax (VariableDeclarator)
// - [x] EventFieldDeclarationSyntax
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -715,6 +782,136 @@ public Dictionary<
private Dictionary<string, BakeSet> _baked = new();

/// <inheritdoc />
public Task<GeneratedSyntax> AfterScrapeAsync(string key, GeneratedSyntax syntax) =>
throw new NotImplementedException();
public override Task<GeneratedSyntax> AfterScrapeAsync(string key, GeneratedSyntax syntax)
{
var cfg = config.Get(key);
var rewriter = new Rewriter { Logger = logger };
var bakery = new Dictionary<string, BakeSet>();
var baked = new List<string>();
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<MemberDeclarationSyntax>(
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<MemberDeclarationSyntax>()
)
)
)
),
EnumDeclarationSyntax enumDecl
=> (
enumDecl.Identifier.ToString(),
enumDecl.WithMembers(
SeparatedList(
enumDecl.Members.Concat(
member.Inner?.Children.Values
.Select(x => x.Syntax)
.OfType<EnumMemberDeclarationSyntax>()
?? Enumerable.Empty<EnumMemberDeclarationSyntax>()
)
)
)
),
DelegateDeclarationSyntax del
=> (
del.Identifier
+ (
del.TypeParameterList is { Parameters.Count: > 0 and var cnt }
? $"`{cnt}"
: string.Empty
),
del
),
var x => (null, x)
};
}
File renamed without changes.
69 changes: 69 additions & 0 deletions sources/SilkTouchX/Mods/Common/Mod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SilkTouchX.Clang;
using SilkTouchX.Naming;

namespace SilkTouchX.Mods;

/// <summary>
/// Represents an <see cref="IMod"/> with common functionality.
/// </summary>
public class Mod : IMod
{
/// <summary>
/// Gets the common namespace determined from the default namespaces within the response file.
/// </summary>
public string? CommonNamespace { get; private set; }

/// <summary>
/// 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.
/// </summary>
/// <param name="fullyQualified">
/// The fully qualified file name without the extension e.g. Silk.NET.Core.SomeType`1
/// </param>
/// <param name="extension">The file extension e.g. .gen.cs</param>
/// <returns>The path within the sources directory to place this file.</returns>
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;
}

/// <inheritdoc />
public virtual Task BeforeJobAsync(string key, SilkTouchConfiguration config) =>
Task.CompletedTask;

/// <inheritdoc />
public virtual Task<List<ResponseFile>> BeforeScrapeAsync(string key, List<ResponseFile> rsps)
{
CommonNamespace = NameUtils.FindCommonPrefix(
rsps.Select(rsp => rsp.GeneratorConfiguration.DefaultNamespace).ToList(),
true,
int.MaxValue,
true
);
return Task.FromResult(rsps);
}

/// <inheritdoc />
public virtual Task<GeneratedSyntax> AfterScrapeAsync(string key, GeneratedSyntax syntax) =>
Task.FromResult(syntax);

/// <inheritdoc />
public virtual Task<GeneratorWorkspace> BeforeOutputAsync(
string key,
GeneratorWorkspace workspace
) => Task.FromResult(workspace);

/// <inheritdoc />
public virtual Task AfterJobAsync(string key) => Task.CompletedTask;
}
1 change: 1 addition & 0 deletions sources/SilkTouchX/SilkTouchX.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

<ItemGroup>
<Content Include="test.json" />

</ItemGroup>


Expand Down
2 changes: 2 additions & 0 deletions sources/SilkTouchX/SilkTouchX.csproj.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mods_005Ccommon/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

0 comments on commit ecf114d

Please sign in to comment.