/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/FlowAnalysis/ControlFlowPass.cs
437 строк
16 KB
Cyrus Najmabadi
[labeled break/continue] Resolve label even when the target loop/switch is missing (experimental) (#83503)
24 июн 2026, 16:49
Не верифицирован
24 июн 2026, 16:49
430147b
Код
Авторство
О чём код?
// 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. #nullable disable using System.Collections.Immutable; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.PooledObjects; namespace Microsoft.CodeAnalysis.CSharp { internal class ControlFlowPass : AbstractFlowPass<ControlFlowPass.LocalState, ControlFlowPass.LocalFunctionState> { private readonly PooledDictionary<LabelSymbol, BoundNode> _labelsDefined = PooledDictionary<LabelSymbol, BoundNode>.GetInstance(); private readonly PooledHashSet<LabelSymbol> _labelsUsed = PooledHashSet<LabelSymbol>.GetInstance(); protected bool _convertInsufficientExecutionStackExceptionToCancelledByStackGuardException = false; // By default, just let the original exception to bubble up. private readonly ArrayBuilder<(LocalSymbol symbol, BoundBlock block)> _usingDeclarations = ArrayBuilder<(LocalSymbol, BoundBlock)>.GetInstance(); private BoundBlock _currentBlock = null; protected override void Free() { _labelsDefined.Free(); _labelsUsed.Free(); _usingDeclarations.Free(); base.Free(); } internal ControlFlowPass(CSharpCompilation compilation, Symbol member, BoundNode node) : base(compilation, member, node) { } internal ControlFlowPass(CSharpCompilation compilation, Symbol member, BoundNode node, BoundNode firstInRegion, BoundNode lastInRegion) : base(compilation, member, node, firstInRegion, lastInRegion) { } internal struct LocalState : ILocalState { internal bool Alive; internal bool Reported; // reported unreachable statement internal LocalState(bool live, bool reported) { this.Alive = live; this.Reported = reported; } /// <summary> /// Produce a duplicate of this flow analysis state. /// </summary> /// <returns></returns> public LocalState Clone() { return this; } public bool Reachable { get { return Alive; } } } internal sealed class LocalFunctionState : AbstractLocalFunctionState { public LocalFunctionState(LocalState unreachableState) : base(unreachableState.Clone(), unreachableState.Clone()) { } } protected override LocalFunctionState CreateLocalFunctionState(LocalFunctionSymbol symbol) => new LocalFunctionState(UnreachableState()); protected override bool Meet(ref LocalState self, ref LocalState other) { var old = self; self.Alive &= other.Alive; self.Reported &= other.Reported; Debug.Assert(!self.Alive || !self.Reported); return self.Alive != old.Alive; } protected override bool Join(ref LocalState self, ref LocalState other) { var old = self; self.Alive |= other.Alive; self.Reported &= other.Reported; Debug.Assert(!self.Alive || !self.Reported); return self.Alive != old.Alive; } protected override string Dump(LocalState state) { return "[alive: " + state.Alive + "; reported: " + state.Reported + "]"; } protected override LocalState TopState() { return new LocalState(true, false); } protected override LocalState UnreachableState() { return new LocalState(false, this.State.Reported); } protected override LocalState LabelState(LabelSymbol label) { LocalState result = base.LabelState(label); // We don't want errors reported in one pass to suppress errors in the next pass. result.Reported = false; return result; } public override BoundNode Visit(BoundNode node) { // there is no need to scan the contents of an expression, as expressions // do not contribute to reachability analysis (except for constants, which // are handled by the caller). if (!(node is BoundExpression)) { return base.Visit(node); } return null; } protected override ImmutableArray<PendingBranch> Scan(ref bool badRegion) { this.Diagnostics.Clear(); // clear reported diagnostics var result = base.Scan(ref badRegion); foreach (var (label, node) in _labelsDefined) { if (node is BoundSwitchStatement) continue; if (!_labelsUsed.Contains(label)) { Diagnostics.Add(ErrorCode.WRN_UnreferencedLabel, label.GetFirstLocation()); } } return result; } /// <summary> /// Perform control flow analysis, reporting all necessary diagnostics. Returns true if the end of /// the body might be reachable... /// </summary> public static bool Analyze(CSharpCompilation compilation, Symbol member, BoundBlock block, DiagnosticBag diagnostics) { var walker = new ControlFlowPass(compilation, member, block); if (diagnostics != null) { walker._convertInsufficientExecutionStackExceptionToCancelledByStackGuardException = true; } try { bool badRegion = false; var result = walker.Analyze(ref badRegion, diagnostics); Debug.Assert(!badRegion); return result; } catch (BoundTreeVisitor.CancelledByStackGuardException ex) when (diagnostics != null) { ex.AddAnError(diagnostics); return true; } finally { walker.Free(); } } protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByStackGuardException() { return _convertInsufficientExecutionStackExceptionToCancelledByStackGuardException; } /// <summary> /// Analyze the body, reporting all necessary diagnostics. Returns true if the end of the /// body might be reachable. /// </summary> /// <returns></returns> protected bool Analyze(ref bool badRegion, DiagnosticBag diagnostics) { ImmutableArray<PendingBranch> returns = Analyze(ref badRegion); if (diagnostics != null) { diagnostics.AddRange(this.Diagnostics); } // TODO: if in the body of a struct constructor, check that "this" is assigned at each return. return State.Alive; } protected override ImmutableArray<PendingBranch> RemoveReturns() { var result = base.RemoveReturns(); foreach (var pending in result) { if (pending.Branch is null) { continue; } switch (pending.Branch.Kind) { case BoundKind.GotoStatement: { var leave = pending.Branch; var loc = new SourceLocation(leave.Syntax.GetFirstToken()); Diagnostics.Add(ErrorCode.ERR_LabelNotFound, loc, ((BoundGotoStatement)pending.Branch).Label.Name); break; } case BoundKind.BreakStatement: case BoundKind.ContinueStatement: { var leave = pending.Branch; // Already-errored break/continue (e.g. ERR_NoBreakId) was reported at bind time; don't // pile on another error here. if (leave.HasErrors) break; var loc = new SourceLocation(leave.Syntax.GetFirstToken()); Diagnostics.Add(ErrorCode.ERR_BadDelegateLeave, loc); break; } case BoundKind.ReturnStatement: break; default: break; // what else could it be? } } return result; } protected override void VisitStatement(BoundStatement statement) { switch (statement.Kind) { case BoundKind.NoOpStatement: case BoundKind.Block: case BoundKind.ThrowStatement: case BoundKind.LabeledStatement: case BoundKind.LocalFunctionStatement: base.VisitStatement(statement); break; case BoundKind.StatementList: base.VisitStatementList((BoundStatementList)statement); break; default: CheckReachable(statement); base.VisitStatement(statement); break; } } private void CheckReachable(BoundStatement statement) { if (!this.State.Alive && !this.State.Reported && !statement.WasCompilerGenerated && statement.Syntax.Span.Length != 0) { var firstToken = statement.Syntax.GetFirstToken(); Diagnostics.Add(ErrorCode.WRN_UnreachableCode, new SourceLocation(firstToken)); this.State.Reported = true; } } protected override void VisitTryBlock(BoundStatement tryBlock, BoundTryStatement node) { if (node.CatchBlocks.IsEmpty) { base.VisitTryBlock(tryBlock, node); return; } var oldPending = SavePending(); // we do not support branches into a try block base.VisitTryBlock(tryBlock, node); RestorePending(oldPending); } public override BoundNode VisitCatchBlock(BoundCatchBlock catchBlock) { var oldPending = SavePending(); // we do not support branches into a catch block base.VisitCatchBlock(catchBlock); RestorePending(oldPending); return null; } protected override void VisitFinallyBlock(BoundStatement finallyBlock) { var oldPending1 = SavePending(); // we do not support branches into a finally block var oldPending2 = SavePending(); // track only the branches out of the finally block base.VisitFinallyBlock(finallyBlock); RestorePending(oldPending2); // resolve branches that remain within the finally block foreach (var branch in PendingBranches.AsEnumerable()) { if (branch.Branch == null) continue; // a tracked exception var location = new SourceLocation(branch.Branch.Syntax.GetFirstToken()); switch (branch.Branch.Kind) { case BoundKind.YieldBreakStatement: case BoundKind.YieldReturnStatement: // ERR_BadYieldInFinally reported during initial binding break; default: Diagnostics.Add(ErrorCode.ERR_BadFinallyLeave, location); break; } } RestorePending(oldPending1); } // For purpose of control flow analysis, awaits do not create pending branches, so asynchronous usings and foreachs don't either public sealed override bool AwaitUsingAndForeachAddsPendingBranch => false; protected override void VisitLabel(BoundLabeledStatement node) { _labelsDefined[node.Label] = _currentBlock; base.VisitLabel(node); } public override BoundNode VisitLabeledStatement(BoundLabeledStatement node) { VisitLabel(node); CheckReachable(node); VisitStatement(node.Body); return null; } public override BoundNode VisitGotoStatement(BoundGotoStatement node) { _labelsUsed.Add(node.Label); // check for illegal jumps across using declarations var sourceLocation = node.Syntax.Location; var sourceStart = sourceLocation.SourceSpan.Start; var targetStart = node.Label.GetFirstLocation().SourceSpan.Start; foreach (var usingDecl in _usingDeclarations) { var usingStart = usingDecl.symbol.GetFirstLocation().SourceSpan.Start; if (sourceStart < usingStart && targetStart > usingStart) { // No forward jumps Diagnostics.Add(ErrorCode.ERR_GoToForwardJumpOverUsingVar, sourceLocation); break; } else if (sourceStart > usingStart && targetStart < usingStart) { // Backwards jump, so we must have already seen the label, or it must be a switch case label, or it might be in outer scope. If it is a switch case label, we know // that either the user received an error for having a using declaration at the top level in a switch statement, or the label is a valid // target to branch to. // Error if label and using are part of the same block if (_labelsDefined.TryGetValue(node.Label, out BoundNode target) && target == usingDecl.block) { Diagnostics.Add(ErrorCode.ERR_GoToBackwardJumpOverUsingVar, sourceLocation); break; } } } return base.VisitGotoStatement(node); } #nullable enable public override BoundNode? VisitBreakStatement(BoundBreakStatement node) { _labelsUsed.AddIfNotNull(node.LabelExpressionOpt?.Label); return base.VisitBreakStatement(node); } public override BoundNode? VisitContinueStatement(BoundContinueStatement node) { _labelsUsed.AddIfNotNull(node.LabelExpressionOpt?.Label); return base.VisitContinueStatement(node); } #nullable disable protected override void VisitSwitchSection(BoundSwitchSection node, bool isLastSection) { base.VisitSwitchSection(node, isLastSection); // Check for switch section fall through error if (this.State.Alive) { var syntax = node.SwitchLabels.Last().Syntax; Diagnostics.Add(isLastSection ? ErrorCode.ERR_SwitchFallOut : ErrorCode.ERR_SwitchFallThrough, new SourceLocation(syntax), syntax.ToString()); } } public override BoundNode VisitSwitchStatement(BoundSwitchStatement node) { foreach (var section in node.SwitchSections) { foreach (var label in section.SwitchLabels) { _labelsDefined[label.Label] = node; } } return base.VisitSwitchStatement(node); } public override BoundNode VisitBlock(BoundBlock node) { var parentBlock = _currentBlock; _currentBlock = node; var initialUsingCount = _usingDeclarations.Count; foreach (var local in node.Locals) { if (local.IsUsing) { _usingDeclarations.Add((local, node)); } } var result = base.VisitBlock(node); _usingDeclarations.Clip(initialUsingCount); _currentBlock = parentBlock; return result; } } }