|
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