use std::collections::BTreeMap; use std::fmt; use Context::*; use rustc_hir as hir; use rustc_hir::def_id::{LocalDefId, LocalModDefId}; use rustc_hir::intravisit::{self, Visitor}; use rustc_hir::{Destination, Node}; use rustc_middle::hir::nested_filter; use rustc_middle::query::Providers; use rustc_middle::span_bug; use rustc_middle::ty::TyCtxt; use rustc_span::hygiene::DesugaringKind; use rustc_span::{BytePos, Span}; use crate::errors::{ BreakInsideClosure, BreakInsideCoroutine, BreakNonLoop, ContinueLabeledBlock, OutsideLoop, OutsideLoopSuggestion, UnlabeledCfInWhileCondition, UnlabeledInLabeledBlock, }; /// The context in which a block is encountered. #[derive(Clone, Copy, Debug, PartialEq)] enum Context { Normal, Fn, Loop(hir::LoopSource), Closure(Span), Coroutine { coroutine_span: Span, kind: hir::CoroutineDesugaring, source: hir::CoroutineSource, }, UnlabeledBlock(Span), UnlabeledIfBlock(Span), LabeledBlock, /// E.g. The labeled block inside `['_'; 'block: { break 'block 1 + 2; }]`. AnonConst, /// E.g. `const { ... }`. ConstBlock, } #[derive(Clone)] struct BlockInfo { name: String, spans: Vec, suggs: Vec, } #[derive(PartialEq)] enum BreakContextKind { Break, Continue, } impl fmt::Display for BreakContextKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BreakContextKind::Break => "break", BreakContextKind::Continue => "continue", } .fmt(f) } } #[derive(Clone)] struct CheckLoopVisitor<'tcx> { tcx: TyCtxt<'tcx>, // Keep track of a stack of contexts, so that suggestions // are not made for contexts where it would be incorrect, // such as adding a label for an `if`. // e.g. `if 'foo: {}` would be incorrect. cx_stack: Vec, block_breaks: BTreeMap, } fn check_mod_loops(tcx: TyCtxt<'_>, module_def_id: LocalModDefId) { let mut check = CheckLoopVisitor { tcx, cx_stack: vec![Normal], block_breaks: Default::default() }; tcx.hir().visit_item_likes_in_module(module_def_id, &mut check); check.report_outside_loop_error(); } pub(crate) fn provide(providers: &mut Providers) { *providers = Providers { check_mod_loops, ..*providers }; } impl<'hir> Visitor<'hir> for CheckLoopVisitor<'hir> { type NestedFilter = nested_filter::OnlyBodies; fn nested_visit_map(&mut self) -> Self::Map { self.tcx.hir() } fn visit_anon_const(&mut self, c: &'hir hir::AnonConst) { self.with_context(AnonConst, |v| intravisit::walk_anon_const(v, c)); } fn visit_inline_const(&mut self, c: &'hir hir::ConstBlock) { self.with_context(ConstBlock, |v| intravisit::walk_inline_const(v, c)); } fn visit_fn( &mut self, fk: hir::intravisit::FnKind<'hir>, fd: &'hir hir::FnDecl<'hir>, b: hir::BodyId, _: Span, id: LocalDefId, ) { self.with_context(Fn, |v| intravisit::walk_fn(v, fk, fd, b, id)); } fn visit_trait_item(&mut self, trait_item: &'hir hir::TraitItem<'hir>) { self.with_context(Fn, |v| intravisit::walk_trait_item(v, trait_item)); } fn visit_impl_item(&mut self, impl_item: &'hir hir::ImplItem<'hir>) { self.with_context(Fn, |v| intravisit::walk_impl_item(v, impl_item)); } fn visit_expr(&mut self, e: &'hir hir::Expr<'hir>) { match e.kind { hir::ExprKind::If(cond, then, else_opt) => { self.visit_expr(cond); let get_block = |ck_loop: &CheckLoopVisitor<'hir>, expr: &hir::Expr<'hir>| -> Option<&hir::Block<'hir>> { if let hir::ExprKind::Block(b, None) = expr.kind && matches!( ck_loop.cx_stack.last(), Some(&Normal) | Some(&AnonConst) | Some(&UnlabeledBlock(_)) | Some(&UnlabeledIfBlock(_)) ) { Some(b) } else { None } }; if let Some(b) = get_block(self, then) { self.with_context(UnlabeledIfBlock(b.span.shrink_to_lo()), |v| { v.visit_block(b) }); } else { self.visit_expr(then); } if let Some(else_expr) = else_opt { if let Some(b) = get_block(self, else_expr) { self.with_context(UnlabeledIfBlock(b.span.shrink_to_lo()), |v| { v.visit_block(b) }); } else { self.visit_expr(else_expr); } } } hir::ExprKind::Loop(ref b, _, source, _) => { self.with_context(Loop(source), |v| v.visit_block(b)); } hir::ExprKind::Closure(&hir::Closure { ref fn_decl, body, fn_decl_span, kind, .. }) => { let cx = match kind { hir::ClosureKind::Coroutine(hir::CoroutineKind::Desugared(kind, source)) => { Coroutine { coroutine_span: fn_decl_span, kind, source } } _ => Closure(fn_decl_span), }; self.visit_fn_decl(fn_decl); self.with_context(cx, |v| v.visit_nested_body(body)); } hir::ExprKind::Block(ref b, Some(_label)) => { self.with_context(LabeledBlock, |v| v.visit_block(b)); } hir::ExprKind::Block(ref b, None) if matches!(self.cx_stack.last(), Some(&Fn) | Some(&ConstBlock)) => { self.with_context(Normal, |v| v.visit_block(b)); } hir::ExprKind::Block( ref b @ hir::Block { rules: hir::BlockCheckMode::DefaultBlock, .. }, None, ) if matches!( self.cx_stack.last(), Some(&Normal) | Some(&AnonConst) | Some(&UnlabeledBlock(_)) ) => { self.with_context(UnlabeledBlock(b.span.shrink_to_lo()), |v| v.visit_block(b)); } hir::ExprKind::Break(break_label, ref opt_expr) => { if let Some(e) = opt_expr { self.visit_expr(e); } if self.require_label_in_labeled_block(e.span, &break_label, "break") { // If we emitted an error about an unlabeled break in a labeled // block, we don't need any further checking for this break any more return; } let loop_id = match break_label.target_id { Ok(loop_id) => Some(loop_id), Err(hir::LoopIdError::OutsideLoopScope) => None, Err(hir::LoopIdError::UnlabeledCfInWhileCondition) => { self.tcx.dcx().emit_err(UnlabeledCfInWhileCondition { span: e.span, cf_type: "break", }); None } Err(hir::LoopIdError::UnresolvedLabel) => None, }; if let Some(Node::Block(_)) = loop_id.map(|id| self.tcx.hir_node(id)) { return; } if let Some(break_expr) = opt_expr { let (head, loop_label, loop_kind) = if let Some(loop_id) = loop_id { match self.tcx.hir().expect_expr(loop_id).kind { hir::ExprKind::Loop(_, label, source, sp) => { (Some(sp), label, Some(source)) } ref r => { span_bug!(e.span, "break label resolved to a non-loop: {:?}", r) } } } else { (None, None, None) }; match loop_kind { None | Some(hir::LoopSource::Loop) => (), Some(kind) => { let suggestion = format!( "break{}", break_label .label .map_or_else(String::new, |l| format!(" {}", l.ident)) ); self.tcx.dcx().emit_err(BreakNonLoop { span: e.span, head, kind: kind.name(), suggestion, loop_label, break_label: break_label.label, break_expr_kind: &break_expr.kind, break_expr_span: break_expr.span, }); } } } let sp_lo = e.span.with_lo(e.span.lo() + BytePos("break".len() as u32)); let label_sp = match break_label.label { Some(label) => sp_lo.with_hi(label.ident.span.hi()), None => sp_lo.shrink_to_lo(), }; self.require_break_cx( BreakContextKind::Break, e.span, label_sp, self.cx_stack.len() - 1, ); } hir::ExprKind::Continue(destination) => { self.require_label_in_labeled_block(e.span, &destination, "continue"); match destination.target_id { Ok(loop_id) => { if let Node::Block(block) = self.tcx.hir_node(loop_id) { self.tcx.dcx().emit_err(ContinueLabeledBlock { span: e.span, block_span: block.span, }); } } Err(hir::LoopIdError::UnlabeledCfInWhileCondition) => { self.tcx.dcx().emit_err(UnlabeledCfInWhileCondition { span: e.span, cf_type: "continue", }); } Err(_) => {} } self.require_break_cx( BreakContextKind::Continue, e.span, e.span, self.cx_stack.len() - 1, ) } _ => intravisit::walk_expr(self, e), } } } impl<'hir> CheckLoopVisitor<'hir> { fn with_context(&mut self, cx: Context, f: F) where F: FnOnce(&mut CheckLoopVisitor<'hir>), { self.cx_stack.push(cx); f(self); self.cx_stack.pop(); } fn require_break_cx( &mut self, br_cx_kind: BreakContextKind, span: Span, break_span: Span, cx_pos: usize, ) { match self.cx_stack[cx_pos] { LabeledBlock | Loop(_) => {} Closure(closure_span) => { self.tcx.dcx().emit_err(BreakInsideClosure { span, closure_span, name: &br_cx_kind.to_string(), }); } Coroutine { coroutine_span, kind, source } => { let kind = match kind { hir::CoroutineDesugaring::Async => "async", hir::CoroutineDesugaring::Gen => "gen", hir::CoroutineDesugaring::AsyncGen => "async gen", }; let source = match source { hir::CoroutineSource::Block => "block", hir::CoroutineSource::Closure => "closure", hir::CoroutineSource::Fn => "function", }; self.tcx.dcx().emit_err(BreakInsideCoroutine { span, coroutine_span, name: &br_cx_kind.to_string(), kind, source, }); } UnlabeledBlock(block_span) if br_cx_kind == BreakContextKind::Break && block_span.eq_ctxt(break_span) => { let block = self.block_breaks.entry(block_span).or_insert_with(|| BlockInfo { name: br_cx_kind.to_string(), spans: vec![], suggs: vec![], }); block.spans.push(span); block.suggs.push(break_span); } UnlabeledIfBlock(_) if br_cx_kind == BreakContextKind::Break => { self.require_break_cx(br_cx_kind, span, break_span, cx_pos - 1); } Normal | AnonConst | Fn | UnlabeledBlock(_) | UnlabeledIfBlock(_) | ConstBlock => { self.tcx.dcx().emit_err(OutsideLoop { spans: vec![span], name: &br_cx_kind.to_string(), is_break: br_cx_kind == BreakContextKind::Break, suggestion: None, }); } } } fn require_label_in_labeled_block( &self, span: Span, label: &Destination, cf_type: &str, ) -> bool { if !span.is_desugaring(DesugaringKind::QuestionMark) && self.cx_stack.last() == Some(&LabeledBlock) && label.label.is_none() { self.tcx.dcx().emit_err(UnlabeledInLabeledBlock { span, cf_type }); return true; } false } fn report_outside_loop_error(&self) { for (s, block) in &self.block_breaks { self.tcx.dcx().emit_err(OutsideLoop { spans: block.spans.clone(), name: &block.name, is_break: true, suggestion: Some(OutsideLoopSuggestion { block_span: *s, break_spans: block.suggs.clone(), }), }); } } }