|
// 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;
using Microsoft.CodeAnalysis.PooledObjects;
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
{
// Walk up the ancestor nodes of currentBodyNode, finding child indexes up to the root.
using var _ = ArrayBuilder<int>.GetInstance(out var indexPath);
GetNodeChildIndexPathToRootReversed(currentBodyNode, indexPath);
// Then use those indexes to walk back down the previous tree to find the equivalent node.
var previousNode = previousRoot;
for (var i = indexPath.Count - 1; i >= 0; i--)
{
var childIndex = indexPath[i];
var children = previousNode.ChildNodesAndTokens();
if (children.Count <= childIndex)
{
Debug.Fail("Member count shouldn't have changed as there were no top level edits.");
return null;
}
var childAsNode = children[childIndex].AsNode();
if (childAsNode is null)
{
Debug.Fail("Child at indicated index should be a node as there were no top level edits.");
return null;
}
previousNode = childAsNode;
}
return previousNode;
}
}
private static void GetNodeChildIndexPathToRootReversed(SyntaxNode node, ArrayBuilder<int> path)
{
var current = node;
var parent = current.Parent;
while (parent != null)
{
var childIndex = 0;
foreach (var child in parent.ChildNodesAndTokens())
{
if (child.AsNode() == current)
{
path.Add(childIndex);
break;
}
childIndex++;
}
current = parent;
parent = current.Parent;
}
}
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;
}
}
}
|