feat: add Roslyn-based compile-time request handler registration
Introduce compile-time registration capabilities with source generation, enabling automatic handler registration.
This commit is contained in:
parent
148a878951
commit
f77c1ff29b
20 changed files with 918 additions and 133 deletions
177
src/request.tests/_helpers/Roslyn.cs
Normal file
177
src/request.tests/_helpers/Roslyn.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
using Assembly = System.Reflection.Assembly;
|
||||
|
||||
namespace Geekeey.Request.Tests;
|
||||
|
||||
internal static class Roslyn
|
||||
{
|
||||
private static CSharpCompilation CreateCompilation(CompilerOptions options)
|
||||
{
|
||||
if (options.SourceCode.Count is 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one source file must be configured.");
|
||||
}
|
||||
|
||||
var syntax = options.SourceCode
|
||||
.Select(static sourceCode => CSharpSyntaxTree.ParseText(sourceCode, CSharpParseOptions.Default
|
||||
.WithLanguageVersion(LanguageVersion.Preview)))
|
||||
.ToArray();
|
||||
|
||||
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
|
||||
.Split(Path.PathSeparator)
|
||||
.Concat(options.References.Select(static assembly => assembly.Location))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Select(static path => MetadataReference.CreateFromFile(path));
|
||||
|
||||
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
|
||||
return CSharpCompilation.Create(options.AssemblyName, syntax, references, compilationOptions);
|
||||
}
|
||||
|
||||
public sealed class CompilerOptions
|
||||
{
|
||||
public string AssemblyName { get; set; } = Guid.NewGuid().ToString();
|
||||
public ICollection<Assembly> References { get; } = new HashSet<Assembly>();
|
||||
public IDictionary<string, string?> Properties { get; } = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
public ICollection<string> SourceCode { get; } = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
public void AddFromEmbeddedSource(string fileName)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var relativePath = $"_fixtures/roslyn/{fileName}";
|
||||
var suffix = relativePath.Replace(Path.DirectorySeparatorChar, '.')
|
||||
.Replace(Path.AltDirectorySeparatorChar, '.');
|
||||
var resourceName = assembly.GetManifestResourceNames()
|
||||
.SingleOrDefault(name => name.EndsWith(suffix, StringComparison.Ordinal));
|
||||
|
||||
if (resourceName is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Embedded fixture '{relativePath}' was not found.");
|
||||
}
|
||||
|
||||
if (assembly.GetManifestResourceStream(resourceName) is not { } stream)
|
||||
{
|
||||
throw new InvalidOperationException($"Embedded fixture '{resourceName}' could not be opened.");
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
SourceCode.Add(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
public static Assembly Compile(Action<CompilerOptions>? configure = null)
|
||||
{
|
||||
var options = new CompilerOptions();
|
||||
configure?.Invoke(options);
|
||||
var compilation = CreateCompilation(options);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
if (compilation.Emit(stream) is { Success: false, Diagnostics: var diagnostics })
|
||||
{
|
||||
throw new InvalidOperationException(string.Join(Environment.NewLine,
|
||||
diagnostics.Select(static diagnostic => diagnostic.ToString())));
|
||||
}
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
return Assembly.Load(stream.ToArray());
|
||||
}
|
||||
|
||||
public static GeneratorDriverRunResult SourceGenerator<TGenerator>(Action<CompilerOptions>? configure = null)
|
||||
where TGenerator : IIncrementalGenerator, new()
|
||||
{
|
||||
var options = new CompilerOptions();
|
||||
configure?.Invoke(options);
|
||||
var compilation = CreateCompilation(options);
|
||||
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create([new TGenerator().AsSourceGenerator()],
|
||||
parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options,
|
||||
optionsProvider: new TestAnalyzerConfigOptionsProvider(
|
||||
new Dictionary<string, string?>(options.Properties, StringComparer.Ordinal)
|
||||
{
|
||||
["build_property.AssemblyName"] = options.AssemblyName,
|
||||
}));
|
||||
|
||||
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
|
||||
return driver.GetRunResult();
|
||||
}
|
||||
|
||||
extension(GeneratorDriverRunResult result)
|
||||
{
|
||||
public string? File([StringSyntax(StringSyntaxAttribute.Regex)] string name)
|
||||
{
|
||||
foreach (var value in result.Results)
|
||||
{
|
||||
foreach (var source in value.GeneratedSources)
|
||||
{
|
||||
if (Regex.IsMatch(source.HintName, name))
|
||||
{
|
||||
return source.SourceText.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
|
||||
{
|
||||
public TestAnalyzerConfigOptionsProvider(IDictionary<string, string?> values)
|
||||
{
|
||||
GlobalOptions = new TestAnalyzerConfigOptions(values);
|
||||
}
|
||||
|
||||
public override AnalyzerConfigOptions GlobalOptions { get; }
|
||||
|
||||
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
|
||||
{
|
||||
return GlobalOptions;
|
||||
}
|
||||
|
||||
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
|
||||
{
|
||||
return GlobalOptions;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions
|
||||
{
|
||||
private readonly ImmutableDictionary<string, string?> _values;
|
||||
|
||||
public TestAnalyzerConfigOptions(IDictionary<string, string?> values)
|
||||
{
|
||||
_values = values.ToImmutableDictionary(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public override bool TryGetValue(string key, out string value)
|
||||
{
|
||||
if (_values.TryGetValue(key, out var existingValue) && existingValue is not null)
|
||||
{
|
||||
value = existingValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.StartsWith("build_property.", StringComparison.Ordinal) &&
|
||||
_values.TryGetValue(key["build_property.".Length..], out existingValue) &&
|
||||
existingValue is not null)
|
||||
{
|
||||
value = existingValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue