File: SolutionCrawler\AbstractDocumentDifferenceService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.SolutionCrawler;
 
internal abstract class AbstractDocumentDifferenceService : IDocumentDifferenceService
{
    public async Task<SyntaxNode?> GetChangedMemberAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken)
    {
        try
        {
            var syntaxFactsService = newDocument.Project.Services.GetService<ISyntaxFactsService>();
            if (syntaxFactsService == null)
            {
                // somehow, we can't get the service. without it, there is nothing we can do.
                return null;
            }
            // this is based on the implementation detail where opened documents use strong references
            // to tree and text rather than recoverable versions.
            if (!oldDocument.TryGetText(out var oldText) ||
                !newDocument.TryGetText(out var newText))
            {
                // no cheap way to determine top level changes. assumes top level has changed
                return null;
            }
            // quick check whether two tree versions are same
            if (oldDocument.TryGetSyntaxVersion(out var oldVersion) &&
                newDocument.TryGetSyntaxVersion(out var newVersion) &&
                oldVersion.Equals(newVersion))
            {
                // nothing has changed. don't do anything.
                // this could happen if a document is opened/closed without any buffer change
                return null;
            }
 
            var range = newText.GetEncompassingTextChangeRange(oldText);
            if (range == default)
            {
                // nothing has changed. don't do anything
                return null;
            }
 
            var incrementalParsingCandidate = range.NewLength != newText.Length;
            // see whether we can get it without explicit parsing
            if (!oldDocument.TryGetSyntaxRoot(out var oldRoot) ||
                !newDocument.TryGetSyntaxRoot(out var newRoot))
            {
                if (!incrementalParsingCandidate)
                {
                    // no cheap way to determine top level changes. assumes top level has changed
                    return null;
                }
 
                // explicitly parse them
                oldRoot = await oldDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
                Contract.ThrowIfNull(oldRoot);
                Contract.ThrowIfNull(newRoot);
            }
 
            // at this point, we must have these version already calculated
            if (!oldDocument.TryGetTopLevelChangeTextVersion(out var oldTopLevelChangeVersion) ||
                !newDocument.TryGetTopLevelChangeTextVersion(out var newTopLevelChangeVersion))
            {
                throw ExceptionUtilities.Unreachable();
            }
 
            // quicker common case
            if (incrementalParsingCandidate)
            {
                if (oldTopLevelChangeVersion.Equals(newTopLevelChangeVersion))
                {
                    return GetChangedMember(syntaxFactsService, oldRoot, newRoot, range);
                }
 
                return GetBestGuessChangedMember(syntaxFactsService, oldRoot, newRoot, range);
            }
 
            if (oldTopLevelChangeVersion.Equals(newTopLevelChangeVersion))
            {
                return null;
            }
 
            return null;
        }
        catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
        {
            throw ExceptionUtilities.Unreachable();
        }
    }
 
    private static SyntaxNode? GetChangedMember(
        ISyntaxFactsService syntaxFactsService, SyntaxNode oldRoot, SyntaxNode newRoot, TextChangeRange range)
    {
        // if either old or new tree contains skipped text, re-analyze whole document
        if (oldRoot.ContainsSkippedText || newRoot.ContainsSkippedText)
        {
            return null;
        }
 
        var oldMember = syntaxFactsService.GetContainingMemberDeclaration(oldRoot, range.Span.Start);
        var newMember = syntaxFactsService.GetContainingMemberDeclaration(newRoot, range.Span.Start);
 
        // reached the top (compilation unit)
        if (oldMember == null || newMember == null)
        {
            return null;
        }
 
        // member doesn't contain the change
        if (!syntaxFactsService.ContainsInMemberBody(oldMember, range.Span))
        {
            return null;
        }
 
        // member signature has changed
        if (!oldMember.IsEquivalentTo(newMember, topLevel: true))
        {
            return null;
        }
 
        // looks like inside of the body has changed
        return newMember;
    }
 
    private static SyntaxNode? GetBestGuessChangedMember(
        ISyntaxFactsService syntaxFactsService, SyntaxNode oldRoot, SyntaxNode newRoot, TextChangeRange range)
    {
        // if either old or new tree contains skipped text, re-analyze whole document
        if (oldRoot.ContainsSkippedText || newRoot.ContainsSkippedText)
        {
            return null;
        }
 
        // there was top level changes, so we can't use equivalent to see whether two members are same.
        // so, we use some simple text based heuristic to find a member that has changed.
        //
        // if we have a differ that do diff on member level or a way to track member between incremental parsing, then
        // that would be preferable. but currently we don't have such thing.
 
        // get top level elements at the position where change has happened
        var oldMember = syntaxFactsService.GetContainingMemberDeclaration(oldRoot, range.Span.Start);
        var newMember = syntaxFactsService.GetContainingMemberDeclaration(newRoot, range.Span.Start);
 
        // reached the top (compilation unit)
        if (oldMember == null || newMember == null)
        {
            return null;
        }
 
        // if old member was empty, just use new member
        if (oldMember.Span.IsEmpty)
        {
            return newMember;
        }
 
        // looks like change doesn't belong to existing member
        if (!oldMember.Span.Contains(range.Span))
        {
            return null;
        }
 
        // change happened inside of the old member, check whether new member seems just delta of that change
        var lengthDelta = range.NewLength - range.Span.Length;
 
        return (oldMember.Span.Length + lengthDelta) == newMember.Span.Length ? newMember : null;
    }
}