File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\Simplification\AbstractSimplificationService.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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Simplification;
 
internal abstract class AbstractSimplificationService<TCompilationUnitSyntax, TExpressionSyntax, TStatementSyntax, TCrefSyntax> : ISimplificationService
    where TCompilationUnitSyntax : SyntaxNode
    where TExpressionSyntax : SyntaxNode
    where TStatementSyntax : SyntaxNode
    where TCrefSyntax : SyntaxNode
{
    protected static readonly Func<SyntaxNode, bool> s_containsAnnotations = n => n.ContainsAnnotations;
    protected static readonly Func<SyntaxNodeOrToken, bool> s_hasSimplifierAnnotation = n => n.HasAnnotation(Simplifier.Annotation);
 
    private readonly ImmutableArray<AbstractReducer> _reducers;
 
    protected AbstractSimplificationService(ImmutableArray<AbstractReducer> reducers)
        => _reducers = reducers;
 
    protected abstract ImmutableArray<NodeOrTokenToReduce> GetNodesAndTokensToReduce(SyntaxNode root, Func<SyntaxNodeOrToken, bool> isNodeOrTokenOutsideSimplifySpans);
    protected abstract SemanticModel GetSpeculativeSemanticModel(ref SyntaxNode nodeToSpeculate, SemanticModel originalSemanticModel, SyntaxNode originalNode);
    protected abstract bool NodeRequiresNonSpeculativeSemanticModel(SyntaxNode node);
    protected abstract void AddImportDeclarations(TCompilationUnitSyntax root, ArrayBuilder<SyntaxNode> importDeclarations);
 
    public abstract SimplifierOptions DefaultOptions { get; }
    public abstract SimplifierOptions GetSimplifierOptions(IOptionsReader options);
 
    protected virtual SyntaxNode TransformReducedNode(SyntaxNode reducedNode, SyntaxNode originalNode)
        => reducedNode;
 
    public abstract SyntaxNode Expand(SyntaxNode node, SemanticModel semanticModel, SyntaxAnnotation? annotationForReplacedAliasIdentifier, Func<SyntaxNode, bool>? expandInsideNode, bool expandParameter, CancellationToken cancellationToken);
    public abstract SyntaxToken Expand(SyntaxToken token, SemanticModel semanticModel, Func<SyntaxNode, bool>? expandInsideNode, CancellationToken cancellationToken);
 
    public async Task<Document> ReduceAsync(
        Document document,
        ImmutableArray<TextSpan> spans,
        SimplifierOptions options,
        ImmutableArray<AbstractReducer> reducers = default,
        CancellationToken cancellationToken = default)
    {
        using (Logger.LogBlock(FunctionId.Simplifier_ReduceAsync, cancellationToken))
        {
            var spanList = spans.NullToEmpty();
 
            // we have no span
            if (!spanList.Any())
            {
                return document;
            }
 
            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
            // Chaining of the Speculative SemanticModel (i.e. Generating a speculative SemanticModel from an existing Speculative SemanticModel) is not supported
            // Hence make sure we always start working off of the actual SemanticModel instead of a speculative SemanticModel.
            Debug.Assert(!semanticModel.IsSpeculativeSemanticModel);
 
            var root = await semanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
 
#if DEBUG
            var originalDocHasErrors = await document.HasAnyErrorsAsync(cancellationToken).ConfigureAwait(false);
#endif
 
            var reduced = await this.ReduceCoreAsync(document, spanList, options, reducers, cancellationToken).ConfigureAwait(false);
 
            if (reduced != document)
            {
#if DEBUG
                if (!originalDocHasErrors)
                {
                    await reduced.VerifyNoErrorsAsync("Error introduced by Simplification Service", cancellationToken).ConfigureAwait(false);
                }
#endif
            }
 
            return reduced;
        }
    }
 
    private async Task<Document> ReduceCoreAsync(
        Document document,
        ImmutableArray<TextSpan> spans,
        SimplifierOptions options,
        ImmutableArray<AbstractReducer> reducers,
        CancellationToken cancellationToken)
    {
        // Create a simple interval tree for simplification spans.
        var spansTree = new TextSpanMutableIntervalTree(spans);
 
        var root = (TCompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        // prep namespace imports marked for simplification 
        var removeIfUnusedAnnotation = new SyntaxAnnotation();
        var originalRoot = root;
        root = PrepareNamespaceImportsForRemovalIfUnused(root, removeIfUnusedAnnotation, IsNodeOrTokenOutsideSimplifySpans);
        var hasImportsToSimplify = root != originalRoot;
 
        if (hasImportsToSimplify)
        {
            document = document.WithSyntaxRoot(root);
            root = (TCompilationUnitSyntax)await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        }
 
        // Get the list of syntax nodes and tokens that need to be reduced.
        var nodesAndTokensToReduce = this.GetNodesAndTokensToReduce(root, IsNodeOrTokenOutsideSimplifySpans);
 
        if (nodesAndTokensToReduce.Any())
        {
            if (reducers.IsDefault)
            {
                reducers = _reducers;
            }
 
            // Take out any reducers that don't even apply with the current
            // set of users options. i.e. no point running 'reduce to var'
            // if the user doesn't have the 'var' preference set.
            reducers = reducers.WhereAsArray(r => r.IsApplicable(options));
 
            var reducedNodesMap = new ConcurrentDictionary<SyntaxNode, SyntaxNode>();
            var reducedTokensMap = new ConcurrentDictionary<SyntaxToken, SyntaxToken>();
 
            // Reduce all the nodesAndTokensToReduce using the given reducers/rewriters and
            // store the reduced nodes and/or tokens in the reduced nodes/tokens maps.
            // Note that this method doesn't update the original syntax tree.
            await this.ReduceAsync(document, root, nodesAndTokensToReduce, reducers, options, reducedNodesMap, reducedTokensMap, cancellationToken).ConfigureAwait(false);
 
            if (reducedNodesMap.Any() || reducedTokensMap.Any())
            {
                // Update the syntax tree with reduced nodes/tokens.
                root = root.ReplaceSyntax(
                    nodes: reducedNodesMap.Keys,
                    computeReplacementNode: (o, n) => TransformReducedNode(reducedNodesMap[o], n),
                    tokens: reducedTokensMap.Keys,
                    computeReplacementToken: (o, n) => reducedTokensMap[o],
                    trivia: [],
                    computeReplacementTrivia: null);
 
                document = document.WithSyntaxRoot(root);
            }
        }
 
        if (hasImportsToSimplify)
        {
            // remove any unused namespace imports that were marked for simplification
            document = await this.RemoveUnusedNamespaceImportsAsync(document, removeIfUnusedAnnotation, cancellationToken).ConfigureAwait(false);
        }
 
        return document;
 
        bool IsNodeOrTokenOutsideSimplifySpans(SyntaxNodeOrToken nodeOrToken)
            => !spansTree.HasIntervalThatOverlapsWith(nodeOrToken.FullSpan.Start, nodeOrToken.FullSpan.Length);
    }
 
    private async Task ReduceAsync(
        Document document,
        SyntaxNode root,
        ImmutableArray<NodeOrTokenToReduce> nodesAndTokensToReduce,
        ImmutableArray<AbstractReducer> reducers,
        SimplifierOptions options,
        ConcurrentDictionary<SyntaxNode, SyntaxNode> reducedNodesMap,
        ConcurrentDictionary<SyntaxToken, SyntaxToken> reducedTokensMap,
        CancellationToken cancellationToken)
    {
        // Debug flag to help processing things serially instead of parallel.
        var executeSerially = Debugger.IsAttached;
 
        Contract.ThrowIfFalse(nodesAndTokensToReduce.Any());
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        // Reduce each node or token in the given list by running it through each reducer.
        if (executeSerially)
        {
            foreach (var nodeOrTokenToReduce in nodesAndTokensToReduce)
                await ReduceOneNodeOrTokenAsync(nodeOrTokenToReduce, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            await RoslynParallel.ForEachAsync(
                source: nodesAndTokensToReduce,
                cancellationToken,
                ReduceOneNodeOrTokenAsync).ConfigureAwait(false);
        }
 
        return;
 
        async ValueTask ReduceOneNodeOrTokenAsync(
            NodeOrTokenToReduce nodeOrTokenToReduce, CancellationToken cancellationToken)
        {
            // Reduce each node or token in the given list by running it through each reducer.
 
            var nodeOrToken = nodeOrTokenToReduce.OriginalNodeOrToken;
            var simplifyAllDescendants = nodeOrTokenToReduce.SimplifyAllDescendants;
            var currentNodeOrToken = nodeOrTokenToReduce.NodeOrToken;
            var semanticModelForReduce = semanticModel;
            var isNode = nodeOrToken.IsNode;
 
            foreach (var reducer in reducers)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                using var rewriter = reducer.GetOrCreateRewriter();
                rewriter.Initialize(document.Project.ParseOptions, options, cancellationToken);
 
                do
                {
                    if (currentNodeOrToken.SyntaxTree != semanticModelForReduce.SyntaxTree)
                    {
                        // currentNodeOrToken was simplified either by a previous reducer or
                        // a previous iteration of the current reducer.
                        // Create a speculative semantic model for the simplified node for semantic queries.
 
                        // Certain node kinds (expressions/statements) require non-null parent nodes during simplification.
                        // However, the reduced nodes haven't been parented yet, so do the required parenting using the original node's parent.
                        if (currentNodeOrToken.Parent == null &&
                            nodeOrToken.Parent != null &&
                            (currentNodeOrToken.IsToken || currentNodeOrToken.AsNode() is TExpressionSyntax or TStatementSyntax or TCrefSyntax))
                        {
                            var annotation = new SyntaxAnnotation();
                            currentNodeOrToken = currentNodeOrToken.WithAdditionalAnnotations(annotation);
 
                            var replacedParent = isNode
                                ? nodeOrToken.Parent.ReplaceNode(nodeOrToken.AsNode()!, currentNodeOrToken.AsNode()!)
                                : nodeOrToken.Parent.ReplaceToken(nodeOrToken.AsToken(), currentNodeOrToken.AsToken());
 
                            currentNodeOrToken = replacedParent
                                .ChildNodesAndTokens()
                                .Single(c => c.HasAnnotation(annotation));
                        }
 
                        if (isNode)
                        {
                            var currentNode = currentNodeOrToken.AsNode()!;
                            if (this.NodeRequiresNonSpeculativeSemanticModel(nodeOrToken.AsNode()!))
                            {
                                // Since this node cannot be speculated, we are replacing the Document with the changes and get a new SemanticModel
                                var marker = new SyntaxAnnotation();
                                var newRoot = root.ReplaceNode(nodeOrToken.AsNode()!, currentNode.WithAdditionalAnnotations(marker));
                                var newDocument = document.WithSyntaxRoot(newRoot);
                                semanticModelForReduce = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                                newRoot = await semanticModelForReduce.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
                                currentNodeOrToken = newRoot.DescendantNodes().Single(c => c.HasAnnotation(marker));
                            }
                            else
                            {
                                // Create speculative semantic model for simplified node.
                                semanticModelForReduce = GetSpeculativeSemanticModel(ref currentNode, semanticModel, nodeOrToken.AsNode()!);
                                currentNodeOrToken = currentNode;
                            }
                        }
                    }
 
                    // Reduce the current node or token.
                    currentNodeOrToken = rewriter.VisitNodeOrToken(currentNodeOrToken, semanticModelForReduce, simplifyAllDescendants);
                }
                while (rewriter.HasMoreWork);
            }
 
            // If nodeOrToken was simplified, add it to the appropriate dictionary of replaced nodes/tokens.
            if (currentNodeOrToken != nodeOrToken)
            {
                if (isNode)
                {
                    reducedNodesMap[nodeOrToken.AsNode()!] = currentNodeOrToken.AsNode()!;
                }
                else
                {
                    reducedTokensMap[nodeOrToken.AsToken()] = currentNodeOrToken.AsToken();
                }
            }
        }
    }
 
    // find any namespace imports / using directives marked for simplification in the specified spans
    // and add removeIfUnused annotation
    private TCompilationUnitSyntax PrepareNamespaceImportsForRemovalIfUnused(
        TCompilationUnitSyntax root,
        SyntaxAnnotation removeIfUnusedAnnotation,
        Func<SyntaxNodeOrToken, bool> isNodeOrTokenOutsideSimplifySpan)
    {
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var importDeclarations);
 
        this.AddImportDeclarations(root, importDeclarations);
 
        return root.ReplaceNodes(
            importDeclarations.Where(n => !isNodeOrTokenOutsideSimplifySpan(n) && n.HasAnnotation(Simplifier.Annotation)),
            (o, r) => r.WithAdditionalAnnotations(removeIfUnusedAnnotation));
    }
 
    private async Task<Document> RemoveUnusedNamespaceImportsAsync(
        Document document,
        SyntaxAnnotation removeIfUnusedAnnotation,
        CancellationToken cancellationToken)
    {
        var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var root = await model.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var addedImports = root.GetAnnotatedNodes(removeIfUnusedAnnotation);
        var unusedImports = new HashSet<SyntaxNode>();
        this.GetUnusedNamespaceImports(model, unusedImports, cancellationToken);
 
        // only remove the unused imports that we added
        unusedImports.IntersectWith(addedImports);
 
        if (unusedImports.Count > 0)
        {
            var gen = SyntaxGenerator.GetGenerator(document);
            var newRoot = gen.RemoveNodes(root, unusedImports);
            return document.WithSyntaxRoot(newRoot);
        }
        else
        {
            return document;
        }
    }
 
    protected abstract void GetUnusedNamespaceImports(SemanticModel model, HashSet<SyntaxNode> namespaceImports, CancellationToken cancellationToken);
}