File: src\roslyn\src\Analyzers\CSharp\CodeFixes\UseLabeledJumpStatements\CSharpUseLabeledJumpStatementsCodeFixProvider.cs
Web Access
Project: 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.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.UseLabeledJumpStatements;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseLabeledJumpStatements), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CSharpUseLabeledJumpStatementsCodeFixProvider()
    : ForkingSyntaxEditorBasedCodeFixProvider<StatementSyntax>
{
    public override ImmutableArray<string> FixableDiagnosticIds { get; }
        = [IDEDiagnosticIds.UseLabeledJumpStatementDiagnosticId];

    protected override (string title, string equivalenceKey) GetTitleAndEquivalenceKey(CodeFixContext context)
        => (CSharpAnalyzersResources.Use_labeled_jump_statement, nameof(CSharpAnalyzersResources.Use_labeled_jump_statement));

    protected override async Task FixAsync(
        Document document,
        SyntaxEditor editor,
        StatementSyntax diagnosticNode,
        ImmutableDictionary<string, string?> properties,
        CancellationToken cancellationToken)
    {
        // Each invocation fixes one diagnostic's entire pattern (all jumps to the same label / all flag sites).  The
        // forking base re-derives on the tree produced by prior fixes, so nested loops compose, and once a pattern's
        // jumps are converted the remaining diagnostics for it resolve to nodes that no longer exist and are skipped.
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

        if (diagnosticNode is GotoStatementSyntax gotoStatement)
        {
            if (CSharpUseLabeledJumpStatementsHelpers.TryGetGotoBreakPattern(
                    gotoStatement, semanticModel, cancellationToken, out var loop, out var labelDeclaration, out var gotos))
            {
                ConvertGotos(editor, semanticModel, loop, labelDeclaration, gotos, isBreak: true, cancellationToken);
            }
            else if (CSharpUseLabeledJumpStatementsHelpers.TryGetGotoContinuePattern(
                    gotoStatement, semanticModel, cancellationToken, out loop, out labelDeclaration, out gotos))
            {
                ConvertGotos(editor, semanticModel, loop, labelDeclaration, gotos, isBreak: false, cancellationToken);
            }
        }
        else if (diagnosticNode is BreakStatementSyntax breakStatement)
        {
            if (CSharpUseLabeledJumpStatementsHelpers.TryGetFlagPatternFromInnerBreak(
                breakStatement, semanticModel, cancellationToken, out var pattern))
            {
                ConvertFlagPattern(editor, semanticModel, pattern, cancellationToken);
            }
        }
    }

    private static void ConvertGotos(
        SyntaxEditor editor,
        SemanticModel semanticModel,
        StatementSyntax loop,
        LabeledStatementSyntax labelDeclaration,
        ImmutableArray<GotoStatementSyntax> gotos,
        bool isBreak,
        CancellationToken cancellationToken)
    {
        // Pick the label to break to: reuse one already on the loop (from a prior fix or the user); otherwise the
        // goto target's own label; otherwise -- when other jumps still need that target label -- a fresh synthesized
        // one.  Separately, only delete the original target label if no other jump still references it.
        var existingLabel = GetReusableLabelName(loop);
        var keepOriginalLabel = LabelIsReferencedOutside(semanticModel, labelDeclaration, gotos, cancellationToken);
        var labelName = existingLabel ?? (keepOriginalLabel ? SynthesizeLabelName(semanticModel, loop, cancellationToken) : labelDeclaration.Identifier.Text);

        var newLoop = loop.ReplaceNodes(gotos, (original, _) => CreateJump(labelName, isBreak).WithTriviaFrom(original));

        if (isBreak)
        {
            ReplaceLoop(editor, loop, labelName, newLoop);

            // The label after the loop is now redundant.  If it sat on an empty statement, remove it entirely;
            // otherwise keep the statement but strip the now-unused label off it.
            if (!keepOriginalLabel)
            {
                if (labelDeclaration.Statement is EmptyStatementSyntax)
                    editor.RemoveNode(labelDeclaration, SyntaxRemoveOptions.KeepUnbalancedDirectives);
                else
                    editor.ReplaceNode(labelDeclaration, labelDeclaration.Statement.WithLeadingTrivia(labelDeclaration.GetLeadingTrivia()));
            }
        }
        else
        {
            // The continue label is the empty last statement of the body; remove it as we relabel the loop.
            if (!keepOriginalLabel)
            {
                var body = (BlockSyntax)newLoop.GetEmbeddedStatement()!;
                newLoop = newLoop.ReplaceNode(body, body.WithStatements(body.Statements.RemoveAt(body.Statements.Count - 1)));
            }

            ReplaceLoop(editor, loop, labelName, newLoop);
        }
    }

    /// <summary>
    /// Whether <paramref name="labelDeclaration"/>'s label is targeted by a <c>goto</c> other than the ones in
    /// <paramref name="gotos"/> (which we are converting); if so, the label must stay and a fresh one is introduced.
    /// </summary>
    private static bool LabelIsReferencedOutside(
        SemanticModel semanticModel,
        LabeledStatementSyntax labelDeclaration,
        ImmutableArray<GotoStatementSyntax> gotos,
        CancellationToken cancellationToken)
    {
        if (semanticModel.GetDeclaredSymbol(labelDeclaration, cancellationToken) is ILabelSymbol label)
        {
            // Labels are function-scoped, so scanning the enclosing member/function body finds every reference.
            var scope = GetLabelScope(labelDeclaration, cancellationToken);

            foreach (var candidate in scope.DescendantNodes().OfType<GotoStatementSyntax>())
            {
                if (!gotos.Contains(candidate) &&
                    candidate is GotoStatementSyntax(SyntaxKind.GotoStatement) { Expression: IdentifierNameSyntax identifier } &&
                    Equals(semanticModel.GetSymbolInfo(identifier, cancellationToken).Symbol, label))
                {
                    return true;
                }
            }
        }

        return false;
    }

    private static void ConvertFlagPattern(SyntaxEditor editor, SemanticModel semanticModel, FlagJumpPattern pattern, CancellationToken cancellationToken)
    {
        var loop = pattern.LoopStatement;
        var declaration = pattern.LocalDeclarationStatement;
        var labelName = GetReusableLabelName(loop) ?? SynthesizeLabelName(semanticModel, loop, cancellationToken);

        // The flag declaration is usually outside the loop we relabel, but can also sit inside it.  When inside, it
        // must be removed as part of the single loop rebuild below (a separate editor.RemoveNode would not compose with
        // the ReplaceNode of the loop); when outside, it is removed on its own afterwards.
        var declarationInsideLoop = loop.Contains(declaration);

        // Rewrite the inner breaks and delete the now-dead flag assignments and guards (and the declaration, if it
        // lives inside the loop) in one rebuild.
        var deadNodes = pattern.AssignmentAndBreakSites.Select(static site => (SyntaxNode)site.Assignment)
            .Concat(pattern.GuardStatements.Select(static guard => (SyntaxNode)guard))
            .Concat(pattern.ResetStatements.Select(static reset => (SyntaxNode)reset));
        if (declarationInsideLoop)
            deadNodes = deadNodes.Concat([(SyntaxNode)declaration]);

        var newLoop = loop.TrackNodes(pattern.AssignmentAndBreakSites
            .Select(static site => (SyntaxNode)site.Break)
            .Concat(deadNodes));

        newLoop = newLoop.ReplaceNodes(
            pattern.AssignmentAndBreakSites.Select(site => newLoop.GetCurrentNode(site.Break)!),
            (original, _) => CreateJump(labelName, pattern.IsBreak).WithTriviaFrom(original));

        newLoop = newLoop.RemoveNodes(
            deadNodes.Select(node => newLoop.GetCurrentNode(node)!),
            SyntaxRemoveOptions.KeepUnbalancedDirectives)!;

        ReplaceLoop(editor, loop, labelName, newLoop);
        if (!declarationInsideLoop)
            editor.RemoveNode(declaration, SyntaxRemoveOptions.KeepUnbalancedDirectives);
    }

    private static string? GetReusableLabelName(StatementSyntax loop)
        => (loop.Parent as LabeledStatementSyntax)?.Identifier.Text;

    /// <summary>
    /// Synthesizes a label for <paramref name="loop"/> (<c>loop_i</c>/<c>loop_x</c> from a for/foreach variable, else
    /// <c>outer</c>), uniquified against the labels in scope at the loop.
    /// </summary>
    private static string SynthesizeLabelName(SemanticModel semanticModel, StatementSyntax loop, CancellationToken cancellationToken)
    {
        var baseName = loop switch
        {
            ForStatementSyntax { Declaration.Variables: [var variable, ..] } => "loop_" + variable.Identifier.ValueText,
            ForEachStatementSyntax forEachStatement => "loop_" + forEachStatement.Identifier.ValueText,
            _ => "outer",
        };

        using var _ = PooledHashSet<string>.GetInstance(out var inScope);

        // Labels in scope at the loop, plus every label declared anywhere in the enclosing function body.  The latter
        // matters because a label a prior fix placed on a nested loop is not in scope at this (outer) loop's position
        // (it lives in a descendant block), yet would still collide with a label we place here.
        foreach (var label in semanticModel.LookupLabels(loop.SpanStart))
            inScope.Add(label.Name);

        foreach (var labeled in GetLabelScope(loop, cancellationToken).DescendantNodes().OfType<LabeledStatementSyntax>())
            inScope.Add(labeled.Identifier.ValueText);

        return NameGenerator.GenerateUniqueName(baseName, name => !inScope.Contains(name));
    }

    private static SyntaxNode GetLabelScope(SyntaxNode node, CancellationToken cancellationToken)
        => node.AncestorsAndSelf().FirstOrDefault(static n => n.IsReturnableConstruct())
            ?? node.SyntaxTree.GetRoot(cancellationToken);

    /// <summary>
    /// Wraps the loop in <c>label: &lt;loop&gt;</c>, unless a prior fix already labeled it (then just updates it).
    /// </summary>
    private static void ReplaceLoop(SyntaxEditor editor, StatementSyntax loop, string labelName, StatementSyntax newLoop)
    {
        // A prior fix already labeled this loop; just swap in the rewritten loop.
        if (loop.Parent is LabeledStatementSyntax)
        {
            editor.ReplaceNode(loop, newLoop);
            return;
        }

        var labeled = CreateLabeledLoop(labelName, loop, newLoop);

        // A labeled statement is only legal directly inside a block, switch section, or as a top-level statement.
        // When the loop is the braceless embedded statement of an if/else/while/using/lock/fixed/etc., wrap the
        // labeled loop in a block so the result remains legal C#.
        editor.ReplaceNode(loop, loop.Parent is BlockSyntax or SwitchSectionSyntax or GlobalStatementSyntax
            ? labeled
            : SyntaxFactory.Block(labeled));
    }

    private static LabeledStatementSyntax CreateLabeledLoop(string labelName, StatementSyntax originalLoop, StatementSyntax newLoop)
        => SyntaxFactory.LabeledStatement(
            SyntaxFactory.Identifier(labelName).WithLeadingTrivia(originalLoop.GetLeadingTrivia()),
            SyntaxFactory.Token(SyntaxKind.ColonToken).WithTrailingTrivia(SyntaxFactory.Space),
            newLoop.WithoutLeadingTrivia());

    private static StatementSyntax CreateJump(string labelName, bool isBreak)
    {
        var name = SyntaxFactory.IdentifierName(labelName);
        return isBreak
            ? SyntaxFactory.BreakStatement(name)
            : SyntaxFactory.ContinueStatement(name);
    }
}