File: Services\SourceGeneration\RemoteSourceGenerationService.cs
Web Access
Project: src\src\Workspaces\Remote\ServiceHub\Microsoft.CodeAnalysis.Remote.ServiceHub.csproj (Microsoft.CodeAnalysis.Remote.ServiceHub)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SourceGeneration;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Remote;
 
// Can use AnalyzerReference as a key here as we will will always get back the same instance back for the same checksum.
using AnalyzerReferenceMap = ConditionalWeakTable<AnalyzerReference, StrongBox<bool>>;
 
internal sealed partial class RemoteSourceGenerationService(in BrokeredServiceBase.ServiceConstructionArguments arguments)
    : BrokeredServiceBase(arguments), IRemoteSourceGenerationService
{
    internal sealed class Factory : FactoryBase<IRemoteSourceGenerationService>
    {
        protected override IRemoteSourceGenerationService CreateService(in ServiceConstructionArguments arguments)
            => new RemoteSourceGenerationService(arguments);
    }
 
    public ValueTask<ImmutableArray<SourceGeneratedDocumentInfo>> GetSourceGeneratedDocumentInfoAsync(
        Checksum solutionChecksum, ProjectId projectId, bool withFrozenSourceGeneratedDocuments, CancellationToken cancellationToken)
    {
        return RunServiceAsync(solutionChecksum, async solution =>
        {
            var project = solution.GetRequiredProject(projectId);
            var documentStates = await solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(
                project.State, withFrozenSourceGeneratedDocuments, cancellationToken).ConfigureAwait(false);
 
            var result = new FixedSizeArrayBuilder<SourceGeneratedDocumentInfo>(documentStates.States.Count);
            foreach (var (id, state) in documentStates.States)
            {
                Contract.ThrowIfFalse(id.IsSourceGenerated);
                result.Add(new(state.Identity, state.GetContentIdentity(), state.GenerationDateTime));
            }
 
            return result.MoveToImmutable();
        }, cancellationToken);
    }
 
    public ValueTask<ImmutableArray<string>> GetContentsAsync(
        Checksum solutionChecksum, ProjectId projectId, ImmutableArray<DocumentId> documentIds, bool withFrozenSourceGeneratedDocuments, CancellationToken cancellationToken)
    {
        return RunServiceAsync(solutionChecksum, async solution =>
        {
            var project = solution.GetRequiredProject(projectId);
            var documentStates = await solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(
                project.State, withFrozenSourceGeneratedDocuments, cancellationToken).ConfigureAwait(false);
 
            var result = new FixedSizeArrayBuilder<string>(documentIds.Length);
            foreach (var id in documentIds)
            {
                Contract.ThrowIfFalse(id.IsSourceGenerated);
                var state = documentStates.GetRequiredState(id);
                var text = await state.GetTextAsync(cancellationToken).ConfigureAwait(false);
                result.Add(text.ToString());
            }
 
            return result.MoveToImmutable();
        }, cancellationToken);
    }
 
    private static readonly Dictionary<string, (AnalyzerReferenceMap analyzerReferenceMap, AnalyzerReferenceMap.CreateValueCallback callback)> s_languageToAnalyzerReferenceMap = new()
    {
        { LanguageNames.CSharp, (new(), static analyzerReference => HasSourceGenerators(analyzerReference, LanguageNames.CSharp)) },
        { LanguageNames.VisualBasic, (new(), static analyzerReference => HasSourceGenerators(analyzerReference, LanguageNames.VisualBasic)) },
    };
 
    private static StrongBox<bool> HasSourceGenerators(
        AnalyzerReference analyzerReference, string language)
    {
        var generators = analyzerReference.GetGenerators(language);
        return new(generators.Any());
    }
 
    public async ValueTask<bool> HasGeneratorsAsync(
        Checksum solutionChecksum,
        ProjectId projectId,
        ImmutableArray<Checksum> analyzerReferenceChecksums,
        string language,
        CancellationToken cancellationToken)
    {
        if (analyzerReferenceChecksums.Length == 0)
            return false;
 
        // Do not use RunServiceAsync here.  We don't want to actually synchronize a solution instance on this remote
        // side to service this request.  Specifically, solution syncing is expensive, and will pull over a lot of data
        // that we don't need (like document contents).  All we need to do is synchronize over the analyzer-references
        // (which are actually quite small as they are represented as file-paths), and then answer the question based on
        // them directly.  We can then cache that result for future requests.
        var workspace = GetWorkspace();
        var assetProvider = workspace.CreateAssetProvider(solutionChecksum, WorkspaceManager.SolutionAssetCache, SolutionAssetSource);
 
        // Fetch the analyzer references specified by the host.  Note: this will only serialize this information over
        // the first time needed. After that, it will be cached in the WorkspaceManager.SolutionAssetCache on the remote
        // side, so it will be a no-op to fetch them in the future.
        //
        // From this point on, the host won't call into us for the same project-state (as it caches the data itself). If
        // the project state changes, it will just call into us with the checksums for its analyzer references.  As
        // those will almost always be the same, we'll just fetch the precomputed values on our end, return them, and
        // the host will cache it.  We'll only actually fetch something new and compute something new when an actual new
        // analyzer reference is added.
 
        var checksumCollection = new ChecksumCollection(analyzerReferenceChecksums);
 
        // Make sure the analyzer references are loaded into an isolated ALC so that we can properly load them if
        // they're a new version of some analyzer reference we've already loaded.
        var isolatedReferences = await IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync(
            useAsync: true,
            checksumCollection,
            workspace.Services.SolutionServices,
            () => assetProvider.GetAssetsArrayAsync<AnalyzerReference>(projectId, checksumCollection, cancellationToken),
            cancellationToken).ConfigureAwait(false);
 
        var (analyzerReferenceMap, callback) = s_languageToAnalyzerReferenceMap[language];
        foreach (var analyzerReference in isolatedReferences)
        {
            var hasGenerators = analyzerReferenceMap.GetValue(analyzerReference, callback);
            if (hasGenerators.Value)
                return true;
        }
 
        return false;
    }
 
    public ValueTask<ImmutableArray<SourceGeneratorIdentity>> GetSourceGeneratorIdentitiesAsync(
        Checksum solutionChecksum,
        ProjectId projectId,
        string analyzerReferenceFullPath,
        CancellationToken cancellationToken)
    {
        return RunServiceAsync(solutionChecksum, solution =>
        {
            var project = solution.GetRequiredProject(projectId);
            var analyzerReference = project.AnalyzerReferences
                .First(r => r.FullPath == analyzerReferenceFullPath);
 
            return ValueTaskFactory.FromResult(SourceGeneratorIdentity.GetIdentities(analyzerReference, project.Language));
        }, cancellationToken);
    }
 
    public ValueTask<bool> HasAnalyzersOrSourceGeneratorsAsync(
        Checksum solutionChecksum,
        ProjectId projectId,
        string analyzerReferenceFullPath,
        CancellationToken cancellationToken)
    {
        return RunServiceAsync(solutionChecksum, solution =>
        {
            var project = solution.GetRequiredProject(projectId);
            var analyzerReference = project.AnalyzerReferences
                .First(r => r.FullPath == analyzerReferenceFullPath);
 
            return ValueTaskFactory.FromResult(analyzerReference.HasAnalyzersOrSourceGenerators(project.Language));
        }, cancellationToken);
    }
}