File: GoToDefinition\CSharpGoToDefinitionSymbolService.cs
Web Access
Project: src\roslyn\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.GoToDefinition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.GoToDefinition;

[ExportLanguageService(typeof(IGoToDefinitionSymbolService), LanguageNames.CSharp), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CSharpGoToDefinitionSymbolService() : AbstractGoToDefinitionSymbolService
{
    protected override Task<ISymbol> FindRelatedExplicitlyDeclaredSymbolAsync(Project project, ISymbol symbol, CancellationToken cancellationToken)
        => Task.FromResult(symbol);

    protected override int? GetTargetPositionIfControlFlow(SemanticModel semanticModel, SyntaxToken token)
    {
        var node = token.GetRequiredParent();

        switch (token.Kind())
        {
            case SyntaxKind.ContinueKeyword:
                return GetBreakOrContinueTargetPosition(semanticModel, node, isBreak: false);

            case SyntaxKind.BreakKeyword:
                if (token.GetPreviousToken().IsKind(SyntaxKind.YieldKeyword))
                    goto case SyntaxKind.YieldKeyword;

                return GetBreakOrContinueTargetPosition(semanticModel, node, isBreak: true);

            case SyntaxKind.YieldKeyword:
            case SyntaxKind.ReturnKeyword:
                {
                    var foundReturnableConstruct = TryFindContainingReturnableConstruct(node);
                    if (foundReturnableConstruct is null)
                    {
                        return null;
                    }

                    var symbol = semanticModel.GetDeclaredSymbol(foundReturnableConstruct);
                    if (symbol is null)
                    {
                        // for lambdas
                        return foundReturnableConstruct.GetFirstToken().Span.Start;
                    }

                    return symbol.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0;
                }

            case SyntaxKind.GotoKeyword:
            case SyntaxKind.DefaultKeyword:
            case SyntaxKind.CaseKeyword:
                {
                    if (node.FirstAncestorOrSelf<GotoStatementSyntax>() is not GotoStatementSyntax gotoStatement)
                        return null;

                    if (semanticModel.GetOperation(gotoStatement) is not IBranchOperation gotoOperation)
                        return null;

                    var target = gotoOperation.Target;
                    return target.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()?.SpanStart;
                }
        }

        return null;

        static int? GetBreakOrContinueTargetPosition(SemanticModel semanticModel, SyntaxNode node, bool isBreak)
        {
            if (semanticModel.GetOperation(node) is not IBranchOperation branchOperation)
                return null;

            // 'corresponding' is the loop or switch that this break/continue transfers control to.
            var corresponding = branchOperation.GetCorrespondingOperation();
            if (corresponding is null)
                return null;

            // break/continue cannot cross a lambda, anonymous method, or local function. If one of those
            // sits between the statement and the loop/switch that GetCorrespondingOperation found, the jump
            // is not actually valid (e.g. 'break' inside a lambda nested in a switch), so don't navigate.
            for (var current = node; current is not null && current != corresponding.Syntax; current = current.Parent)
            {
                if (current.IsReturnableConstruct())
                    return null;
            }

            // 'break' transfers control to just past the construct it exits, so navigate to its end.
            // 'continue' transfers control back to the construct itself, so navigate to its start.
            return isBreak
                ? corresponding.Syntax.GetLastToken().Span.End
                : corresponding.Syntax.GetFirstToken().Span.Start;
        }

        static SyntaxNode? TryFindContainingReturnableConstruct(SyntaxNode? node)
        {
            while (node is not null && !node.IsReturnableConstruct())
            {
                if (SyntaxFacts.GetTypeDeclarationKind(node.Kind()) != SyntaxKind.None)
                {
                    return null;
                }

                node = node.Parent;
            }

            return node;
        }
    }
}