177 lines
5.4 KiB
C#
177 lines
5.4 KiB
C#
|
|
// 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|