File: Workspace\Solution\DocumentState_LinkedFileReuse.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.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal partial class DocumentState
    {
        /// <summary>
        /// Returns a new instance of this document state that points to <paramref name="siblingTextSource"/> as the
        /// text contents of the document, and which will produce a syntax tree that reuses from <paramref
        /// name="siblingTextSource"/> if possible, or which will incrementally parse the current tree to bring it up to
        /// date with <paramref name="siblingTextSource"/> otherwise.
        /// </summary>
        public DocumentState UpdateTextAndTreeContents(ITextAndVersionSource siblingTextSource, ValueSource<TreeAndVersion>? siblingTreeSource)
        {
            if (!SupportsSyntaxTree)
            {
                return new DocumentState(
                    LanguageServices,
                    Services,
                    Attributes,
                    _options,
                    siblingTextSource,
                    LoadTextOptions,
                    treeSource: null);
            }
 
            Contract.ThrowIfNull(siblingTreeSource);
 
            // Always pass along the sibling text.  We will always be in sync with that.
 
            // if a sibling tree source is provided, then we'll want to attempt to use the tree it creates, to share as
            // much memory as possible with linked files.  However, we can't point at that source directly.  If we did,
            // we'd produce the *exact* same tree-reference as another file.  That would be bad as it would break the
            // invariant that each document gets a unique SyntaxTree.  So, instead, we produce a ValueSource that defers
            // to the provided source, gets the tree from it, and then wraps its root in a new tree for us.
 
            // copy data from this entity, and pass to static helper, so we don't keep this green node alive.
 
            var filePath = this.Attributes.SyntaxTreeFilePath;
            var languageServices = this.LanguageServices;
            var loadTextOptions = this.LoadTextOptions;
            var parseOptions = this.ParseOptions;
            var textAndVersionSource = this.TextAndVersionSource;
            var treeSource = this.TreeSource;
 
            var newTreeSource = GetReuseTreeSource(
                filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource);
 
            return new DocumentState(
                languageServices,
                Services,
                Attributes,
                _options,
                siblingTextSource,
                LoadTextOptions,
                newTreeSource);
 
            static AsyncLazy<TreeAndVersion> GetReuseTreeSource(
                string filePath,
                LanguageServices languageServices,
                LoadTextOptions loadTextOptions,
                ParseOptions parseOptions,
                ValueSource<TreeAndVersion> treeSource,
                ITextAndVersionSource siblingTextSource,
                ValueSource<TreeAndVersion> siblingTreeSource)
            {
                return new AsyncLazy<TreeAndVersion>(
                    cancellationToken => TryReuseSiblingTreeAsync(filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, cancellationToken),
                    cancellationToken => TryReuseSiblingTree(filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, cancellationToken),
                    cacheResult: true);
            }
 
            static bool TryReuseSiblingRoot(
                string filePath,
                LanguageServices languageServices,
                LoadTextOptions loadTextOptions,
                ParseOptions parseOptions,
                SyntaxNode siblingRoot,
                VersionStamp siblingVersion,
                [NotNullWhen(true)] out TreeAndVersion? newTreeAndVersion)
            {
                var siblingTree = siblingRoot.SyntaxTree;
 
                // Look for things that disqualify us from being able to use our sibling's root.
                if (!CanReuseSiblingRoot())
                {
                    newTreeAndVersion = null;
                    return false;
                }
 
                var treeFactory = languageServices.GetRequiredService<ISyntaxTreeFactoryService>();
 
                // Note: passing along siblingTree.Encoding is a bit suspect.  Ideally we would only populate this tree
                // with our own data (*except* for the new root).  However, we think it's safe as the encoding really is
                // a property of the file, and that should stay the same even if linked into multiple projects.
 
                var newTree = treeFactory.CreateSyntaxTree(
                    filePath,
                    parseOptions,
                    siblingTree.Encoding,
                    loadTextOptions.ChecksumAlgorithm,
                    siblingRoot);
 
                newTreeAndVersion = new TreeAndVersion(newTree, siblingVersion);
                return true;
 
                // Determines if the root of a tree from a different project can be used in this project.  The general
                // intuition (explained below) is that files without `#if` directives in them can be reused as the parse
                // trees will be the same.
                //
                // This is *technically* not completely accurate as language-version can affect the parse tree as well.
                // For example, `record X() { }` is a method prior to the addition of records to the language.  However,
                // in practice this should not be an issue.  Specifically, either user code does not have a construct
                // like this, in which case they are not affected by sharing.  *Or*, they do have such a construct, but
                // are being deliberately pathological.  In other words, there are no realistic programs that depend on
                // having one interpretation in one version, and another interpretation in another version.  So we are
                // ok saying we don't care about having that not work in the IDE (it will still work fine in the
                // compiler).
                //
                // Note: we deliberately do not look at language version because it often is different across project
                // flavors.  So we would often get no benefit to sharing if we restricted to only when the lang version
                // is the same.
                bool CanReuseSiblingRoot()
                {
                    Interlocked.Increment(ref s_tryReuseSyntaxTree);
                    var siblingParseOptions = siblingTree.Options;
 
                    var ppSymbolsNames1 = parseOptions.PreprocessorSymbolNames;
                    var ppSymbolsNames2 = siblingParseOptions.PreprocessorSymbolNames;
 
                    // If both documents have the same preprocessor directives defined, then they'll always produce the
                    // same trees.  So we can trivially reuse the tree from one for the other.
                    if (ppSymbolsNames1.SetEquals(ppSymbolsNames2))
                    {
                        Interlocked.Increment(ref s_couldReuseBecauseOfEqualPPNames);
                        return true;
                    }
 
                    // If the tree contains no `#` directives whatsoever, then you'll parse out the same tree and can reuse it.
                    if (!siblingRoot.ContainsDirectives)
                    {
                        Interlocked.Increment(ref s_couldReuseBecauseOfNoDirectives);
                        return true;
                    }
 
                    // It's ok to contain directives like #nullable, or #region.  They don't affect parsing.  But if we have a
                    // `#if` we can't share as each side might parse this differently.
                    var syntaxKinds = languageServices.GetRequiredService<ISyntaxKindsService>();
                    if (!siblingRoot.ContainsDirective(syntaxKinds.IfDirectiveTrivia))
                    {
                        Interlocked.Increment(ref s_couldReuseBecauseOfNoPPDirectives);
                        return true;
                    }
 
                    // If the tree contains a #if directive, and the pp-symbol-names are different, then the files
                    // absolutely may be parsed differently, and so they should not be shared.
                    //
                    // TODO(cyrusn): We could potentially be smarter here as well.  We can check what pp-symbols the file
                    // actually uses. (e.g. 'DEBUG'/'NETCORE'/etc.) and see if the project parse options actually differ
                    // for these values.  If not, we could reuse the trees even then.
                    Interlocked.Increment(ref s_couldNotReuse);
                    return false;
                }
            }
 
            static async Task<TreeAndVersion> TryReuseSiblingTreeAsync(
                string filePath,
                LanguageServices languageServices,
                LoadTextOptions loadTextOptions,
                ParseOptions parseOptions,
                ValueSource<TreeAndVersion> treeSource,
                ITextAndVersionSource siblingTextSource,
                ValueSource<TreeAndVersion> siblingTreeSource,
                CancellationToken cancellationToken)
            {
                var siblingTreeAndVersion = await siblingTreeSource.GetValueAsync(cancellationToken).ConfigureAwait(false);
                var siblingTree = siblingTreeAndVersion.Tree;
 
                var siblingRoot = await siblingTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
 
                if (TryReuseSiblingRoot(filePath, languageServices, loadTextOptions, parseOptions, siblingRoot, siblingTreeAndVersion.Version, out var newTreeAndVersion))
                    return newTreeAndVersion;
 
                // Couldn't use the sibling file to get the tree contents.  Instead, incrementally parse our tree to the text passed in.
                return await IncrementallyParseTreeAsync(treeSource, siblingTextSource, loadTextOptions, cancellationToken).ConfigureAwait(false);
            }
 
            static TreeAndVersion TryReuseSiblingTree(
                string filePath,
                LanguageServices languageServices,
                LoadTextOptions loadTextOptions,
                ParseOptions parseOptions,
                ValueSource<TreeAndVersion> treeSource,
                ITextAndVersionSource siblingTextSource,
                ValueSource<TreeAndVersion> siblingTreeSource,
                CancellationToken cancellationToken)
            {
                var siblingTreeAndVersion = siblingTreeSource.GetValue(cancellationToken);
                var siblingTree = siblingTreeAndVersion.Tree;
 
                var siblingRoot = siblingTree.GetRoot(cancellationToken);
 
                if (TryReuseSiblingRoot(filePath, languageServices, loadTextOptions, parseOptions, siblingRoot, siblingTreeAndVersion.Version, out var newTreeAndVersion))
                    return newTreeAndVersion;
 
                // Couldn't use the sibling file to get the tree contents.  Instead, incrementally parse our tree to the text passed in.
                return IncrementallyParseTree(treeSource, siblingTextSource, loadTextOptions, cancellationToken);
            }
        }
 
        // Values just kept around for benchmark tests.
        private static int s_tryReuseSyntaxTree;
        private static int s_couldNotReuse;
        private static int s_couldReuseBecauseOfEqualPPNames;
        private static int s_couldReuseBecauseOfNoDirectives;
        private static int s_couldReuseBecauseOfNoPPDirectives;
 
        public struct TestAccessor
        {
            public static int TryReuseSyntaxTree => s_tryReuseSyntaxTree;
            public static int CouldNotReuse => s_couldNotReuse;
            public static int CouldReuseBecauseOfEqualPPNames => s_couldReuseBecauseOfEqualPPNames;
            public static int CouldReuseBecauseOfNoDirectives => s_couldReuseBecauseOfNoDirectives;
            public static int CouldReuseBecauseOfNoPPDirectives => s_couldReuseBecauseOfNoPPDirectives;
        }
    }
}