File: ExtractMethod\CSharpMethodExtractor.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.
 
#nullable disable
 
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ExtractMethod;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod;
 
internal sealed partial class CSharpMethodExtractor(CSharpSelectionResult result, ExtractMethodGenerationOptions options, bool localFunction)
    : MethodExtractor<CSharpSelectionResult, StatementSyntax, ExpressionSyntax>(result, options, localFunction)
{
    protected override CodeGenerator CreateCodeGenerator(AnalyzerResult analyzerResult)
        => CSharpCodeGenerator.Create(this.OriginalSelectionResult, analyzerResult, (CSharpCodeGenerationOptions)this.Options.CodeGenerationOptions, this.LocalFunction);
 
    protected override AnalyzerResult Analyze(CSharpSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken)
        => CSharpAnalyzer.Analyze(selectionResult, localFunction, cancellationToken);
 
    protected override SyntaxNode GetInsertionPointNode(
        AnalyzerResult analyzerResult, CancellationToken cancellationToken)
    {
        var originalSpanStart = OriginalSelectionResult.OriginalSpan.Start;
        Contract.ThrowIfFalse(originalSpanStart >= 0);
 
        var document = this.OriginalSelectionResult.SemanticDocument;
        var root = document.Root;
 
        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.
            var outermostCapturedVariable = analyzerResult.GetOutermostVariableToMoveIntoMethodDefinition(cancellationToken);
            var baseNode = outermostCapturedVariable != null
                ? outermostCapturedVariable.GetIdentifierTokenAtDeclaration(document).Parent
                : this.OriginalSelectionResult.GetOutermostCallSiteContainerToProcess(cancellationToken);
 
            if (baseNode is CompilationUnitSyntax)
            {
                // In some sort of global statement.  Have to special case these a bit for script files.
                var globalStatement = root.FindToken(originalSpanStart).GetAncestor<GlobalStatementSyntax>();
                if (globalStatement is null)
                    return null;
 
                return GetInsertionPointForGlobalStatement(globalStatement, globalStatement);
            }
 
            var currentNode = baseNode;
            while (currentNode is not null)
            {
                if (currentNode is AnonymousFunctionExpressionSyntax anonymousFunction)
                {
                    if (OriginalSelectionWithin(anonymousFunction.Body) || OriginalSelectionWithin(anonymousFunction.ExpressionBody))
                        return currentNode;
 
                    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))
                        return currentNode;
 
                    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;
                    }
                }
 
                if (currentNode is AccessorDeclarationSyntax)
                    return currentNode;
 
                if (currentNode is BaseMethodDeclarationSyntax)
                    return currentNode;
 
                if (currentNode is GlobalStatementSyntax globalStatement)
                {
                    // 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 globalStatement.Parent;
                    }
 
                    return globalStatement.Statement;
                }
 
                currentNode = currentNode.Parent;
            }
 
            return null;
        }
        else
        {
            var baseToken = root.FindToken(originalSpanStart);
            var memberNode = baseToken.GetAncestor<MemberDeclarationSyntax>();
            Contract.ThrowIfNull(memberNode);
            Contract.ThrowIfTrue(memberNode.Kind() == SyntaxKind.NamespaceDeclaration);
 
            if (memberNode is GlobalStatementSyntax globalStatement)
                return GetInsertionPointForGlobalStatement(globalStatement, memberNode);
 
            return memberNode;
        }
 
        SyntaxNode GetInsertionPointForGlobalStatement(GlobalStatementSyntax globalStatement, MemberDeclarationSyntax memberNode)
        {
            // check whether we are extracting whole global statement out
            if (OriginalSelectionResult.FinalSpan.Contains(memberNode.Span))
                return globalStatement.Parent;
 
            // 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 globalStatement.Parent;
            }
 
            return globalStatement.Statement;
        }
    }
 
    private bool OriginalSelectionWithin(SyntaxNode node)
    {
        if (node is null)
        {
            return false;
        }
 
        return node.Span.Contains(OriginalSelectionResult.OriginalSpan);
    }
 
    protected override async Task<TriviaResult> PreserveTriviaAsync(CSharpSelectionResult selectionResult, CancellationToken cancellationToken)
        => await CSharpTriviaResult.ProcessAsync(selectionResult, cancellationToken).ConfigureAwait(false);
 
    protected override Task<GeneratedCode> GenerateCodeAsync(InsertionPoint insertionPoint, CSharpSelectionResult selectionResult, AnalyzerResult analyzeResult, CodeGenerationOptions options, CancellationToken cancellationToken)
        => CSharpCodeGenerator.GenerateAsync(insertionPoint, selectionResult, analyzeResult, (CSharpCodeGenerationOptions)options, LocalFunction, cancellationToken);
 
    protected override AbstractFormattingRule GetCustomFormattingRule(Document document)
        => FormattingRule.Instance;
 
    protected override SyntaxToken? GetInvocationNameToken(IEnumerable<SyntaxToken> methodNames)
        => methodNames.FirstOrNull(t => !t.Parent.IsKind(SyntaxKind.MethodDeclaration));
 
    protected override SyntaxNode ParseTypeName(string name)
        => SyntaxFactory.ParseTypeName(name);
 
    protected override async Task<(Document document, SyntaxToken? invocationNameToken)> InsertNewLineBeforeLocalFunctionIfNecessaryAsync(
        Document document,
        SyntaxToken? invocationNameToken,
        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(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(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);
 
            if (invocationNameToken != null)
                invocationNameToken = newRoot.FindToken(invocationNameToken.Value.SpanStart);
        }
 
        return (document, invocationNameToken);
    }
}