BizHawk/ExternalProjects/BizHawk.SrcGen.ReflectionCache/ReflectionCacheGenerator.cs

111 lines
3.8 KiB
C#

namespace BizHawk.SrcGen.ReflectionCache;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
[Generator]
public sealed class ReflectionCacheGenerator : ISourceGenerator
{
private sealed class ReflectionCacheGenSyntaxReceiver : ISyntaxReceiver
{
/// <remarks>
/// I may have just added RNG to the build process...
/// Increase this sample size to decrease chance of random failure.
/// Alternatively, if you can come up with a better way of getting the project name in <see cref="ISourceGenerator.Execute"/> (I tried like 5 different things), do that instead.
/// --yoshi
/// </remarks>
private const int SAMPLE_SIZE = 20;
private string? _namespace;
private readonly List<string> _namespaces = new();
public string Namespace => _namespace ??= CalcNamespace();
private string CalcNamespace()
{
// black magic wizardry to find common prefix https://stackoverflow.com/a/35081977
var ns = new string(_namespaces[0]
.Substring(0, _namespaces.Min(s => s.Length))
.TakeWhile((c, i) => _namespaces.TrueForAll(s => s[i] == c))
.ToArray());
return ns[ns.Length - 1] == '.' ? ns.Substring(0, ns.Length - 1) : ns; // trim trailing '.' (can't use BizHawk.Common from Source Generators)
}
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
static string Ser(NameSyntax nameSyn) => nameSyn switch
{
SimpleNameSyntax simple => simple.Identifier.ValueText,
QualifiedNameSyntax qual => $"{Ser(qual.Left)}.{Ser(qual.Right)}",
_ => throw new InvalidOperationException()
};
if (_namespace != null || syntaxNode is not NamespaceDeclarationSyntax syn) return;
var newNS = Ser(syn.Name);
if (!newNS.StartsWith("BizHawk.", StringComparison.Ordinal)) return;
_namespaces.Add(newNS);
if (_namespaces.Count == SAMPLE_SIZE) _namespace = CalcNamespace();
}
}
public void Initialize(GeneratorInitializationContext context)
=> context.RegisterForSyntaxNotifications(() => new ReflectionCacheGenSyntaxReceiver());
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not ReflectionCacheGenSyntaxReceiver receiver) return;
var nSpace = receiver.Namespace;
var src = $@"#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
{(nSpace == "BizHawk.Common" ? string.Empty : "\nusing BizHawk.Common;")}
using BizHawk.Common.StringExtensions;
namespace {nSpace}
{{
public static class ReflectionCache
{{
private const string EMBED_PREFIX = ""{nSpace}."";
private static Type[]? _types = null;
private static readonly Assembly Asm = typeof({nSpace}.ReflectionCache).Assembly;
public static readonly Version AsmVersion = Asm.GetName().Version!;
public static Type[] Types => _types ??= Asm.GetTypesWithoutLoadErrors().ToArray();
public static IEnumerable<string> EmbeddedResourceList(string extraPrefix)
{{
var fullPrefix = EMBED_PREFIX + extraPrefix;
return Asm.GetManifestResourceNames().Where(s => s.StartsWithOrdinal(fullPrefix)) // seems redundant with `RemovePrefix`, but we only want these in the final list
.Select(s => s.RemovePrefix(fullPrefix));
}}
public static IEnumerable<string> EmbeddedResourceList()
=> EmbeddedResourceList(string.Empty); // can't be simplified to `Asm.GetManifestResourceNames` call
/// <exception cref=""ArgumentException"">not found</exception>
public static Stream EmbeddedResourceStream(string embedPath)
{{
var fullPath = EMBED_PREFIX + embedPath;
return Asm.GetManifestResourceStream(fullPath)
?? throw new ArgumentException(paramName: nameof(embedPath), message: $""resource at {{fullPath}} not found"");
}}
}}
}}
";
context.AddSource("ReflectionCache.cs", SourceText.From(src, Encoding.UTF8));
}
}