Change:
use rustc_hir::def_id::DefId; use rustc_hir::intravisit::FnKind; use rustc_index::bit_set::BitSet; <ins> use rustc_index::vec::IndexVec; </ins> use rustc_middle::hir::map::blocks::FnLikeNode; <del> use rustc_middle::mir::{self, Body, TerminatorKind}; </del> <ins> use rustc_middle::mir::{BasicBlock, Body, ReadOnlyBodyAndCache, TerminatorKind, START_BLOCK}; </ins> use rustc_middle::ty::subst::InternalSubsts; use rustc_middle::ty::{self, AssocItem, AssocItemContainer, Instance, TyCtxt}; use rustc_session::lint::builtin::UNCONDITIONAL_RECURSION; <ins> use std::collections::VecDeque; </ins> <del> crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &Body<'tcx>, def_id: DefId) { </del> <ins> crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &ReadOnlyBodyAndCache<'_, 'tcx>, def_id: DefId) { </ins> let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap(); if let Some(fn_like_node) = FnLikeNode::from_node(tcx.hir().get(hir_id)) { <del> check_fn_for_unconditional_recursion(tcx, fn_like_node.kind(), body, def_id); </del> <ins> if let FnKind::Closure(_) = fn_like_node.kind() { // closures can't recur, so they don't matter. return; } check_fn_for_unconditional_recursion(tcx, body, def_id); </ins> } } fn check_fn_for_unconditional_recursion<'tcx>( tcx: TyCtxt<'tcx>, <del> fn_kind: FnKind<'_>, body: &Body<'tcx>, </del> <ins> body: &ReadOnlyBodyAndCache<'_, 'tcx>, </ins> def_id: DefId, ) { <del> if let FnKind::Closure(_) = fn_kind { // closures can't recur, so they don't matter. return; } </del> <ins> let self_calls = find_blocks_calling_self(tcx, &body, def_id); </ins> <del> //FIXME(#54444) rewrite this lint to use the dataflow framework // Walk through this function (say `f`) looking to see if // every possible path references itself, i.e., the function is // called recursively unconditionally. This is done by trying // to find a path from the entry node to the exit node that // *doesn't* call `f` by traversing from the entry while // pretending that calls of `f` are sinks (i.e., ignoring any // exit edges from them). // // NB. this has an edge case with non-returning statements, // like `loop {}` or `panic!()`: control flow never reaches // the exit node through these, so one can have a function // that never actually calls itself but is still picked up by // this lint: // //     fn f(cond: bool) { //         if !cond { panic!() } // could come from `assert!(cond)` //         f(false) //     } // // In general, functions of that form may be able to call // itself a finite number of times and then diverge. The lint // considers this to be an error for two reasons, (a) it is // easier to implement, and (b) it seems rare to actually want // to have behaviour like the above, rather than // e.g., accidentally recursing after an assert. let basic_blocks = body.basic_blocks(); let mut reachable_without_self_call_queue = vec![mir::START_BLOCK]; let mut reached_exit_without_self_call = false; let mut self_call_locations = vec![]; let mut visited = BitSet::new_empty(basic_blocks.len()); </del> <ins> // Stores a list of `Span`s for every basic block. Those are the spans of self-calls where we // know that one of them will definitely be reached. If the list is empty, the block either // wasn't processed yet or will not always go to a self-call. let mut results = IndexVec::from_elem_n(vec![], body.basic_blocks().len()); </ins> <del> let param_env = tcx.param_env(def_id); let trait_substs_count = match tcx.opt_associated_item(def_id) { Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => { tcx.generics_of(trait_def_id).count() } _ => 0, }; let caller_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count]; </del> <ins> // We start the analysis at the self calls and work backwards. let mut queue: VecDeque<_> = self_calls.iter().collect(); </ins> <del> while let Some(bb) = reachable_without_self_call_queue.pop() { if !visited.insert(bb) { //already done </del> <ins> while let Some(bb) = queue.pop_front() { if !results[bb].is_empty() { // Already propagated. </ins> continue; } <del> let block = &basic_blocks[bb]; if let Some(ref terminator) = block.terminator { match terminator.kind { TerminatorKind::Call { ref func, .. } => { let func_ty = func.ty(body, tcx); if let ty::FnDef(fn_def_id, substs) = func_ty.kind { let (call_fn_id, call_substs) = if let Some(instance) = Instance::resolve(tcx, param_env, fn_def_id, substs) { (instance.def_id(), instance.substs) } else { (fn_def_id, substs) }; let is_self_call = call_fn_id == def_id && &call_substs[..caller_substs.len()] == caller_substs; if is_self_call { self_call_locations.push(terminator.source_info); //this is a self call so we shouldn't explore //further down this path continue; } } </del> <ins> let locations = if self_calls.contains(bb) { // `bb` *is* a self-call. // We don't look at successors here because they are irrelevant here and we don't want // to lint them (eg. `f(); f()` should only lint the first call). vec![bb] } else { // If *all* successors of `bb` lead to a self-call, emit notes at all of their // locations. // Determine all "relevant" successors. We ignore successors only reached via unwinding. let terminator = body[bb].terminator(); let relevant_successors = match &terminator.kind { TerminatorKind::Call { destination: None, .. } | TerminatorKind::Yield { .. } | TerminatorKind::GeneratorDrop => None.into_iter().chain(&[]), TerminatorKind::SwitchInt { targets, .. } => None.into_iter().chain(targets), TerminatorKind::Goto { target } | TerminatorKind::Drop { target, .. } | TerminatorKind::DropAndReplace { target, .. } | TerminatorKind::Assert { target, .. } | TerminatorKind::FalseEdges { real_target: target, .. } | TerminatorKind::FalseUnwind { real_target: target, .. } | TerminatorKind::Call { destination: Some((_, target)), .. } => { Some(target).into_iter().chain(&[]) </ins> } <del> TerminatorKind::Abort | TerminatorKind::Return => { //found a path! reached_exit_without_self_call = true; break; </del> <ins> TerminatorKind::Resume | TerminatorKind::Abort | TerminatorKind::Return | TerminatorKind::Unreachable => { // We propagate backwards, so these should never be encountered here. unreachable!("unexpected terminator {:?}", terminator.kind) </ins> } <del> _ => {} </del> <ins> }; // If all our successors are known to lead to self-calls, then we do too. let all_are_self_calls = relevant_successors.clone().all(|&succ| !results[succ].is_empty()); if all_are_self_calls { // We'll definitely lead to a self-call. Merge all call locations of the successors // for linting them later. relevant_successors.flat_map(|&succ| results[succ].iter().copied()).collect() } else { // At least 1 successor does not always lead to a self-call, so we also don't. vec![] </ins> } <ins> }; </ins> <del> for successor in terminator.successors() { reachable_without_self_call_queue.push(*successor); } </del> <ins> if !locations.is_empty() { // This is a newly confirmed-to-always-reach-self-call block. results[bb] = locations; // Propagate backwards through the CFG. debug!("propagate loc={:?} in {:?} -> {:?}", results[bb], bb, body.predecessors()[bb]); queue.extend(body.predecessors()[bb].iter().copied()); </ins> } } <del> // Check the number of self calls because a function that // doesn't return (e.g., calls a `-> !` function or `loop { /* // no break */ }`) shouldn't be linted unless it actually // recurs. if !reached_exit_without_self_call && !self_call_locations.is_empty() { </del> <ins> debug!("unconditional recursion results: {:?}", results); let self_call_locations = &mut results[START_BLOCK]; self_call_locations.sort(); self_call_locations.dedup(); if !self_call_locations.is_empty() { </ins> let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap(); let sp = tcx.sess.source_map().guess_head_span(tcx.hir().span(hir_id)); tcx.struct_span_lint_hir(UNCONDITIONAL_RECURSION, hir_id, sp, |lint| { let mut db = lint.build("function cannot return without recursing"); db.span_label(sp, "cannot return without recursing"); // offer some help to the programmer. <del> for location in &self_call_locations { db.span_label(location.span, "recursive call site"); </del> <ins> for bb in self_call_locations { let span = body.basic_blocks()[*bb].terminator().source_info.span; db.span_label(span, "recursive call site"); </ins> } db.help("a `loop` may express intention better if this is on purpose"); db.emit(); }); } } <ins> /// Finds blocks with `Call` terminators that would end up calling back into the same method. fn find_blocks_calling_self<'tcx>( tcx: TyCtxt<'tcx>, body: &Body<'tcx>, def_id: DefId, ) -> BitSet<BasicBlock> { let param_env = tcx.param_env(def_id); // If this is trait/impl method, extract the trait's substs. let trait_substs_count = match tcx.opt_associated_item(def_id) { Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => { tcx.generics_of(trait_def_id).count() } _ => 0, }; let trait_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count]; let mut self_calls = BitSet::new_empty(body.basic_blocks().len()); for (bb, data) in body.basic_blocks().iter_enumerated() { if let TerminatorKind::Call { func, .. } = &data.terminator().kind { let func_ty = func.ty(body, tcx); if let ty::FnDef(fn_def_id, substs) = func_ty.kind { let (call_fn_id, call_substs) = if let Some(instance) = Instance::resolve(tcx, param_env, fn_def_id, substs) { (instance.def_id(), instance.substs) } else { (fn_def_id, substs) }; // FIXME(#57965): Make this work across function boundaries // If this is a trait fn, the substs on the trait have to match, or we might be // calling into an entirely different method (for example, a call from the default // method in the trait to `<A as Trait<B>>::method`, where `A` and/or `B` are // specific types). let is_self_call = call_fn_id == def_id && &call_substs[..trait_substs.len()] == trait_substs; if is_self_call { self_calls.insert(bb); } } } } self_calls } </ins>

Discussion:
noticed these cases: thinks the way to resolve this is to reimplement the lint on top of the analysis stuff.
Isn't the first one kind of correct, if misleading? The second one is indeed problematic.
Technically correct, because it can't return at all ;)
It's wrong because the function can diverge without recursively calling itself. They're both literally the same example, I just replaced one form of divergence with another.
mmmm.... In my mind I read this as while the latter, being it is also , but can terminate (while the has no side-effects). You're correct, but I'd love it if we could somehow differentiate when evaluating divergence between "can't return" and "can't return but can panic".
Both "stuck in an infinite loop" and "panics" are different from "infinitely self-recurses". What we want to detect is unguarded self-recursion, that is, when you forgot to check some condition that would give you a convergent or divergent "base case", that would stop the infinite recursion. The message of the lint doesn't help my point, it should be more clearly about "infinite recursion". While usually you're missing a condition, or you're calling the wrong function, it's also possible that you're using infinite recursion to infinitely repeat something - but we don't have guaranteed tail calls, so what you really want to do is rewrite the infinite recursion into an infinite loop.
Triage: no change
Since we currently lint this: Wouldn't this need a backwards dataflow analysis to catch this case? It doesn't looks like the bit would ever be propagated to a return block like this. A backwards analysis would instead propagate it to the entry block, if I understand correctly.
You're right, there's no merge for either the or the examples, that could collect the information. Presumably we want to tell these apart: vs So I guess the property you'd propagate backwards is "always reaches self-call", which means every block starts at (except those that contain a self-call) and the merge operator is . The funny thing about this being a 1-bit-per-block analysis is that instead of a you can use a simple and fixpoint is also easier to achieve. Rough sketch: