File: Workspace\Solution\SourceGeneratedDocumentState.cs
Web Access
Project: src\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.Diagnostics.Contracts;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.SourceGeneration;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
internal sealed class SourceGeneratedDocumentState : DocumentState
{
    /// <summary>
    /// Backing store for <see cref="GetOriginalSourceTextContentHash"/>.
    /// </summary>
    private readonly Lazy<Checksum> _lazyContentHash;
 
    public SourceGeneratedDocumentIdentity Identity { get; }
 
    public string HintName => Identity.HintName;
 
    /// <summary>
    /// It's reasonable to capture 'text' here and keep it alive.  We're already holding onto the generated text
    /// strongly in the ConstantTextAndVersionSource we're passing to our base type. 
    /// </summary>
    public SourceText SourceText { get; }
 
    /// <summary>
    /// Checksum of <see cref="SourceText"/> when it was <em>originally</em> created.  This is subtly, but importantly
    /// different from the checksum acquired from <see cref="SourceText.GetChecksum"/>.  Specifically, the original
    /// source text may have been created from a <see cref="System.IO.Stream"/> in a lossy fashion (for example,
    /// removing BOM marks and the like) on the OOP side. As such, its checksum might not be reconstructible from the
    /// actual text and hash algorithm that were used to create the SourceText on the host side.  To ensure both the
    /// host and OOP are in agreement about the true content checksum, we store this separately.
    /// </summary>
    public Checksum GetOriginalSourceTextContentHash()
        => _lazyContentHash.Value;
 
    public readonly DateTime GenerationDateTime;
 
    public static SourceGeneratedDocumentState Create(
        SourceGeneratedDocumentIdentity documentIdentity,
        SourceText generatedSourceText,
        ParseOptions parseOptions,
        LanguageServices languageServices,
        Checksum? originalSourceTextChecksum,
        DateTime generationDateTime)
    {
        // If the caller explicitly provided us with the checksum for the source text, then we always defer to that.
        // This happens on the host side, when we are given the data computed by the OOP side.
        //
        // If the caller didn't provide us with the checksum, then we'll compute it on demand.  This happens on the OOP
        // side when we're actually producing the SG doc in the first place.
        var lazyTextChecksum = new Lazy<Checksum>(() => originalSourceTextChecksum ?? ComputeContentHash(generatedSourceText));
        return Create(documentIdentity, generatedSourceText, parseOptions, languageServices, lazyTextChecksum, generationDateTime);
    }
 
    private static SourceGeneratedDocumentState Create(
        SourceGeneratedDocumentIdentity documentIdentity,
        SourceText generatedSourceText,
        ParseOptions parseOptions,
        LanguageServices languageServices,
        Lazy<Checksum> lazyTextChecksum,
        DateTime generationDateTime)
    {
        var loadTextOptions = new LoadTextOptions(generatedSourceText.ChecksumAlgorithm);
        var textAndVersion = TextAndVersion.Create(generatedSourceText, VersionStamp.Create());
        var textSource = new ConstantTextAndVersionSource(textAndVersion);
        var treeSource = CreateLazyFullyParsedTree(
            textSource,
            loadTextOptions,
            documentIdentity.FilePath,
            parseOptions,
            languageServices);
 
        return new SourceGeneratedDocumentState(
            documentIdentity,
            languageServices,
            documentServiceProvider: SourceGeneratedTextDocumentServiceProvider.Instance,
            new DocumentInfo.DocumentAttributes(
                documentIdentity.DocumentId,
                name: documentIdentity.HintName,
                folders: [],
                parseOptions.Kind,
                filePath: documentIdentity.FilePath,
                isGenerated: true,
                designTimeOnly: false),
            parseOptions,
            textSource,
            generatedSourceText,
            loadTextOptions,
            treeSource,
            lazyTextChecksum,
            generationDateTime);
    }
 
    private SourceGeneratedDocumentState(
        SourceGeneratedDocumentIdentity documentIdentity,
        LanguageServices languageServices,
        IDocumentServiceProvider? documentServiceProvider,
        DocumentInfo.DocumentAttributes attributes,
        ParseOptions options,
        ITextAndVersionSource textSource,
        SourceText text,
        LoadTextOptions loadTextOptions,
        ITreeAndVersionSource treeSource,
        Lazy<Checksum> lazyContentHash,
        DateTime generationDateTime)
        : base(languageServices, documentServiceProvider, attributes, textSource, loadTextOptions, options, treeSource)
    {
        Identity = documentIdentity;
 
        SourceText = text;
        _lazyContentHash = lazyContentHash;
        GenerationDateTime = generationDateTime;
    }
 
    private static Checksum ComputeContentHash(SourceText text)
        => Checksum.From(text.GetContentHash());
 
    // The base allows for parse options to be null for non-C#/VB languages, but we'll always have parse options
    public new ParseOptions ParseOptions => base.ParseOptions!;
 
    public SourceGeneratedDocumentContentIdentity GetContentIdentity()
        => new(this.GetOriginalSourceTextContentHash(), this.SourceText.Encoding?.WebName, this.SourceText.ChecksumAlgorithm);
 
    protected override TextDocumentState UpdateAttributes(DocumentInfo.DocumentAttributes newAttributes)
        => throw new NotSupportedException(WorkspacesResources.The_contents_of_a_SourceGeneratedDocument_may_not_be_changed);
 
    protected override TextDocumentState UpdateDocumentServiceProvider(IDocumentServiceProvider? newProvider)
        => throw new NotSupportedException(WorkspacesResources.The_contents_of_a_SourceGeneratedDocument_may_not_be_changed);
 
    protected override TextDocumentState UpdateText(ITextAndVersionSource newTextSource, PreservationMode mode, bool incremental)
        => throw new NotSupportedException(WorkspacesResources.The_contents_of_a_SourceGeneratedDocument_may_not_be_changed);
 
    public SourceGeneratedDocumentState WithText(SourceText sourceText)
    {
        // See if we can reuse this instance directly
        var newSourceTextChecksum = ComputeContentHash(sourceText);
        if (this.GetOriginalSourceTextContentHash() == newSourceTextChecksum)
            return this;
 
        return Create(
            Identity,
            sourceText,
            ParseOptions,
            LanguageServices,
            // Just pass along the checksum for the new source text since we've already computed it.
            newSourceTextChecksum,
            GenerationDateTime);
    }
 
    public SourceGeneratedDocumentState WithParseOptions(ParseOptions parseOptions)
    {
        // See if we can reuse this instance directly
        if (ParseOptions.Equals(parseOptions))
            return this;
 
        return Create(
            Identity,
            SourceText,
            parseOptions,
            LanguageServices,
            // We're just changing the parse options.  So the checksum will remain as is.
            _lazyContentHash,
            GenerationDateTime);
    }
 
    public SourceGeneratedDocumentState WithGenerationDateTime(DateTime generationDateTime)
    {
        // See if we can reuse this instance directly
        if (this.GenerationDateTime == generationDateTime)
            return this;
 
        // Copy over all state as-is.  The generation time doesn't change any actual state (the same tree will be
        // produced for example).
        return new(
            this.Identity,
            this.LanguageServices,
            this.DocumentServiceProvider,
            this.Attributes,
            this.ParseOptions,
            this.TextAndVersionSource,
            this.SourceText,
            this.LoadTextOptions,
            this.TreeSource!,
            this._lazyContentHash,
            generationDateTime);
    }
 
    /// <summary>
    /// This is modeled after <see cref="DefaultTextDocumentServiceProvider"/>, but sets
    /// <see cref="IDocumentOperationService.CanApplyChange"/> to <see langword="false"/> for source generated
    /// documents.
    /// </summary>
    internal sealed class SourceGeneratedTextDocumentServiceProvider : IDocumentServiceProvider
    {
        public static readonly SourceGeneratedTextDocumentServiceProvider Instance = new();
 
        private SourceGeneratedTextDocumentServiceProvider()
        {
        }
 
        public TService? GetService<TService>()
            where TService : class, IDocumentService
        {
            if (SourceGeneratedDocumentOperationService.Instance is TService documentOperationService)
            {
                return documentOperationService;
            }
 
            if (DocumentPropertiesService.Default is TService documentPropertiesService)
            {
                return documentPropertiesService;
            }
 
            return null;
        }
 
        private sealed class SourceGeneratedDocumentOperationService : IDocumentOperationService
        {
            public static readonly SourceGeneratedDocumentOperationService Instance = new();
 
            public bool CanApplyChange => false;
            public bool SupportDiagnostics => true;
        }
    }
}