|
1 | 1 | use rustc_hir::def_id::DefId; |
2 | 2 | use rustc_hir::intravisit::FnKind; |
3 | 3 | use rustc_index::bit_set::BitSet; |
| 4 | +use rustc_index::vec::IndexVec; |
4 | 5 | use rustc_middle::hir::map::blocks::FnLikeNode; |
5 | | -use rustc_middle::mir::{self, Body, TerminatorKind}; |
| 6 | +use rustc_middle::mir::{BasicBlock, Body, ReadOnlyBodyAndCache, TerminatorKind, START_BLOCK}; |
6 | 7 | use rustc_middle::ty::subst::InternalSubsts; |
7 | 8 | use rustc_middle::ty::{self, AssocItem, AssocItemContainer, Instance, TyCtxt}; |
8 | 9 | use rustc_session::lint::builtin::UNCONDITIONAL_RECURSION; |
| 10 | +use std::collections::VecDeque; |
9 | 11 |
|
10 | | -crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &Body<'tcx>, def_id: DefId) { |
| 12 | +crate fn check<'tcx>(tcx: TyCtxt<'tcx>, body: &ReadOnlyBodyAndCache<'_, 'tcx>, def_id: DefId) { |
11 | 13 | let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap(); |
12 | 14 |
|
13 | 15 | if let Some(fn_like_node) = FnLikeNode::from_node(tcx.hir().get(hir_id)) { |
14 | | - check_fn_for_unconditional_recursion(tcx, fn_like_node.kind(), body, def_id); |
| 16 | + if let FnKind::Closure(_) = fn_like_node.kind() { |
| 17 | + // closures can't recur, so they don't matter. |
| 18 | + return; |
| 19 | + } |
| 20 | + |
| 21 | + check_fn_for_unconditional_recursion(tcx, body, def_id); |
15 | 22 | } |
16 | 23 | } |
17 | 24 |
|
18 | 25 | fn check_fn_for_unconditional_recursion<'tcx>( |
19 | 26 | tcx: TyCtxt<'tcx>, |
20 | | - fn_kind: FnKind<'_>, |
21 | | - body: &Body<'tcx>, |
| 27 | + body: &ReadOnlyBodyAndCache<'_, 'tcx>, |
22 | 28 | def_id: DefId, |
23 | 29 | ) { |
24 | | - if let FnKind::Closure(_) = fn_kind { |
25 | | - // closures can't recur, so they don't matter. |
26 | | - return; |
27 | | - } |
| 30 | + let self_calls = find_blocks_calling_self(tcx, &body, def_id); |
28 | 31 |
|
29 | | - //FIXME(#54444) rewrite this lint to use the dataflow framework |
30 | | - |
31 | | - // Walk through this function (say `f`) looking to see if |
32 | | - // every possible path references itself, i.e., the function is |
33 | | - // called recursively unconditionally. This is done by trying |
34 | | - // to find a path from the entry node to the exit node that |
35 | | - // *doesn't* call `f` by traversing from the entry while |
36 | | - // pretending that calls of `f` are sinks (i.e., ignoring any |
37 | | - // exit edges from them). |
38 | | - // |
39 | | - // NB. this has an edge case with non-returning statements, |
40 | | - // like `loop {}` or `panic!()`: control flow never reaches |
41 | | - // the exit node through these, so one can have a function |
42 | | - // that never actually calls itself but is still picked up by |
43 | | - // this lint: |
44 | | - // |
45 | | - // fn f(cond: bool) { |
46 | | - // if !cond { panic!() } // could come from `assert!(cond)` |
47 | | - // f(false) |
48 | | - // } |
49 | | - // |
50 | | - // In general, functions of that form may be able to call |
51 | | - // itself a finite number of times and then diverge. The lint |
52 | | - // considers this to be an error for two reasons, (a) it is |
53 | | - // easier to implement, and (b) it seems rare to actually want |
54 | | - // to have behaviour like the above, rather than |
55 | | - // e.g., accidentally recursing after an assert. |
56 | | - |
57 | | - let basic_blocks = body.basic_blocks(); |
58 | | - let mut reachable_without_self_call_queue = vec![mir::START_BLOCK]; |
59 | | - let mut reached_exit_without_self_call = false; |
60 | | - let mut self_call_locations = vec![]; |
61 | | - let mut visited = BitSet::new_empty(basic_blocks.len()); |
| 32 | + // Stores a list of `Span`s for every basic block. Those are the spans of self-calls where we |
| 33 | + // know that one of them will definitely be reached. If the list is empty, the block either |
| 34 | + // wasn't processed yet or will not always go to a self-call. |
| 35 | + let mut results = IndexVec::from_elem_n(vec![], body.basic_blocks().len()); |
62 | 36 |
|
63 | | - let param_env = tcx.param_env(def_id); |
64 | | - let trait_substs_count = match tcx.opt_associated_item(def_id) { |
65 | | - Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => { |
66 | | - tcx.generics_of(trait_def_id).count() |
67 | | - } |
68 | | - _ => 0, |
69 | | - }; |
70 | | - let caller_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count]; |
| 37 | + // We start the analysis at the self calls and work backwards. |
| 38 | + let mut queue: VecDeque<_> = self_calls.iter().collect(); |
71 | 39 |
|
72 | | - while let Some(bb) = reachable_without_self_call_queue.pop() { |
73 | | - if !visited.insert(bb) { |
74 | | - //already done |
| 40 | + while let Some(bb) = queue.pop_front() { |
| 41 | + if !results[bb].is_empty() { |
| 42 | + // Already propagated. |
75 | 43 | continue; |
76 | 44 | } |
77 | 45 |
|
78 | | - let block = &basic_blocks[bb]; |
79 | | - |
80 | | - if let Some(ref terminator) = block.terminator { |
81 | | - match terminator.kind { |
82 | | - TerminatorKind::Call { ref func, .. } => { |
83 | | - let func_ty = func.ty(body, tcx); |
84 | | - |
85 | | - if let ty::FnDef(fn_def_id, substs) = func_ty.kind { |
86 | | - let (call_fn_id, call_substs) = if let Some(instance) = |
87 | | - Instance::resolve(tcx, param_env, fn_def_id, substs) |
88 | | - { |
89 | | - (instance.def_id(), instance.substs) |
90 | | - } else { |
91 | | - (fn_def_id, substs) |
92 | | - }; |
93 | | - |
94 | | - let is_self_call = call_fn_id == def_id |
95 | | - && &call_substs[..caller_substs.len()] == caller_substs; |
96 | | - |
97 | | - if is_self_call { |
98 | | - self_call_locations.push(terminator.source_info); |
99 | | - |
100 | | - //this is a self call so we shouldn't explore |
101 | | - //further down this path |
102 | | - continue; |
103 | | - } |
104 | | - } |
| 46 | + let locations = if self_calls.contains(bb) { |
| 47 | + // `bb` *is* a self-call. |
| 48 | + // We don't look at successors here because they are irrelevant here and we don't want |
| 49 | + // to lint them (eg. `f(); f()` should only lint the first call). |
| 50 | + vec![bb] |
| 51 | + } else { |
| 52 | + // If *all* successors of `bb` lead to a self-call, emit notes at all of their |
| 53 | + // locations. |
| 54 | + |
| 55 | + // Determine all "relevant" successors. We ignore successors only reached via unwinding. |
| 56 | + let terminator = body[bb].terminator(); |
| 57 | + let relevant_successors = match &terminator.kind { |
| 58 | + TerminatorKind::Call { destination: None, .. } |
| 59 | + | TerminatorKind::Yield { .. } |
| 60 | + | TerminatorKind::GeneratorDrop => None.into_iter().chain(&[]), |
| 61 | + TerminatorKind::SwitchInt { targets, .. } => None.into_iter().chain(targets), |
| 62 | + TerminatorKind::Goto { target } |
| 63 | + | TerminatorKind::Drop { target, .. } |
| 64 | + | TerminatorKind::DropAndReplace { target, .. } |
| 65 | + | TerminatorKind::Assert { target, .. } |
| 66 | + | TerminatorKind::FalseEdges { real_target: target, .. } |
| 67 | + | TerminatorKind::FalseUnwind { real_target: target, .. } |
| 68 | + | TerminatorKind::Call { destination: Some((_, target)), .. } => { |
| 69 | + Some(target).into_iter().chain(&[]) |
105 | 70 | } |
106 | | - TerminatorKind::Abort | TerminatorKind::Return => { |
107 | | - //found a path! |
108 | | - reached_exit_without_self_call = true; |
109 | | - break; |
| 71 | + TerminatorKind::Resume |
| 72 | + | TerminatorKind::Abort |
| 73 | + | TerminatorKind::Return |
| 74 | + | TerminatorKind::Unreachable => { |
| 75 | + // We propagate backwards, so these should never be encountered here. |
| 76 | + unreachable!("unexpected terminator {:?}", terminator.kind) |
110 | 77 | } |
111 | | - _ => {} |
| 78 | + }; |
| 79 | + |
| 80 | + // If all our successors are known to lead to self-calls, then we do too. |
| 81 | + let all_are_self_calls = |
| 82 | + relevant_successors.clone().all(|&succ| !results[succ].is_empty()); |
| 83 | + |
| 84 | + if all_are_self_calls { |
| 85 | + // We'll definitely lead to a self-call. Merge all call locations of the successors |
| 86 | + // for linting them later. |
| 87 | + relevant_successors.flat_map(|&succ| results[succ].iter().copied()).collect() |
| 88 | + } else { |
| 89 | + // At least 1 successor does not always lead to a self-call, so we also don't. |
| 90 | + vec![] |
112 | 91 | } |
| 92 | + }; |
113 | 93 |
|
114 | | - for successor in terminator.successors() { |
115 | | - reachable_without_self_call_queue.push(*successor); |
116 | | - } |
| 94 | + if !locations.is_empty() { |
| 95 | + // This is a newly confirmed-to-always-reach-self-call block. |
| 96 | + results[bb] = locations; |
| 97 | + |
| 98 | + // Propagate backwards through the CFG. |
| 99 | + debug!("propagate loc={:?} in {:?} -> {:?}", results[bb], bb, body.predecessors()[bb]); |
| 100 | + queue.extend(body.predecessors()[bb].iter().copied()); |
117 | 101 | } |
118 | 102 | } |
119 | 103 |
|
120 | | - // Check the number of self calls because a function that |
121 | | - // doesn't return (e.g., calls a `-> !` function or `loop { /* |
122 | | - // no break */ }`) shouldn't be linted unless it actually |
123 | | - // recurs. |
124 | | - if !reached_exit_without_self_call && !self_call_locations.is_empty() { |
| 104 | + debug!("unconditional recursion results: {:?}", results); |
| 105 | + |
| 106 | + let self_call_locations = &mut results[START_BLOCK]; |
| 107 | + self_call_locations.sort(); |
| 108 | + self_call_locations.dedup(); |
| 109 | + |
| 110 | + if !self_call_locations.is_empty() { |
125 | 111 | let hir_id = tcx.hir().as_local_hir_id(def_id).unwrap(); |
126 | 112 | let sp = tcx.sess.source_map().guess_head_span(tcx.hir().span(hir_id)); |
127 | 113 | tcx.struct_span_lint_hir(UNCONDITIONAL_RECURSION, hir_id, sp, |lint| { |
128 | 114 | let mut db = lint.build("function cannot return without recursing"); |
129 | 115 | db.span_label(sp, "cannot return without recursing"); |
130 | 116 | // offer some help to the programmer. |
131 | | - for location in &self_call_locations { |
132 | | - db.span_label(location.span, "recursive call site"); |
| 117 | + for bb in self_call_locations { |
| 118 | + let span = body.basic_blocks()[*bb].terminator().source_info.span; |
| 119 | + db.span_label(span, "recursive call site"); |
133 | 120 | } |
134 | 121 | db.help("a `loop` may express intention better if this is on purpose"); |
135 | 122 | db.emit(); |
136 | 123 | }); |
137 | 124 | } |
138 | 125 | } |
| 126 | + |
| 127 | +/// Finds blocks with `Call` terminators that would end up calling back into the same method. |
| 128 | +fn find_blocks_calling_self<'tcx>( |
| 129 | + tcx: TyCtxt<'tcx>, |
| 130 | + body: &Body<'tcx>, |
| 131 | + def_id: DefId, |
| 132 | +) -> BitSet<BasicBlock> { |
| 133 | + let param_env = tcx.param_env(def_id); |
| 134 | + |
| 135 | + // If this is trait/impl method, extract the trait's substs. |
| 136 | + let trait_substs_count = match tcx.opt_associated_item(def_id) { |
| 137 | + Some(AssocItem { container: AssocItemContainer::TraitContainer(trait_def_id), .. }) => { |
| 138 | + tcx.generics_of(trait_def_id).count() |
| 139 | + } |
| 140 | + _ => 0, |
| 141 | + }; |
| 142 | + let trait_substs = &InternalSubsts::identity_for_item(tcx, def_id)[..trait_substs_count]; |
| 143 | + |
| 144 | + let mut self_calls = BitSet::new_empty(body.basic_blocks().len()); |
| 145 | + |
| 146 | + for (bb, data) in body.basic_blocks().iter_enumerated() { |
| 147 | + if let TerminatorKind::Call { func, .. } = &data.terminator().kind { |
| 148 | + let func_ty = func.ty(body, tcx); |
| 149 | + |
| 150 | + if let ty::FnDef(fn_def_id, substs) = func_ty.kind { |
| 151 | + let (call_fn_id, call_substs) = |
| 152 | + if let Some(instance) = Instance::resolve(tcx, param_env, fn_def_id, substs) { |
| 153 | + (instance.def_id(), instance.substs) |
| 154 | + } else { |
| 155 | + (fn_def_id, substs) |
| 156 | + }; |
| 157 | + |
| 158 | + // FIXME(#57965): Make this work across function boundaries |
| 159 | + |
| 160 | + // If this is a trait fn, the substs on the trait have to match, or we might be |
| 161 | + // calling into an entirely different method (for example, a call from the default |
| 162 | + // method in the trait to `<A as Trait<B>>::method`, where `A` and/or `B` are |
| 163 | + // specific types). |
| 164 | + let is_self_call = |
| 165 | + call_fn_id == def_id && &call_substs[..trait_substs.len()] == trait_substs; |
| 166 | + |
| 167 | + if is_self_call { |
| 168 | + self_calls.insert(bb); |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + self_calls |
| 175 | +} |
0 commit comments