File: Workspace\Solution\Document.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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
/// <summary>
/// Represents a source code document that is part of a project.
/// It provides access to the source text, parsed syntax tree and the corresponding semantic model.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
public class Document : TextDocument
{
    /// <summary>
    /// A cached reference to the <see cref="SemanticModel"/>.
    /// </summary>
    private WeakReference<SemanticModel>? _model;
 
    /// <summary>
    /// A cached reference to the <see cref="SemanticModel"/>.
    /// </summary>
    private WeakReference<SemanticModel>? _nullableDisabledModel;
 
    /// <summary>
    /// A cached task that can be returned once the tree has already been created. This is only set if <see cref="SupportsSyntaxTree"/> returns true,
    /// so the inner value can be non-null.
    /// </summary>
    private Task<SyntaxTree>? _syntaxTreeResultTask;
 
    internal Document(Project project, DocumentState state)
        : base(project, state, TextDocumentKind.Document)
    {
    }
 
    internal DocumentState DocumentState => (DocumentState)State;
 
    /// <summary>
    /// The kind of source code this document contains.
    /// </summary>
    public SourceCodeKind SourceCodeKind => DocumentState.SourceCodeKind;
 
    /// <summary>
    /// True if the info of the document change (name, folders, file path; not the content)
    /// </summary>
    internal override bool HasInfoChanged(TextDocument otherTextDocument)
    {
        var otherDocument = otherTextDocument as Document ??
            throw new ArgumentException($"{nameof(otherTextDocument)} isn't a regular document.", nameof(otherTextDocument));
 
        return base.HasInfoChanged(otherDocument) ||
               DocumentState.SourceCodeKind != otherDocument.SourceCodeKind;
    }
 
    [Obsolete("Use TextDocument.HasTextChanged")]
    internal bool HasTextChanged(Document otherDocument)
        => HasTextChanged(otherDocument, ignoreUnchangeableDocument: false);
 
    /// <summary>
    /// Get the current syntax tree for the document if the text is already loaded and the tree is already parsed.
    /// In almost all cases, you should call <see cref="GetSyntaxTreeAsync"/> to fetch the tree, which will parse the tree
    /// if it's not already parsed.
    /// </summary>
    public bool TryGetSyntaxTree([NotNullWhen(returnValue: true)] out SyntaxTree? syntaxTree)
    {
        // if we already have cache, use it
        if (_syntaxTreeResultTask != null)
        {
            syntaxTree = _syntaxTreeResultTask.Result;
            return true;
        }
 
        if (!DocumentState.TryGetSyntaxTree(out syntaxTree))
        {
            return false;
        }
 
        // cache the result if it is not already cached
        if (_syntaxTreeResultTask == null)
        {
            var result = Task.FromResult(syntaxTree);
            Interlocked.CompareExchange(ref _syntaxTreeResultTask, result, null);
        }
 
        return true;
    }
 
    /// <summary>
    /// Get the current syntax tree version for the document if the text is already loaded and the tree is already parsed.
    /// In almost all cases, you should call <see cref="GetSyntaxVersionAsync"/> to fetch the version, which will load the tree
    /// if it's not already available.
    /// </summary>
    public bool TryGetSyntaxVersion(out VersionStamp version)
    {
        version = default;
        if (!this.TryGetTextVersion(out var textVersion))
        {
            return false;
        }
 
        var projectVersion = this.Project.Version;
        version = textVersion.GetNewerVersion(projectVersion);
        return true;
    }
 
    /// <summary>
    /// Gets the version of the document's top level signature if it is already loaded and available.
    /// </summary>
    internal bool TryGetTopLevelChangeTextVersion(out VersionStamp version)
        => DocumentState.TryGetTopLevelChangeTextVersion(out version);
 
    /// <summary>
    /// Gets the version of the syntax tree. This is generally the newer of the text version and the project's version.
    /// </summary>
    public async Task<VersionStamp> GetSyntaxVersionAsync(CancellationToken cancellationToken = default)
    {
        var textVersion = await this.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
        var projectVersion = this.Project.Version;
        return textVersion.GetNewerVersion(projectVersion);
    }
 
    /// <summary>
    /// <see langword="true"/> if this Document supports providing data through the
    /// <see cref="GetSyntaxTreeAsync"/> and <see cref="GetSyntaxRootAsync"/> methods.
    ///
    /// If <see langword="false"/> then these methods will return <see langword="null"/> instead.
    /// </summary>
    public bool SupportsSyntaxTree => DocumentState.SupportsSyntaxTree;
 
    /// <summary>
    /// <see langword="true"/> if this Document supports providing data through the
    /// <see cref="GetSemanticModelAsync(CancellationToken)"/> method.
    ///
    /// If <see langword="false"/> then that method will return <see langword="null"/> instead.
    /// </summary>
    public bool SupportsSemanticModel
    {
        get
        {
            return this.SupportsSyntaxTree && this.Project.SupportsCompilation;
        }
    }
 
    /// <summary>
    /// Gets the <see cref="SyntaxTree" /> for this document asynchronously.
    /// </summary>
    /// <returns>
    /// The returned syntax tree can be <see langword="null"/> if the <see cref="SupportsSyntaxTree"/> returns <see
    /// langword="false"/>. This function may cause computation to occur the first time it is called, but will return
    /// a cached result every subsequent time.  <see cref="SyntaxTree"/>'s can hold onto their roots lazily. So calls 
    /// to <see cref="SyntaxTree.GetRoot"/> or <see cref="SyntaxTree.GetRootAsync"/> may end up causing computation
    /// to occur at that point.
    /// </returns>
    public Task<SyntaxTree?> GetSyntaxTreeAsync(CancellationToken cancellationToken = default)
    {
        // If the language doesn't support getting syntax trees for a document, then bail out immediately.
        if (!this.SupportsSyntaxTree)
        {
            return SpecializedTasks.Null<SyntaxTree>();
        }
 
        // if we have a cached result task use it
        if (_syntaxTreeResultTask != null)
        {
            return _syntaxTreeResultTask.AsNullable();
        }
 
        // check to see if we already have the tree before actually going async
        if (TryGetSyntaxTree(out var tree))
        {
            // stash a completed result task for this value for the next request (to reduce extraneous allocations of tasks)
            // don't use the actual async task because it depends on a specific cancellation token
            // its okay to cache the task and hold onto the SyntaxTree, because the DocumentState already keeps the SyntaxTree alive.
            Interlocked.CompareExchange(ref _syntaxTreeResultTask, Task.FromResult(tree), null);
 
            return _syntaxTreeResultTask.AsNullable();
        }
 
        // do it async for real.
        return DocumentState.GetSyntaxTreeAsync(cancellationToken).AsTask().AsNullable();
    }
 
    internal SyntaxTree? GetSyntaxTreeSynchronously(CancellationToken cancellationToken)
    {
        if (!this.SupportsSyntaxTree)
        {
            return null;
        }
 
        return DocumentState.GetSyntaxTree(cancellationToken);
    }
 
    /// <summary>
    /// Gets the root node of the current syntax tree if the syntax tree has already been parsed and the tree is still cached.
    /// In almost all cases, you should call <see cref="GetSyntaxRootAsync"/> to fetch the root node, which will parse
    /// the document if necessary.
    /// </summary>
    public bool TryGetSyntaxRoot([NotNullWhen(returnValue: true)] out SyntaxNode? root)
    {
        root = null;
        return this.TryGetSyntaxTree(out var tree) && tree.TryGetRoot(out root) && root != null;
    }
 
    /// <summary>
    /// Gets the root node of the syntax tree asynchronously.
    /// </summary>
    /// <returns>
    /// The returned <see cref="SyntaxNode"/> will be <see langword="null"/> if <see
    /// cref="SupportsSyntaxTree"/> returns <see langword="false"/>.  This function will return
    /// the same value if called multiple times.
    /// </returns>
    public async Task<SyntaxNode?> GetSyntaxRootAsync(CancellationToken cancellationToken = default)
    {
        if (!this.SupportsSyntaxTree)
        {
            return null;
        }
 
        var tree = (await this.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!;
        return await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Only for features that absolutely must run synchronously (probably because they're
    /// on the UI thread).  Right now, the only feature this is for is Outlining as VS will
    /// block on that feature from the UI thread when a document is opened.
    /// </summary>
    internal SyntaxNode? GetSyntaxRootSynchronously(CancellationToken cancellationToken)
    {
        if (!this.SupportsSyntaxTree)
        {
            return null;
        }
 
        var tree = this.GetSyntaxTreeSynchronously(cancellationToken)!;
        return tree.GetRoot(cancellationToken);
    }
 
    /// <summary>
    /// Gets the current semantic model for this document if the model is already computed and still cached.
    /// In almost all cases, you should call <see cref="GetSemanticModelAsync(CancellationToken)"/>, which will compute the semantic model
    /// if necessary.
    /// </summary>
    public bool TryGetSemanticModel([NotNullWhen(returnValue: true)] out SemanticModel? semanticModel)
    {
        semanticModel = null;
        return _model != null && _model.TryGetTarget(out semanticModel);
    }
 
    /// <summary>
    /// Gets the current nullable disabled semantic model for this document if the model is already computed and still cached.
    /// In almost all cases, you should call <see cref="GetSemanticModelAsync(CancellationToken)"/>, which will compute the semantic model
    /// if necessary.
    /// </summary>
    internal bool TryGetNullableDisabledSemanticModel([NotNullWhen(returnValue: true)] out SemanticModel? semanticModel)
    {
        semanticModel = null;
        return _nullableDisabledModel != null && _nullableDisabledModel.TryGetTarget(out semanticModel);
    }
 
    /// <summary>
    /// Gets the nullable disabled semantic model for this document asynchronously.
    /// </summary>
    /// <returns>
    /// The returned <see cref="SemanticModel"/> may be <see langword="null"/> if <see
    /// cref="SupportsSemanticModel"/> returns <see langword="false"/>. This function will
    /// return the same value if called multiple times.
    /// </returns>
#pragma warning disable RSEXPERIMENTAL001 // sym-shipped usage of experimental API
    internal async Task<SemanticModel?> GetSemanticModelAsync(SemanticModelOptions options, CancellationToken cancellationToken = default)
    {
        return await GetSemanticModelHelperAsync(
            disableNullableAnalysis: (options & SemanticModelOptions.DisableNullableAnalysis) == SemanticModelOptions.DisableNullableAnalysis,
            cancellationToken
        ).ConfigureAwait(false);
    }
#pragma warning restore RSEXPERIMENTAL001
 
    // If the API used in this method is made public, we can consider moving this method to DocumentExtensions.
    internal async ValueTask<SemanticModel> GetRequiredNullableDisabledSemanticModelAsync(CancellationToken cancellationToken)
    {
        if (this.TryGetNullableDisabledSemanticModel(out var semanticModel))
            return semanticModel;
 
#pragma warning disable RSEXPERIMENTAL001 // Sym-shipped usage of experimental API
        semanticModel = await this.GetSemanticModelAsync(SemanticModelOptions.DisableNullableAnalysis, cancellationToken).ConfigureAwait(false);
#pragma warning restore RSEXPERIMENTAL001
        return semanticModel ?? throw new InvalidOperationException(string.Format(WorkspaceExtensionsResources.SyntaxTree_is_required_to_accomplish_the_task_but_is_not_supported_by_document_0, this.Name));
    }
 
    /// <summary>
    /// Gets the semantic model for this document asynchronously.
    /// </summary>
    /// <returns>
    /// The returned <see cref="SemanticModel"/> may be <see langword="null"/> if <see
    /// cref="SupportsSemanticModel"/> returns <see langword="false"/>. This function will
    /// return the same value if called multiple times.
    /// </returns>
    public Task<SemanticModel?> GetSemanticModelAsync(CancellationToken cancellationToken = default)
    {
        return GetSemanticModelHelperAsync(disableNullableAnalysis: false, cancellationToken);
    }
 
    /// <summary>
    /// Gets the semantic model for this document asynchronously.
    /// </summary>
    /// <returns>
    /// The returned <see cref="SemanticModel"/> may be <see langword="null"/> if <see
    /// cref="SupportsSemanticModel"/> returns <see langword="false"/>. This function will
    /// return the same value if called multiple times.
    /// </returns>
    private async Task<SemanticModel?> GetSemanticModelHelperAsync(bool disableNullableAnalysis, CancellationToken cancellationToken)
    {
        if (!this.SupportsSemanticModel)
            return null;
 
        var semanticModel = await GetSemanticModelWorkerAsync().ConfigureAwait(false);
        this.Project.Solution.OnSemanticModelObtained(this.Id, semanticModel);
        return semanticModel;
 
        async Task<SemanticModel> GetSemanticModelWorkerAsync()
        {
            try
            {
                if (disableNullableAnalysis)
                {
                    if (this.TryGetNullableDisabledSemanticModel(out var semanticModel))
                        return semanticModel;
                }
                else
                {
                    if (this.TryGetSemanticModel(out var semanticModel))
                        return semanticModel;
                }
 
                var syntaxTree = await this.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var compilation = await this.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
 
#pragma warning disable RSEXPERIMENTAL001 // sym-shipped usage of experimental API
                var result = compilation.GetSemanticModel(syntaxTree, disableNullableAnalysis ? SemanticModelOptions.DisableNullableAnalysis : SemanticModelOptions.None);
#pragma warning restore RSEXPERIMENTAL001
                Contract.ThrowIfNull(result);
                var original = Interlocked.CompareExchange(ref disableNullableAnalysis ? ref _nullableDisabledModel : ref _model, new WeakReference<SemanticModel>(result), null);
 
                // okay, it is first time.
                if (original == null)
                    return result;
 
                // It looks like someone has set it. Try to reuse same semantic model, or assign the new model if that
                // fails. The lock is required since there is no compare-and-set primitive for WeakReference<T>.
                lock (original)
                {
                    if (original.TryGetTarget(out var semanticModel))
                        return semanticModel;
 
                    original.SetTarget(result);
                    return result;
                }
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }
    }
 
    /// <summary>
    /// Creates a new instance of this document updated to have the source code kind specified.
    /// </summary>
    public Document WithSourceCodeKind(SourceCodeKind kind)
        => this.Project.Solution.WithDocumentSourceCodeKind(this.Id, kind).GetRequiredDocument(Id);
 
    /// <summary>
    /// Creates a new instance of this document updated to have the text specified.
    /// </summary>
    public Document WithText(SourceText text)
        => this.Project.Solution.WithDocumentText(this.Id, text, PreservationMode.PreserveIdentity).GetRequiredDocument(Id);
 
    /// <summary>
    /// Creates a new instance of this document updated to have a syntax tree rooted by the specified syntax node.
    /// </summary>
    public Document WithSyntaxRoot(SyntaxNode root)
        => this.Project.Solution.WithDocumentSyntaxRoot(this.Id, root, PreservationMode.PreserveIdentity).GetRequiredDocument(Id);
 
    /// <summary>
    /// Creates a new instance of this document updated to have the specified name.
    /// </summary>
    public Document WithName(string name)
        => this.Project.Solution.WithDocumentName(this.Id, name).GetRequiredDocument(Id);
 
    /// <summary>
    /// Creates a new instance of this document updated to have the specified folders.
    /// </summary>
    public Document WithFolders(IEnumerable<string> folders)
        => this.Project.Solution.WithDocumentFolders(this.Id, folders).GetRequiredDocument(Id);
 
    /// <summary>
    /// Creates a new instance of this document updated to have the specified file path.
    /// </summary>
    public Document WithFilePath(string? filePath)
        => this.Project.Solution.WithDocumentFilePath(this.Id, filePath).GetRequiredDocument(Id);
 
    /// <summary>
    /// Get the text changes between this document and a prior version of the same document.
    /// The changes, when applied to the text of the old document, will produce the text of the current document.
    /// </summary>
    public async Task<IEnumerable<TextChange>> GetTextChangesAsync(Document oldDocument, CancellationToken cancellationToken = default)
    {
        try
        {
            using (Logger.LogBlock(FunctionId.Workspace_Document_GetTextChanges, this.Name, cancellationToken))
            {
                // no changes
                if (oldDocument == this)
                    return [];
 
                if (this.Id != oldDocument.Id)
                {
                    throw new ArgumentException(WorkspacesResources.The_specified_document_is_not_a_version_of_this_document);
                }
 
                // first try to see if text already knows its changes
                if (this.TryGetText(out var text) && oldDocument.TryGetText(out var oldText))
                {
                    if (text == oldText)
                        return [];
 
                    var container = text.Container;
                    if (container != null)
                    {
                        var textChanges = text.GetTextChanges(oldText).ToList();
 
                        // if changes are significant (not the whole document being replaced) then use these changes
                        if (textChanges.Count > 1 || (textChanges.Count == 1 && textChanges[0].Span != new TextSpan(0, oldText.Length)))
                        {
                            return textChanges;
                        }
                    }
                }
 
                // get changes by diffing the trees
                if (this.SupportsSyntaxTree)
                {
                    var tree = (await this.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!;
                    var oldTree = await oldDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
                    RoslynDebug.Assert(oldTree is object);
                    return tree.GetChanges(oldTree);
                }
 
                text = await this.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
                return text.GetTextChanges(oldText).ToList();
            }
        }
        catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
        {
            throw ExceptionUtilities.Unreachable();
        }
    }
 
    /// <summary>
    /// Gets the list of <see cref="DocumentId"/>s that are linked to this
    /// <see cref="Document" />. <see cref="Document"/>s are considered to be linked if they
    /// share the same <see cref="TextDocument.FilePath" />. This <see cref="DocumentId"/> is excluded from the
    /// result.
    /// </summary>
    public ImmutableArray<DocumentId> GetLinkedDocumentIds()
    {
        var filteredDocumentIds = this.Project.Solution.GetRelatedDocumentIds(this.Id);
        return filteredDocumentIds.Remove(this.Id);
    }
 
    /// <summary>
    /// Creates a branched version of this document that has its semantic model frozen in whatever state it is available at the time,
    /// assuming a background process is constructing the semantics asynchronously. Repeated calls to this method may return
    /// documents with increasingly more complete semantics.
    /// <para/>
    /// Use this method to gain access to potentially incomplete semantics quickly.
    /// <para/> Note: this will give back a solution where this <see cref="Document"/>'s project will not run
    /// generators when getting its compilation.  However, all other projects will still run generators when their
    /// compilations are requested.
    /// </summary>
    internal virtual Document WithFrozenPartialSemantics(CancellationToken cancellationToken)
    {
        var solution = this.Project.Solution;
 
        // only produce doc with frozen semantics if this workspace has support for that, as without
        // background compilation the semantics won't be moving toward completeness.  Also,
        // ensure that the project that this document is part of actually supports compilations,
        // as partial semantics don't make sense otherwise.
        if (solution.PartialSemanticsEnabled &&
            this.Project.SupportsCompilation)
        {
            var newSolution = this.Project.Solution.WithFrozenPartialCompilationIncludingSpecificDocument(this.Id, cancellationToken);
            return newSolution.GetRequiredDocument(this.Id);
        }
        else
        {
            return this;
        }
    }
 
    private string GetDebuggerDisplay()
        => this.Name;
 
#pragma warning disable RS0030 // Do not used banned APIs (backwards compat)
    private AsyncLazy<DocumentOptionSet>? _cachedOptions;
 
    /// <summary>
    /// Returns the options that should be applied to this document. This consists of global options from <see cref="Solution.Options"/>,
    /// merged with any settings the user has specified at the document levels.
    /// </summary>
    /// <remarks>
    /// This method is async because this may require reading other files. In files that are already open, this is expected to be cheap and complete synchronously.
    /// </remarks>
    [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", AllowCaptures = false)]
    public Task<DocumentOptionSet> GetOptionsAsync(CancellationToken cancellationToken = default)
    {
        if (_cachedOptions == null)
        {
            InitializeCachedOptions(Project.Solution.Options);
        }
 
        Contract.ThrowIfNull(_cachedOptions);
        return _cachedOptions.GetValueAsync(cancellationToken);
    }
 
    private void InitializeCachedOptions(OptionSet solutionOptions)
    {
        var newAsyncLazy = AsyncLazy.Create(static async (arg, cancellationToken) =>
        {
            var options = await arg.self.GetAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false);
            return new DocumentOptionSet(options, arg.solutionOptions, arg.self.Project.Language);
        },
        arg: (self: this, solutionOptions));
 
        Interlocked.CompareExchange(ref _cachedOptions, newAsyncLazy, comparand: null);
    }
#pragma warning restore
 
    internal async ValueTask<StructuredAnalyzerConfigOptions> GetAnalyzerConfigOptionsAsync(CancellationToken cancellationToken)
    {
        var provider = (ProjectState.ProjectAnalyzerConfigOptionsProvider)Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider;
        return await provider.GetOptionsAsync(DocumentState, cancellationToken).ConfigureAwait(false);
    }
 
    internal ValueTask<ImmutableArray<byte>> GetContentHashAsync(CancellationToken cancellationToken)
        => this.DocumentState.GetContentHashAsync(cancellationToken);
}