Пример #1
0
            private async Task <bool> TryInitializeAsync(
                SemanticDocument document,
                TextSpan textSpan,
                CancellationToken cancellationToken)
            {
                cancellationToken.ThrowIfCancellationRequested();

                Expression = await document.Document.TryGetRelevantNodeAsync <TExpressionSyntax>(textSpan, cancellationToken).ConfigureAwait(false);

                if (Expression == null || CodeRefactoringHelpers.IsNodeUnderselected(Expression, textSpan))
                {
                    return(false);
                }

                // Don't introduce constant for another constant. Doesn't apply to sub-expression of constant.
                if (IsInitializerOfConstant(document, Expression))
                {
                    return(false);
                }

                var expressionType = Document.SemanticModel.GetTypeInfo(Expression, cancellationToken).Type;

                if (expressionType is IErrorTypeSymbol)
                {
                    return(false);
                }

                var containingType = Expression.AncestorsAndSelf()
                                     .Select(n => Document.SemanticModel.GetDeclaredSymbol(n, cancellationToken))
                                     .OfType <INamedTypeSymbol>()
                                     .FirstOrDefault();

                containingType ??= Document.SemanticModel.Compilation.ScriptClass;

                if (containingType == null || containingType.TypeKind == TypeKind.Interface)
                {
                    return(false);
                }

                if (!CanIntroduceVariable(textSpan.IsEmpty, cancellationToken))
                {
                    return(false);
                }

                IsConstant = IsExpressionConstant(Document, Expression, _service, cancellationToken);

                // Note: the ordering of these clauses are important.  They go, generally, from
                // innermost to outermost order.
                if (IsInQueryContext(cancellationToken))
                {
                    if (CanGenerateInto <TQueryExpressionSyntax>(cancellationToken))
                    {
                        InQueryContext = true;
                        return(true);
                    }

                    return(false);
                }

                if (IsInConstructorInitializerContext(cancellationToken))
                {
                    if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken))
                    {
                        InConstructorInitializerContext = true;
                        return(true);
                    }

                    return(false);
                }

                var enclosingBlocks = _service.GetContainingExecutableBlocks(Expression);

                if (enclosingBlocks.Any())
                {
                    // If we're inside a block, then don't even try the other options (like field,
                    // constructor initializer, etc.).  This is desirable behavior.  If we're in a
                    // block in a field, then we're in a lambda, and we want to offer to generate
                    // a local, and not a field.
                    if (IsInBlockContext(cancellationToken))
                    {
                        InBlockContext = true;
                        return(true);
                    }

                    return(false);
                }

                // NOTE: All checks from this point forward are intentionally ordered to be AFTER the check for Block Context.

                // If we are inside a block within an Expression bodied member we should generate inside the block,
                // instead of rewriting a concise expression bodied member to its equivalent that has a body with a block.
                if (_service.IsInExpressionBodiedMember(Expression))
                {
                    if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken))
                    {
                        InExpressionBodiedMemberContext = true;
                        return(true);
                    }

                    return(false);
                }

                if (_service.IsInAutoPropertyInitializer(Expression))
                {
                    if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken))
                    {
                        InAutoPropertyInitializerContext = true;
                        return(true);
                    }

                    return(false);
                }

                if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken))
                {
                    if (IsInParameterContext(cancellationToken))
                    {
                        InParameterContext = true;
                        return(true);
                    }
                    else if (IsInFieldContext(cancellationToken))
                    {
                        InFieldContext = true;
                        return(true);
                    }
                    else if (IsInAttributeContext())
                    {
                        InAttributeContext = true;
                        return(true);
                    }
                }

                return(false);
Пример #2
0
        public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            var(document, textSpan, cancellationToken) = context;
            var possibleExpressions = await context.GetRelevantNodesAsync <TExpressionSyntax>().ConfigureAwait(false);

            var syntaxFacts   = document.GetLanguageService <ISyntaxFactsService>();
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            // let's take the largest (last) StringConcat we can given current textSpan
            var top = possibleExpressions
                      .Where(expr => IsStringConcat(syntaxFacts, expr, semanticModel, cancellationToken))
                      .LastOrDefault();

            if (top == null)
            {
                return;
            }

            // if there is a const keyword, the refactoring shouldn't show because interpolated string is not const string
            var declarator = top.FirstAncestorOrSelf <SyntaxNode>(syntaxFacts.IsVariableDeclarator);

            if (declarator != null)
            {
                var generator = SyntaxGenerator.GetGenerator(document);
                if (generator.GetModifiers(declarator).IsConst)
                {
                    return;
                }
            }

            // Currently we can concatenate only full subtrees. Therefore we can't support arbitrary selection. We could
            // theoretically support selecting the selections that correspond to full sub-trees (e.g. prefixes of
            // correct length but from UX point of view that it would feel arbitrary).
            // Thus, we only support selection that takes the whole topmost expression. It breaks some leniency around under-selection
            // but it's the best solution so far.
            if (CodeRefactoringHelpers.IsNodeUnderselected(top, textSpan) ||
                IsStringConcat(syntaxFacts, top.Parent, semanticModel, cancellationToken))
            {
                return;
            }

            // Now walk down the concatenation collecting all the pieces that we are
            // concatenating.
            using var _ = ArrayBuilder <SyntaxNode> .GetInstance(out var pieces);

            CollectPiecesDown(syntaxFacts, pieces, top, semanticModel, cancellationToken);

            var stringLiterals = pieces
                                 .Where(x => syntaxFacts.IsStringLiteralExpression(x) || syntaxFacts.IsCharacterLiteralExpression(x))
                                 .ToImmutableArray();

            // If the entire expression is just concatenated strings, then don't offer to
            // make an interpolated string.  The user likely manually split this for
            // readability.
            if (stringLiterals.Length == pieces.Count)
            {
                return;
            }

            var isVerbatimStringLiteral = false;

            if (stringLiterals.Length > 0)
            {
                // Make sure that all the string tokens we're concatenating are the same type
                // of string literal.  i.e. if we have an expression like: @" "" " + " \r\n "
                // then we don't merge this.  We don't want to be munging different types of
                // escape sequences in these strings, so we only support combining the string
                // tokens if they're all the same type.
                var firstStringToken = stringLiterals[0].GetFirstToken();
                isVerbatimStringLiteral = syntaxFacts.IsVerbatimStringLiteral(firstStringToken);
                if (stringLiterals.Any(
                        lit => isVerbatimStringLiteral != syntaxFacts.IsVerbatimStringLiteral(lit.GetFirstToken())))
                {
                    return;
                }
            }

            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            var interpolatedString = CreateInterpolatedString(document, isVerbatimStringLiteral, pieces);

            context.RegisterRefactoring(
                new MyCodeAction(
                    _ => UpdateDocumentAsync(document, root, top, interpolatedString)),
                top.Span);
        }
Пример #3
0
        public async Task <ImmutableArray <TSyntaxNode> > GetRelevantNodesAsync <TSyntaxNode>(
            Document document, TextSpan selectionRaw,
            CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
        {
            // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends
            // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing
            // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia
            // (whitespace) of following statement.

            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            if (root == null)
            {
                return(ImmutableArray <TSyntaxNode> .Empty);
            }

            var syntaxFacts      = document.Project.LanguageServices.GetRequiredService <ISyntaxFactsService>();
            var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpan(document, selectionRaw, cancellationToken).ConfigureAwait(false);

            // If user selected only whitespace we don't want to return anything. We could do following:
            //  1) Consider token that owns (as its trivia) the whitespace.
            //  2) Consider start/beginning of whitespace as location (empty selection)
            // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the
            // most consistent option.
            if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty)
            {
                return(ImmutableArray <TSyntaxNode> .Empty);
            }

            using var relevantNodesBuilderDisposer = ArrayBuilder <TSyntaxNode> .GetInstance(out var relevantNodesBuilder);

            // Every time a Node is considered an extractNodes method is called to add all nodes around the original one
            // that should also be considered.
            //
            // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially
            // lang. & situation dependent) into Children descending code here.  We can't just try extracted Node because we might
            // want the whole node `var a = b;`

            // Handle selections:
            // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]`
            //   - The smallest node whose FullSpan includes the whole (trimmed) selection
            //   - Using FullSpan is important because it handles over-selection with comments
            //   - Travels upwards through same-sized (FullSpan) nodes, extracting
            // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`)
            // Note: Whether we have selection or location has to be checked against original selection because selecting just
            // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[|   |]token`
            // registering as `   [||]token`.
            if (!selectionTrimmed.IsEmpty)
            {
                AddRelevantNodesForSelection(syntaxFacts, root, selectionTrimmed, relevantNodesBuilder, cancellationToken);
            }
            else
            {
                // No more selection -> Handle what current selection is touching:
                //
                // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as
                // touching the Method's Node (through the left edge, see below) which is something the user probably
                // didn't want since they specifically selected only the return type.
                //
                // What the selection is touching is used in two ways.
                // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted Node.
                // While having the (even empty) selection inside such token or to left of such Token is already handle
                // by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that returns Args node).
                // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's direct
                // ancestor is TypeNode for the return type but it is still reasonable to expect that the user might want to
                // be given refactorings for the whole method (as he has caret on the edge of it). Therefore we travel the
                // Node tree upwards and as long as we're on the left edge of a Node's span we consider such node & potentially
                // continue traveling upwards. The situation for right edge (`C methodName(){}[||]`) is analogical.
                // E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> BlockSyntax -> LocalFunctionStatement -> null (higher
                // node doesn't end on position anymore)
                // Note: left-edge climbing needs to handle AttributeLists explicitly, see below for more information.
                // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia location is in.
                // more about that below.
                // - Fourthly, if we're in an expression / argument we consider touching a parent expression whenever we're within it
                // as long as it is on the first line of such expression (arbitrary heuristic).

                // First we need to get tokens we might potentially be touching, tokenToRightOrIn and tokenToLeft.
                var(tokenToRightOrIn, tokenToLeft, location) = await GetTokensToRightOrInToLeftAndUpdatedLocation(
                    document, root, selectionTrimmed, cancellationToken).ConfigureAwait(false);


                // In addition to per-node extr also check if current location (if selection is empty) is in a header of higher level
                // desired node once. We do that only for locations because otherwise `[|int|] A { get; set; }) would trigger all refactorings for
                // Property Decl.
                // We cannot check this any sooner because the above code could've changed current location.
                AddNonHiddenCorrectTypeNodes(ExtractNodesInHeader(root, location, syntaxFacts), relevantNodesBuilder, cancellationToken);

                // Add Nodes for touching tokens as described above.
                AddNodesForTokenToRightOrIn(syntaxFacts, root, relevantNodesBuilder, location, tokenToRightOrIn, cancellationToken);
                AddNodesForTokenToLeft(syntaxFacts, relevantNodesBuilder, location, tokenToLeft, cancellationToken);

                // If the wanted node is an expression syntax -> traverse upwards even if location is deep within a SyntaxNode.
                // We want to treat more types like expressions, e.g.: ArgumentSyntax should still trigger even if deep-in.
                if (IsWantedTypeExpressionLike <TSyntaxNode>())
                {
                    // Reason to treat Arguments (and potentially others) as Expression-like:
                    // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904
                    await AddNodesDeepIn(document, location, relevantNodesBuilder, cancellationToken).ConfigureAwait(false);
                }
            }

            return(relevantNodesBuilder.ToImmutable());
        }
Пример #4
0
        protected async Task <SyntaxNode> TryGetSelectedNodeAsync(
            Document document, TextSpan selectionRaw,
            Func <SyntaxNode, bool> predicate,
            Func <SyntaxNode, ISyntaxFactsService, IEnumerable <SyntaxNode> > extractNodes,
            Func <SyntaxNode, int, ISyntaxFactsService, IEnumerable <SyntaxNode> > extracNodestIfInHeader,
            CancellationToken cancellationToken)
        {
            // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends
            // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing
            // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia
            // (whitespace) of following statement.

            var syntaxFacts = document.GetLanguageService <ISyntaxFactsService>();
            var root        = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpan(document, selectionRaw, cancellationToken).ConfigureAwait(false);

            // If user selected only whitespace we don't want to return anything. We could do following:
            //  1) Consider token that owns (as its trivia) the whitespace.
            //  2) Consider start/beginning of whitespace as location (empty selection)
            // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the
            // most consistent option.
            if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty)
            {
                return(null);
            }

            // Every time a Node is considered by an extractNodes method is called to check & potentially return nodes around the original one
            // that should also be considered.
            //
            // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially
            // lang. & situation dependent) into Children descending code here.  We can't just try extracted Node because we might
            // want the whole node `var a = b;`
            //
            // In addition to per-node extractions we also check if current location (if selection is empty) is in a header of higher level
            // desired node once. We do that only for locations because otherwise `[|int|] A { get; set; }) would trigger all refactorings for
            // Property Decl.

            // Handle selections:
            // - The smallest node whose FullSpan includes the whole (trimmed) selection
            //   - Using FullSpan is important because it handles over-selection with comments
            // - Travels upwards through same-sized (FullSpan) nodes, extracting and testing predicate
            // - Handles situations where:
            //  - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`)
            //  - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]`
            var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true);
            var prevNode      = selectionNode;

            do
            {
                var acceptedNode = extractNodes(selectionNode, syntaxFacts).FirstOrDefault(predicate);
                if (acceptedNode != null)
                {
                    // For selections we need to handle an edge case where only AttributeLists are within selection (e.g. `Func([|[in][out]|] arg1);`).
                    // In that case the smallest encompassing node is still the whole argument node but it's hard to justify showing refactorings for it
                    // if user selected only its attributes.

                    // Selection contains only AttributeLists -> don't consider current Node
                    var spanWithoutAttributes = GetSpanWithoutAttributes(acceptedNode, root, syntaxFacts);
                    if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes))
                    {
                        break;
                    }

                    return(acceptedNode);
                }

                prevNode      = selectionNode;
                selectionNode = selectionNode.Parent;
            }while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth());

            // Handle what current selection is touching:
            //
            // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as
            // touching the Method's Node (through the left edge, see below) which is something the user probably
            // didn't want since they specifically selected only the return type.
            //
            // Whether we have selection of location has to be checked against original selection because selecting just
            // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[|   |]token`
            // registering as `   [||]token`.
            //
            // What the selection is touching is used in two ways.
            // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted Node.
            // While having the (even empty) selection inside such token or to left of such Token is already handle
            // by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that returns Args node).
            // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's direct
            // ancestor is TypeNode for the return type but it is still reasonable to expect that the user might want to
            // be given refactorings for the whole method (as he has caret on the edge of it). Therefore we travel the
            // Node tree upwards and as long as we're on the left edge of a Node's span we consider such node & potentially
            // continue traveling upwards. The situation for right edge (`C methodName(){}[||]`) is analogical.
            // E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> BlockSyntax -> LocalFunctionStatement -> null (higher
            // node doesn't end on position anymore)
            // Note: left-edge climbing needs to handle AttributeLists explicitly, see below for more information.
            // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia location is in.
            // more about that below.
            if (!selectionRaw.IsEmpty)
            {
                return(null);
            }

            // get Token for current location
            var location        = selectionTrimmed.Start;
            var tokenOnLocation = root.FindToken(location);

            // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`)
            var tokenToRightOrIn = tokenOnLocation.Span.Contains(location)
                ? tokenOnLocation
                : default;

            // A token can be to the left only when there's either no tokenDirectlyToRightOrIn or there's one  directly starting at current location.
            // Otherwise (otherwise tokenToRightOrIn is also left from location, e.g: `tok[||]enToRightOrIn`)
            var tokenToLeft = default(SyntaxToken);

            if (tokenToRightOrIn == default || tokenToRightOrIn.FullSpan.Start == location)
            {
                var tokenPreLocation = (tokenOnLocation.Span.End == location)
                    ? tokenOnLocation
                    : tokenOnLocation.GetPreviousToken();

                tokenToLeft = (tokenPreLocation.Span.End == location)
                    ? tokenPreLocation
                    : default;
            }

            // If both tokens directly to left & right are empty -> we're somewhere in the middle of whitespace.
            // Since there wouldn't be (m)any other refactorings we can try to offer at least the ones for (semantically)
            // closest token/Node. Thus, we move the location to the token in whose `.FullSpan` the original location was.
            if (tokenToLeft == default && tokenToRightOrIn == default)
            {
                var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

                var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

                // assume non-trivia token can't span multiple lines
                var tokenLine    = sourceText.Lines.GetLineFromPosition(tokenOnLocation.Span.Start);
                var locationLine = sourceText.Lines.GetLineFromPosition(location);

                // Change location to nearest token only if the token is off by one line or less
                if (Math.Abs(tokenLine.LineNumber - locationLine.LineNumber) <= 1)
                {
                    // Note: being a line below a tokenOnLocation is impossible in current model as whitespace
                    // trailing trivia ends on new line. Which is fine because if you're a line _after_ some node
                    // you usually don't want refactorings for what's above you.

                    // tokenOnLocation: token in whose trivia location is at
                    if (tokenOnLocation.Span.Start >= location)
                    {
                        tokenToRightOrIn = tokenOnLocation;
                        location         = tokenToRightOrIn.Span.Start;
                    }
                    else
                    {
                        tokenToLeft = tokenOnLocation;
                        location    = tokenToLeft.Span.End;
                    }
                }
            }

            // First check if we're in a header of some higher-level node what would pass predicate & if we are -> return it
            // We can't check any sooner because the code above (that figures out tokenToLeft, ...) can change current
            // `location`.
            var acceptedHeaderNode = extracNodestIfInHeader(root, location, syntaxFacts).FirstOrDefault(predicate);

            if (acceptedHeaderNode != null)
            {
                return(acceptedHeaderNode);
            }

            if (tokenToRightOrIn != default)
            {
                var rightNode = tokenOnLocation.Parent;
                do
                {
                    // Consider either a Node that is:
                    // - Parent of touched Token (location can be within)
                    // - Ancestor Node of such Token as long as their span starts on location (it's still on the edge)
                    var acceptedNode = extractNodes(rightNode, syntaxFacts).FirstOrDefault(predicate);
                    if (acceptedNode != null)
                    {
                        return(acceptedNode);
                    }

                    rightNode = rightNode.Parent;
                    if (rightNode == null)
                    {
                        break;
                    }

                    // The edge climbing for node to the right needs to handle Attributes e.g.:
                    // [Test1]
                    // //Comment1
                    // [||]object Property1 { get; set; }
                    // In essence:
                    // - On the left edge of the node (-> left edge of first AttributeLists)
                    // - On the left edge of the node sans AttributeLists (& as everywhere comments)
                    if (rightNode.Span.Start != location)
                    {
                        var rightNodeSpanWithoutAttributes = GetSpanWithoutAttributes(rightNode, root, syntaxFacts);
                        if (rightNodeSpanWithoutAttributes.Start != location)
                        {
                            break;
                        }
                    }
                }while (true);
            }

            if (tokenToLeft != default)
            {
                var leftNode = tokenToLeft.Parent;
                do
                {
                    // Consider either a Node that is:
                    // - Ancestor Node of such Token as long as their span ends on location (it's still on the edge)
                    var acceptedNode = extractNodes(leftNode, syntaxFacts).FirstOrDefault(predicate);
                    if (acceptedNode != null)
                    {
                        return(acceptedNode);
                    }

                    leftNode = leftNode.Parent;
                    if (leftNode == null || leftNode.GetLastToken().Span.End != location)
                    {
                        break;
                    }
                }while (true);
            }

            // nothing found -> return null

            return(null);
        }
        public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            var(document, textSpan, cancellationToken) = context;
            if (document.Project.Solution.Workspace.Kind == WorkspaceKind.MiscellaneousFiles)
            {
                return;
            }

            var expression = await document.TryGetRelevantNodeAsync <TExpressionSyntax>(textSpan, cancellationToken).ConfigureAwait(false);

            if (expression == null || CodeRefactoringHelpers.IsNodeUnderselected(expression, textSpan))
            {
                return;
            }

            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            var expressionType = semanticModel.GetTypeInfo(expression, cancellationToken).Type;

            if (expressionType is null or IErrorTypeSymbol)
            {
                return;
            }

            var syntaxFacts = document.GetRequiredLanguageService <ISyntaxFactsService>();

            // Need to special case for expressions that are contained within a parameter
            // because it is technically "contained" within a method, but an expression in a parameter does not make
            // sense to introduce.
            var parameterNode = expression.FirstAncestorOrSelf <SyntaxNode>(node => syntaxFacts.IsParameter(node));

            if (parameterNode is not null)
            {
                return;
            }

            // Need to special case for highlighting of method types because they are also "contained" within a method,
            // but it does not make sense to introduce a parameter in that case.
            if (syntaxFacts.IsInNamespaceOrTypeContext(expression))
            {
                return;
            }

            // Need to special case for expressions whose direct parent is a MemberAccessExpression since they will
            // never introduce a parameter that makes sense in that case.
            if (syntaxFacts.IsNameOfAnyMemberAccessExpression(expression))
            {
                return;
            }

            var generator        = SyntaxGenerator.GetGenerator(document);
            var containingMethod = expression.FirstAncestorOrSelf <SyntaxNode>(node => generator.GetParameterListNode(node) is not null);

            if (containingMethod is null)
            {
                return;
            }

            var containingSymbol = semanticModel.GetDeclaredSymbol(containingMethod, cancellationToken);

            if (containingSymbol is not IMethodSymbol methodSymbol)
            {
                return;
            }

            var expressionSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol;

            if (expressionSymbol is IParameterSymbol parameterSymbol && parameterSymbol.ContainingSymbol.Equals(containingSymbol))
            {
                return;
            }

            // Code actions for trampoline and overloads will not be offered if the method is a constructor.
            // Code actions for overloads will not be offered if the method if the method is a local function.
            var methodKind = methodSymbol.MethodKind;

            if (methodKind is not(MethodKind.Ordinary or MethodKind.LocalFunction or MethodKind.Constructor))
            {
                return;
            }

            if (IsDestructor(methodSymbol))
            {
                return;
            }

            var actions = await GetActionsAsync(document, expression, methodSymbol, containingMethod, context.Options, cancellationToken).ConfigureAwait(false);

            if (actions is null)
            {
                return;
            }

            var singleLineExpression = syntaxFacts.ConvertToSingleLine(expression);
            var nodeString           = singleLineExpression.ToString();

            if (actions.Value.actions.Length > 0)
            {
                context.RegisterRefactoring(CodeActionWithNestedActions.Create(
                                                string.Format(FeaturesResources.Introduce_parameter_for_0, nodeString), actions.Value.actions, isInlinable: false, priority: CodeActionPriority.Low), textSpan);
            }

            if (actions.Value.actionsAllOccurrences.Length > 0)
            {
                context.RegisterRefactoring(CodeActionWithNestedActions.Create(
                                                string.Format(FeaturesResources.Introduce_parameter_for_all_occurrences_of_0, nodeString), actions.Value.actionsAllOccurrences, isInlinable: false,
                                                priority: CodeActionPriority.Low), textSpan);
            }
        }
Пример #6
0
        /// <summary>
        /// <para>
        /// Returns a Node for refactoring given specified selection that passes <paramref name="predicate"/>
        /// or null if no such instance exists.
        /// </para>
        /// <para>
        /// A node instance is return if:
        /// - Selection is zero-width and inside/touching a Token with direct parent passing <paramref name="predicate"/>.
        /// - Selection is zero-width and touching a Token whose ancestor Node passing <paramref name="predicate"/> ends/starts precisely on current selection.
        /// - Token whose direct parent passing <paramref name="predicate"/> is selected.
        /// - Whole node passing <paramref name="predicate"/> is selected.
        /// </para>
        /// <para>
        /// The <paramref name="extractNode"/> enables testing with <paramref name="predicate"/> and potentially returning Nodes
        /// that are under those that might be selected / considered (as described above). It is a <see cref="Func{SyntaxNode, SyntaxNode}"/> that
        /// should always return either given Node or a Node somewhere below it that should be tested with <paramref name="predicate"/> and
        /// potentially returned instead of current Node.
        /// </para>
        /// <para>
        /// Note: this function trims all whitespace from both the beginning and the end of given <paramref name="selection"/>.
        /// The trimmed version is then used to determine relevant <see cref="SyntaxNode"/>. It also handles incomplete selections
        /// of tokens gracefully.
        /// </para>
        /// </summary>
        protected async Task <SyntaxNode> TryGetSelectedNodeAsync(Document document, TextSpan selection, Predicate <SyntaxNode> predicate, Func <SyntaxNode, ISyntaxFactsService, SyntaxNode> extractNode, CancellationToken cancellationToken)
        {
            // Given selection is trimmed first to enable overselection that spans multiple lines. Since trailing whitespace ends
            // at newline boundary overselection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing
            // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia
            // (whitespace) of following statement.

            var syntaxFacts = document.GetLanguageService <ISyntaxFactsService>();
            var root        = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpan(document, selection, cancellationToken).ConfigureAwait(false);

            // Everytime a Node is considered by following algorithm (and tested with predicate) and the predicate fails
            // extractNode is called on the node and the result is tested with predicate again. If any of those succeed
            // a respective Node gets returned.
            //
            // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially
            // lang. & situation dependant) into Children descending code here.  We can't just try extracted Node because we might
            // want the whole node `var a = b;`
            //
            // See local function TryGetAcceptedNodeOrExtracted DefaultNodeExtractor for more info.


            // Handle selections:
            // - The smallest node whose FullSpan inlcudes the whole (trimmed) selection
            // - Travels upwards through same-sized (FullSpan) nodes, extracting and testing predicate
            // - Handles situations where:
            //  - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`)
            //  - Most/the whole wanted Node is seleted (e.g. `C [|Fun() {}|]`
            var        node = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true);
            SyntaxNode prevNode;

            do
            {
                var wantedNode = TryGetAcceptedNodeOrExtracted(node, predicate, extractNode, syntaxFacts);
                if (wantedNode != null)
                {
                    return(wantedNode);
                }

                prevNode = node;
                node     = node.Parent;
            }while (node != null && prevNode.FullWidth() == node.FullWidth());

            // Handle what current selection is touching:
            //
            // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as
            // touching the Method's Node (through the left edge, see below) which is something the user probably
            // didn't want since they specifically selected only the return type.
            //
            // What the selection is touching is used in two ways.
            // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted Node.
            // While having the (even empty) selection inside such token or to left of such Token is already handle
            // by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that returns Args node).
            // - Secondly it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's direct
            // ancestor is TypeNode for the return type but it is still resonable to expect that the user might want to
            // be given refactorings for the whole method (as he has caret on the edge of it). Therefore we travel the
            // Node tree upwards and as long as we're on the left edge of a Node's span we consider such node & potentially
            // continue travelling upwards. The situation for right edge (`C methodName(){}[||]`) is analogical.
            // E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> BlockSyntax -> LocalFunctionStatement -> null (higher
            // node doesn't end on position anymore)
            if (!selection.IsEmpty)
            {
                return(null);
            }

            // get Token for current selection (empty) location
            var tokenOnSelection = root.FindToken(selectionTrimmed.Start);

            // Gets a token that is directly to the right of current (empty) selection or that encompases current selection (`[||]tokenITORightOrIn` or `tok[||]enITORightOrIn`)
            var tokenToRightOrIn = tokenOnSelection.Span.Contains(selectionTrimmed.Start)
                ? tokenOnSelection
                : default;

            if (tokenToRightOrIn != default)
            {
                var rightNode = tokenOnSelection.Parent;
                do
                {
                    // Consider either a Node that is:
                    // - Parent of touched Token (selection can be within)
                    // - Ancestor Node of such Token as long as their their span starts on selection (it's still on the edge)
                    var wantedNode = TryGetAcceptedNodeOrExtracted(rightNode, predicate, extractNode, syntaxFacts);
                    if (wantedNode != null)
                    {
                        return(wantedNode);
                    }

                    rightNode = rightNode?.Parent;
                }while (rightNode != null && rightNode.Span.Start == selection.Start);
            }

            // if the selection is inside tokenToRightOrIn -> no Token can be to Left (tokenToRightOrIn is also left from selection, e.g: `tok[||]enITORightOrIn`)
            if (tokenToRightOrIn != default && tokenToRightOrIn.Span.Start != selectionTrimmed.Start)
            {
                return(null);
            }

            // Token to left: a token whose span ends on current (empty) selection
            var tokenPreSelection = (tokenOnSelection.Span.End == selectionTrimmed.Start)
                ? tokenOnSelection
                : tokenOnSelection.GetPreviousToken();

            var tokenToLeft = (tokenPreSelection.Span.End == selectionTrimmed.Start)
                ? tokenPreSelection
                : default;

            if (tokenToLeft != default)
            {
                var leftNode = tokenToLeft.Parent;
                do
                {
                    // Consider either a Node that is:
                    // - Ancestor Node of such Token as long as their their span ends on selection (it's still on the edge)
                    var wantedNode = TryGetAcceptedNodeOrExtracted(leftNode, predicate, extractNode, syntaxFacts);
                    if (wantedNode != null)
                    {
                        return(wantedNode);
                    }

                    leftNode = leftNode?.Parent;
                }while (leftNode != null && leftNode.Span.End == selection.Start);
            }

            // nothing found -> return null
            return(null);