File: ProjectSetup.cs
Web Access
Project: src\src\Razor\src\Compiler\perf\Microsoft.AspNetCore.Razor.Microbenchmarks.Generator\Microsoft.AspNetCore.Razor.Microbenchmarks.Generator.csproj (Microsoft.AspNetCore.Razor.Microbenchmarks.Generator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Text;
using Microsoft.NET.Sdk.Razor.SourceGenerators;
 
namespace Microsoft.AspNetCore.Razor.Microbenchmarks.Generator;
public static class ProjectSetup
{
    internal static async Task<RazorProject> GetRazorProjectAsync(bool cold = true)
    {
        var workspace = MSBuildWorkspace.Create();
        var project = await workspace.OpenProjectAsync("SampleApp/SampleApp.csproj");
 
        if (workspace.Diagnostics.Count != 0)
        {
            throw new InvalidOperationException(string.Join(Environment.NewLine, workspace.Diagnostics));
        }
 
        // remove any generators from the project as we don't want generated files in our initial compilation
        foreach (var analyzerRef in project.AnalyzerReferences)
        {
            project = project.RemoveAnalyzerReference(analyzerRef);
        }
 
        // get the constituent parts
        var compilation = await project.GetCompilationAndCheckSuccess();
 
        List<AdditionalText> additionalTexts = new List<AdditionalText>();
        foreach (var additionalDocument in project.AdditionalDocuments)
        {
            var text = await additionalDocument.GetTextAsync();
            additionalTexts.Add(new InMemoryAdditionalText(text, additionalDocument.FilePath!));
        }
 
        var parseOptions = (CSharpParseOptions)project.ParseOptions!;
 
        var optionsProvider = new TargetPathAnalyzerConfigOptionsProvider(project.AnalyzerOptions.AnalyzerConfigOptionsProvider);
 
        // create the generator driver we'll use for the tests
        // the generator we use will be dependent on the build configuration the benchmark is built in
        GeneratorDriver driver = CSharpGeneratorDriver.Create(generators: new[] { new RazorSourceGenerator().AsSourceGenerator() },
                                                              additionalTexts: additionalTexts,
                                                              parseOptions: parseOptions,
                                                              optionsProvider: optionsProvider);
 
        // if we request a warm project, run the driver once through to start with, priming the caches
        if (!cold)
        {
            driver = driver.RunGenerators(compilation);
        }
 
        return new(driver, compilation, additionalTexts.ToImmutableArray(), parseOptions, optionsProvider);
    }
 
    internal static RazorProject GetRazorProject(bool cold = true) => Task.Run(() => GetRazorProjectAsync(cold)).GetAwaiter().GetResult();
 
    public static async Task<Compilation> GetCompilationAndCheckSuccess(this Project project)
    {
        var comp = await project.GetCompilationAsync();
        var diagnostics = comp!.GetDiagnostics();
        if (diagnostics.Any(d => d.Severity != DiagnosticSeverity.Hidden))
        {
            //Debug.Fail("Compilation contained non-hidden diagnostics");
        }
        return comp;
    }
 
    public record RazorProject(GeneratorDriver GeneratorDriver, Compilation Compilation, ImmutableArray<AdditionalText> AdditionalTexts, CSharpParseOptions ParseOptions, AnalyzerConfigOptionsProvider OptionsProvider);
 
    internal sealed class InMemoryAdditionalText : AdditionalText
    {
        private readonly SourceText _text;
 
        public InMemoryAdditionalText(SourceText text, string path)
        {
            _text = text;
            Path = path;
        }
 
        public InMemoryAdditionalText(string text, string path)
        {
            _text = SourceText.From(text, Encoding.UTF8);
            Path = path;
        }
 
        public override string Path { get; }
 
        public override SourceText? GetText(CancellationToken cancellationToken = default) => _text;
    }
 
    /// <summary>
    /// An options provider that will add the required razor metadata if it's missing.
    /// </summary>
    internal sealed class TargetPathAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
    {
        private readonly AnalyzerConfigOptionsProvider _provider;
 
        public TargetPathAnalyzerConfigOptionsProvider(AnalyzerConfigOptionsProvider provider)
        {
            _provider = provider;
        }
 
        public override AnalyzerConfigOptions GlobalOptions { get => _provider.GlobalOptions; }
 
        public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => _provider.GetOptions(tree);
 
        public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
        {
            return new TargetPathAnalyzerOptions(textFile.Path, _provider.GetOptions(textFile));
        }
 
        internal class TargetPathAnalyzerOptions : AnalyzerConfigOptions
        {
            private readonly string _targetPath;
 
            private readonly AnalyzerConfigOptions _baseOptions;
 
            public TargetPathAnalyzerOptions(string name, AnalyzerConfigOptions baseOptions)
            {
                _targetPath = Convert.ToBase64String(Encoding.UTF8.GetBytes(name));
                _baseOptions = baseOptions;
            }
 
            public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
            {
                if (!_baseOptions.TryGetValue(key, out value))
                {
                    value = key == "build_metadata.AdditionalFiles.TargetPath" ? _targetPath : string.Empty;
                }
                return true;
            }
        }
    }
}