File: SemanticModelReuse\AbstractSemanticModelReuseLanguageService.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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
 
namespace Microsoft.CodeAnalysis.SemanticModelReuse;
 
internal abstract class AbstractSemanticModelReuseLanguageService<
    TMemberDeclarationSyntax,
    TBasePropertyDeclarationSyntax,
    TAccessorDeclarationSyntax> : ISemanticModelReuseLanguageService, IDisposable
    where TMemberDeclarationSyntax : SyntaxNode
    where TBasePropertyDeclarationSyntax : TMemberDeclarationSyntax
    where TAccessorDeclarationSyntax : SyntaxNode
{
    private readonly CountLogAggregator<bool> _logAggregator = new();
 
    protected abstract ISyntaxFacts SyntaxFacts { get; }
 
    public abstract SyntaxNode? TryGetContainingMethodBodyForSpeculation(SyntaxNode node);
 
    protected abstract SemanticModel? TryGetSpeculativeSemanticModelWorker(SemanticModel previousSemanticModel, SyntaxNode previousBodyNode, SyntaxNode currentBodyNode);
    protected abstract SyntaxList<TAccessorDeclarationSyntax> GetAccessors(TBasePropertyDeclarationSyntax baseProperty);
    protected abstract TBasePropertyDeclarationSyntax GetBasePropertyDeclaration(TAccessorDeclarationSyntax accessor);
 
    public void Dispose()
    {
        Logger.Log(FunctionId.SemanticModelReuseLanguageService_TryGetSpeculativeSemanticModelAsync_Equivalent, KeyValueLogMessage.Create(m =>
        {
            foreach (var kv in _logAggregator)
                m[kv.Key.ToString()] = kv.Value.GetCount();
        }));
    }
 
    public async Task<SemanticModel?> TryGetSpeculativeSemanticModelAsync(SemanticModel previousSemanticModel, SyntaxNode currentBodyNode, CancellationToken cancellationToken)
    {
        var previousSyntaxTree = previousSemanticModel.SyntaxTree;
        var currentSyntaxTree = currentBodyNode.SyntaxTree;
 
        // This operation is only valid if top-level equivalent trees were passed in.  If they're not equivalent
        // then something very bad happened as we did that document.Project.GetDependentSemanticVersionAsync was
        // still the same.  Log information so we can be alerted if this isn't being as successful as we expect.
        var isEquivalentTo = previousSyntaxTree.IsEquivalentTo(currentSyntaxTree, topLevel: true);
        _logAggregator.IncreaseCount(isEquivalentTo);
 
        if (!isEquivalentTo)
            return null;
 
        var previousRoot = await previousSemanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var currentRoot = await currentBodyNode.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var previousBodyNode = GetPreviousBodyNode(previousRoot, currentRoot, currentBodyNode);
 
        // Trivia is ignore when comparing two trees for equivalence at top level, since it has no effect to API shape
        // and it'd be safe to drop in the new method body as long as the shape doesn't change. However, trivia changes
        // around the method do make it tricky to decide whether a position is safe for speculation.
 
        // class C { void M() { return; } }";
        //                    ^ this is the position used to set OriginalPositionForSpeculation when creating the speculative model.
        //
        // class C {            void M() { return null; } }";
        //                               ^ it's unsafe to use the speculative model at this position, even though it's part of the
        //                                 method body and after OriginalPositionForSpeculation. 
 
        // Given that the common use case for us is continuously editing/typing inside a method body, we believe we can be conservative
        // in creating speculative model with those kind of trivia change, by requiring the method body block not to shift position,
        // w/o sacrificing performance in those common scenarios.
        if (previousBodyNode.SpanStart != currentBodyNode.SpanStart)
            return null;
 
        return TryGetSpeculativeSemanticModelWorker(previousSemanticModel, previousBodyNode, currentBodyNode);
    }
 
    protected SyntaxNode GetPreviousBodyNode(SyntaxNode previousRoot, SyntaxNode currentRoot, SyntaxNode currentBodyNode)
    {
        if (currentBodyNode is TAccessorDeclarationSyntax currentAccessor)
        {
            // in the case of an accessor, have to find the previous accessor in the previous prop/event corresponding
            // to the current prop/event.
 
            var currentContainer = GetBasePropertyDeclaration(currentAccessor);
            var previousContainer = GetPreviousBodyNode(previousRoot, currentRoot, currentContainer);
 
            if (previousContainer is not TBasePropertyDeclarationSyntax previousMember)
            {
                Debug.Fail("Previous container didn't map back to a normal accessor container.");
                return null;
            }
 
            var currentAccessors = GetAccessors(currentContainer);
            var previousAccessors = GetAccessors(previousMember);
 
            if (currentAccessors.Count != previousAccessors.Count)
            {
                Debug.Fail("Accessor count shouldn't have changed as there were no top level edits.");
                return null;
            }
 
            return previousAccessors[currentAccessors.IndexOf(currentAccessor)];
        }
        else
        {
            using var pooledCurrentMembers = this.SyntaxFacts.GetMethodLevelMembers(currentRoot);
            var currentMembers = pooledCurrentMembers.Object;
 
            var index = currentMembers.IndexOf(currentBodyNode);
            if (index < 0)
            {
                Debug.Fail($"Unhandled member type in {nameof(GetPreviousBodyNode)}");
                return null;
            }
 
            using var pooledPreviousMembers = this.SyntaxFacts.GetMethodLevelMembers(previousRoot);
            var previousMembers = pooledPreviousMembers.Object;
 
            if (currentMembers.Count != previousMembers.Count)
            {
                Debug.Fail("Member count shouldn't have changed as there were no top level edits.");
                return null;
            }
 
            return previousMembers[index];
        }
    }
 
    private sealed class NonEquivalentTreeException : Exception
    {
        // Used for analyzing dumps
#pragma warning disable IDE0052 // Remove unread private members
        private readonly SyntaxTree _originalSyntaxTree;
        private readonly SyntaxTree _updatedSyntaxTree;
#pragma warning restore IDE0052 // Remove unread private members
 
        public NonEquivalentTreeException(string message, SyntaxTree originalSyntaxTree, SyntaxTree updatedSyntaxTree)
            : base(message)
        {
            _originalSyntaxTree = originalSyntaxTree;
            _updatedSyntaxTree = updatedSyntaxTree;
        }
    }
}