File: ExtractMethod\MethodExtractor.Analyzer.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.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.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractMethod;
 
internal abstract partial class AbstractExtractMethodService<
    TStatementSyntax,
    TExecutableStatementSyntax,
    TExpressionSyntax>
{
    internal abstract partial class MethodExtractor
    {
        protected abstract partial class Analyzer
        {
            protected readonly CancellationToken CancellationToken;
            protected readonly SelectionResult SelectionResult;
            protected readonly bool LocalFunction;
 
            private SemanticDocument SemanticDocument => SelectionResult.SemanticDocument;
            protected SemanticModel SemanticModel => SemanticDocument.SemanticModel;
 
            protected ISemanticFactsService SemanticFacts => this.SemanticDocument.Document.GetRequiredLanguageService<ISemanticFactsService>();
            protected ISyntaxFactsService SyntaxFacts => this.SemanticDocument.Document.GetRequiredLanguageService<ISyntaxFactsService>();
 
            protected Analyzer(SelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(selectionResult);
 
                SelectionResult = selectionResult;
                CancellationToken = cancellationToken;
                LocalFunction = localFunction;
            }
 
            protected abstract bool IsInPrimaryConstructorBaseType();
 
            protected virtual bool IsReadOutside(ISymbol symbol, HashSet<ISymbol> readOutsideMap)
                => readOutsideMap.Contains(symbol);
 
            protected abstract bool TreatOutAsRef { get; }
 
            /// <summary>
            /// get type of the range variable symbol
            /// </summary>
            protected abstract ITypeSymbol? GetRangeVariableType(IRangeVariableSymbol symbol);
 
            /// <summary>
            /// check whether the selection is at the placed where read-only field is allowed to be extracted out
            /// </summary>
            protected abstract bool ReadOnlyFieldAllowed();
 
            protected abstract ExtractMethodFlowControlInformation GetStatementFlowControlInformation(
                ControlFlowAnalysis controlFlowAnalysis);
 
            public AnalyzerResult Analyze()
            {
                // do data flow analysis
                var model = this.SemanticModel;
                var dataFlowAnalysisData = this.SelectionResult.GetDataFlowAnalysis();
 
                // build symbol map for the identifiers used inside of the selection
                var symbolMap = GetSymbolMap();
 
                var isInPrimaryConstructorBaseType = this.IsInPrimaryConstructorBaseType();
 
                // gather initial local or parameter variable info
                GenerateVariableInfoMap(
                    bestEffort: false, dataFlowAnalysisData, symbolMap, isInPrimaryConstructorBaseType, out var variableInfoMap, out var failedVariables);
                if (failedVariables.Count > 0)
                {
                    // If we weren't able to figure something out, go back and regenerate the map
                    // this time in 'best effort' mode.  We'll give the user a message saying there
                    // was a problem, but we allow them to proceed so they're not unnecessarily
                    // blocked just because we didn't understand something.
                    GenerateVariableInfoMap(
                        bestEffort: true, dataFlowAnalysisData, symbolMap, isInPrimaryConstructorBaseType, out variableInfoMap, out var unused);
                    Contract.ThrowIfFalse(unused.Count == 0);
                }
 
                var thisParameterBeingRead = (IParameterSymbol?)dataFlowAnalysisData.ReadInside.FirstOrDefault(IsThisParameter);
                var isThisParameterWritten = dataFlowAnalysisData.WrittenInside.Any(static s => IsThisParameter(s));
 
                // Need to generate an instance method if any primary constructor parameter is read or written inside the
                // selection.  This does not apply if we're in the base-type-list as that will still need a static method.
                var primaryConstructorParameterReadOrWritten = !isInPrimaryConstructorBaseType && dataFlowAnalysisData.ReadInside
                    .Concat(dataFlowAnalysisData.WrittenInside)
                    .OfType<IParameterSymbol>()
                    .FirstOrDefault(s => s.IsPrimaryConstructor(this.CancellationToken)) != null;
 
                var localFunctionCallsNotWithinSpan = symbolMap.Keys.Where(s => s.IsLocalFunction() && !s.Locations.Any(static (l, self) => self.SelectionResult.FinalSpan.Contains(l.SourceSpan), this));
 
                // Checks to see if selection includes a local function call + if the given local function declaration is not included in the selection.
                var containsAnyLocalFunctionCallNotWithinSpan = localFunctionCallsNotWithinSpan.Any();
 
                // Checks to see if selection includes a non-static local function call + if the given local function declaration is not included in the selection.
                var containsNonStaticLocalFunctionCallNotWithinSpan = containsAnyLocalFunctionCallNotWithinSpan && localFunctionCallsNotWithinSpan.Where(s => !s.IsStatic).Any();
 
                var instanceMemberIsUsed = thisParameterBeingRead != null
                    || isThisParameterWritten
                    || containsNonStaticLocalFunctionCallNotWithinSpan
                    || primaryConstructorParameterReadOrWritten;
 
                var shouldBeReadOnly = !isThisParameterWritten
                    && thisParameterBeingRead is { Type: { TypeKind: TypeKind.Struct, IsReadOnly: false } };
 
                // check whether the selection contains "&" over a symbol exist
                var unsafeAddressTakenUsed = dataFlowAnalysisData.UnsafeAddressTaken.Intersect(variableInfoMap.Keys).Any();
 
                var flowControlInformation = GetFlowControlInformation();
                var (variables, returnType, returnsByRef) = GetSignatureInformation(variableInfoMap, flowControlInformation);
 
                // collect method type variable used in selected code
                var sortedMap = new SortedDictionary<int, ITypeParameterSymbol>();
                var typeParametersInConstraintList = GetMethodTypeParametersInConstraintList(variableInfoMap, symbolMap, sortedMap);
                var typeParametersInDeclaration = GetMethodTypeParametersInDeclaration(returnType, sortedMap);
 
                // check various error cases
                var operationStatus = GetOperationStatus(
                    symbolMap, variables, failedVariables, unsafeAddressTakenUsed, returnType.ContainsAnonymousType(), containsAnyLocalFunctionCallNotWithinSpan);
 
                return new AnalyzerResult(
                    typeParametersInDeclaration,
                    typeParametersInConstraintList,
                    variables,
                    returnType,
                    returnsByRef,
                    instanceMemberIsUsed,
                    shouldBeReadOnly,
                    flowControlInformation,
                    operationStatus);
            }
 
            private (ImmutableArray<VariableInfo> finalOrderedVariableInfos, ITypeSymbol returnType, bool returnsByRef)
                GetSignatureInformation(Dictionary<ISymbol, VariableInfo> symbolMap, ExtractMethodFlowControlInformation flowControlInformation)
            {
                var allVariableInfos = symbolMap.Values.Order().ToImmutableArray();
                if (this.SelectionResult.IsExtractMethodOnExpression ||
                    flowControlInformation.ReturnStatementCount > 0)
                {
                    // If we're just selecting an expression, then we want the extract method to have the type of that
                    // expression.  Similarly, if we're selecting a statement that contains a return statement, then we
                    // want the extracted method to have the return type of the containing method.
                    //
                    // Note: if there's more complex flow control, that will be handled later on with an additional
                    // value added to the return value.
                    var (returnType, returnsByRef) = SelectionResult.GetReturnTypeInfo(this.CancellationToken);
                    return (allVariableInfos, UnwrapTaskIfNeeded(returnType), returnsByRef);
                }
                else
                {
                    var finalOrderedVariableInfos = MarkVariableInfosToUseAsReturnValueIfPossible(
                        allVariableInfos, flowControlInformation.NeedsControlFlowValue());
                    var variablesToUseAsReturnValue = finalOrderedVariableInfos.WhereAsArray(v => v.UseAsReturnValue);
 
                    var returnType = GetReturnType(variablesToUseAsReturnValue);
                    return (finalOrderedVariableInfos, returnType, returnsByRef: false);
                }
 
                ITypeSymbol UnwrapTaskIfNeeded(ITypeSymbol returnType)
                {
                    if (this.SelectionResult.ContainingScopeHasAsyncKeyword())
                    {
                        // We compute the desired return type for the extract method from our own return type.  But for
                        // the purposes of manipulating the return type, we need to get to the underlying type if this
                        // was wrapped in a Task in an explicitly 'async' method.  In other words, if we're in an `async
                        // Task<int>` method, then we want the extract method to return `int`.  Note: we will possibly
                        // then wrap that as `Task<int>` again if we see that we extracted out any await-expressions.
 
                        var compilation = this.SemanticModel.Compilation;
                        var knownTaskTypes = new KnownTaskTypes(compilation);
 
                        // Map from `Task/ValueTask` to `void`
                        if (returnType.Equals(knownTaskTypes.TaskType) || returnType.Equals(knownTaskTypes.ValueTaskType))
                            return compilation.GetSpecialType(SpecialType.System_Void);
 
                        // Map from `Task<T>/ValueTask<T>` to `T`
                        if (returnType.OriginalDefinition.Equals(knownTaskTypes.TaskOfTType) || returnType.OriginalDefinition.Equals(knownTaskTypes.ValueTaskOfTType))
                            return returnType.GetTypeArguments().Single();
                    }
 
                    return returnType;
                }
 
                ITypeSymbol GetReturnType(ImmutableArray<VariableInfo> variablesToUseAsReturnValue)
                {
                    var compilation = this.SemanticModel.Compilation;
 
                    if (variablesToUseAsReturnValue.IsEmpty)
                        return compilation.GetSpecialType(SpecialType.System_Void);
 
                    if (variablesToUseAsReturnValue is [var info])
                        return info.SymbolType;
 
                    return compilation.CreateTupleTypeSymbol(
                        variablesToUseAsReturnValue.SelectAsArray(v => v.SymbolType),
                        variablesToUseAsReturnValue.SelectAsArray(v => v.Name)!);
                }
            }
 
            private ExtractMethodFlowControlInformation GetFlowControlInformation()
            {
                return this.SelectionResult.IsExtractMethodOnExpression
                    ? ExtractMethodFlowControlInformation.Create(this.SemanticModel.Compilation, supportsComplexFlowControl: true, breakStatementCount: 0, continueStatementCount: 0, returnStatementCount: 0, endPointIsReachable: true)
                    : GetStatementFlowControlInformation(this.SelectionResult.GetStatementControlFlowAnalysis());
            }
 
            private OperationStatus GetOperationStatus(
                MultiDictionary<ISymbol, SyntaxToken> symbolMap,
                ImmutableArray<VariableInfo> variables,
                IList<ISymbol> failedVariables,
                bool unsafeAddressTakenUsed,
                bool returnTypeHasAnonymousType,
                bool containsAnyLocalFunctionCallNotWithinSpan)
            {
                var readonlyFieldStatus = CheckReadOnlyFields(symbolMap);
 
                var namesWithAnonymousTypes = variables.Where(v => v.OriginalTypeHadAnonymousTypeOrDelegate).Select(v => v.Name ?? string.Empty);
                if (returnTypeHasAnonymousType)
                {
                    namesWithAnonymousTypes = namesWithAnonymousTypes.Concat("return type");
                }
 
                var anonymousTypeStatus = !namesWithAnonymousTypes.Any()
                    ? OperationStatus.SucceededStatus
                    : new OperationStatus(succeeded: true,
                        string.Format(
                            FeaturesResources.Parameters_type_or_return_type_cannot_be_an_anonymous_type_colon_bracket_0_bracket,
                            string.Join(", ", namesWithAnonymousTypes)));
 
                var unsafeAddressStatus = unsafeAddressTakenUsed
                    ? OperationStatus.UnsafeAddressTaken
                    : OperationStatus.SucceededStatus;
 
                var asyncRefOutParameterStatus = CheckAsyncMethodRefOutParameters(variables);
 
                var variableMapStatus = failedVariables.Count == 0
                    ? OperationStatus.SucceededStatus
                    : new OperationStatus(succeeded: true,
                        string.Format(
                            FeaturesResources.Failed_to_analyze_data_flow_for_0,
                            string.Join(", ", failedVariables.Select(v => v.Name))));
 
                var localFunctionStatus = (containsAnyLocalFunctionCallNotWithinSpan && !LocalFunction)
                    ? OperationStatus.LocalFunctionCallWithoutDeclaration
                    : OperationStatus.SucceededStatus;
 
                return readonlyFieldStatus
                    .With(anonymousTypeStatus)
                    .With(unsafeAddressStatus)
                    .With(asyncRefOutParameterStatus)
                    .With(variableMapStatus)
                    .With(localFunctionStatus);
            }
 
            private OperationStatus CheckAsyncMethodRefOutParameters(IList<VariableInfo> parameters)
            {
                if (SelectionResult.ContainsAwaitExpression())
                {
                    var names = parameters
                        .Where(v => v is { UseAsReturnValue: false, ParameterModifier: ParameterBehavior.Out or ParameterBehavior.Ref })
                        .Select(p => p.Name ?? string.Empty);
 
                    if (names.Any())
                        return new OperationStatus(succeeded: true, string.Format(FeaturesResources.Asynchronous_method_cannot_have_ref_out_parameters_colon_bracket_0_bracket, string.Join(", ", names)));
                }
 
                return OperationStatus.SucceededStatus;
            }
 
            private MultiDictionary<ISymbol, SyntaxToken> GetSymbolMap()
            {
                var symbolMap = new MultiDictionary<ISymbol, SyntaxToken>();
 
                var semanticModel = this.SemanticModel;
                var syntaxFacts = this.SyntaxFacts;
                var context = SelectionResult.GetContainingScope();
 
                foreach (var token in context.DescendantTokens())
                {
                    if (token.IsMissing ||
                        token.Width() <= 0 ||
                        !this.SelectionResult.FinalSpan.Contains(token.Span) ||
                        !syntaxFacts.IsIdentifier(token) ||
                        syntaxFacts.IsNameOfNamedArgument(token.Parent))
                    {
                        continue;
                    }
 
                    var symbolInfo = semanticModel.GetSymbolInfo(token, this.CancellationToken);
                    foreach (var sym in symbolInfo.GetAllSymbols())
                        symbolMap.Add(sym, token);
                }
 
                return symbolMap;
            }
 
            private ImmutableArray<VariableInfo> MarkVariableInfosToUseAsReturnValueIfPossible(
                ImmutableArray<VariableInfo> variableInfos, bool hasFlowControlResult)
            {
                var index = GetIndexOfVariableInfoToUseAsReturnValue(variableInfos, out var numberOfOutParameters, out var numberOfRefParameters);
 
                // If there are any variables we'd make out/ref and this is async, then we need to make these the return
                // values of the method since we can't actually have out/ref with an async method.
                //
                // Similarly, if we have a flow control variable, then make that one of the return values of the method
                // so that the caller can see what the called method wants to do.
                var outRefCount = numberOfOutParameters + numberOfRefParameters;
                var createAsyncTuple = outRefCount > 0 &&
                    this.SelectionResult.ContainsAwaitExpression() &&
                    this.SyntaxFacts.SupportsTupleDeconstruction(this.SemanticDocument.Document.Project.ParseOptions!);
                if (createAsyncTuple || hasFlowControlResult)
                {
                    var result = new FixedSizeArrayBuilder<VariableInfo>(variableInfos.Length);
                    foreach (var info in variableInfos)
                    {
                        result.Add(info.CanBeUsedAsReturnValue && info.ParameterModifier is ParameterBehavior.Out or ParameterBehavior.Ref
                            ? VariableInfo.CreateReturnValue(info)
                            : info);
                    }
 
                    return result.MoveToImmutable();
                }
 
                // If there's just one variable that would be ref/out, then make that the return value of the final method.
                if (index >= 0)
                    return variableInfos.SetItem(index, VariableInfo.CreateReturnValue(variableInfos[index]));
 
                return variableInfos;
            }
 
            /// <summary>
            /// among variables that will be used as parameters at the extracted method, check whether one of the parameter can be used as return
            /// </summary>
            private int GetIndexOfVariableInfoToUseAsReturnValue(
                ImmutableArray<VariableInfo> variableInfo,
                out int numberOfOutParameters,
                out int numberOfRefParameters)
            {
                numberOfOutParameters = 0;
                numberOfRefParameters = 0;
 
                var outSymbolIndex = -1;
                var refSymbolIndex = -1;
 
                for (var i = 0; i < variableInfo.Length; i++)
                {
                    var variable = variableInfo[i];
 
                    // there should be no-one set as return value yet
                    Contract.ThrowIfTrue(variable.UseAsReturnValue);
 
                    if (!variable.CanBeUsedAsReturnValue)
                    {
                        continue;
                    }
 
                    // check modifier
                    if (variable.ParameterModifier == ParameterBehavior.Ref ||
                        (variable.ParameterModifier == ParameterBehavior.Out && TreatOutAsRef))
                    {
                        numberOfRefParameters++;
                        refSymbolIndex = i;
                    }
                    else if (variable.ParameterModifier == ParameterBehavior.Out)
                    {
                        numberOfOutParameters++;
                        outSymbolIndex = i;
                    }
                }
 
                // if there is only one "out" or "ref", that will be converted to return statement.
                if (numberOfOutParameters == 1)
                {
                    return outSymbolIndex;
                }
 
                if (numberOfRefParameters == 1)
                {
                    return refSymbolIndex;
                }
 
                return -1;
            }
 
            /// <param name="bestEffort">When false, variables whose data flow is not understood
            /// will be returned in <paramref name="failedVariables"/>. When true, we assume any
            /// variable we don't understand has <see cref="VariableStyle.None"/></param>
            private void GenerateVariableInfoMap(
                bool bestEffort,
                DataFlowAnalysis dataFlowAnalysisData,
                MultiDictionary<ISymbol, SyntaxToken> symbolMap,
                bool isInPrimaryConstructorBaseType,
                out Dictionary<ISymbol, VariableInfo> variableInfoMap,
                out List<ISymbol> failedVariables)
            {
                Contract.ThrowIfNull(dataFlowAnalysisData);
 
                variableInfoMap = [];
                failedVariables = [];
 
                // create map of each data
                using var _0 = GetPooledSymbolSet(dataFlowAnalysisData.Captured, out var capturedMap);
                using var _1 = GetPooledSymbolSet(dataFlowAnalysisData.DataFlowsIn, out var dataFlowInMap);
                using var _2 = GetPooledSymbolSet(dataFlowAnalysisData.DataFlowsOut, out var dataFlowOutMap);
                using var _3 = GetPooledSymbolSet(dataFlowAnalysisData.AlwaysAssigned, out var alwaysAssignedMap);
                using var _4 = GetPooledSymbolSet(dataFlowAnalysisData.VariablesDeclared, out var variableDeclaredMap);
                using var _5 = GetPooledSymbolSet(dataFlowAnalysisData.ReadInside, out var readInsideMap);
                using var _6 = GetPooledSymbolSet(dataFlowAnalysisData.WrittenInside, out var writtenInsideMap);
                using var _7 = GetPooledSymbolSet(dataFlowAnalysisData.ReadOutside, out var readOutsideMap);
                using var _8 = GetPooledSymbolSet(dataFlowAnalysisData.WrittenOutside, out var writtenOutsideMap);
                using var _9 = GetPooledSymbolSet(dataFlowAnalysisData.UnsafeAddressTaken, out var unsafeAddressTakenMap);
 
                // gather all meaningful symbols for the span.
                var candidates = new HashSet<ISymbol>(readInsideMap);
                candidates.UnionWith(writtenInsideMap);
                candidates.UnionWith(variableDeclaredMap);
 
                // Need to analyze from the start of what we're extracting to the end of the scope that this variable could
                // have been referenced in.
                var containingScope = SelectionResult.GetContainingScope();
                var analysisRange = TextSpan.FromBounds(SelectionResult.FinalSpan.Start, containingScope.Span.End);
                var selectionOperation = this.SemanticModel.GetOperation(containingScope);
 
                foreach (var symbol in candidates)
                {
                    // We don't care about the 'this' parameter.  It will be available to an extracted method already.
                    if (symbol.IsThisParameter())
                        continue;
 
                    // Primary constructor parameters will be in scope for any instance extracted method.  No need to do
                    // anything special with them.  They won't be in scope for a static method generated in a primary
                    // constructor base type list.
                    if (!isInPrimaryConstructorBaseType && symbol is IParameterSymbol parameter && parameter.IsPrimaryConstructor(this.CancellationToken))
                        continue;
 
                    if (IsInteractiveSynthesizedParameter(symbol))
                        continue;
 
                    var captured = capturedMap.Contains(symbol);
                    var dataFlowIn = dataFlowInMap.Contains(symbol);
                    var dataFlowOut = dataFlowOutMap.Contains(symbol);
                    var alwaysAssigned = alwaysAssignedMap.Contains(symbol);
                    var variableDeclared = variableDeclaredMap.Contains(symbol);
                    var readInside = readInsideMap.Contains(symbol);
                    var writtenInside = writtenInsideMap.Contains(symbol);
                    var readOutside = IsReadOutside(symbol, readOutsideMap);
                    var writtenOutside = writtenOutsideMap.Contains(symbol);
                    var unsafeAddressTaken = unsafeAddressTakenMap.Contains(symbol);
 
                    // if it is static local, make sure it is not defined inside
                    if (symbol.IsStatic)
                    {
                        dataFlowIn = dataFlowIn && !variableDeclared;
                    }
 
                    // make sure readoutside is true when dataflowout is true (bug #3790)
                    // when a variable is only used inside of loop, a situation where dataflowout == true and readOutside == false
                    // can happen. but for extract method's point of view, this is not an information that would affect output.
                    // so, here we adjust flags to follow predefined assumption.
                    readOutside = readOutside || dataFlowOut;
 
                    // make sure data flow out is true when declared inside/written inside/read outside/not written outside are true (bug #6277)
                    dataFlowOut = dataFlowOut || (variableDeclared && writtenInside && readOutside && !writtenOutside);
 
                    // variable that is declared inside but never referenced outside. just ignore it and move to next one.
                    if (variableDeclared && !dataFlowOut && !readOutside && !writtenOutside)
                        continue;
 
                    // parameter defined inside of the selection (such as lambda parameter) will be ignored (bug # 10964)
                    if (symbol is IParameterSymbol && variableDeclared)
                        continue;
 
                    var type = GetSymbolTypeWithUpdatedNullability(symbol);
                    if (type == null)
                        continue;
 
                    // If the variable doesn't have a name, it is invalid.
                    if (symbol.Name.IsEmpty())
                        continue;
 
                    if (!TryGetVariableStyle(
                            bestEffort, symbolMap, symbol, type,
                            captured, dataFlowIn, dataFlowOut, alwaysAssigned, variableDeclared,
                            readInside, writtenInside, readOutside, writtenOutside, unsafeAddressTaken,
                            out var variableStyle))
                    {
                        Contract.ThrowIfTrue(bestEffort, "Should never fail if bestEffort is true");
                        failedVariables.Add(symbol);
                        continue;
                    }
 
                    // An assignment to a VB 'function value'.  (e.g. `MethodName = value`) needs to be treated as a
                    // return value from the inner function which the outer function then still assigns to its function
                    // value.
                    if (symbol is ILocalSymbol { IsFunctionValue: true } &&
                        variableStyle.ParameterStyle.DeclarationBehavior != DeclarationBehavior.None)
                    {
                        Contract.ThrowIfFalse(variableStyle.ParameterStyle.DeclarationBehavior == DeclarationBehavior.MoveIn || variableStyle.ParameterStyle.DeclarationBehavior == DeclarationBehavior.SplitIn);
                        variableStyle = AlwaysReturn(variableStyle);
                    }
 
                    AddVariableToMap(
                        variableInfoMap,
                        symbol,
                        CreateFromSymbol(symbol, type, variableStyle));
                }
 
                return;
 
                PooledDisposer<PooledHashSet<ISymbol>> GetPooledSymbolSet(ImmutableArray<ISymbol> symbols, out PooledHashSet<ISymbol> symbolSet)
                {
                    var disposer = PooledHashSet<ISymbol>.GetInstance(out symbolSet);
                    symbolSet.AddRange(symbols);
                    return disposer;
                }
 
                ITypeSymbol? GetSymbolTypeWithUpdatedNullability(ISymbol symbol)
                {
                    var type = GetUnderlyingSymbolType(symbol);
                    if (type is null)
                        return type;
 
                    // Check if null is possibly assigned to the symbol. If it is, leave nullable annotation as is, otherwise we
                    // can modify the annotation to be NotAnnotated to code that more likely matches the user's intent.
 
                    if (type.NullableAnnotation is not NullableAnnotation.Annotated)
                        return type;
 
                    // For Extract-Method we don't care about analyzing the declaration of this variable. For example, even if
                    // it was initially assigned 'null' for the purposes of determining the type of it for a return value, all
                    // we care is if it is null at the end of the selection.  If it is only assigned non-null values, for
                    // example, we want to treat it as non-null.
                    if (selectionOperation is not null &&
                        NullableHelpers.IsSymbolAssignedPossiblyNullValue(
                            this.SemanticFacts, this.SemanticModel, selectionOperation, symbol, analysisRange, includeDeclaration: false, this.CancellationToken) == false)
                    {
                        return type.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
                    }
 
                    return type;
                }
 
                static VariableInfo CreateFromSymbol(
                   ISymbol symbol,
                   ITypeSymbol type,
                   VariableStyle style)
                {
                    return symbol switch
                    {
                        ILocalSymbol local => new VariableInfo(
                            new LocalVariableSymbol(local, type),
                            style,
                            useAsReturnValue: false),
                        IParameterSymbol parameter => new VariableInfo(new ParameterVariableSymbol(parameter, type), style, useAsReturnValue: false),
                        IRangeVariableSymbol rangeVariable => new VariableInfo(new QueryVariableSymbol(rangeVariable, type), style, useAsReturnValue: false),
                        _ => throw ExceptionUtilities.UnexpectedValue(symbol)
                    };
                }
            }
 
            private ITypeSymbol? GetUnderlyingSymbolType(ISymbol symbol)
            {
                return symbol switch
                {
                    ILocalSymbol local => local.Type,
                    IParameterSymbol parameter => parameter.Type,
                    IRangeVariableSymbol rangeVariable => GetRangeVariableType(rangeVariable),
                    _ => throw ExceptionUtilities.UnexpectedValue(symbol)
                };
            }
 
            private static void AddVariableToMap(IDictionary<ISymbol, VariableInfo> variableInfoMap, ISymbol localOrParameter, VariableInfo variableInfo)
                => variableInfoMap.Add(localOrParameter, variableInfo);
 
            private bool TryGetVariableStyle(
                bool bestEffort,
                MultiDictionary<ISymbol, SyntaxToken> symbolMap,
                ISymbol symbol,
                ITypeSymbol type,
                bool captured,
                bool dataFlowIn,
                bool dataFlowOut,
                bool alwaysAssigned,
                bool variableDeclared,
                bool readInside,
                bool writtenInside,
                bool readOutside,
                bool writtenOutside,
                bool unsafeAddressTaken,
                out VariableStyle variableStyle)
            {
                Contract.ThrowIfNull(type);
 
                if (!ExtractMethodMatrix.TryGetVariableStyle(
                        bestEffort, dataFlowIn, dataFlowOut, alwaysAssigned, variableDeclared,
                        readInside, writtenInside, readOutside, writtenOutside, unsafeAddressTaken,
                        out variableStyle))
                {
                    Contract.ThrowIfTrue(bestEffort, "Should never fail if bestEffort is true");
                    return false;
                }
 
                if (SelectionContainsOnlyIdentifierWithSameType(type))
                {
                    return true;
                }
 
                // for captured variable, never try to move the decl into extracted method
                if (captured && variableStyle == VariableStyle.MoveIn)
                {
                    variableStyle = VariableStyle.Out;
                    return true;
                }
 
                // check special value type cases
                if (type.IsValueType && !IsWrittenInsideForFrameworkValueType(symbolMap, symbol, writtenInside))
                {
                    return true;
                }
 
                // don't blindly always return. make sure there is a write inside of the selection
                if (!writtenInside)
                {
                    return true;
                }
 
                variableStyle = AlwaysReturn(variableStyle);
                return true;
            }
 
            private bool IsWrittenInsideForFrameworkValueType(
                MultiDictionary<ISymbol, SyntaxToken> symbolMap, ISymbol symbol, bool writtenInside)
            {
                var tokens = symbolMap[symbol];
                if (tokens.Count == 0)
                    return writtenInside;
 
                var semanticFacts = this.SemanticFacts;
                return tokens.Any(t => semanticFacts.IsWrittenTo(this.SemanticModel, t.Parent, this.CancellationToken));
            }
 
            private bool SelectionContainsOnlyIdentifierWithSameType(ITypeSymbol type)
            {
                if (!SelectionResult.IsExtractMethodOnExpression)
                {
                    return false;
                }
 
                var firstToken = SelectionResult.GetFirstTokenInSelection();
                var lastToken = SelectionResult.GetLastTokenInSelection();
 
                if (!firstToken.Equals(lastToken))
                {
                    return false;
                }
 
                return type.Equals(SelectionResult.GetReturnType(this.CancellationToken));
            }
 
            protected static VariableStyle AlwaysReturn(VariableStyle style)
            {
                if (style == VariableStyle.InputOnly)
                {
                    return VariableStyle.Ref;
                }
 
                if (style == VariableStyle.MoveIn)
                {
                    return VariableStyle.Out;
                }
 
                if (style == VariableStyle.SplitIn)
                {
                    return VariableStyle.Out;
                }
 
                if (style == VariableStyle.SplitOut)
                {
                    return VariableStyle.OutWithMoveOut;
                }
 
                return style;
            }
 
            private static bool IsThisParameter(ISymbol localOrParameter)
            {
                if (localOrParameter is not IParameterSymbol parameter)
                {
                    return false;
                }
 
                return parameter.IsThis;
            }
 
            private static bool IsInteractiveSynthesizedParameter(ISymbol localOrParameter)
            {
                if (localOrParameter is not IParameterSymbol parameter)
                {
                    return false;
                }
 
                return parameter.IsImplicitlyDeclared &&
                       parameter.ContainingAssembly.IsInteractive &&
                       parameter.ContainingSymbol != null &&
                       parameter.ContainingSymbol.ContainingType != null &&
                       parameter.ContainingSymbol.ContainingType.IsScriptClass;
            }
 
            private void AddTypeParametersToMap(IEnumerable<ITypeParameterSymbol> typeParameters, IDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                foreach (var typeParameter in typeParameters)
                    AddTypeParameterToMap(typeParameter, sortedMap);
            }
 
            private void AddTypeParameterToMap(ITypeParameterSymbol typeParameter, IDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                if (typeParameter == null ||
                    typeParameter.DeclaringMethod == null ||
                    sortedMap.ContainsKey(typeParameter.Ordinal))
                {
                    return;
                }
 
                // Only care about type parameters declared outside of the span being selected.  If the type parameter
                // is within the selection, that means it comes from a generic local function and would not otherwise be
                // usable by the calling method.
                var selectionSpan = this.SelectionResult.FinalSpan;
                if (typeParameter.Locations is not [var location] || selectionSpan.Contains(location.SourceSpan))
                    return;
 
                sortedMap[typeParameter.Ordinal] = typeParameter;
            }
 
            private void AppendMethodTypeVariableFromDataFlowAnalysis(
                IDictionary<ISymbol, VariableInfo> variableInfoMap,
                IDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                foreach (var symbol in variableInfoMap.Keys)
                    AddTypeParametersToMap(TypeParameterCollector.Collect(GetUnderlyingSymbolType(symbol)), sortedMap);
            }
 
            private void AppendMethodTypeParameterFromConstraint(SortedDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                var typeParametersInConstraint = new List<ITypeParameterSymbol>();
 
                // collect all type parameter appears in constraint
                foreach (var typeParameter in sortedMap.Values)
                {
                    var constraintTypes = typeParameter.ConstraintTypes;
                    if (constraintTypes.IsDefaultOrEmpty)
                        continue;
 
                    foreach (var type in constraintTypes)
                    {
                        // constraint itself is type parameter
                        typeParametersInConstraint.AddRange(TypeParameterCollector.Collect(type));
                    }
                }
 
                // pick up only valid type parameter and add them to the map
                foreach (var typeParameter in typeParametersInConstraint)
                    AddTypeParameterToMap(typeParameter, sortedMap);
            }
 
            private void AppendMethodTypeParameterUsedDirectly(MultiDictionary<ISymbol, SyntaxToken> symbolMap, IDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                foreach (var typeParameter in symbolMap.Keys.OfType<ITypeParameterSymbol>())
                    AddTypeParameterToMap(typeParameter, sortedMap);
            }
 
            private ImmutableArray<ITypeParameterSymbol> GetMethodTypeParametersInConstraintList(
                IDictionary<ISymbol, VariableInfo> variableInfoMap,
                MultiDictionary<ISymbol, SyntaxToken> symbolMap,
                SortedDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                // find starting points
                AppendMethodTypeVariableFromDataFlowAnalysis(variableInfoMap, sortedMap);
                AppendMethodTypeParameterUsedDirectly(symbolMap, sortedMap);
 
                // recursively dive into constraints to find all constraints needed
                AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(sortedMap);
 
                return [.. sortedMap.Values];
            }
 
            private void AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(SortedDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                using var _1 = PooledHashSet<ITypeSymbol>.GetInstance(out var visited);
                using var _2 = PooledHashSet<ITypeParameterSymbol>.GetInstance(out var candidates);
 
                // collect all type parameter appears in constraint
                foreach (var typeParameter in sortedMap.Values)
                {
                    var constraintTypes = typeParameter.ConstraintTypes;
                    if (constraintTypes.IsDefaultOrEmpty)
                        continue;
 
                    foreach (var type in constraintTypes)
                        AddTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(type, visited, candidates);
                }
 
                // pick up only valid type parameter and add them to the map
                foreach (var typeParameter in candidates)
                    AddTypeParameterToMap(typeParameter, sortedMap);
            }
 
            private static void AddTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(
                ITypeSymbol type, HashSet<ITypeSymbol> visited, HashSet<ITypeParameterSymbol> typeParameters)
            {
                if (!visited.Add(type))
                    return;
 
                if (type.OriginalDefinition.Equals(type))
                    return;
 
                if (type is not INamedTypeSymbol constructedType)
                    return;
 
                var parameters = constructedType.GetAllTypeParameters();
                var arguments = constructedType.GetAllTypeArguments();
 
                Contract.ThrowIfFalse(parameters.Length == arguments.Length);
 
                for (var i = 0; i < parameters.Length; i++)
                {
                    if (arguments[i] is ITypeParameterSymbol argument)
                    {
                        if (parameters[i] is not
                            {
                                HasConstructorConstraint: false,
                                HasReferenceTypeConstraint: false,
                                HasValueTypeConstraint: false,
                                AllowsRefLikeType: false,
                                ConstraintTypes.IsDefaultOrEmpty: true
                            })
                        {
                            typeParameters.Add(argument);
                        }
                    }
                    else if (arguments[i] is INamedTypeSymbol candidate)
                    {
                        AddTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(candidate, visited, typeParameters);
                    }
                }
            }
 
            private ImmutableArray<ITypeParameterSymbol> GetMethodTypeParametersInDeclaration(ITypeSymbol returnType, SortedDictionary<int, ITypeParameterSymbol> sortedMap)
            {
                // add return type to the map
                AddTypeParametersToMap(TypeParameterCollector.Collect(returnType), sortedMap);
 
                AppendMethodTypeParameterFromConstraint(sortedMap);
 
                return [.. sortedMap.Values];
            }
 
            private OperationStatus CheckReadOnlyFields(MultiDictionary<ISymbol, SyntaxToken> symbolMap)
            {
                if (ReadOnlyFieldAllowed())
                    return OperationStatus.SucceededStatus;
 
                using var _ = ArrayBuilder<string>.GetInstance(out var names);
                var semanticFacts = this.SemanticFacts;
                foreach (var pair in symbolMap.Where(p => p.Key.Kind == SymbolKind.Field))
                {
                    var field = (IFieldSymbol)pair.Key;
                    if (!field.IsReadOnly)
                        continue;
 
                    var tokens = pair.Value;
                    if (tokens.All(t => !semanticFacts.IsWrittenTo(this.SemanticModel, t.Parent, CancellationToken)))
                        continue;
 
                    names.Add(field.Name ?? string.Empty);
                }
 
                if (names.Count > 0)
                    return new OperationStatus(succeeded: true, string.Format(FeaturesResources.Assigning_to_readonly_fields_must_be_done_in_a_constructor_colon_bracket_0_bracket, string.Join(", ", names)));
 
                return OperationStatus.SucceededStatus;
            }
        }
    }
}