File: Workspace\Solution\SolutionState.GeneratedFileReplacingCompilationTracker.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal partial class SolutionState
    {
        /// <summary>
        /// An implementation of <see cref="ICompilationTracker"/> that takes a compilation from another compilation tracker and updates it
        /// to return a generated document with a specific content, regardless of what the generator actually produces. In other words, it says
        /// "take the compilation this other thing produced, and pretend the generator gave this content, even if it wouldn't."
        /// </summary>
        private class GeneratedFileReplacingCompilationTracker : ICompilationTracker
        {
            private readonly SourceGeneratedDocumentState _replacedGeneratedDocumentState;
 
            private AsyncLazy<Checksum>? _lazyDependentChecksum;
 
            /// <summary>
            /// The lazily-produced compilation that has the generated document updated. This is initialized by call to
            /// <see cref="GetCompilationAsync"/>.
            /// </summary>
            [DisallowNull]
            private Compilation? _compilationWithReplacement;
 
            public ICompilationTracker UnderlyingTracker { get; }
            public SkeletonReferenceCache SkeletonReferenceCache { get; }
            public ProjectState ProjectState => UnderlyingTracker.ProjectState;
 
            public GeneratedFileReplacingCompilationTracker(ICompilationTracker underlyingTracker, SourceGeneratedDocumentState replacementDocumentState)
            {
                UnderlyingTracker = underlyingTracker;
                _replacedGeneratedDocumentState = replacementDocumentState;
                SkeletonReferenceCache = underlyingTracker.SkeletonReferenceCache.Clone();
            }
 
            public GeneratorDriver? GeneratorDriver => UnderlyingTracker.GeneratorDriver;
 
            public bool ContainsAssemblyOrModuleOrDynamic(ISymbol symbol, bool primary)
            {
                if (_compilationWithReplacement == null)
                {
                    // We don't have a compilation yet, so this couldn't have came from us
                    return false;
                }
                else
                {
                    return UnrootedSymbolSet.Create(_compilationWithReplacement).ContainsAssemblyOrModuleOrDynamic(symbol, primary);
                }
            }
 
            public ICompilationTracker Fork(ProjectState newProject, CompilationAndGeneratorDriverTranslationAction? translate)
            {
                // TODO: This only needs to be implemented if a feature that operates from a source generated file then makes
                // further mutations to that project, which isn't needed for now. This will be need to be fixed up when we complete
                // https://github.com/dotnet/roslyn/issues/49533.
                throw new NotImplementedException();
            }
 
            public ICompilationTracker FreezePartialStateWithTree(SolutionState solution, DocumentState docState, SyntaxTree tree, CancellationToken cancellationToken)
            {
                // Because we override SourceGeneratedDocument.WithFrozenPartialSemantics directly, we shouldn't be able to get here.
                throw ExceptionUtilities.Unreachable();
            }
 
            public async Task<Compilation> GetCompilationAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                // Fast path if we've definitely already done this before
                if (_compilationWithReplacement != null)
                {
                    return _compilationWithReplacement;
                }
 
                var underlyingCompilation = await UnderlyingTracker.GetCompilationAsync(solution, cancellationToken).ConfigureAwait(false);
                var underlyingSourceGeneratedDocuments = await UnderlyingTracker.GetSourceGeneratedDocumentStatesAsync(solution, cancellationToken).ConfigureAwait(false);
 
                underlyingSourceGeneratedDocuments.TryGetState(_replacedGeneratedDocumentState.Id, out var existingState);
 
                Compilation newCompilation;
 
                var newSyntaxTree = await _replacedGeneratedDocumentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
                if (existingState != null)
                {
                    // The generated file still exists in the underlying compilation, but the contents may not match the open file if the open file
                    // is stale. Replace the syntax tree so we have a tree that matches the text.
                    var existingSyntaxTree = await existingState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                    newCompilation = underlyingCompilation.ReplaceSyntaxTree(existingSyntaxTree, newSyntaxTree);
                }
                else
                {
                    // The existing output no longer exists in the underlying compilation. This could happen if the user made
                    // an edit which would cause this file to no longer exist, but they're still operating on an open representation
                    // of that file. To ensure that this snapshot is still usable, we'll just add this document back in. This is not a
                    // semantically correct operation, but working on stale snapshots never has that guarantee.
                    newCompilation = underlyingCompilation.AddSyntaxTrees(newSyntaxTree);
                }
 
                Interlocked.CompareExchange(ref _compilationWithReplacement, newCompilation, null);
 
                return _compilationWithReplacement;
            }
 
            public Task<VersionStamp> GetDependentVersionAsync(SolutionState solution, CancellationToken cancellationToken)
                => UnderlyingTracker.GetDependentVersionAsync(solution, cancellationToken);
 
            public Task<VersionStamp> GetDependentSemanticVersionAsync(SolutionState solution, CancellationToken cancellationToken)
                => UnderlyingTracker.GetDependentSemanticVersionAsync(solution, cancellationToken);
 
            public Task<Checksum> GetDependentChecksumAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                if (_lazyDependentChecksum == null)
                {
                    var tmp = solution; // temp. local to avoid a closure allocation for the fast path
                    // note: solution is captured here, but it will go away once GetValueAsync executes.
                    Interlocked.CompareExchange(ref _lazyDependentChecksum, new AsyncLazy<Checksum>(c => ComputeDependentChecksumAsync(tmp, c), cacheResult: true), null);
                }
 
                return _lazyDependentChecksum.GetValueAsync(cancellationToken);
            }
 
            private async Task<Checksum> ComputeDependentChecksumAsync(SolutionState solution, CancellationToken cancellationToken)
                => Checksum.Create(
                    await UnderlyingTracker.GetDependentChecksumAsync(solution, cancellationToken).ConfigureAwait(false),
                    await _replacedGeneratedDocumentState.GetChecksumAsync(cancellationToken).ConfigureAwait(false));
 
            public MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference)
            {
                // This method is used if you're forking a solution with partial semantics, and used to quickly produce references.
                // So this method should only be called if:
                //
                // 1. Project A has a open source generated document, and this CompilationTracker represents A
                // 2. Project B references that A, and is being frozen for partial semantics.
                //
                // We generally don't use partial semantics in a different project than the open file, so this isn't a scenario we need to support.
                throw new NotImplementedException();
            }
 
            public async ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSourceGeneratedDocumentStatesAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                var underlyingGeneratedDocumentStates = await UnderlyingTracker.GetSourceGeneratedDocumentStatesAsync(solution, cancellationToken).ConfigureAwait(false);
 
                if (underlyingGeneratedDocumentStates.Contains(_replacedGeneratedDocumentState.Id))
                {
                    // The generated file still exists in the underlying compilation, but the contents may not match the open file if the open file
                    // is stale. Replace the syntax tree so we have a tree that matches the text.
                    return underlyingGeneratedDocumentStates.SetState(_replacedGeneratedDocumentState.Id, _replacedGeneratedDocumentState);
                }
                else
                {
                    // The generated output no longer exists in the underlying compilation. This could happen if the user made
                    // an edit which would cause this file to no longer exist, but they're still operating on an open representation
                    // of that file. To ensure that this snapshot is still usable, we'll just add this document back in. This is not a
                    // semantically correct operation, but working on stale snapshots never has that guarantee.
                    return underlyingGeneratedDocumentStates.AddRange(ImmutableArray.Create(_replacedGeneratedDocumentState));
                }
            }
 
            public Task<bool> HasSuccessfullyLoadedAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                return UnderlyingTracker.HasSuccessfullyLoadedAsync(solution, cancellationToken);
            }
 
            public bool TryGetCompilation([NotNullWhen(true)] out Compilation? compilation)
            {
                compilation = _compilationWithReplacement;
                return compilation != null;
            }
 
            public SourceGeneratedDocumentState? TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(DocumentId documentId)
            {
                if (documentId == _replacedGeneratedDocumentState.Id)
                {
                    return _replacedGeneratedDocumentState;
                }
                else
                {
                    return UnderlyingTracker.TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentId);
                }
            }
 
            public ValueTask<ImmutableArray<Diagnostic>> GetSourceGeneratorDiagnosticsAsync(SolutionState solution, CancellationToken cancellationToken)
            {
                // We can directly return the diagnostics from the underlying tracker; this is because
                // a generated document cannot have any diagnostics that are produced by a generator:
                // a generator cannot add diagnostics to it's own file outputs, and generators don't see the
                // outputs of each other.
                return UnderlyingTracker.GetSourceGeneratorDiagnosticsAsync(solution, cancellationToken);
            }
        }
    }
}