Skip to content

Commit 5ca596f

Browse files
committed
Auto merge of #85556 - FabianWolff:issue-85071, r=estebank,jackh726
Warn about unreachable code following an expression with an uninhabited type This pull request fixes #85071. The issue is that liveness analysis currently is "smarter" than reachability analysis when it comes to detecting uninhabited types: Unreachable code is detected during type checking, where full type information is not yet available. Therefore, the check for type inhabitedness is quite crude: https://github.com/rust-lang/rust/blob/fc81ad22c453776de16acf9938976930cf8c9401/compiler/rustc_typeck/src/check/expr.rs#L202-L205 i.e. it only checks for `!`, but not other, non-trivially uninhabited types, such as empty enums, structs containing an uninhabited type, etc. By contrast, liveness analysis, which runs after type checking, can benefit from the more sophisticated `tcx.is_ty_uninhabited_from()`: https://github.com/rust-lang/rust/blob/fc81ad22c453776de16acf9938976930cf8c9401/compiler/rustc_passes/src/liveness.rs#L981 https://github.com/rust-lang/rust/blob/fc81ad22c453776de16acf9938976930cf8c9401/compiler/rustc_passes/src/liveness.rs#L996 This can lead to confusing warnings when a variable is reported as unused, but the use of the variable is not reported as unreachable. For instance: ```rust enum Foo {} fn f() -> Foo {todo!()} fn main() { let x = f(); let _ = x; } ``` currently leads to ``` warning: unused variable: `x` --> t1.rs:5:9 | 5 | let x = f(); | ^ help: if this is intentional, prefix it with an underscore: `_x` | = note: `#[warn(unused_variables)]` on by default warning: 1 warning emitted ``` which is confusing, because `x` _appears_ to be used in line 6. With my changes, I get: ``` warning: unreachable expression --> t1.rs:6:13 | 5 | let x = f(); | --- any code following this expression is unreachable 6 | let _ = x; | ^ unreachable expression | = note: `#[warn(unreachable_code)]` on by default note: this expression has type `Foo`, which is uninhabited --> t1.rs:5:13 | 5 | let x = f(); | ^^^ warning: unused variable: `x` --> t1.rs:5:9 | 5 | let x = f(); | ^ help: if this is intentional, prefix it with an underscore: `_x` | = note: `#[warn(unused_variables)]` on by default warning: 2 warnings emitted ``` My implementation is slightly inelegant because unreachable code warnings can now be issued in two different places (during type checking and during liveness analysis), but I think it is the solution with the least amount of unnecessary code duplication, given that the new warning integrates nicely with liveness analysis, where unreachable code is already implicitly detected for the purpose of finding unused variables.
2 parents de42550 + 7a98fd4 commit 5ca596f

File tree

5 files changed

+181
-31
lines changed

5 files changed

+181
-31
lines changed

compiler/rustc_passes/src/liveness.rs

+72-31
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ use rustc_hir::{Expr, HirId, HirIdMap, HirIdSet};
9595
use rustc_index::vec::IndexVec;
9696
use rustc_middle::hir::map::Map;
9797
use rustc_middle::ty::query::Providers;
98-
use rustc_middle::ty::{self, DefIdTree, RootVariableMinCaptureList, TyCtxt};
98+
use rustc_middle::ty::{self, DefIdTree, RootVariableMinCaptureList, Ty, TyCtxt};
9999
use rustc_session::lint;
100100
use rustc_span::symbol::{kw, sym, Symbol};
101101
use rustc_span::Span;
@@ -123,8 +123,8 @@ rustc_index::newtype_index! {
123123
#[derive(Copy, Clone, PartialEq, Debug)]
124124
enum LiveNodeKind {
125125
UpvarNode(Span),
126-
ExprNode(Span),
127-
VarDefNode(Span),
126+
ExprNode(Span, HirId),
127+
VarDefNode(Span, HirId),
128128
ClosureNode,
129129
ExitNode,
130130
}
@@ -133,8 +133,8 @@ fn live_node_kind_to_string(lnk: LiveNodeKind, tcx: TyCtxt<'_>) -> String {
133133
let sm = tcx.sess.source_map();
134134
match lnk {
135135
UpvarNode(s) => format!("Upvar node [{}]", sm.span_to_diagnostic_string(s)),
136-
ExprNode(s) => format!("Expr node [{}]", sm.span_to_diagnostic_string(s)),
137-
VarDefNode(s) => format!("Var def node [{}]", sm.span_to_diagnostic_string(s)),
136+
ExprNode(s, _) => format!("Expr node [{}]", sm.span_to_diagnostic_string(s)),
137+
VarDefNode(s, _) => format!("Var def node [{}]", sm.span_to_diagnostic_string(s)),
138138
ClosureNode => "Closure node".to_owned(),
139139
ExitNode => "Exit node".to_owned(),
140140
}
@@ -297,7 +297,7 @@ impl IrMaps<'tcx> {
297297
}
298298

299299
pat.each_binding(|_, hir_id, _, ident| {
300-
self.add_live_node_for_node(hir_id, VarDefNode(ident.span));
300+
self.add_live_node_for_node(hir_id, VarDefNode(ident.span, hir_id));
301301
self.add_variable(Local(LocalInfo {
302302
id: hir_id,
303303
name: ident.name,
@@ -396,14 +396,14 @@ impl<'tcx> Visitor<'tcx> for IrMaps<'tcx> {
396396
hir::ExprKind::Path(hir::QPath::Resolved(_, ref path)) => {
397397
debug!("expr {}: path that leads to {:?}", expr.hir_id, path.res);
398398
if let Res::Local(_var_hir_id) = path.res {
399-
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
399+
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
400400
}
401401
intravisit::walk_expr(self, expr);
402402
}
403403
hir::ExprKind::Closure(..) => {
404404
// Interesting control flow (for loops can contain labeled
405405
// breaks or continues)
406-
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
406+
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
407407

408408
// Make a live_node for each captured variable, with the span
409409
// being the location that the variable is used. This results
@@ -436,11 +436,11 @@ impl<'tcx> Visitor<'tcx> for IrMaps<'tcx> {
436436

437437
// live nodes required for interesting control flow:
438438
hir::ExprKind::If(..) | hir::ExprKind::Match(..) | hir::ExprKind::Loop(..) => {
439-
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
439+
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
440440
intravisit::walk_expr(self, expr);
441441
}
442442
hir::ExprKind::Binary(op, ..) if op.node.is_lazy() => {
443-
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
443+
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
444444
intravisit::walk_expr(self, expr);
445445
}
446446

@@ -992,32 +992,13 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
992992
}
993993

994994
hir::ExprKind::Call(ref f, ref args) => {
995-
let m = self.ir.tcx.parent_module(expr.hir_id).to_def_id();
996-
let succ = if self.ir.tcx.is_ty_uninhabited_from(
997-
m,
998-
self.typeck_results.expr_ty(expr),
999-
self.param_env,
1000-
) {
1001-
self.exit_ln
1002-
} else {
1003-
succ
1004-
};
995+
let succ = self.check_is_ty_uninhabited(expr, succ);
1005996
let succ = self.propagate_through_exprs(args, succ);
1006997
self.propagate_through_expr(&f, succ)
1007998
}
1008999

10091000
hir::ExprKind::MethodCall(.., ref args, _) => {
1010-
let m = self.ir.tcx.parent_module(expr.hir_id).to_def_id();
1011-
let succ = if self.ir.tcx.is_ty_uninhabited_from(
1012-
m,
1013-
self.typeck_results.expr_ty(expr),
1014-
self.param_env,
1015-
) {
1016-
self.exit_ln
1017-
} else {
1018-
succ
1019-
};
1020-
1001+
let succ = self.check_is_ty_uninhabited(expr, succ);
10211002
self.propagate_through_exprs(args, succ)
10221003
}
10231004

@@ -1289,6 +1270,66 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
12891270

12901271
ln
12911272
}
1273+
1274+
fn check_is_ty_uninhabited(&mut self, expr: &Expr<'_>, succ: LiveNode) -> LiveNode {
1275+
let ty = self.typeck_results.expr_ty(expr);
1276+
let m = self.ir.tcx.parent_module(expr.hir_id).to_def_id();
1277+
if self.ir.tcx.is_ty_uninhabited_from(m, ty, self.param_env) {
1278+
match self.ir.lnks[succ] {
1279+
LiveNodeKind::ExprNode(succ_span, succ_id) => {
1280+
self.warn_about_unreachable(expr.span, ty, succ_span, succ_id, "expression");
1281+
}
1282+
LiveNodeKind::VarDefNode(succ_span, succ_id) => {
1283+
self.warn_about_unreachable(expr.span, ty, succ_span, succ_id, "definition");
1284+
}
1285+
_ => {}
1286+
};
1287+
self.exit_ln
1288+
} else {
1289+
succ
1290+
}
1291+
}
1292+
1293+
fn warn_about_unreachable(
1294+
&mut self,
1295+
orig_span: Span,
1296+
orig_ty: Ty<'tcx>,
1297+
expr_span: Span,
1298+
expr_id: HirId,
1299+
descr: &str,
1300+
) {
1301+
if !orig_ty.is_never() {
1302+
// Unreachable code warnings are already emitted during type checking.
1303+
// However, during type checking, full type information is being
1304+
// calculated but not yet available, so the check for diverging
1305+
// expressions due to uninhabited result types is pretty crude and
1306+
// only checks whether ty.is_never(). Here, we have full type
1307+
// information available and can issue warnings for less obviously
1308+
// uninhabited types (e.g. empty enums). The check above is used so
1309+
// that we do not emit the same warning twice if the uninhabited type
1310+
// is indeed `!`.
1311+
1312+
self.ir.tcx.struct_span_lint_hir(
1313+
lint::builtin::UNREACHABLE_CODE,
1314+
expr_id,
1315+
expr_span,
1316+
|lint| {
1317+
let msg = format!("unreachable {}", descr);
1318+
lint.build(&msg)
1319+
.span_label(expr_span, &msg)
1320+
.span_label(orig_span, "any code following this expression is unreachable")
1321+
.span_note(
1322+
orig_span,
1323+
&format!(
1324+
"this expression has type `{}`, which is uninhabited",
1325+
orig_ty
1326+
),
1327+
)
1328+
.emit();
1329+
},
1330+
);
1331+
}
1332+
}
12921333
}
12931334

12941335
// _______________________________________________________________________
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// A slight variation of issue-85071.rs. Here, a method is called instead
2+
// of a function, and the warning is about an unreachable definition
3+
// instead of an unreachable expression.
4+
5+
// check-pass
6+
7+
#![warn(unused_variables,unreachable_code)]
8+
9+
enum Foo {}
10+
11+
struct S;
12+
impl S {
13+
fn f(&self) -> Foo {todo!()}
14+
}
15+
16+
fn main() {
17+
let s = S;
18+
let x = s.f();
19+
//~^ WARNING: unused variable: `x`
20+
let _y = x;
21+
//~^ WARNING: unreachable definition
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
warning: unreachable definition
2+
--> $DIR/issue-85071-2.rs:20:9
3+
|
4+
LL | let x = s.f();
5+
| ----- any code following this expression is unreachable
6+
LL |
7+
LL | let _y = x;
8+
| ^^ unreachable definition
9+
|
10+
note: the lint level is defined here
11+
--> $DIR/issue-85071-2.rs:7:26
12+
|
13+
LL | #![warn(unused_variables,unreachable_code)]
14+
| ^^^^^^^^^^^^^^^^
15+
note: this expression has type `Foo`, which is uninhabited
16+
--> $DIR/issue-85071-2.rs:18:13
17+
|
18+
LL | let x = s.f();
19+
| ^^^^^
20+
21+
warning: unused variable: `x`
22+
--> $DIR/issue-85071-2.rs:18:9
23+
|
24+
LL | let x = s.f();
25+
| ^ help: if this is intentional, prefix it with an underscore: `_x`
26+
|
27+
note: the lint level is defined here
28+
--> $DIR/issue-85071-2.rs:7:9
29+
|
30+
LL | #![warn(unused_variables,unreachable_code)]
31+
| ^^^^^^^^^^^^^^^^
32+
33+
warning: 2 warnings emitted
34+
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Checks that an unreachable code warning is emitted when an expression is
2+
// preceded by an expression with an uninhabited type. Previously, the
3+
// variable liveness analysis was "smarter" than the reachability analysis
4+
// in this regard, which led to confusing "unused variable" warnings
5+
// without an accompanying explanatory "unreachable expression" warning.
6+
7+
// check-pass
8+
9+
#![warn(unused_variables,unreachable_code)]
10+
11+
enum Foo {}
12+
fn f() -> Foo {todo!()}
13+
14+
fn main() {
15+
let x = f();
16+
//~^ WARNING: unused variable: `x`
17+
let _ = x;
18+
//~^ WARNING: unreachable expression
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
warning: unreachable expression
2+
--> $DIR/issue-85071.rs:17:13
3+
|
4+
LL | let x = f();
5+
| --- any code following this expression is unreachable
6+
LL |
7+
LL | let _ = x;
8+
| ^ unreachable expression
9+
|
10+
note: the lint level is defined here
11+
--> $DIR/issue-85071.rs:9:26
12+
|
13+
LL | #![warn(unused_variables,unreachable_code)]
14+
| ^^^^^^^^^^^^^^^^
15+
note: this expression has type `Foo`, which is uninhabited
16+
--> $DIR/issue-85071.rs:15:13
17+
|
18+
LL | let x = f();
19+
| ^^^
20+
21+
warning: unused variable: `x`
22+
--> $DIR/issue-85071.rs:15:9
23+
|
24+
LL | let x = f();
25+
| ^ help: if this is intentional, prefix it with an underscore: `_x`
26+
|
27+
note: the lint level is defined here
28+
--> $DIR/issue-85071.rs:9:9
29+
|
30+
LL | #![warn(unused_variables,unreachable_code)]
31+
| ^^^^^^^^^^^^^^^^
32+
33+
warning: 2 warnings emitted
34+

0 commit comments

Comments
 (0)