// 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 References { get; } = new HashSet(); public IDictionary Properties { get; } = new Dictionary(StringComparer.Ordinal); public ICollection SourceCode { get; } = new HashSet(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? 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(Action? 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(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 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 _values; public TestAnalyzerConfigOptions(IDictionary 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; } } }