// 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); } } |