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.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.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractMethod;
 
internal abstract partial class MethodExtractor<TSelectionResult, TStatementSyntax, TExpressionSyntax>
{
    protected abstract partial class Analyzer
    {
        private readonly SemanticDocument _semanticDocument;
 
        protected readonly CancellationToken CancellationToken;
        protected readonly TSelectionResult SelectionResult;
        protected readonly bool LocalFunction;
 
        protected Analyzer(TSelectionResult selectionResult, bool localFunction, CancellationToken cancellationToken)
        {
            Contract.ThrowIfNull(selectionResult);
 
            SelectionResult = selectionResult;
            _semanticDocument = selectionResult.SemanticDocument;
            CancellationToken = cancellationToken;
            LocalFunction = localFunction;
        }
 
        /// <summary>
        /// convert text span to node range for the flow analysis API
        /// </summary>
        private (TStatementSyntax, TStatementSyntax) GetFlowAnalysisNodeRange()
        {
            var first = this.SelectionResult.GetFirstStatement();
            var last = this.SelectionResult.GetLastStatement();
 
            // single statement case
            if (first == last ||
                first.Span.Contains(last.Span))
            {
                return (first, first);
            }
 
            // multiple statement case
            var firstUnderContainer = this.SelectionResult.GetFirstStatementUnderContainer();
            var lastUnderContainer = this.SelectionResult.GetLastStatementUnderContainer();
            return (firstUnderContainer, lastUnderContainer);
        }
 
        /// <summary>
        /// check whether selection contains return statement or not
        /// </summary>
        protected abstract bool ContainsReturnStatementInSelectedCode(IEnumerable<SyntaxNode> jumpOutOfRegionStatements);
 
        /// <summary>
        /// create VariableInfo type
        /// </summary>
        protected abstract VariableInfo CreateFromSymbol(Compilation compilation, ISymbol symbol, ITypeSymbol type, VariableStyle variableStyle, bool variableDeclared);
 
        /// <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(IList<VariableInfo> variableInfo)
        {
            var numberOfOutParameters = 0;
            var numberOfRefParameters = 0;
 
            var outSymbolIndex = -1;
            var refSymbolIndex = -1;
 
            for (var i = 0; i < variableInfo.Count; 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;
        }
 
        protected abstract bool TreatOutAsRef { get; }
 
        /// <summary>
        /// get type of the range variable symbol
        /// </summary>
        protected abstract ITypeSymbol GetRangeVariableType(SemanticModel model, IRangeVariableSymbol symbol);
 
        /// <summary>
        /// check whether the selection is at the placed where read-only field is allowed to be extracted out
        /// </summary>
        /// <returns></returns>
        protected abstract bool ReadOnlyFieldAllowed();
 
        public AnalyzerResult Analyze()
        {
            // do data flow analysis
            var model = _semanticDocument.SemanticModel;
            var dataFlowAnalysisData = GetDataFlowAnalysisData(model);
 
            // build symbol map for the identifiers used inside of the selection
            var symbolMap = GetSymbolMap(model);
 
            // gather initial local or parameter variable info
            GenerateVariableInfoMap(
                bestEffort: false, model, dataFlowAnalysisData, symbolMap,
                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, model, dataFlowAnalysisData, symbolMap,
                    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));
 
            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;
            var shouldBeReadOnly = !isThisParameterWritten
                && thisParameterBeingRead != null
                && thisParameterBeingRead.Type is { TypeKind: TypeKind.Struct, IsReadOnly: false };
 
            // check whether end of selection is reachable
            var endOfSelectionReachable = IsEndOfSelectionReachable(model);
 
            // collects various variable informations
            // extracted code contains return value
            var isInExpressionOrHasReturnStatement = IsInExpressionOrHasReturnStatement(model);
            var (parameters, returnType, returnsByRef, variableToUseAsReturnValue, unsafeAddressTakenUsed) =
                GetSignatureInformation(dataFlowAnalysisData, variableInfoMap, isInExpressionOrHasReturnStatement);
 
            var returnTypeTuple = AdjustReturnType(model, returnType);
 
            returnType = returnTypeTuple.typeSymbol;
            var returnTypeHasAnonymousType = returnTypeTuple.hasAnonymousType;
            var awaitTaskReturn = returnTypeTuple.awaitTaskReturn;
 
            // collect method type variable used in selected code
            var sortedMap = new SortedDictionary<int, ITypeParameterSymbol>();
            var typeParametersInConstraintList = GetMethodTypeParametersInConstraintList(model, variableInfoMap, symbolMap, sortedMap);
            var typeParametersInDeclaration = GetMethodTypeParametersInDeclaration(returnType, sortedMap);
 
            // check various error cases
            var operationStatus = GetOperationStatus(
                model, symbolMap, parameters, failedVariables, unsafeAddressTakenUsed, returnTypeHasAnonymousType, containsAnyLocalFunctionCallNotWithinSpan);
 
            return new AnalyzerResult(
                typeParametersInDeclaration,
                typeParametersInConstraintList,
                parameters,
                variableToUseAsReturnValue,
                returnType,
                returnsByRef,
                awaitTaskReturn,
                instanceMemberIsUsed,
                shouldBeReadOnly,
                endOfSelectionReachable,
                operationStatus);
        }
 
        private (ITypeSymbol typeSymbol, bool hasAnonymousType, bool awaitTaskReturn) AdjustReturnType(SemanticModel model, ITypeSymbol returnType)
        {
            // check whether return type contains anonymous type and if it does, fix it up by making it object
            var returnTypeHasAnonymousType = returnType.ContainsAnonymousType();
            returnType = returnTypeHasAnonymousType ? returnType.RemoveAnonymousTypes(model.Compilation) : returnType;
 
            // if selection contains await which is not under async lambda or anonymous delegate,
            // change return type to be wrapped in Task
            var shouldPutAsyncModifier = SelectionResult.ShouldPutAsyncModifier();
            if (shouldPutAsyncModifier)
            {
                WrapReturnTypeInTask(model, ref returnType, out var awaitTaskReturn);
 
                return (returnType, returnTypeHasAnonymousType, awaitTaskReturn);
            }
 
            // unwrap task if needed
            UnwrapTaskIfNeeded(model, ref returnType);
            return (returnType, returnTypeHasAnonymousType, false);
        }
 
        private void UnwrapTaskIfNeeded(SemanticModel model, ref ITypeSymbol returnType)
        {
            // nothing to unwrap
            if (!SelectionResult.ContainingScopeHasAsyncKeyword() ||
                !ContainsReturnStatementInSelectedCode(model))
            {
                return;
            }
 
            var originalDefinition = returnType.OriginalDefinition;
 
            // see whether it needs to be unwrapped
            var taskType = model.Compilation.TaskType();
            if (originalDefinition.Equals(taskType))
            {
                returnType = model.Compilation.GetSpecialType(SpecialType.System_Void);
                return;
            }
 
            var genericTaskType = model.Compilation.TaskOfTType();
            if (originalDefinition.Equals(genericTaskType))
            {
                returnType = ((INamedTypeSymbol)returnType).TypeArguments[0];
                return;
            }
 
            // nothing to unwrap
            return;
        }
 
        private void WrapReturnTypeInTask(SemanticModel model, ref ITypeSymbol returnType, out bool awaitTaskReturn)
        {
            awaitTaskReturn = false;
 
            var taskType = model.Compilation.TaskType();
 
            if (taskType is object && returnType.Equals(model.Compilation.GetSpecialType(SpecialType.System_Void)))
            {
                // convert void to Task type
                awaitTaskReturn = true;
                returnType = taskType;
                return;
            }
 
            if (!SelectionResult.SelectionInExpression && ContainsReturnStatementInSelectedCode(model))
            {
                // check whether we will use return type as it is or not.
                awaitTaskReturn = returnType.Equals(taskType);
                return;
            }
 
            var genericTaskType = model.Compilation.TaskOfTType();
 
            if (genericTaskType is object)
            {
                // okay, wrap the return type in Task<T>
                returnType = genericTaskType.Construct(returnType);
            }
        }
 
        private (ImmutableArray<VariableInfo> parameters, ITypeSymbol returnType, bool returnsByRef, VariableInfo? variableToUseAsReturnValue, bool unsafeAddressTakenUsed)
            GetSignatureInformation(
                DataFlowAnalysis dataFlowAnalysisData,
                Dictionary<ISymbol, VariableInfo> variableInfoMap,
                bool isInExpressionOrHasReturnStatement)
        {
            var model = _semanticDocument.SemanticModel;
            var compilation = model.Compilation;
            if (isInExpressionOrHasReturnStatement)
            {
                // check whether current selection contains return statement
                var parameters = GetMethodParameters(variableInfoMap);
                var (returnType, returnsByRef) = SelectionResult.GetReturnType();
                returnType ??= compilation.GetSpecialType(SpecialType.System_Object);
 
                var unsafeAddressTakenUsed = ContainsVariableUnsafeAddressTaken(dataFlowAnalysisData, variableInfoMap.Keys);
                return (parameters, returnType, returnsByRef, null, unsafeAddressTakenUsed);
            }
            else
            {
                // no return statement
                var parameters = MarkVariableInfoToUseAsReturnValueIfPossible(GetMethodParameters(variableInfoMap));
                var variableToUseAsReturnValue = parameters.FirstOrDefault(v => v.UseAsReturnValue);
                var returnType = variableToUseAsReturnValue != null
                    ? variableToUseAsReturnValue.GetVariableType()
                    : compilation.GetSpecialType(SpecialType.System_Void);
 
                var unsafeAddressTakenUsed = ContainsVariableUnsafeAddressTaken(dataFlowAnalysisData, variableInfoMap.Keys);
                return (parameters, returnType, returnsByRef: false, variableToUseAsReturnValue, unsafeAddressTakenUsed);
            }
        }
 
        private bool IsInExpressionOrHasReturnStatement(SemanticModel model)
        {
            var isInExpressionOrHasReturnStatement = SelectionResult.SelectionInExpression;
            if (!isInExpressionOrHasReturnStatement)
            {
                var containsReturnStatement = ContainsReturnStatementInSelectedCode(model);
                isInExpressionOrHasReturnStatement |= containsReturnStatement;
            }
 
            return isInExpressionOrHasReturnStatement;
        }
 
        private OperationStatus GetOperationStatus(
            SemanticModel model, Dictionary<ISymbol, List<SyntaxToken>> symbolMap,
            IList<VariableInfo> parameters, IList<ISymbol> failedVariables,
            bool unsafeAddressTakenUsed, bool returnTypeHasAnonymousType,
            bool containsAnyLocalFunctionCallNotWithinSpan)
        {
            var readonlyFieldStatus = CheckReadOnlyFields(model, symbolMap);
 
            var namesWithAnonymousTypes = parameters.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(parameters);
 
            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.ShouldPutAsyncModifier())
            {
                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 Dictionary<ISymbol, List<SyntaxToken>> GetSymbolMap(SemanticModel model)
        {
            var syntaxFactsService = _semanticDocument.Document.Project.Services.GetService<ISyntaxFactsService>();
            var context = SelectionResult.GetContainingScope();
            var symbolMap = SymbolMapBuilder.Build(syntaxFactsService, model, context, SelectionResult.FinalSpan, CancellationToken);
            return symbolMap;
        }
 
        private static bool ContainsVariableUnsafeAddressTaken(DataFlowAnalysis dataFlowAnalysisData, IEnumerable<ISymbol> symbols)
        {
            // check whether the selection contains "&" over a symbol exist
            var map = new HashSet<ISymbol>(dataFlowAnalysisData.UnsafeAddressTaken);
            return symbols.Any(map.Contains);
        }
 
        private DataFlowAnalysis GetDataFlowAnalysisData(SemanticModel model)
        {
            if (SelectionResult.SelectionInExpression)
                return model.AnalyzeDataFlow(SelectionResult.GetNodeForDataFlowAnalysis());
 
            var pair = GetFlowAnalysisNodeRange();
            return model.AnalyzeDataFlow(pair.Item1, pair.Item2);
        }
 
        private bool IsEndOfSelectionReachable(SemanticModel model)
        {
            if (SelectionResult.SelectionInExpression)
            {
                return true;
            }
 
            var pair = GetFlowAnalysisNodeRange();
            var analysis = model.AnalyzeControlFlow(pair.Item1, pair.Item2);
            return analysis.EndPointIsReachable;
        }
 
        private ImmutableArray<VariableInfo> MarkVariableInfoToUseAsReturnValueIfPossible(ImmutableArray<VariableInfo> variableInfo)
        {
            var index = GetIndexOfVariableInfoToUseAsReturnValue(variableInfo);
            if (index < 0)
                return variableInfo;
 
            return variableInfo.SetItem(index, VariableInfo.CreateReturnValue(variableInfo[index]));
        }
 
        private static ImmutableArray<VariableInfo> GetMethodParameters(Dictionary<ISymbol, VariableInfo> variableInfoMap)
        {
            var list = new FixedSizeArrayBuilder<VariableInfo>(variableInfoMap.Count);
            list.AddRange(variableInfoMap.Values);
            list.Sort();
            return list.MoveToImmutable();
        }
 
        /// <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,
            SemanticModel model,
            DataFlowAnalysis dataFlowAnalysisData,
            Dictionary<ISymbol, List<SyntaxToken>> symbolMap,
            out Dictionary<ISymbol, VariableInfo> variableInfoMap,
            out List<ISymbol> failedVariables)
        {
            Contract.ThrowIfNull(model);
            Contract.ThrowIfNull(dataFlowAnalysisData);
 
            variableInfoMap = [];
            failedVariables = [];
 
            // create map of each data
            var capturedMap = new HashSet<ISymbol>(dataFlowAnalysisData.Captured);
            var dataFlowInMap = new HashSet<ISymbol>(dataFlowAnalysisData.DataFlowsIn);
            var dataFlowOutMap = new HashSet<ISymbol>(dataFlowAnalysisData.DataFlowsOut);
            var alwaysAssignedMap = new HashSet<ISymbol>(dataFlowAnalysisData.AlwaysAssigned);
            var variableDeclaredMap = new HashSet<ISymbol>(dataFlowAnalysisData.VariablesDeclared);
            var readInsideMap = new HashSet<ISymbol>(dataFlowAnalysisData.ReadInside);
            var writtenInsideMap = new HashSet<ISymbol>(dataFlowAnalysisData.WrittenInside);
            var readOutsideMap = new HashSet<ISymbol>(dataFlowAnalysisData.ReadOutside);
            var writtenOutsideMap = new HashSet<ISymbol>(dataFlowAnalysisData.WrittenOutside);
            var unsafeAddressTakenMap = new HashSet<ISymbol>(dataFlowAnalysisData.UnsafeAddressTaken);
 
            // gather all meaningful symbols for the span.
            var candidates = new HashSet<ISymbol>(readInsideMap);
            candidates.UnionWith(writtenInsideMap);
            candidates.UnionWith(variableDeclaredMap);
 
            foreach (var symbol in candidates)
            {
                if (symbol.IsThisParameter() ||
                    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 = readOutsideMap.Contains(symbol);
                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 = GetSymbolType(model, 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, model, 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;
                }
 
                AddVariableToMap(variableInfoMap, symbol, CreateFromSymbol(model.Compilation, symbol, type, variableStyle, variableDeclared));
            }
        }
 
        private static void AddVariableToMap(IDictionary<ISymbol, VariableInfo> variableInfoMap, ISymbol localOrParameter, VariableInfo variableInfo)
            => variableInfoMap.Add(localOrParameter, variableInfo);
 
        private bool TryGetVariableStyle(
            bool bestEffort,
            Dictionary<ISymbol, List<SyntaxToken>> symbolMap,
            ISymbol symbol,
            SemanticModel model,
            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(model);
            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, model, 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(
            Dictionary<ISymbol, List<SyntaxToken>> symbolMap, SemanticModel model, ISymbol symbol, bool writtenInside)
        {
            if (!symbolMap.TryGetValue(symbol, out var tokens))
            {
                return writtenInside;
            }
 
            // this relies on the fact that our IsWrittenTo only cares about syntax to figure out whether
            // something is written to or not. but not semantic. 
            // we probably need to move the API to syntaxFact service not semanticFact.
            //
            // if one wants to get result that also considers semantic, he should use data control flow analysis API.
            var semanticFacts = _semanticDocument.Document.Project.Services.GetRequiredService<ISemanticFactsService>();
            return tokens.Any(t => semanticFacts.IsWrittenTo(model, t.Parent, CancellationToken.None));
        }
 
        private bool SelectionContainsOnlyIdentifierWithSameType(ITypeSymbol type)
        {
            if (!SelectionResult.SelectionInExpression)
            {
                return false;
            }
 
            var firstToken = SelectionResult.GetFirstTokenInSelection();
            var lastToken = SelectionResult.GetLastTokenInSelection();
 
            if (!firstToken.Equals(lastToken))
            {
                return false;
            }
 
            return type.Equals(SelectionResult.GetContainingScopeType());
        }
 
        protected virtual ITypeSymbol GetSymbolType(SemanticModel model, ISymbol symbol)
            => symbol switch
            {
                ILocalSymbol local => local.Type,
                IParameterSymbol parameter => parameter.Type,
                IRangeVariableSymbol rangeVariable => GetRangeVariableType(model, rangeVariable),
                _ => throw ExceptionUtilities.UnexpectedValue(symbol)
            };
 
        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 bool ContainsReturnStatementInSelectedCode(SemanticModel model)
        {
            Contract.ThrowIfTrue(SelectionResult.SelectionInExpression);
 
            var pair = GetFlowAnalysisNodeRange();
            var controlFlowAnalysisData = model.AnalyzeControlFlow(pair.Item1, pair.Item2);
 
            return ContainsReturnStatementInSelectedCode(controlFlowAnalysisData.ExitPoints);
        }
 
        private static void AddTypeParametersToMap(IEnumerable<ITypeParameterSymbol> typeParameters, IDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            foreach (var typeParameter in typeParameters)
            {
                AddTypeParameterToMap(typeParameter, sortedMap);
            }
        }
 
        private static void AddTypeParameterToMap(ITypeParameterSymbol typeParameter, IDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            if (typeParameter == null ||
                typeParameter.DeclaringMethod == null ||
                sortedMap.ContainsKey(typeParameter.Ordinal))
            {
                return;
            }
 
            sortedMap[typeParameter.Ordinal] = typeParameter;
        }
 
        private void AppendMethodTypeVariableFromDataFlowAnalysis(
            SemanticModel model,
            IDictionary<ISymbol, VariableInfo> variableInfoMap,
            IDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            foreach (var symbol in variableInfoMap.Keys)
            {
                switch (symbol)
                {
                    case IParameterSymbol parameter:
                        AddTypeParametersToMap(TypeParameterCollector.Collect(parameter.Type), sortedMap);
                        continue;
 
                    case ILocalSymbol local:
                        AddTypeParametersToMap(TypeParameterCollector.Collect(local.Type), sortedMap);
                        continue;
 
                    case IRangeVariableSymbol rangeVariable:
                        var type = GetRangeVariableType(model, rangeVariable);
                        AddTypeParametersToMap(TypeParameterCollector.Collect(type), sortedMap);
                        continue;
 
                    default:
                        throw ExceptionUtilities.UnexpectedValue(symbol);
                }
            }
        }
 
        private static 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 static void AppendMethodTypeParameterUsedDirectly(IDictionary<ISymbol, List<SyntaxToken>> symbolMap, IDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            foreach (var pair in symbolMap.Where(p => p.Key.Kind == SymbolKind.TypeParameter))
            {
                var typeParameter = (ITypeParameterSymbol)pair.Key;
                if (typeParameter.DeclaringMethod == null ||
                    sortedMap.ContainsKey(typeParameter.Ordinal))
                {
                    continue;
                }
 
                sortedMap[typeParameter.Ordinal] = typeParameter;
            }
        }
 
        private IEnumerable<ITypeParameterSymbol> GetMethodTypeParametersInConstraintList(
            SemanticModel model,
            IDictionary<ISymbol, VariableInfo> variableInfoMap,
            IDictionary<ISymbol, List<SyntaxToken>> symbolMap,
            SortedDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            // find starting points
            AppendMethodTypeVariableFromDataFlowAnalysis(model, variableInfoMap, sortedMap);
            AppendMethodTypeParameterUsedDirectly(symbolMap, sortedMap);
 
            // recursively dive into constraints to find all constraints needed
            AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(sortedMap);
 
            return sortedMap.Values.ToList();
        }
 
        private static void AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(SortedDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            var visited = new HashSet<ITypeSymbol>();
            var candidates = SpecializedCollections.EmptyEnumerable<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)
                {
                    candidates = candidates.Concat(AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(type, visited));
                }
            }
 
            // pick up only valid type parameter and add them to the map
            foreach (var typeParameter in candidates)
            {
                AddTypeParameterToMap(typeParameter, sortedMap);
            }
        }
 
        private static IEnumerable<ITypeParameterSymbol> AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(
            ITypeSymbol type, HashSet<ITypeSymbol> visited)
        {
            if (visited.Contains(type))
                return [];
 
            visited.Add(type);
 
            if (type.OriginalDefinition.Equals(type))
                return [];
 
            if (type is not INamedTypeSymbol constructedType)
                return [];
 
            var parameters = constructedType.GetAllTypeParameters().ToList();
            var arguments = constructedType.GetAllTypeArguments().ToList();
 
            Contract.ThrowIfFalse(parameters.Count == arguments.Count);
 
            var typeParameters = new List<ITypeParameterSymbol>();
            for (var i = 0; i < parameters.Count; i++)
            {
                var parameter = parameters[i];
 
                if (arguments[i] is ITypeParameterSymbol argument)
                {
                    // no constraint, nothing to do
                    if (!parameter.HasConstructorConstraint &&
                        !parameter.HasReferenceTypeConstraint &&
                        !parameter.HasValueTypeConstraint &&
                        !parameter.AllowsRefLikeType &&
                        parameter.ConstraintTypes.IsDefaultOrEmpty)
                    {
                        continue;
                    }
 
                    typeParameters.Add(argument);
                    continue;
                }
 
                if (arguments[i] is not INamedTypeSymbol candidate)
                {
                    continue;
                }
 
                typeParameters.AddRange(AppendTypeParametersInConstraintsUsedByConstructedTypeWithItsOwnConstraints(candidate, visited));
            }
 
            return typeParameters;
        }
 
        private static IEnumerable<ITypeParameterSymbol> GetMethodTypeParametersInDeclaration(ITypeSymbol returnType, SortedDictionary<int, ITypeParameterSymbol> sortedMap)
        {
            // add return type to the map
            AddTypeParametersToMap(TypeParameterCollector.Collect(returnType), sortedMap);
 
            AppendMethodTypeParameterFromConstraint(sortedMap);
 
            return sortedMap.Values.ToList();
        }
 
        private OperationStatus CheckReadOnlyFields(SemanticModel semanticModel, Dictionary<ISymbol, List<SyntaxToken>> symbolMap)
        {
            if (ReadOnlyFieldAllowed())
                return OperationStatus.SucceededStatus;
 
            using var _ = ArrayBuilder<string>.GetInstance(out var names);
            var semanticFacts = _semanticDocument.Document.Project.Services.GetRequiredService<ISemanticFactsService>();
            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(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;
        }
 
        protected static VariableInfo CreateFromSymbolCommon<T>(
            Compilation compilation,
            ISymbol symbol,
            ITypeSymbol type,
            VariableStyle style,
            HashSet<int> nonNoisySyntaxKindSet) where T : SyntaxNode
        {
            return symbol switch
            {
                ILocalSymbol local => new VariableInfo(
                    new LocalVariableSymbol<T>(compilation, local, type, nonNoisySyntaxKindSet),
                    style),
                IParameterSymbol parameter => new VariableInfo(new ParameterVariableSymbol(compilation, parameter, type), style),
                IRangeVariableSymbol rangeVariable => new VariableInfo(new QueryVariableSymbol(compilation, rangeVariable, type), style),
                _ => throw ExceptionUtilities.UnexpectedValue(symbol)
            };
        }
    }
}