|
// 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>(ImmutableArray<AbstractReducer> reducers) : 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 = 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);
reducers = 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));
return await this.ReduceCoreAsync(document, spanList, options, reducers, cancellationToken).ConfigureAwait(false);
}
}
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())
{
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.IsEmpty || !reducedTokensMap.IsEmpty)
{
// 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);
}
|