File: UseExpressionBody\UseExpressionBodyCodeRefactoringProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody;
 
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.UseExpressionBody), Shared]
[ExtensionOrder(Before = PredefinedCodeRefactoringProviderNames.ExtractClass)]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed class UseExpressionBodyCodeRefactoringProvider() : SyntaxEditorBasedCodeRefactoringProvider
{
    private static readonly ImmutableArray<UseExpressionBodyHelper> _helpers = UseExpressionBodyHelper.Helpers;
 
    private static readonly BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string> s_equivalenceKeyMap
        = CreateEquivalanceKeyMap(UseExpressionBodyHelper.Helpers);
 
    private static BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string> CreateEquivalanceKeyMap(
        ImmutableArray<UseExpressionBodyHelper> helpers)
    {
        return new BidirectionalMap<(UseExpressionBodyHelper helper, bool useExpressionBody), string>(GetKeyValuePairs(helpers));
 
        static IEnumerable<KeyValuePair<(UseExpressionBodyHelper helper, bool useExpressionBody), string>> GetKeyValuePairs(
            ImmutableArray<UseExpressionBodyHelper> helpers)
        {
            foreach (var helper in helpers)
            {
                yield return KeyValuePairUtil.Create((helper, useExpressionBody: true), helper.GetType().Name + "_UseExpressionBody");
                yield return KeyValuePairUtil.Create((helper, useExpressionBody: false), helper.GetType().Name + "_UseBlockBody");
            }
        }
    }
 
    protected override ImmutableArray<FixAllScope> SupportedFixAllScopes => AllFixAllScopes;
 
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, textSpan, cancellationToken) = context;
        if (textSpan.Length > 0)
            return;
 
        var position = textSpan.Start;
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var node = root.FindToken(position).Parent!;
 
        var containingLambda = node.FirstAncestorOrSelf<LambdaExpressionSyntax>();
        if (containingLambda != null &&
            node.AncestorsAndSelf().Contains(containingLambda.Body))
        {
            // don't offer inside a lambda.  Lambdas can be quite large, and it will be very noisy
            // inside the body of one to be offering to use a block/expression body for the containing
            // class member.
            return;
        }
 
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        foreach (var helper in _helpers)
        {
            var declaration = TryGetDeclaration(helper, text, node, position);
            if (declaration == null)
                continue;
 
            var succeeded = TryComputeRefactoring(context, root, declaration, options, helper, cancellationToken);
            if (succeeded)
                return;
        }
    }
 
    private static SyntaxNode? TryGetDeclaration(
        UseExpressionBodyHelper helper, SourceText text, SyntaxNode node, int position)
    {
        var declaration = GetDeclaration(node, helper);
        if (declaration == null)
            return null;
 
        if (position < declaration.SpanStart)
        {
            // The user is allowed to be before the starting point of this node, as long as
            // they're only between the start of the node and the start of the same line the
            // node starts on.  This prevents unnecessarily showing this feature in areas like
            // the comment of a method.
            if (!text.AreOnSameLine(position, declaration.SpanStart))
                return null;
        }
 
        return declaration;
    }
 
    private static bool TryComputeRefactoring(
        CodeRefactoringContext context, SyntaxNode root, SyntaxNode declaration,
        CSharpCodeGenerationOptions options, UseExpressionBodyHelper helper, CancellationToken cancellationToken)
    {
        var document = context.Document;
        var preference = helper.GetExpressionBodyPreference(options);
 
        var succeeded = false;
        if (helper.CanOfferUseExpressionBody(preference, declaration, forAnalyzer: false, cancellationToken))
        {
            context.RegisterRefactoring(
                CodeAction.Create(
                    helper.UseExpressionBodyTitle.ToString(),
                    cancellationToken => UpdateDocumentAsync(
                        document, root, declaration, helper,
                        useExpressionBody: true, cancellationToken),
                    s_equivalenceKeyMap[(helper, useExpressionBody: true)]),
                declaration.Span);
            succeeded = true;
        }
 
        if (helper.CanOfferUseBlockBody(preference, declaration, forAnalyzer: false, out _, out _))
        {
            context.RegisterRefactoring(
                CodeAction.Create(
                    helper.UseBlockBodyTitle.ToString(),
                    cancellationToken => UpdateDocumentAsync(
                        document, root, declaration, helper,
                        useExpressionBody: false, cancellationToken),
                    s_equivalenceKeyMap[(helper, useExpressionBody: false)]),
                declaration.Span);
            succeeded = true;
        }
 
        return succeeded;
    }
 
    private static SyntaxNode? GetDeclaration(SyntaxNode node, UseExpressionBodyHelper helper)
    {
        for (var current = node; current != null; current = current.Parent)
        {
            if (helper.SyntaxKinds.Contains(current.Kind()))
                return current;
        }
 
        return null;
    }
 
    private static async Task<Document> UpdateDocumentAsync(
        Document document, SyntaxNode root, SyntaxNode declaration,
        UseExpressionBodyHelper helper, bool useExpressionBody,
        CancellationToken cancellationToken)
    {
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var newRoot = GetUpdatedRoot(semanticModel, root, declaration, helper, useExpressionBody, cancellationToken);
        return document.WithSyntaxRoot(newRoot);
    }
 
    private static SyntaxNode GetUpdatedRoot(
        SemanticModel semanticModel, SyntaxNode root, SyntaxNode declaration,
        UseExpressionBodyHelper helper, bool useExpressionBody, CancellationToken cancellationToken)
    {
        var updatedDeclaration = helper.Update(semanticModel, declaration, useExpressionBody, cancellationToken);
 
        var parent = declaration is AccessorDeclarationSyntax
            ? declaration.Parent
            : declaration;
        RoslynDebug.Assert(parent is object);
        var updatedParent = parent.ReplaceNode(declaration, updatedDeclaration)
                                  .WithAdditionalAnnotations(Formatter.Annotation);
 
        return root.ReplaceNode(parent, updatedParent);
    }
 
    protected override async Task FixAllAsync(
        Document document,
        ImmutableArray<TextSpan> fixAllSpans,
        SyntaxEditor editor,
        string? equivalenceKey,
        CancellationToken cancellationToken)
    {
        Debug.Assert(equivalenceKey != null);
        var (helper, useExpressionBody) = s_equivalenceKeyMap[equivalenceKey];
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(cancellationToken).ConfigureAwait(false);
        var declarationsToFix = GetDeclarationsToFix(fixAllSpans, root, helper, useExpressionBody, options, cancellationToken);
        await FixDeclarationsAsync(document, editor, root, declarationsToFix.ToImmutableArray(), helper, useExpressionBody, cancellationToken).ConfigureAwait(false);
        return;
 
        // Local functions.
        static IEnumerable<SyntaxNode> GetDeclarationsToFix(
            ImmutableArray<TextSpan> fixAllSpans,
            SyntaxNode root,
            UseExpressionBodyHelper helper,
            bool useExpressionBody,
            CSharpCodeGenerationOptions options,
            CancellationToken cancellationToken)
        {
            var preference = helper.GetExpressionBodyPreference(options);
            foreach (var span in fixAllSpans)
            {
                var spanNode = root.FindNode(span);
 
                foreach (var node in spanNode.DescendantNodesAndSelf())
                {
                    if (!helper.IsRelevantDeclarationNode(node) || !helper.SyntaxKinds.Contains(node.Kind()))
                        continue;
 
                    if (useExpressionBody && helper.CanOfferUseExpressionBody(preference, node, forAnalyzer: false, cancellationToken))
                    {
                        yield return node;
                    }
                    else if (!useExpressionBody && helper.CanOfferUseBlockBody(preference, node, forAnalyzer: false, out _, out _))
                    {
                        yield return node;
                    }
                }
            }
        }
 
        static async Task FixDeclarationsAsync(
            Document document,
            SyntaxEditor editor,
            SyntaxNode root,
            ImmutableArray<SyntaxNode> declarationsToFix,
            UseExpressionBodyHelper helper,
            bool useExpressionBody,
            CancellationToken cancellationToken)
        {
            // Track all the declaration nodes to be fixed so we can get the latest declaration node in the current root during updates.
            var currentRoot = root.TrackNodes(declarationsToFix);
 
            // Process all declaration nodes in reverse to handle nested declaration updates properly.
            foreach (var declaration in declarationsToFix.Reverse())
            {
                // Get the current document, root, semanticModel and declaration.
                document = document.WithSyntaxRoot(currentRoot);
                currentRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var currentDeclaration = currentRoot.GetCurrentNodes(declaration).Single();
 
                // Fix the current declaration and get updated current root
                currentRoot = GetUpdatedRoot(semanticModel, currentRoot, currentDeclaration, helper, useExpressionBody, cancellationToken);
            }
 
            // Finally apply the latest current root to the editor.
            editor.ReplaceNode(editor.OriginalRoot, currentRoot);
        }
    }
}