Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new lint for catching unbound lifetimes in return values #4908

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,7 @@ Released 2018-09-13
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
[`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds
[`unbound_return_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#unbound_return_lifetimes
[`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc
[`unimplemented`]: https://rust-lang.github.io/rust-clippy/master/index.html#unimplemented
[`uninit_assumed_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#uninit_assumed_init
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.

[There are 346 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are 347 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)

We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you:

Expand Down
5 changes: 5 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ pub mod transmuting_null;
pub mod trivially_copy_pass_by_ref;
pub mod try_err;
pub mod types;
pub mod unbound_return_lifetimes;
pub mod unicode;
pub mod unsafe_removed_from_name;
pub mod unused_io_amount;
Expand Down Expand Up @@ -786,6 +787,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
&types::UNIT_CMP,
&types::UNNECESSARY_CAST,
&types::VEC_BOX,
&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES,
&unicode::NON_ASCII_LITERAL,
&unicode::UNICODE_NOT_NFC,
&unicode::ZERO_WIDTH_SPACE,
Expand Down Expand Up @@ -953,6 +955,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
store.register_late_pass(|| box mul_add::MulAddCheck);
store.register_late_pass(|| box mut_key::MutableKeyType);
store.register_late_pass(|| box modulo_arithmetic::ModuloArithmetic);
store.register_late_pass(|| box unbound_return_lifetimes::UnboundReturnLifetimes);
store.register_early_pass(|| box reference::DerefAddrOf);
store.register_early_pass(|| box reference::RefInDeref);
store.register_early_pass(|| box double_parens::DoubleParens);
Expand Down Expand Up @@ -1326,6 +1329,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
LintId::of(&types::UNIT_CMP),
LintId::of(&types::UNNECESSARY_CAST),
LintId::of(&types::VEC_BOX),
LintId::of(&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES),
LintId::of(&unicode::ZERO_WIDTH_SPACE),
LintId::of(&unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME),
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
Expand Down Expand Up @@ -1577,6 +1581,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
LintId::of(&types::CAST_PTR_ALIGNMENT),
LintId::of(&types::CAST_REF_TO_MUT),
LintId::of(&types::UNIT_CMP),
LintId::of(&unbound_return_lifetimes::UNBOUND_RETURN_LIFETIMES),
LintId::of(&unicode::ZERO_WIDTH_SPACE),
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
LintId::of(&unwrap::PANICKING_UNWRAP),
Expand Down
185 changes: 185 additions & 0 deletions clippy_lints/src/unbound_return_lifetimes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use if_chain::if_chain;
use rustc::declare_lint_pass;
use rustc::hir::map::Map;
use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
use rustc_hir::intravisit::*;
use rustc_hir::*;
use rustc_session::declare_tool_lint;

use crate::utils::span_lint;

declare_clippy_lint! {
/// **What it does:** Checks for lifetime annotations on return values
/// which might not always get bound.
///
/// **Why is this bad?** If the function contains unsafe code which
/// transmutes to the lifetime, the resulting lifetime may live longer
/// than was intended.
///
/// **Known problems*: None.
///
/// **Example:**
/// ```rust
/// struct WrappedStr(str);
///
/// // Bad: unbound return lifetime causing unsoundness (e.g. when x is String)
/// fn unbound<'a>(x: impl AsRef<str> + 'a) -> &'a WrappedStr {
/// let s = x.as_ref();
/// unsafe { &*(s as *const str as *const WrappedStr) }
/// }
///
/// // Good: bound return lifetime is sound
/// fn bound<'a>(x: &'a str) -> &'a WrappedStr {
/// unsafe { &*(x as *const str as *const WrappedStr) }
/// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move WrappedStr above the Bad comment and use it in bad function as return type for more symmetry between the two examples? Also could you please separate the } and // with a newline?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also say that when the bad foo takes a String, 'a can be equal to 'static?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an example note.

/// ```
pub UNBOUND_RETURN_LIFETIMES,
correctness,
"unbound lifetimes in function return values"
}

declare_lint_pass!(UnboundReturnLifetimes => [UNBOUND_RETURN_LIFETIMES]);

impl<'a, 'tcx> LateLintPass<'a, 'tcx> for UnboundReturnLifetimes {
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx Item<'tcx>) {
if let ItemKind::Fn(ref sig, ref generics, _id) = item.kind {
check_fn_inner(cx, &sig.decl, generics, None);
}
}

fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx ImplItem<'tcx>) {
if let ImplItemKind::Method(ref sig, _) = item.kind {
let parent_generics = impl_generics(cx, item.hir_id);
check_fn_inner(cx, &sig.decl, &item.generics, parent_generics);
}
}
}

fn check_fn_inner<'a, 'tcx>(
cx: &LateContext<'a, 'tcx>,
decl: &'tcx FnDecl<'tcx>,
generics: &'tcx Generics<'tcx>,
parent_data: Option<(&'tcx Generics<'tcx>, &'tcx Ty<'tcx>)>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can inner functions refer to the lifetimes of outer functions? If yes, going one level up the stack might not be enough.

) {
let output_type = if let FunctionRetTy::Return(ref output_type) = decl.output {
output_type
} else {
return;
};

let lt = if let TyKind::Rptr(ref lt, _) = output_type.kind {
lt
} else {
return;
Copy link
Contributor

@llogiq llogiq Jan 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about lifetimes in the output type generics, e.g. Cow<'x, str>? Shouldn't those be hamdled, too?

Also what about arrays or tuples containing refs?

};

if matches!(lt.name, LifetimeName::Param(_)) {
let target_lt = lt;

// check function generics
// the target lifetime parameter must appear on the left of some outlives relation
if lifetime_outlives_something(target_lt, generics) {
return;
}

// check parent generics
// the target lifetime parameter must appear on the left of some outlives relation
if let Some((ref parent_generics, _)) = parent_data {
if lifetime_outlives_something(target_lt, parent_generics) {
return;
}
}

// check type generics
// the target lifetime parameter must be included in the type
if let Some((_, ref parent_ty)) = parent_data {
if TypeVisitor::type_contains_lifetime(parent_ty, target_lt) {
return;
}
}

// check arguments the target lifetime parameter must be included as a
// lifetime of a reference, either directly or through the gneric
// parameters of the argument type.
for input in decl.inputs.iter() {
if TypeVisitor::type_contains_lifetime(input, target_lt) {
return;
}
}

span_lint(
cx,
UNBOUND_RETURN_LIFETIMES,
target_lt.span,
"lifetime is unconstrained",
);
}
}

struct TypeVisitor<'tcx> {
found: bool,
target_lt: &'tcx Lifetime,
}

impl<'tcx> TypeVisitor<'tcx> {
fn type_contains_lifetime(ty: &Ty<'_>, target_lt: &'tcx Lifetime) -> bool {
let mut visitor = TypeVisitor {
found: false,
target_lt,
};
walk_ty(&mut visitor, ty);
visitor.found
}
}

impl<'tcx> Visitor<'tcx> for TypeVisitor<'tcx> {
type Map = Map<'tcx>;

fn visit_lifetime(&mut self, lt: &'tcx Lifetime) {
if lt.name == self.target_lt.name {
self.found = true;
}
}

fn visit_ty(&mut self, ty: &'tcx Ty<'_>) {
match ty.kind {
TyKind::Rptr(ref lt, _) => {
if lt.name == self.target_lt.name {
self.found = true;
}
},
TyKind::Path(ref qpath) => {
if !self.found {
walk_qpath(self, qpath, ty.hir_id, ty.span);
}
},
_ => (),
}
}

fn nested_visit_map(&mut self) -> NestedVisitorMap<'_, Self::Map> {
NestedVisitorMap::None
}
}

fn lifetime_outlives_something<'tcx>(target_lt: &'tcx Lifetime, generics: &'tcx Generics<'tcx>) -> bool {
if let Some(param) = generics.get_named(target_lt.name.ident().name) {
if param.bounds.iter().any(|b| matches!(b, GenericBound::Outlives(_))) {
return true;
}
}
false
}

fn impl_generics<'tcx>(cx: &LateContext<'_, 'tcx>, hir_id: HirId) -> Option<(&'tcx Generics<'tcx>, &'tcx Ty<'tcx>)> {
let parent_impl = cx.tcx.hir().get_parent_item(hir_id);
if_chain! {
if parent_impl != CRATE_HIR_ID;
if let Node::Item(item) = cx.tcx.hir().get(parent_impl);
if let ItemKind::Impl(_, _, _, ref parent_generics, _, ref ty, _) = item.kind;
then {
return Some((parent_generics, ty))
}
}
None
}
9 changes: 8 additions & 1 deletion src/lintlist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use lint::Lint;
pub use lint::LINT_LEVELS;

// begin lint list, do not remove this comment, it’s used in `update_lints`
pub const ALL_LINTS: [Lint; 346] = [
pub const ALL_LINTS: [Lint; 347] = [
Lint {
name: "absurd_extreme_comparisons",
group: "correctness",
Expand Down Expand Up @@ -2079,6 +2079,13 @@ pub const ALL_LINTS: [Lint; 346] = [
deprecation: None,
module: "trait_bounds",
},
Lint {
name: "unbound_return_lifetimes",
group: "correctness",
desc: "unbound lifetimes in function return values",
deprecation: None,
module: "unbound_return_lifetimes",
},
Lint {
name: "unicode_not_nfc",
group: "pedantic",
Expand Down
3 changes: 2 additions & 1 deletion tests/ui/extra_unused_lifetimes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
dead_code,
clippy::needless_lifetimes,
clippy::needless_pass_by_value,
clippy::trivially_copy_pass_by_ref
clippy::trivially_copy_pass_by_ref,
clippy::unbound_return_lifetimes
)]
#![warn(clippy::extra_unused_lifetimes)]

Expand Down
8 changes: 4 additions & 4 deletions tests/ui/extra_unused_lifetimes.stderr
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
error: this lifetime isn't used in the function definition
--> $DIR/extra_unused_lifetimes.rs:14:14
--> $DIR/extra_unused_lifetimes.rs:15:14
|
LL | fn unused_lt<'a>(x: u8) {}
| ^^
|
= note: `-D clippy::extra-unused-lifetimes` implied by `-D warnings`

error: this lifetime isn't used in the function definition
--> $DIR/extra_unused_lifetimes.rs:16:25
--> $DIR/extra_unused_lifetimes.rs:17:25
|
LL | fn unused_lt_transitive<'a, 'b: 'a>(x: &'b u8) {
| ^^

error: this lifetime isn't used in the function definition
--> $DIR/extra_unused_lifetimes.rs:41:10
--> $DIR/extra_unused_lifetimes.rs:42:10
|
LL | fn x<'a>(&self) {}
| ^^

error: this lifetime isn't used in the function definition
--> $DIR/extra_unused_lifetimes.rs:67:22
--> $DIR/extra_unused_lifetimes.rs:68:22
|
LL | fn unused_lt<'a>(x: u8) {}
| ^^
Expand Down
7 changes: 6 additions & 1 deletion tests/ui/needless_lifetimes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#![warn(clippy::needless_lifetimes)]
#![allow(dead_code, clippy::needless_pass_by_value, clippy::trivially_copy_pass_by_ref)]
#![allow(
dead_code,
clippy::needless_pass_by_value,
clippy::trivially_copy_pass_by_ref,
clippy::unbound_return_lifetimes
)]

fn distinct_lifetimes<'a, 'b>(_x: &'a u8, _y: &'b u8, _z: u8) {}

Expand Down
Loading