File: RazorProjectGeneratorRunResult.cs
Web Access
Project: Microsoft.CodeAnalysis.Razor.DotNetFormat.csproj (Microsoft.CodeAnalysis.Razor.DotNetFormat)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.NET.Sdk.Razor.SourceGenerators;

namespace Microsoft.CodeAnalysis.Razor.Formatting;

// A slimmed-down view of GeneratorRunResult exposing the Razor and generated C# documents needed for formatting.
internal sealed class RazorProjectGeneratorRunResult(RazorGeneratorResult generatorResult, Project project)
{
    private const string RazorSourceGeneratorTypeName = "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator";

    public RazorCodeDocument GetRequiredCodeDocument(string filePath)
        => generatorResult.GetCodeDocument(filePath)
           ?? throw new InvalidOperationException($"The Razor source generator did not produce a code document for '{filePath}'.");

    public async Task<SourceGeneratedDocument> GetRequiredSourceGeneratedDocumentAsync(
        string filePath,
        bool declarationDocument,
        CancellationToken cancellationToken)
    {
        var implementationHintName = generatorResult.GetHintName(filePath)
            ?? throw new InvalidOperationException($"The Razor source generator did not produce a hint name for '{filePath}'.");
        var hintName = declarationDocument
            ? RazorSourceGenerator.GetDeclIdentifierFromHintName(implementationHintName)
            : implementationHintName;

        // TODO: Match by hint name until generator assembly locations are compared case-insensitively on Windows:
        // https://github.com/dotnet/roslyn/issues/76869
        var generatedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false);
        return generatedDocuments.SingleOrDefault(document => document.HintName == hintName)
            ?? throw new InvalidOperationException($"The Razor source generator did not produce generated document '{hintName}'.");
    }

    public static async Task<RazorProjectGeneratorRunResult?> TryCreateAsync(
        Project project,
        CancellationToken cancellationToken)
    {
        var result = await project.GetSourceGeneratorRunResultAsync(cancellationToken).ConfigureAwait(false);
        if (result is null)
        {
            return null;
        }

        var expectedAssembly = typeof(RazorSourceGenerator).Assembly;
        // Compare Assembly objects before reading the typed host output so an older or separately loaded Razor generator
        // produces an actionable identity error instead of a misleading cast failure.
        var runResult = result.Results.FirstOrDefault(r => r.Generator.GetGeneratorType().Assembly == expectedAssembly);
        if (runResult.Generator is null)
        {
            var incompatibleGenerator = result.Results.FirstOrDefault(
                r => r.Generator.GetGeneratorType().FullName == RazorSourceGeneratorTypeName);

            if (incompatibleGenerator.Generator is not null)
            {
                var actualAssembly = incompatibleGenerator.Generator.GetGeneratorType().Assembly;
                throw new InvalidOperationException(
                    $"Project '{project.Name}' loaded an incompatible Razor source generator from {GetAssemblyDisplay(actualAssembly)}. " +
                    $"dotnet format requires the Razor compiler at {GetAssemblyDisplay(expectedAssembly)}.");
            }

            return null;
        }

#pragma warning disable RSEXPERIMENTAL004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
        if (!runResult.HostOutputs.TryGetValue(nameof(RazorGeneratorResult), out var hostOutput))
#pragma warning restore RSEXPERIMENTAL004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
        {
            throw new InvalidOperationException(
                $"The Razor source generator for project '{project.Name}' did not produce its formatting host output. " +
                string.Join(Environment.NewLine, runResult.Diagnostics));
        }

        if (hostOutput is not RazorGeneratorResult generatorResult)
        {
            throw new InvalidOperationException(
                $"The Razor source generator for project '{project.Name}' produced an incompatible formatting host output. " +
                string.Join(Environment.NewLine, runResult.Diagnostics));
        }

        return new(generatorResult, project);
    }

    private static string GetAssemblyDisplay(Assembly assembly)
    {
        var location = assembly.IsDynamic ? "<dynamic assembly>" : assembly.Location;
        var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
        return $"'{location}' (version '{assembly.GetName().Version}', informational version '{informationalVersion ?? "<unknown>"}')";
    }
}