File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\Extensions\SyntaxEditorExtensions.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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Shared.Extensions;
 
internal static class SyntaxEditorExtensions
{
    /// <summary>
    /// Performs several edits to a document.  If multiple edits are made within the same
    /// expression context, then the document/semantic-model will be forked after each edit 
    /// so that further edits can see if they're still safe to apply.
    /// </summary>
    public static Task ApplyExpressionLevelSemanticEditsAsync<TType, TNode>(
        this SyntaxEditor editor, Document document,
        ImmutableArray<TType> originalNodes,
        Func<TType, (TNode semanticNode, IEnumerable<TNode> additionalNodes)> selector,
        Func<SemanticModel, TType, TNode, bool> canReplace,
        Func<SemanticModel, SyntaxNode, TType, TNode, SyntaxNode> updateRoot,
        CancellationToken cancellationToken) where TNode : SyntaxNode
    {
        return ApplySemanticEditsAsync(
            editor, document,
            originalNodes,
            selector,
            GetExpressionSemanticBoundary,
            canReplace,
            updateRoot,
            cancellationToken);
    }
 
    /// <summary>
    /// Performs several edits to a document.  If multiple edits are made within the same
    /// expression context, then the document/semantic-model will be forked after each edit 
    /// so that further edits can see if they're still safe to apply.
    /// </summary>
    public static Task ApplyExpressionLevelSemanticEditsAsync<TType, TNode>(
        this SyntaxEditor editor, Document document,
        ImmutableArray<TType> originalNodes,
        Func<TType, TNode> selector,
        Func<SemanticModel, TType, TNode, bool> canReplace,
        Func<SemanticModel, SyntaxNode, TType, TNode, SyntaxNode> updateRoot,
        CancellationToken cancellationToken) where TNode : SyntaxNode
    {
        return ApplySemanticEditsAsync(
            editor, document,
            originalNodes,
            t => (selector(t), Enumerable.Empty<TNode>()),
            GetExpressionSemanticBoundary,
            canReplace,
            updateRoot,
            cancellationToken);
    }
 
    /// <summary>
    /// Performs several edits to a document.  If multiple edits are made within the same
    /// expression context, then the document/semantic-model will be forked after each edit 
    /// so that further edits can see if they're still safe to apply.
    /// </summary>
    public static Task ApplyExpressionLevelSemanticEditsAsync<TNode>(
        this SyntaxEditor editor, Document document,
        ImmutableArray<TNode> originalNodes,
        Func<SemanticModel, TNode, bool> canReplace,
        Func<SemanticModel, SyntaxNode, TNode, SyntaxNode> updateRoot,
        CancellationToken cancellationToken) where TNode : SyntaxNode
    {
        return ApplyExpressionLevelSemanticEditsAsync(
            editor, document,
            originalNodes,
            t => (t, Enumerable.Empty<TNode>()),
            (semanticModel, _, node) => canReplace(semanticModel, node),
            (semanticModel, currentRoot, _, node) => updateRoot(semanticModel, currentRoot, node),
            cancellationToken);
    }
 
    /// <summary>
    /// Performs several edits to a document.  If multiple edits are made within a method
    /// body then the document/semantic-model will be forked after each edit so that further
    /// edits can see if they're still safe to apply.
    /// </summary>
    public static Task ApplyMethodBodySemanticEditsAsync<TType, TNode>(
        this SyntaxEditor editor, Document document,
        ImmutableArray<TType> originalNodes,
        Func<TType, (TNode semanticNode, IEnumerable<TNode> additionalNodes)> selector,
        Func<SemanticModel, TType, TNode, bool> canReplace,
        Func<SemanticModel, SyntaxNode, TType, TNode, SyntaxNode> updateRoot,
        CancellationToken cancellationToken) where TNode : SyntaxNode
    {
        return ApplySemanticEditsAsync(
            editor, document,
            originalNodes,
            selector,
            GetMethodBodySemanticBoundary,
            canReplace,
            updateRoot,
            cancellationToken);
    }
 
    /// <summary>
    /// Performs several edits to a document.  If multiple edits are made within a method
    /// body then the document/semantic-model will be forked after each edit so that further
    /// edits can see if they're still safe to apply.
    /// </summary>
    public static Task ApplyMethodBodySemanticEditsAsync<TNode>(
        this SyntaxEditor editor, Document document,
        ImmutableArray<TNode> originalNodes,
        Func<SemanticModel, TNode, bool> canReplace,
        Func<SemanticModel, SyntaxNode, TNode, SyntaxNode> updateRoot,
        CancellationToken cancellationToken) where TNode : SyntaxNode
    {
        return ApplyMethodBodySemanticEditsAsync(
            editor, document,
            originalNodes,
            t => (t, Enumerable.Empty<TNode>()),
            (semanticModel, node, _) => canReplace(semanticModel, node),
            (semanticModel, currentRoot, _, node) => updateRoot(semanticModel, currentRoot, node),
            cancellationToken);
    }
 
    /// <summary>
    /// Helper function for fix-all fixes where individual fixes may affect the viability
    /// of another.  For example, consider the following code:
    /// 
    ///     if ((double)x == (double)y)
    ///     
    /// In this code either cast can be removed, but at least one cast must remain.  Even
    /// though an analyzer marks both, a fixer must not remove both.  One way to accomplish
    /// this would be to have the fixer do a semantic check after each application.  However
    /// This is extremely expensive, especially for hte common cases where one fix does
    /// not affect each other.
    /// 
    /// To address that, this helper groups fixes at certain boundary points.  i.e. at 
    /// statement boundaries.  If there is only one fix within the boundary, it does not
    /// do any semantic verification.  However, if there are multiple fixes in a boundary
    /// it will call into <paramref name="canReplace"/> to validate if the subsequent fix
    /// can be made or not.
    /// </summary>
    private static async Task ApplySemanticEditsAsync<TType, TNode>(
        this SyntaxEditor editor, Document document,
        ImmutableArray<TType> originalNodes,
        Func<TType, (TNode semanticNode, IEnumerable<TNode> additionalNodes)> selector,
        Func<ISyntaxFactsService, SyntaxNode, SyntaxNode> getSemanticBoundary,
        Func<SemanticModel, TType, TNode, bool> canReplace,
        Func<SemanticModel, SyntaxNode, TType, TNode, SyntaxNode> updateRoot,
        CancellationToken cancellationToken) where TNode : SyntaxNode
    {
        IEnumerable<(TType instance, (TNode semanticNode, IEnumerable<TNode> additionalNodes) nodes)> originalNodePairs = originalNodes.Select(n => (n, selector(n)));
 
        // This code fix will not make changes that affect the semantics of a statement
        // or declaration. Therefore, we can skip the expensive verification step in 
        // cases where only one expression appears within the group.
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        var nodesBySemanticBoundary = originalNodePairs.GroupBy(pair => getSemanticBoundary(syntaxFacts, pair.nodes.semanticNode));
        var nodesToVerify = nodesBySemanticBoundary.Where(group => group.Skip(1).Any()).Flatten().ToSet();
 
        // We're going to be continually editing this tree.  Track all the nodes we
        // care about so we can find them across each edit.
        var originalRoot = editor.OriginalRoot;
        document = document.WithSyntaxRoot(originalRoot.TrackNodes(originalNodePairs.SelectMany(pair => pair.nodes.additionalNodes.Concat(pair.nodes.semanticNode))));
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var currentRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        foreach (var nodePair in originalNodePairs)
        {
            var (instance, (node, _)) = nodePair;
            var currentNode = currentRoot.GetCurrentNode(node);
            var skipVerification = !nodesToVerify.Contains(nodePair);
 
            if (skipVerification || canReplace(semanticModel, instance, currentNode))
            {
                var replacementRoot = updateRoot(semanticModel, currentRoot, instance, currentNode);
 
                document = document.WithSyntaxRoot(replacementRoot);
 
                semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                currentRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            }
        }
 
        editor.ReplaceNode(originalRoot, currentRoot);
    }
 
    private static SyntaxNode GetExpressionSemanticBoundary(ISyntaxFactsService syntaxFacts, SyntaxNode node)
    {
        // Notes:
        // 1. Syntax which doesn't fall into one of the "safe buckets" will get placed into a 
        //    single group keyed off the root of the tree. If more than one such node exists
        //    in the document, all will be verified.
        // 2. Cannot include ArgumentSyntax because it could affect generic argument inference.
        return node.FirstAncestorOrSelf<SyntaxNode, ISyntaxFactsService>(
            (n, syntaxFacts) => syntaxFacts.IsExecutableStatement(n) ||
                 syntaxFacts.IsParameter(n) ||
                 syntaxFacts.IsVariableDeclarator(n) ||
                 n.Parent == null,
            syntaxFacts);
    }
 
    private static SyntaxNode GetMethodBodySemanticBoundary(ISyntaxFactsService syntaxFacts, SyntaxNode node)
    {
        return node.FirstAncestorOrSelf<SyntaxNode, ISyntaxFactsService>(
            (n, syntaxFacts) => syntaxFacts.IsMethodBody(n) ||
                 n.Parent == null,
            syntaxFacts);
    }
}