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

New lint: unit_as_impl_trait #13925

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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 @@ -6146,6 +6146,7 @@ Released 2018-09-13
[`uninit_vec`]: https://rust-lang.github.io/rust-clippy/master/index.html#uninit_vec
[`uninlined_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
[`unit_arg`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_arg
[`unit_as_impl_trait`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_as_impl_trait
[`unit_cmp`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_cmp
[`unit_hash`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_hash
[`unit_return_expecting_ord`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_return_expecting_ord
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::unicode::UNICODE_NOT_NFC_INFO,
crate::uninhabited_references::UNINHABITED_REFERENCES_INFO,
crate::uninit_vec::UNINIT_VEC_INFO,
crate::unit_as_impl_trait::UNIT_AS_IMPL_TRAIT_INFO,
crate::unit_return_expecting_ord::UNIT_RETURN_EXPECTING_ORD_INFO,
crate::unit_types::LET_UNIT_VALUE_INFO,
crate::unit_types::UNIT_ARG_INFO,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ mod undocumented_unsafe_blocks;
mod unicode;
mod uninhabited_references;
mod uninit_vec;
mod unit_as_impl_trait;
mod unit_return_expecting_ord;
mod unit_types;
mod unnecessary_box_returns;
Expand Down Expand Up @@ -972,5 +973,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
store.register_late_pass(|_| Box::new(unit_as_impl_trait::UnitAsImplTrait));
// add lints here, do not remove this comment, it's used in `new_lint`
}
95 changes: 95 additions & 0 deletions clippy_lints/src/unit_as_impl_trait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use clippy_utils::diagnostics::span_lint_and_then;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::intravisit::FnKind;
use rustc_hir::{Body, ExprKind, FnDecl, FnRetTy, TyKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::{BytePos, Span};

declare_clippy_lint! {
/// ### What it does
/// Checks for function whose return type is an `impl` trait
/// implemented by `()`, and whose `()` return value is implicit.
///
/// ### Why is this bad?
/// Since the required trait is implemented by `()`, adding an extra `;`
/// at the end of the function last expression will compile with no
/// indication that the expected value may have been silently swallowed.
///
/// ### Example
/// ```no_run
/// fn key() -> impl Eq {
/// [1, 10, 2, 0].sort_unstable()
/// }
/// ```
/// Use instead:
/// ```no_run
/// fn key() -> impl Eq {
/// let mut arr = [1, 10, 2, 0];
/// arr.sort_unstable();
/// arr
/// }
/// ```
/// or
/// ```no_run
/// fn key() -> impl Eq {
/// [1, 10, 2, 0].sort_unstable();
/// ()
/// }
/// ```
/// if returning `()` is intentional.
#[clippy::version = "1.86.0"]
pub UNIT_AS_IMPL_TRAIT,
suspicious,
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that this is warn by default, I'd like to make sure we remove all possible false positives:

  • Whenever the swallowed expression would not impl the returned trait
  • Perhaps there are some #[cfg(..)] shenanigans going on that change the final call's return to () because there's nothing meaningful to return on some systems

"`()` which implements the required trait might be returned unexpectedly"
}

declare_lint_pass!(UnitAsImplTrait => [UNIT_AS_IMPL_TRAIT]);

impl<'tcx> LateLintPass<'tcx> for UnitAsImplTrait {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
_: FnKind<'tcx>,
decl: &'tcx FnDecl<'tcx>,
body: &'tcx Body<'tcx>,
span: Span,
_: LocalDefId,
) {
if !span.from_expansion()
&& let FnRetTy::Return(ty) = decl.output
&& let TyKind::OpaqueDef(_) = ty.kind
&& cx.typeck_results().expr_ty(body.value).is_unit()
&& let ExprKind::Block(block, None) = body.value.kind
&& block.expr.is_none_or(|e| !matches!(e.kind, ExprKind::Tup([])))
{
let block_end_span = block.span.with_hi(block.span.hi() - BytePos(1)).shrink_to_hi();

span_lint_and_then(
cx,
UNIT_AS_IMPL_TRAIT,
ty.span,
"this function returns `()` which implements the required trait",
|diag| {
if let Some(expr) = block.expr {
diag.span_note(expr.span, "this expression evaluates to `()`")
Copy link
Contributor

Choose a reason for hiding this comment

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

Here it gets tricky, because the expression might return something else on a different target.

.span_help(
expr.span.shrink_to_hi(),
"consider being explicit, and terminate the body with `()`",
Copy link
Contributor

Choose a reason for hiding this comment

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

also the messaging might be shorter:

Suggested change
"consider being explicit, and terminate the body with `()`",
"add `; ()` for explicitness",

);
} else if let Some(last) = block.stmts.last() {
diag.span_note(last.span, "this statement evaluates to `()` because it ends with `;`")
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we in this case at least check that the expression the semicolon consumed also implements the required trait? Otherwise I could create an example that won't compile when removing the semicolon, and reducing false positives is always a good thing.

.span_note(block.span, "therefore the function body evaluates to `()`")
.span_help(
block_end_span,
"if this is intentional, consider being explicit, and terminate the body with `()`",
);
} else {
diag.span_note(block.span, "the empty body evaluates to `()`")
Copy link
Contributor

Choose a reason for hiding this comment

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

Especially in this case, having some #[cfg(..)] in the plaintext source would suggest this is intentional.

.span_help(block_end_span, "consider being explicit and use `()` in the body");
}
},
);
}
}
}
1 change: 1 addition & 0 deletions tests/ui/crashes/ice-11422.fixed
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![warn(clippy::implied_bounds_in_impls)]
#![allow(clippy::unit_as_impl_trait)]

use std::fmt::Debug;
use std::ops::*;
Expand Down
1 change: 1 addition & 0 deletions tests/ui/crashes/ice-11422.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![warn(clippy::implied_bounds_in_impls)]
#![allow(clippy::unit_as_impl_trait)]

use std::fmt::Debug;
use std::ops::*;
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/crashes/ice-11422.stderr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
error: this bound is already specified as the supertrait of `PartialOrd`
--> tests/ui/crashes/ice-11422.rs:6:33
--> tests/ui/crashes/ice-11422.rs:7:33
|
LL | fn r#gen() -> impl PartialOrd + PartialEq + Debug {}
| ^^^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/implied_bounds_in_impls.fixed
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![warn(clippy::implied_bounds_in_impls)]
#![allow(dead_code)]
#![allow(dead_code, clippy::unit_as_impl_trait)]
#![feature(impl_trait_in_assoc_type, type_alias_impl_trait)]

use std::ops::{Deref, DerefMut};
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/implied_bounds_in_impls.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![warn(clippy::implied_bounds_in_impls)]
#![allow(dead_code)]
#![allow(dead_code, clippy::unit_as_impl_trait)]
#![feature(impl_trait_in_assoc_type, type_alias_impl_trait)]

use std::ops::{Deref, DerefMut};
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/new_ret_no_self.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![feature(type_alias_impl_trait)]
#![warn(clippy::new_ret_no_self)]
#![allow(dead_code)]
#![allow(dead_code, clippy::unit_as_impl_trait)]

fn main() {}

Expand Down
45 changes: 45 additions & 0 deletions tests/ui/unit_as_impl_trait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#![warn(clippy::unit_as_impl_trait)]
#![allow(clippy::unused_unit)]

Copy link
Contributor

Choose a reason for hiding this comment

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

How about a test against the type not implementing the trait, e.g.

struct NoEq;

fn false_positive_no_trait_impl() -> impl Eq { NoEq; }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, will do. I'll have to investigate how to properly check that the type of the expression implements the bounds of the RPIT, probably similar to what is done in needless_borrows_for_generic_arg which also has to do a similar check.

fn implicit_unit() -> impl Copy {
//~^ ERROR: function returns `()` which implements the required trait
}

fn explicit_unit() -> impl Copy {
()
}

fn not_unit(x: u32) -> impl Copy {
x
}

fn never(x: u32) -> impl Copy {
panic!();
}

fn with_generic_param<T: Eq>(x: T) -> impl Eq {
//~^ ERROR: function returns `()` which implements the required trait
x;
}

fn non_empty_implicit_unit() -> impl Copy {
//~^ ERROR: function returns `()` which implements the required trait
println!("foobar");
}

fn last_expression_returning_unit() -> impl Eq {
//~^ ERROR: function returns `()` which implements the required trait
[1, 10, 2, 0].sort_unstable()
}

#[derive(Clone)]
struct S;

impl S {
fn clone(&self) -> impl Clone {
//~^ ERROR: function returns `()` which implements the required trait
S;
}
}

fn main() {}
120 changes: 120 additions & 0 deletions tests/ui/unit_as_impl_trait.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
error: this function returns `()` which implements the required trait
--> tests/ui/unit_as_impl_trait.rs:4:23
|
LL | fn implicit_unit() -> impl Copy {
| ^^^^^^^^^
|
note: the empty body evaluates to `()`
--> tests/ui/unit_as_impl_trait.rs:4:33
|
LL | fn implicit_unit() -> impl Copy {
| _________________________________^
LL | |
LL | | }
| |_^
help: consider being explicit and use `()` in the body
--> tests/ui/unit_as_impl_trait.rs:6:1
|
LL | }
| ^
= note: `-D clippy::unit-as-impl-trait` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::unit_as_impl_trait)]`

error: this function returns `()` which implements the required trait
--> tests/ui/unit_as_impl_trait.rs:20:39
|
LL | fn with_generic_param<T: Eq>(x: T) -> impl Eq {
| ^^^^^^^
|
note: this statement evaluates to `()` because it ends with `;`
--> tests/ui/unit_as_impl_trait.rs:22:5
|
LL | x;
| ^^
note: therefore the function body evaluates to `()`
--> tests/ui/unit_as_impl_trait.rs:20:47
|
LL | fn with_generic_param<T: Eq>(x: T) -> impl Eq {
| _______________________________________________^
LL | |
LL | | x;
LL | | }
| |_^
help: if this is intentional, consider being explicit, and terminate the body with `()`
--> tests/ui/unit_as_impl_trait.rs:23:1
|
LL | }
| ^

error: this function returns `()` which implements the required trait
--> tests/ui/unit_as_impl_trait.rs:25:33
|
LL | fn non_empty_implicit_unit() -> impl Copy {
| ^^^^^^^^^
|
note: this statement evaluates to `()` because it ends with `;`
--> tests/ui/unit_as_impl_trait.rs:27:5
|
LL | println!("foobar");
| ^^^^^^^^^^^^^^^^^^
note: therefore the function body evaluates to `()`
--> tests/ui/unit_as_impl_trait.rs:25:43
|
LL | fn non_empty_implicit_unit() -> impl Copy {
| ___________________________________________^
LL | |
LL | | println!("foobar");
LL | | }
| |_^
help: if this is intentional, consider being explicit, and terminate the body with `()`
--> tests/ui/unit_as_impl_trait.rs:28:1
|
LL | }
| ^
= note: this error originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

error: this function returns `()` which implements the required trait
--> tests/ui/unit_as_impl_trait.rs:30:40
|
LL | fn last_expression_returning_unit() -> impl Eq {
| ^^^^^^^
|
note: this expression evaluates to `()`
--> tests/ui/unit_as_impl_trait.rs:32:5
|
LL | [1, 10, 2, 0].sort_unstable()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: consider being explicit, and terminate the body with `()`
--> tests/ui/unit_as_impl_trait.rs:32:34
|
LL | [1, 10, 2, 0].sort_unstable()
| ^

error: this function returns `()` which implements the required trait
--> tests/ui/unit_as_impl_trait.rs:39:24
|
LL | fn clone(&self) -> impl Clone {
| ^^^^^^^^^^
|
note: this statement evaluates to `()` because it ends with `;`
--> tests/ui/unit_as_impl_trait.rs:41:9
|
LL | S;
| ^^
note: therefore the function body evaluates to `()`
--> tests/ui/unit_as_impl_trait.rs:39:35
|
LL | fn clone(&self) -> impl Clone {
| ___________________________________^
LL | |
LL | | S;
LL | | }
| |_____^
help: if this is intentional, consider being explicit, and terminate the body with `()`
--> tests/ui/unit_as_impl_trait.rs:42:5
|
LL | }
| ^

error: aborting due to 5 previous errors

Loading