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.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 CSharpExtractMethodService
{
    internal sealed partial class CSharpMethodExtractor(
        SelectionResult result, ExtractMethodGenerationOptions options, bool localFunction)
        : MethodExtractor(result, options, localFunction)
    {
        protected override CodeGenerator CreateCodeGenerator(SelectionResult selectionResult, AnalyzerResult analyzerResult)
            => CSharpCodeGenerator.Create(selectionResult, analyzerResult, this.Options, this.LocalFunction);
 
        protected override AnalyzerResult Analyze(CancellationToken cancellationToken)
        {
            var analyzer = new CSharpAnalyzer(this.OriginalSelectionResult, this.LocalFunction, cancellationToken);
            return analyzer.Analyze();
        }
 
        protected override SyntaxNode GetInsertionPointNode(
            AnalyzerResult analyzerResult, CancellationToken cancellationToken)
        {
            var originalSpanStart = OriginalSelectionResult.FinalSpan.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();
                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 (SelectionWithin(anonymousFunction.Body) || SelectionWithin(anonymousFunction.ExpressionBody))
                            return currentNode;
 
                        if (!OriginalSelectionResult.FinalSpan.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 (SelectionWithin(localFunction.ExpressionBody) || SelectionWithin(localFunction.Body))
                            return currentNode;
 
                        if (!OriginalSelectionResult.FinalSpan.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 primaryConstructorBaseType = baseToken.GetAncestor<PrimaryConstructorBaseTypeSyntax>();
                if (primaryConstructorBaseType != null)
                    return primaryConstructorBaseType;
 
                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 SelectionWithin(SyntaxNode node)
        {
            if (node is null)
            {
                return false;
            }
 
            return node.Span.Contains(OriginalSelectionResult.FinalSpan);
        }
 
        protected override async Task<TriviaResult> PreserveTriviaAsync(SyntaxNode root, CancellationToken cancellationToken)
        {
            var semanticDocument = this.OriginalSelectionResult.SemanticDocument;
            var preservationService = semanticDocument.Document.Project.Services.GetService<ISyntaxTriviaService>();
            var result = preservationService.SaveTriviaAroundSelection(root, this.OriginalSelectionResult.FinalSpan);
            return new CSharpTriviaResult(
                await semanticDocument.WithSyntaxRootAsync(result.Root, cancellationToken).ConfigureAwait(false),
                result);
        }
 
        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) || t.GetStructure() is EndIfDirectiveTriviaSyntax) &&
                !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);
        }
    }
}