/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Analyzers/CSharp/CodeFixes/UseLabeledJumpStatements/CSharpUseLabeledJumpStatementsCodeFixProvider.cs
249 строк
12 KB
Fred Silberberg
Add C# 15 language version (#84799)
11 авг 2026, 23:51
Не верифицирован
11 авг 2026, 23:51
1284a4a
Код
Авторство
О чём код?
// 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: <loop></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); } }