File: ExtractMethod\CSharpMethodExtractor.cs
Web Access
Project: ..\..\..\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.
 
#nullable disable
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ExtractMethod;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod
{
    internal partial class CSharpMethodExtractor : MethodExtractor
    {
        public CSharpMethodExtractor(CSharpSelectionResult result, ExtractMethodGenerationOptions options, bool localFunction)
            : base(result, options, localFunction)
        {
        }
 
        protected override Task<AnalyzerResult> AnalyzeAsync(SelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken)
            => CSharpAnalyzer.AnalyzeAsync(selectionResult, localFunction, cancellationToken);
 
        protected override async Task<InsertionPoint> GetInsertionPointAsync(SemanticDocument document, CancellationToken cancellationToken)
        {
            var originalSpanStart = OriginalSelectionResult.OriginalSpan.Start;
            Contract.ThrowIfFalse(originalSpanStart >= 0);
 
            var root = await document.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var basePosition = root.FindToken(originalSpanStart);
 
            if (LocalFunction)
            {
                // If we are extracting a local function and are within a local function, then we want the new function to be created within the
                // existing local function instead of the overarching method.
                SyntaxNode functionNode = null;
 
                var currentNode = basePosition.Parent;
                while (currentNode is not null)
                {
                    if (currentNode is AnonymousFunctionExpressionSyntax anonymousFunction)
                    {
                        if (OriginalSelectionWithin(anonymousFunction.Body) || OriginalSelectionWithin(anonymousFunction.ExpressionBody))
                        {
                            functionNode = currentNode;
                            break;
                        }
 
                        if (!OriginalSelectionResult.OriginalSpan.Contains(anonymousFunction.Span))
                        {
                            // If we encountered a function but the selection isn't within the body, it's likely the user
                            // is attempting to move the function (which is behavior that is supported). Stop looking up the 
                            // tree and assume the encapsulating member is the right place to put the local function. This is to help
                            // maintain the behavior introduced with https://github.com/dotnet/roslyn/pull/41377
                            break;
                        }
                    }
 
                    if (currentNode is LocalFunctionStatementSyntax localFunction)
                    {
                        if (OriginalSelectionWithin(localFunction.ExpressionBody) || OriginalSelectionWithin(localFunction.Body))
                        {
                            functionNode = currentNode;
                            break;
                        }
 
                        if (!OriginalSelectionResult.OriginalSpan.Contains(localFunction.Span))
                        {
                            // If we encountered a function but the selection isn't within the body, it's likely the user
                            // is attempting to move the function (which is behavior that is supported). Stop looking up the 
                            // tree and assume the encapsulating member is the right place to put the local function. This is to help
                            // maintain the behavior introduced with https://github.com/dotnet/roslyn/pull/41377
                            break;
                        }
                    }
 
                    currentNode = currentNode.Parent;
                }
 
                if (functionNode is not null)
                {
                    return await InsertionPoint.CreateAsync(document, functionNode, cancellationToken).ConfigureAwait(false);
                }
            }
 
            var memberNode = basePosition.GetAncestor<MemberDeclarationSyntax>();
            Contract.ThrowIfNull(memberNode);
            Contract.ThrowIfTrue(memberNode.Kind() == SyntaxKind.NamespaceDeclaration);
 
            if (LocalFunction && memberNode is BasePropertyDeclarationSyntax propertyDeclaration)
            {
                var accessorNode = basePosition.GetAncestor<AccessorDeclarationSyntax>();
                if (accessorNode is object)
                {
                    return await InsertionPoint.CreateAsync(document, accessorNode, cancellationToken).ConfigureAwait(false);
                }
            }
 
            if (memberNode is GlobalStatementSyntax globalStatement)
            {
                // check whether we are extracting whole global statement out
                if (OriginalSelectionResult.FinalSpan.Contains(memberNode.Span))
                {
                    return await InsertionPoint.CreateAsync(document, globalStatement.Parent, cancellationToken).ConfigureAwait(false);
                }
 
                // check whether the global statement is a statement container
                if (!globalStatement.Statement.IsStatementContainerNode() && !root.SyntaxTree.IsScript())
                {
                    // The extracted function will be a new global statement
                    return await InsertionPoint.CreateAsync(document, globalStatement.Parent, cancellationToken).ConfigureAwait(false);
                }
 
                return await InsertionPoint.CreateAsync(document, globalStatement.Statement, cancellationToken).ConfigureAwait(false);
            }
 
            return await InsertionPoint.CreateAsync(document, memberNode, cancellationToken).ConfigureAwait(false);
        }
 
        private bool OriginalSelectionWithin(SyntaxNode node)
        {
            if (node is null)
            {
                return false;
            }
 
            return node.Span.Contains(OriginalSelectionResult.OriginalSpan);
        }
 
        protected override async Task<TriviaResult> PreserveTriviaAsync(SelectionResult selectionResult, CancellationToken cancellationToken)
            => await CSharpTriviaResult.ProcessAsync(selectionResult, cancellationToken).ConfigureAwait(false);
 
        protected override async Task<SemanticDocument> ExpandAsync(SelectionResult selection, CancellationToken cancellationToken)
        {
            var lastExpression = selection.GetFirstTokenInSelection().GetCommonRoot(selection.GetLastTokenInSelection()).GetAncestors<ExpressionSyntax>().LastOrDefault();
            if (lastExpression == null)
            {
                return selection.SemanticDocument;
            }
 
            var newExpression = await Simplifier.ExpandAsync(lastExpression, selection.SemanticDocument.Document, n => n != selection.GetContainingScope(), expandParameter: false, cancellationToken: cancellationToken).ConfigureAwait(false);
            return await selection.SemanticDocument.WithSyntaxRootAsync(selection.SemanticDocument.Root.ReplaceNode(lastExpression, newExpression), cancellationToken).ConfigureAwait(false);
        }
 
        protected override Task<GeneratedCode> GenerateCodeAsync(InsertionPoint insertionPoint, SelectionResult selectionResult, AnalyzerResult analyzeResult, CodeGenerationOptions options, CancellationToken cancellationToken)
            => CSharpCodeGenerator.GenerateAsync(insertionPoint, selectionResult, analyzeResult, (CSharpCodeGenerationOptions)options, LocalFunction, cancellationToken);
 
        protected override ImmutableArray<AbstractFormattingRule> GetCustomFormattingRules(Document document)
            => ImmutableArray.Create<AbstractFormattingRule>(new FormattingRule());
 
        protected override SyntaxToken GetMethodNameAtInvocation(IEnumerable<SyntaxNodeOrToken> methodNames)
            => (SyntaxToken)methodNames.FirstOrDefault(t => !t.Parent.IsKind(SyntaxKind.MethodDeclaration));
 
        protected override async Task<OperationStatus> CheckTypeAsync(
            Document document,
            SyntaxNode contextNode,
            Location location,
            ITypeSymbol type,
            CancellationToken cancellationToken)
        {
            Contract.ThrowIfNull(type);
 
            // this happens when there is no return type
            if (type.SpecialType == SpecialType.System_Void)
            {
                return OperationStatus.Succeeded;
            }
 
            if (type.TypeKind is TypeKind.Error or
                TypeKind.Unknown)
            {
                return OperationStatus.ErrorOrUnknownType;
            }
 
            // if it is type parameter, make sure we are getting same type parameter
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
            foreach (var typeParameter in TypeParameterCollector.Collect(type))
            {
                var typeName = SyntaxFactory.ParseTypeName(typeParameter.Name);
                var currentType = semanticModel.GetSpeculativeTypeInfo(contextNode.SpanStart, typeName, SpeculativeBindingOption.BindAsTypeOrNamespace).Type;
                if (currentType == null || !SymbolEqualityComparer.Default.Equals(currentType, semanticModel.ResolveType(typeParameter)))
                {
                    return new OperationStatus(OperationStatusFlag.BestEffort,
                        string.Format(FeaturesResources.Type_parameter_0_is_hidden_by_another_type_parameter_1,
                            typeParameter.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
                            currentType == null ? string.Empty : currentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
                }
            }
 
            return OperationStatus.Succeeded;
        }
 
        protected override async Task<(Document document, SyntaxToken methodName, SyntaxNode methodDefinition)> InsertNewLineBeforeLocalFunctionIfNecessaryAsync(
            Document document,
            SyntaxToken methodName,
            SyntaxNode methodDefinition,
            CancellationToken cancellationToken)
        {
            // Checking to see if there is already an empty line before the local method declaration.
            var leadingTrivia = methodDefinition.GetLeadingTrivia();
            if (!leadingTrivia.Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) && !methodDefinition.FindTokenOnLeftOfPosition(methodDefinition.SpanStart).IsKind(SyntaxKind.OpenBraceToken))
            {
                var originalMethodDefinition = methodDefinition;
                var newLine = Options.LineFormattingOptions.NewLine;
                methodDefinition = methodDefinition.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(SyntaxFactory.EndOfLine(newLine)));
 
                if (!originalMethodDefinition.FindTokenOnLeftOfPosition(originalMethodDefinition.SpanStart).TrailingTrivia.Any(SyntaxKind.EndOfLineTrivia))
                {
                    // Add a second new line since there were no line endings in the original form
                    methodDefinition = methodDefinition.WithPrependedLeadingTrivia(SpecializedCollections.SingletonEnumerable(SyntaxFactory.EndOfLine(newLine)));
                }
 
                // Generating the new document and associated variables.
                var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                document = document.WithSyntaxRoot(root.ReplaceNode(originalMethodDefinition, methodDefinition));
 
                var newRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                methodName = newRoot.FindToken(methodName.SpanStart);
            }
 
            return (document, methodName, methodDefinition);
        }
    }
}