Skip to content

Commit e5fe67a

Browse files
committed
Add lint for functions which never return
1 parent 4cb53cd commit e5fe67a

23 files changed

+307
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5761,6 +5761,7 @@ Released 2018-09-13
57615761
[`neg_multiply`]: https://rust-lang.github.io/rust-clippy/master/index.html#neg_multiply
57625762
[`negative_feature_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#negative_feature_names
57635763
[`never_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#never_loop
5764+
[`never_returns`]: https://rust-lang.github.io/rust-clippy/master/index.html#never_returns
57645765
[`new_ret_no_self`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_ret_no_self
57655766
[`new_without_default`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_without_default
57665767
[`new_without_default_derive`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_without_default_derive

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

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

8-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
1111
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

book/src/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
A collection of lints to catch common mistakes and improve your
77
[Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
Lints are divided into categories, each with a default [lint
1212
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how

book/src/lint_configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ Suppress lints whenever the suggested change would cause breakage for other crat
349349
* [`large_types_passed_by_value`](https://rust-lang.github.io/rust-clippy/master/index.html#large_types_passed_by_value)
350350
* [`linkedlist`](https://rust-lang.github.io/rust-clippy/master/index.html#linkedlist)
351351
* [`needless_pass_by_ref_mut`](https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_ref_mut)
352+
* [`never_returns`](https://rust-lang.github.io/rust-clippy/master/index.html#never_returns)
352353
* [`option_option`](https://rust-lang.github.io/rust-clippy/master/index.html#option_option)
353354
* [`rc_buffer`](https://rust-lang.github.io/rust-clippy/master/index.html#rc_buffer)
354355
* [`rc_mutex`](https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex)

clippy_config/src/conf.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ define_Conf! {
398398
large_types_passed_by_value,
399399
linkedlist,
400400
needless_pass_by_ref_mut,
401+
never_returns,
401402
option_option,
402403
rc_buffer,
403404
rc_mutex,

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
546546
crate::needless_update::NEEDLESS_UPDATE_INFO,
547547
crate::neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD_INFO,
548548
crate::neg_multiply::NEG_MULTIPLY_INFO,
549+
crate::never_returns::NEVER_RETURNS_INFO,
549550
crate::new_without_default::NEW_WITHOUT_DEFAULT_INFO,
550551
crate::no_effect::NO_EFFECT_INFO,
551552
crate::no_effect::NO_EFFECT_UNDERSCORE_BINDING_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ mod needless_question_mark;
268268
mod needless_update;
269269
mod neg_cmp_op_on_partial_ord;
270270
mod neg_multiply;
271+
mod never_returns;
271272
mod new_without_default;
272273
mod no_effect;
273274
mod no_mangle_with_rust_abi;
@@ -951,5 +952,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
951952
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
952953
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
953954
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
955+
store.register_late_pass(|_| Box::new(never_returns::NeverReturns::new(conf)));
954956
// add lints here, do not remove this comment, it's used in `new_lint`
955957
}

clippy_lints/src/never_returns.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::source::snippet_opt;
4+
use clippy_utils::{ReturnType, ReturnVisitor, is_entrypoint_fn, visit_returns};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::def_id::LocalDefId;
7+
use rustc_hir::{BodyId, FnRetTy, FnSig, ImplItem, ImplItemKind, Item, ItemKind, TyKind};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_middle::ty::TypeckResults;
10+
use rustc_middle::ty::adjustment::{Adjust, Adjustment};
11+
use rustc_session::impl_lint_pass;
12+
13+
declare_clippy_lint! {
14+
/// ### What it does
15+
///
16+
/// Detects functions that do not return, but do not have `!` as their return type.
17+
///
18+
/// ### Why is this bad?
19+
///
20+
/// Returning `!` is a more accurate API for your callers, and allows for optimisations/further linting.
21+
///
22+
/// ### Example
23+
/// ```no_run
24+
/// fn run() {
25+
/// loop {
26+
/// do_thing()
27+
/// }
28+
/// }
29+
/// ```
30+
/// Use instead:
31+
/// ```no_run
32+
/// fn run() -> ! {
33+
/// loop {
34+
/// do_thing()
35+
/// }
36+
/// }
37+
/// ```
38+
#[clippy::version = "1.83.0"]
39+
pub NEVER_RETURNS,
40+
pedantic,
41+
"functions that never return, but are typed to"
42+
}
43+
44+
#[derive(Clone, Copy)]
45+
pub(crate) struct NeverReturns {
46+
avoid_breaking_exported_api: bool,
47+
}
48+
49+
impl_lint_pass!(NeverReturns => [NEVER_RETURNS]);
50+
51+
impl NeverReturns {
52+
pub fn new(conf: &Conf) -> Self {
53+
Self {
54+
avoid_breaking_exported_api: conf.avoid_breaking_exported_api,
55+
}
56+
}
57+
58+
fn check_item_fn(self, cx: &LateContext<'_>, sig: FnSig<'_>, def_id: LocalDefId, body_id: BodyId) {
59+
let returns_unit = if let FnRetTy::Return(ret_ty) = sig.decl.output {
60+
if let TyKind::Never = ret_ty.kind {
61+
return;
62+
}
63+
64+
matches!(ret_ty.kind, TyKind::Tup([]))
65+
} else {
66+
true
67+
};
68+
69+
if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(def_id) {
70+
return;
71+
}
72+
73+
// We shouldn't try to change the signature of a lang item!
74+
if cx.tcx.lang_items().from_def_id(def_id.to_def_id()).is_some() {
75+
return;
76+
}
77+
78+
let body = cx.tcx.hir().body(body_id);
79+
let typeck_results = cx.tcx.typeck_body(body_id);
80+
let mut visitor = NeverReturnVisitor {
81+
typeck_results,
82+
returns_unit,
83+
found_implicit_return: false,
84+
};
85+
86+
if visit_returns(&mut visitor, body.value).is_continue() && visitor.found_implicit_return {
87+
let mut applicability = Applicability::MachineApplicable;
88+
let (lint_span, mut snippet, sugg) = match sig.decl.output {
89+
FnRetTy::DefaultReturn(span) => (span, String::new(), " -> !"),
90+
FnRetTy::Return(ret_ty) => {
91+
let snippet = if let Some(snippet) = snippet_opt(cx, ret_ty.span) {
92+
format!(" a `{snippet}`")
93+
} else {
94+
applicability = Applicability::HasPlaceholders;
95+
String::new()
96+
};
97+
98+
(ret_ty.span, snippet, "!")
99+
},
100+
};
101+
102+
snippet.insert_str(0, "function never returns, but is typed to return");
103+
span_lint_and_sugg(
104+
cx,
105+
NEVER_RETURNS,
106+
lint_span,
107+
snippet,
108+
"replace with",
109+
sugg.into(),
110+
applicability,
111+
);
112+
}
113+
}
114+
}
115+
116+
impl LateLintPass<'_> for NeverReturns {
117+
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
118+
if let ItemKind::Fn(sig, _, body_id) = item.kind {
119+
let local_def_id = item.owner_id.def_id;
120+
if is_entrypoint_fn(cx, local_def_id.to_def_id()) {
121+
return;
122+
}
123+
124+
self.check_item_fn(cx, sig, local_def_id, body_id);
125+
}
126+
}
127+
128+
fn check_impl_item(&mut self, cx: &LateContext<'_>, item: &ImplItem<'_>) {
129+
if let ImplItemKind::Fn(sig, body_id) = item.kind {
130+
let local_def_id = item.owner_id.def_id;
131+
self.check_item_fn(cx, sig, local_def_id, body_id);
132+
}
133+
}
134+
}
135+
136+
struct NeverReturnVisitor<'tcx> {
137+
typeck_results: &'tcx TypeckResults<'tcx>,
138+
found_implicit_return: bool,
139+
returns_unit: bool,
140+
}
141+
142+
impl ReturnVisitor for &mut NeverReturnVisitor<'_> {
143+
type Result = std::ops::ControlFlow<()>;
144+
145+
fn visit_return(&mut self, kind: ReturnType<'_>) -> Self::Result {
146+
let expression = match kind {
147+
ReturnType::Explicit(expr) => expr,
148+
ReturnType::UnitReturnExplicit(_) => {
149+
return Self::Result::Break(());
150+
},
151+
ReturnType::Implicit(expr) | ReturnType::MissingElseImplicit(expr) => {
152+
self.found_implicit_return = true;
153+
expr
154+
},
155+
ReturnType::DivergingImplicit(_) => {
156+
// If this function returns unit, a diverging implicit may just
157+
// be an implicit unit return, in which case we should not lint.
158+
return if self.returns_unit {
159+
Self::Result::Break(())
160+
} else {
161+
Self::Result::Continue(())
162+
};
163+
},
164+
};
165+
166+
if expression.span.from_expansion() {
167+
return Self::Result::Break(());
168+
}
169+
170+
let adjustments = self.typeck_results.expr_adjustments(expression);
171+
if adjustments.iter().any(is_never_to_any) {
172+
Self::Result::Continue(())
173+
} else {
174+
Self::Result::Break(())
175+
}
176+
}
177+
}
178+
179+
fn is_never_to_any(adjustment: &Adjustment<'_>) -> bool {
180+
matches!(adjustment.kind, Adjust::NeverToAny)
181+
}

lintcheck/src/driver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fn run_clippy(addr: &str) -> Option<i32> {
5454
}
5555
}
5656

57-
pub fn drive(addr: &str) {
57+
pub fn drive(addr: &str) -> ! {
5858
process::exit(run_clippy(addr).unwrap_or_else(|| {
5959
Command::new("rustc")
6060
.args(env::args_os().skip(2))

src/driver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ const BUG_REPORT_URL: &str = "https://github.com/rust-lang/rust-clippy/issues/ne
181181

182182
#[allow(clippy::too_many_lines)]
183183
#[allow(clippy::ignored_unit_patterns)]
184-
pub fn main() {
184+
pub fn main() -> ! {
185185
let early_dcx = EarlyDiagCtxt::new(ErrorOutputType::default());
186186

187187
rustc_driver::init_rustc_env_logger(&early_dcx);

tests/compile-test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ impl LintMetadata {
583583
}
584584
}
585585

586-
fn applicability_str(&self) -> &str {
586+
fn applicability_str(&self) -> &'static str {
587587
match self.applicability {
588588
Applicability::MachineApplicable => "MachineApplicable",
589589
Applicability::HasPlaceholders => "HasPlaceholders",

tests/ui-toml/panic/panic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn main() {
1313
}
1414
}
1515

16-
fn issue_13292() {
16+
fn issue_13292() -> ! {
1717
panic_any("should lint")
1818
}
1919

tests/ui/empty_loop.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
extern crate proc_macros;
66
use proc_macros::{external, inline_macros};
77

8-
fn should_trigger() {
8+
fn should_trigger() -> ! {
99
loop {}
1010
#[allow(clippy::never_loop)]
1111
loop {

tests/ui/implicit_return.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ fn loop_macro_test() -> bool {
118118
}
119119
}
120120

121+
#[expect(clippy::never_returns)]
121122
fn divergent_test() -> bool {
122123
fn diverge() -> ! {
123124
panic!()

tests/ui/implicit_return.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ fn loop_macro_test() -> bool {
118118
}
119119
}
120120

121+
#[expect(clippy::never_returns)]
121122
fn divergent_test() -> bool {
122123
fn diverge() -> ! {
123124
panic!()

tests/ui/implicit_return.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ LL + }
170170
|
171171

172172
error: missing `return` statement
173-
--> tests/ui/implicit_return.rs:130:5
173+
--> tests/ui/implicit_return.rs:131:5
174174
|
175175
LL | true
176176
| ^^^^

tests/ui/infinite_loops.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//@no-rustfix
22
//@aux-build:proc_macros.rs
33

4-
#![allow(clippy::never_loop)]
4+
#![allow(clippy::never_loop, clippy::never_returns)]
55
#![warn(clippy::infinite_loop)]
66

77
extern crate proc_macros;

tests/ui/needless_continue.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ fn main() {
5555
}
5656
}
5757

58-
fn simple_loop() {
58+
fn simple_loop() -> ! {
5959
loop {
6060
continue;
6161
//~^ ERROR: this `continue` expression is redundant
6262
}
6363
}
6464

65-
fn simple_loop2() {
65+
fn simple_loop2() -> ! {
6666
loop {
6767
println!("bleh");
6868
continue;
@@ -71,15 +71,15 @@ fn simple_loop2() {
7171
}
7272

7373
#[rustfmt::skip]
74-
fn simple_loop3() {
74+
fn simple_loop3() -> ! {
7575
loop {
7676
continue
7777
//~^ ERROR: this `continue` expression is redundant
7878
}
7979
}
8080

8181
#[rustfmt::skip]
82-
fn simple_loop4() {
82+
fn simple_loop4() -> ! {
8383
loop {
8484
println!("bleh");
8585
continue
@@ -94,7 +94,7 @@ mod issue_2329 {
9494
fn update_condition() {}
9595

9696
// only the outer loop has a label
97-
fn foo() {
97+
fn foo() -> ! {
9898
'outer: loop {
9999
println!("Entry");
100100
while condition() {
@@ -118,7 +118,7 @@ mod issue_2329 {
118118
}
119119

120120
// both loops have labels
121-
fn bar() {
121+
fn bar() -> ! {
122122
'outer: loop {
123123
println!("Entry");
124124
'inner: while condition() {

0 commit comments

Comments
 (0)