Skip to content

Commit 9bac017

Browse files
committed
Auto merge of #20373 - huonw:self-call-lint, r=luqmana
E.g. `fn foo() { foo() }`, or, more subtlely impl Foo for Box<Foo+'static> { fn bar(&self) { self.bar(); } } The compiler will warn and point out the points where recursion occurs, if it determines that the function cannot return without calling itself. Closes #17899. --- This is highly non-perfect, in particular, my wording above is quite precise, and I have some unresolved questions: This currently will warn about ```rust fn foo() { if bar { loop {} } foo() } ``` even though `foo` may never be called (i.e. our apparent "unconditional" recursion is actually conditional). I don't know if we should handle this case, and ones like it with `panic!()` instead of `loop` (or anything else that "returns" `!`). However, strictly speaking, it seems to me that changing the above to not warn will require changing ```rust fn foo() { while bar {} foo() } ``` to also not warn since it could be that the `while` is an infinite loop and doesn't ever hit the `foo`. I'm inclined to think we let these cases warn since true edge cases like the first one seem rare, and if they do occur they seem like they would usually be typos in the function call. (I could imagine someone accidentally having code like `fn foo() { assert!(bar()); foo() /* meant to be boo() */ }` which won't warn if the `loop` case is "fixed".) (Part of the reason this is unresolved is wanting feedback, part of the reason is I couldn't devise a strategy that worked in all cases.) --- The name `unconditional_self_calls` is kinda clunky; and this reconstructs the CFG for each function that is linted which may or may not be very expensive, I don't know.
2 parents 43046be + 0684c8e commit 9bac017

File tree

10 files changed

+299
-9
lines changed

10 files changed

+299
-9
lines changed

src/librustc/lint/builtin.rs

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ use middle::subst::Substs;
3232
use middle::ty::{self, Ty};
3333
use middle::{def, pat_util, stability};
3434
use middle::const_eval::{eval_const_expr_partial, const_int, const_uint};
35+
use middle::cfg;
3536
use util::ppaux::{ty_to_string};
3637
use util::nodemap::{FnvHashMap, NodeSet};
37-
use lint::{Context, LintPass, LintArray, Lint};
38+
use lint::{Level, Context, LintPass, LintArray, Lint};
3839

40+
use std::collections::BitvSet;
3941
use std::collections::hash_map::Entry::{Occupied, Vacant};
4042
use std::num::SignedInt;
4143
use std::{cmp, slice};
@@ -1788,6 +1790,194 @@ impl LintPass for Stability {
17881790
}
17891791
}
17901792

1793+
declare_lint! {
1794+
pub UNCONDITIONAL_RECURSION,
1795+
Warn,
1796+
"functions that cannot return without calling themselves"
1797+
}
1798+
1799+
#[derive(Copy)]
1800+
pub struct UnconditionalRecursion;
1801+
1802+
1803+
impl LintPass for UnconditionalRecursion {
1804+
fn get_lints(&self) -> LintArray {
1805+
lint_array![UNCONDITIONAL_RECURSION]
1806+
}
1807+
1808+
fn check_fn(&mut self, cx: &Context, fn_kind: visit::FnKind, _: &ast::FnDecl,
1809+
blk: &ast::Block, sp: Span, id: ast::NodeId) {
1810+
type F = for<'tcx> fn(&ty::ctxt<'tcx>,
1811+
ast::NodeId, ast::NodeId, ast::Ident, ast::NodeId) -> bool;
1812+
1813+
let (name, checker) = match fn_kind {
1814+
visit::FkItemFn(name, _, _, _) => (name, id_refers_to_this_fn as F),
1815+
visit::FkMethod(name, _, _) => (name, id_refers_to_this_method as F),
1816+
// closures can't recur, so they don't matter.
1817+
visit::FkFnBlock => return
1818+
};
1819+
1820+
let impl_def_id = ty::impl_of_method(cx.tcx, ast_util::local_def(id))
1821+
.unwrap_or(ast_util::local_def(ast::DUMMY_NODE_ID));
1822+
assert!(ast_util::is_local(impl_def_id));
1823+
let impl_node_id = impl_def_id.node;
1824+
1825+
// Walk through this function (say `f`) looking to see if
1826+
// every possible path references itself, i.e. the function is
1827+
// called recursively unconditionally. This is done by trying
1828+
// to find a path from the entry node to the exit node that
1829+
// *doesn't* call `f` by traversing from the entry while
1830+
// pretending that calls of `f` are sinks (i.e. ignoring any
1831+
// exit edges from them).
1832+
//
1833+
// NB. this has an edge case with non-returning statements,
1834+
// like `loop {}` or `panic!()`: control flow never reaches
1835+
// the exit node through these, so one can have a function
1836+
// that never actually calls itselfs but is still picked up by
1837+
// this lint:
1838+
//
1839+
// fn f(cond: bool) {
1840+
// if !cond { panic!() } // could come from `assert!(cond)`
1841+
// f(false)
1842+
// }
1843+
//
1844+
// In general, functions of that form may be able to call
1845+
// itself a finite number of times and then diverge. The lint
1846+
// considers this to be an error for two reasons, (a) it is
1847+
// easier to implement, and (b) it seems rare to actually want
1848+
// to have behaviour like the above, rather than
1849+
// e.g. accidentally recurring after an assert.
1850+
1851+
let cfg = cfg::CFG::new(cx.tcx, blk);
1852+
1853+
let mut work_queue = vec![cfg.entry];
1854+
let mut reached_exit_without_self_call = false;
1855+
let mut self_call_spans = vec![];
1856+
let mut visited = BitvSet::new();
1857+
1858+
while let Some(idx) = work_queue.pop() {
1859+
let cfg_id = idx.node_id();
1860+
if idx == cfg.exit {
1861+
// found a path!
1862+
reached_exit_without_self_call = true;
1863+
break
1864+
} else if visited.contains(&cfg_id) {
1865+
// already done
1866+
continue
1867+
}
1868+
visited.insert(cfg_id);
1869+
let node_id = cfg.graph.node_data(idx).id;
1870+
1871+
// is this a recursive call?
1872+
if node_id != ast::DUMMY_NODE_ID && checker(cx.tcx, impl_node_id, id, name, node_id) {
1873+
1874+
self_call_spans.push(cx.tcx.map.span(node_id));
1875+
// this is a self call, so we shouldn't explore past
1876+
// this node in the CFG.
1877+
continue
1878+
}
1879+
// add the successors of this node to explore the graph further.
1880+
cfg.graph.each_outgoing_edge(idx, |_, edge| {
1881+
let target_idx = edge.target();
1882+
let target_cfg_id = target_idx.node_id();
1883+
if !visited.contains(&target_cfg_id) {
1884+
work_queue.push(target_idx)
1885+
}
1886+
true
1887+
});
1888+
}
1889+
1890+
// check the number of sell calls because a function that
1891+
// doesn't return (e.g. calls a `-> !` function or `loop { /*
1892+
// no break */ }`) shouldn't be linted unless it actually
1893+
// recurs.
1894+
if !reached_exit_without_self_call && self_call_spans.len() > 0 {
1895+
cx.span_lint(UNCONDITIONAL_RECURSION, sp,
1896+
"function cannot return without recurring");
1897+
1898+
// FIXME #19668: these could be span_lint_note's instead of this manual guard.
1899+
if cx.current_level(UNCONDITIONAL_RECURSION) != Level::Allow {
1900+
let sess = cx.sess();
1901+
// offer some help to the programmer.
1902+
for call in self_call_spans.iter() {
1903+
sess.span_note(*call, "recursive call site")
1904+
}
1905+
sess.span_help(sp, "a `loop` may express intention better if this is on purpose")
1906+
}
1907+
}
1908+
1909+
// all done
1910+
return;
1911+
1912+
// Functions for identifying if the given NodeId `id`
1913+
// represents a call to the function `fn_id`/method
1914+
// `method_id`.
1915+
1916+
fn id_refers_to_this_fn<'tcx>(tcx: &ty::ctxt<'tcx>,
1917+
_: ast::NodeId,
1918+
fn_id: ast::NodeId,
1919+
_: ast::Ident,
1920+
id: ast::NodeId) -> bool {
1921+
tcx.def_map.borrow().get(&id)
1922+
.map_or(false, |def| {
1923+
let did = def.def_id();
1924+
ast_util::is_local(did) && did.node == fn_id
1925+
})
1926+
}
1927+
1928+
// check if the method call `id` refers to method `method_id`
1929+
// (with name `method_name` contained in impl `impl_id`).
1930+
fn id_refers_to_this_method<'tcx>(tcx: &ty::ctxt<'tcx>,
1931+
impl_id: ast::NodeId,
1932+
method_id: ast::NodeId,
1933+
method_name: ast::Ident,
1934+
id: ast::NodeId) -> bool {
1935+
let did = match tcx.method_map.borrow().get(&ty::MethodCall::expr(id)) {
1936+
None => return false,
1937+
Some(m) => match m.origin {
1938+
// There's no way to know if a method call via a
1939+
// vtable is recursion, so we assume it's not.
1940+
ty::MethodTraitObject(_) => return false,
1941+
1942+
// This `did` refers directly to the method definition.
1943+
ty::MethodStatic(did) | ty::MethodStaticUnboxedClosure(did) => did,
1944+
1945+
// MethodTypeParam are methods from traits:
1946+
1947+
// The `impl ... for ...` of this method call
1948+
// isn't known, e.g. it might be a default method
1949+
// in a trait, so we get the def-id of the trait
1950+
// method instead.
1951+
ty::MethodTypeParam(
1952+
ty::MethodParam { ref trait_ref, method_num, impl_def_id: None, }) => {
1953+
ty::trait_item(tcx, trait_ref.def_id, method_num).def_id()
1954+
}
1955+
1956+
// The `impl` is known, so we check that with a
1957+
// special case:
1958+
ty::MethodTypeParam(
1959+
ty::MethodParam { impl_def_id: Some(impl_def_id), .. }) => {
1960+
1961+
let name = match tcx.map.expect_expr(id).node {
1962+
ast::ExprMethodCall(ref sp_ident, _, _) => sp_ident.node,
1963+
_ => tcx.sess.span_bug(
1964+
tcx.map.span(id),
1965+
"non-method call expr behaving like a method call?")
1966+
};
1967+
// it matches if it comes from the same impl,
1968+
// and has the same method name.
1969+
return ast_util::is_local(impl_def_id)
1970+
&& impl_def_id.node == impl_id
1971+
&& method_name.name == name.name
1972+
}
1973+
}
1974+
};
1975+
1976+
ast_util::is_local(did) && did.node == method_id
1977+
}
1978+
}
1979+
}
1980+
17911981
declare_lint! {
17921982
pub UNUSED_IMPORTS,
17931983
Warn,

src/librustc/lint/context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ impl LintStore {
211211
UnusedAllocation,
212212
MissingCopyImplementations,
213213
UnstableFeatures,
214+
UnconditionalRecursion,
214215
);
215216

216217
add_builtin_with_new!(sess,

src/librustc/middle/astencode.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ impl<'tcx> tr for MethodOrigin<'tcx> {
627627
// def-id is already translated when we read it out
628628
trait_ref: mp.trait_ref.clone(),
629629
method_num: mp.method_num,
630+
impl_def_id: mp.impl_def_id.tr(dcx),
630631
}
631632
)
632633
}
@@ -879,6 +880,16 @@ impl<'a, 'tcx> rbml_writer_helpers<'tcx> for Encoder<'a> {
879880
try!(this.emit_struct_field("method_num", 0, |this| {
880881
this.emit_uint(p.method_num)
881882
}));
883+
try!(this.emit_struct_field("impl_def_id", 0, |this| {
884+
this.emit_option(|this| {
885+
match p.impl_def_id {
886+
None => this.emit_option_none(),
887+
Some(did) => this.emit_option_some(|this| {
888+
Ok(this.emit_def_id(did))
889+
})
890+
}
891+
})
892+
}));
882893
Ok(())
883894
})
884895
})
@@ -1452,6 +1463,17 @@ impl<'a, 'tcx> rbml_decoder_decoder_helpers<'tcx> for reader::Decoder<'a> {
14521463
this.read_struct_field("method_num", 1, |this| {
14531464
this.read_uint()
14541465
}).unwrap()
1466+
},
1467+
impl_def_id: {
1468+
this.read_struct_field("impl_def_id", 2, |this| {
1469+
this.read_option(|this, b| {
1470+
if b {
1471+
Ok(Some(this.read_def_id(dcx)))
1472+
} else {
1473+
Ok(None)
1474+
}
1475+
})
1476+
}).unwrap()
14551477
}
14561478
}))
14571479
}).unwrap()

src/librustc/middle/ty.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,14 @@ pub struct MethodParam<'tcx> {
453453
// never contains bound regions; those regions should have been
454454
// instantiated with fresh variables at this point.
455455
pub trait_ref: Rc<ty::TraitRef<'tcx>>,
456-
457456
// index of uint in the list of methods for the trait
458457
pub method_num: uint,
458+
459+
/// The impl for the trait from which the method comes. This
460+
/// should only be used for certain linting/heuristic purposes
461+
/// since there is no guarantee that this is Some in every
462+
/// situation that it could/should be.
463+
pub impl_def_id: Option<ast::DefId>,
459464
}
460465

461466
// details for a method invoked with a receiver whose type is an object

src/librustc/middle/ty_fold.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ impl<'tcx> TypeFoldable<'tcx> for ty::MethodOrigin<'tcx> {
310310
ty::MethodTypeParam(ref param) => {
311311
ty::MethodTypeParam(ty::MethodParam {
312312
trait_ref: param.trait_ref.fold_with(folder),
313-
method_num: param.method_num
313+
method_num: param.method_num,
314+
impl_def_id: param.impl_def_id,
314315
})
315316
}
316317
ty::MethodTraitObject(ref object) => {

src/librustc/util/ppaux.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ impl<'tcx, T:Repr<'tcx>> Repr<'tcx> for Option<T> {
544544

545545
impl<'tcx, T:Repr<'tcx>> Repr<'tcx> for P<T> {
546546
fn repr(&self, tcx: &ctxt<'tcx>) -> String {
547-
(*self).repr(tcx)
547+
(**self).repr(tcx)
548548
}
549549
}
550550

src/librustc_trans/trans/meth.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ pub fn trans_method_callee<'blk, 'tcx>(bcx: Block<'blk, 'tcx>,
132132

133133
ty::MethodTypeParam(ty::MethodParam {
134134
ref trait_ref,
135-
method_num
135+
method_num,
136+
impl_def_id: _
136137
}) => {
137138
let trait_ref = ty::Binder(bcx.monomorphize(trait_ref));
138139
let span = bcx.tcx().map.span(method_call.expr_id);

src/librustc_typeck/check/method/confirm.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ impl<'a,'tcx> ConfirmContext<'a,'tcx> {
256256
&impl_polytype.substs,
257257
&ty::impl_trait_ref(self.tcx(), impl_def_id).unwrap());
258258
let origin = MethodTypeParam(MethodParam { trait_ref: impl_trait_ref.clone(),
259-
method_num: method_num });
259+
method_num: method_num,
260+
impl_def_id: Some(impl_def_id) });
260261
(impl_trait_ref.substs.clone(), origin)
261262
}
262263

@@ -275,7 +276,8 @@ impl<'a,'tcx> ConfirmContext<'a,'tcx> {
275276
let trait_ref =
276277
Rc::new(ty::TraitRef::new(trait_def_id, self.tcx().mk_substs(substs.clone())));
277278
let origin = MethodTypeParam(MethodParam { trait_ref: trait_ref,
278-
method_num: method_num });
279+
method_num: method_num,
280+
impl_def_id: None });
279281
(substs, origin)
280282
}
281283

@@ -285,7 +287,8 @@ impl<'a,'tcx> ConfirmContext<'a,'tcx> {
285287
let trait_ref = self.replace_late_bound_regions_with_fresh_var(&*poly_trait_ref);
286288
let substs = trait_ref.substs.clone();
287289
let origin = MethodTypeParam(MethodParam { trait_ref: trait_ref,
288-
method_num: method_num });
290+
method_num: method_num,
291+
impl_def_id: None });
289292
(substs, origin)
290293
}
291294
}

src/librustc_typeck/check/method/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ pub fn lookup_in_trait_adjusted<'a, 'tcx>(fcx: &FnCtxt<'a, 'tcx>,
287287

288288
let callee = MethodCallee {
289289
origin: MethodTypeParam(MethodParam{trait_ref: trait_ref.clone(),
290-
method_num: method_num}),
290+
method_num: method_num,
291+
impl_def_id: None}),
291292
ty: fty,
292293
substs: trait_ref.substs.clone()
293294
};

0 commit comments

Comments
 (0)