-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
New lint: unit_as_impl_trait
#13925
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||||||
"`()` 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 `()`") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 `()`", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also the messaging might be shorter:
Suggested change
|
||||||
); | ||||||
} else if let Some(last) = block.stmts.last() { | ||||||
diag.span_note(last.span, "this statement evaluates to `()` because it ends with `;`") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 `()`") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Especially in this case, having some |
||||||
.span_help(block_end_span, "consider being explicit and use `()` in the body"); | ||||||
} | ||||||
}, | ||||||
); | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
#![warn(clippy::unit_as_impl_trait)] | ||
#![allow(clippy::unused_unit)] | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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() {} |
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 | ||
|
There was a problem hiding this comment.
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:
#[cfg(..)]
shenanigans going on that change the final call's return to()
because there's nothing meaningful to return on some systems