Skip to content

Commit 470cbc0

Browse files
committed
Require parentheses to avoid confusions around labeled break and loop expressions
1 parent 7069a8c commit 470cbc0

File tree

6 files changed

+152
-21
lines changed

6 files changed

+152
-21
lines changed

compiler/rustc_lint/src/context.rs

+5
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,11 @@ pub trait LintContext: Sized {
750750
db.note(&format!("to ignore the value produced by the macro, add a semicolon after the invocation of `{name}`"));
751751
}
752752
}
753+
BuiltinLintDiagnostics::BreakWithLabelAndLoop(span) => {
754+
if let Ok(code) = sess.source_map().span_to_snippet(span) {
755+
db.span_suggestion(span, "wrap this expression in parentheses", format!("({})", &code), Applicability::MachineApplicable);
756+
}
757+
}
753758
}
754759
// Rewrap `db`, and pass control to the user.
755760
decorate(LintDiagnosticBuilder::new(db));

compiler/rustc_lint_defs/src/builtin.rs

+30
Original file line numberDiff line numberDiff line change
@@ -2975,6 +2975,7 @@ declare_lint_pass! {
29752975
RUST_2021_PRELUDE_COLLISIONS,
29762976
RUST_2021_PREFIXES_INCOMPATIBLE_SYNTAX,
29772977
UNSUPPORTED_CALLING_CONVENTIONS,
2978+
BREAK_WITH_LABEL_AND_LOOP,
29782979
]
29792980
}
29802981

@@ -3348,3 +3349,32 @@ declare_lint! {
33483349
reference: "issue #00000 <https://github.com/rust-lang/rust/issues/00000>",
33493350
};
33503351
}
3352+
3353+
declare_lint! {
3354+
/// The `break_with_label_and_loop` lint detects labeled `break` expressions with
3355+
/// an unlabeled loop as their value expression.
3356+
///
3357+
/// ### Example
3358+
///
3359+
/// ```rust
3360+
/// 'label: loop {
3361+
/// break 'label loop { break 42; };
3362+
/// };
3363+
/// ```
3364+
///
3365+
/// {{produces}}
3366+
///
3367+
/// ### Explanation
3368+
///
3369+
/// In Rust, loops can have a label, and `break` expressions can refer to that label to
3370+
/// break out of specific loops (and not necessarily the innermost one). `break` expressions
3371+
/// can also carry a value expression, which can be another loop. A labeled `break` with an
3372+
/// unlabeled loop as its value expression is easy to confuse with an unlabeled break with
3373+
/// a labeled loop and is thus discouraged (but allowed for compatibility); use parentheses
3374+
/// around the loop expression to silence this warning. Unlabeled `break` expressions with
3375+
/// labeled loops yield a hard error, which can also be silenced by wrapping the expression
3376+
/// in parentheses.
3377+
pub BREAK_WITH_LABEL_AND_LOOP,
3378+
Warn,
3379+
"`break` expression with label and unlabeled loop as value expression"
3380+
}

compiler/rustc_lint_defs/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ pub enum BuiltinLintDiagnostics {
304304
OrPatternsBackCompat(Span, String),
305305
ReservedPrefix(Span),
306306
TrailingMacro(bool, Ident),
307+
BreakWithLabelAndLoop(Span),
307308
}
308309

309310
/// Lints that are buffered up early on in the `Session` before the

compiler/rustc_parse/src/parser/expr.rs

+51-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use rustc_ast::{AnonConst, BinOp, BinOpKind, FnDecl, FnRetTy, MacCall, Param, Ty
1515
use rustc_ast::{Arm, Async, BlockCheckMode, Expr, ExprKind, Label, Movability, RangeLimits};
1616
use rustc_ast_pretty::pprust;
1717
use rustc_errors::{Applicability, DiagnosticBuilder, PResult};
18+
use rustc_session::lint::builtin::BREAK_WITH_LABEL_AND_LOOP;
19+
use rustc_session::lint::BuiltinLintDiagnostics;
1820
use rustc_span::edition::LATEST_STABLE_EDITION;
1921
use rustc_span::source_map::{self, Span, Spanned};
2022
use rustc_span::symbol::{kw, sym, Ident, Symbol};
@@ -1375,14 +1377,59 @@ impl<'a> Parser<'a> {
13751377
self.maybe_recover_from_bad_qpath(expr, true)
13761378
}
13771379

1378-
/// Parse `"('label ":")? break expr?`.
1380+
/// Parse `"break" (('label (:? expr)?) | expr?)` with `"break"` token already eaten.
1381+
/// If the label is followed immediately by a `:` token, the label and `:` are
1382+
/// parsed as part of the expression (i.e. a labeled loop). The language team has
1383+
/// decided in #87026 to require parentheses as a visual aid to avoid confusion if
1384+
/// the break expression of an unlabeled break is a labeled loop (as in
1385+
/// `break 'lbl: loop {}`); a labeled break with an unlabeled loop as its value
1386+
/// expression only gets a warning for compatibility reasons; and a labeled break
1387+
/// with a labeled loop does not even get a warning because there is no ambiguity.
13791388
fn parse_break_expr(&mut self, attrs: AttrVec) -> PResult<'a, P<Expr>> {
13801389
let lo = self.prev_token.span;
1381-
let label = self.eat_label();
1382-
let kind = if self.token != token::OpenDelim(token::Brace)
1390+
let mut label = self.eat_label();
1391+
let kind = if label.is_some() && self.token == token::Colon {
1392+
// The value expression can be a labeled loop, see issue #86948, e.g.:
1393+
// `loop { break 'label: loop { break 'label 42; }; }`
1394+
let lexpr = self.parse_labeled_expr(label.take().unwrap(), AttrVec::new(), true)?;
1395+
self.struct_span_err(
1396+
lexpr.span,
1397+
"parentheses are required around this expression to avoid confusion with a labeled break expression",
1398+
)
1399+
.multipart_suggestion(
1400+
"wrap the expression in parentheses",
1401+
vec![
1402+
(lexpr.span.shrink_to_lo(), "(".to_string()),
1403+
(lexpr.span.shrink_to_hi(), ")".to_string()),
1404+
],
1405+
Applicability::MachineApplicable,
1406+
)
1407+
.emit();
1408+
Some(lexpr)
1409+
} else if self.token != token::OpenDelim(token::Brace)
13831410
|| !self.restrictions.contains(Restrictions::NO_STRUCT_LITERAL)
13841411
{
1385-
self.parse_expr_opt()?
1412+
let expr = self.parse_expr_opt()?;
1413+
if let Some(ref expr) = expr {
1414+
if label.is_some()
1415+
&& matches!(
1416+
expr.kind,
1417+
ExprKind::While(_, _, None)
1418+
| ExprKind::ForLoop(_, _, _, None)
1419+
| ExprKind::Loop(_, None)
1420+
| ExprKind::Block(_, None)
1421+
)
1422+
{
1423+
self.sess.buffer_lint_with_diagnostic(
1424+
BREAK_WITH_LABEL_AND_LOOP,
1425+
lo.to(expr.span),
1426+
ast::CRATE_NODE_ID,
1427+
"this labeled break expression is easy to confuse with an unlabeled break with a labeled value expression",
1428+
BuiltinLintDiagnostics::BreakWithLabelAndLoop(expr.span),
1429+
);
1430+
}
1431+
}
1432+
expr
13861433
} else {
13871434
None
13881435
};
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
1+
#![allow(unused, dead_code)]
2+
13
fn foo() -> u32 {
24
return 'label: loop { break 'label 42; };
35
}
46

57
fn bar() -> u32 {
68
loop { break 'label: loop { break 'label 42; }; }
7-
//~^ ERROR expected identifier, found keyword `loop`
8-
//~| ERROR expected type, found keyword `loop`
9+
//~^ ERROR: parentheses are required around this expression to avoid confusion
10+
//~| HELP: wrap the expression in parentheses
11+
}
12+
13+
fn baz() -> u32 {
14+
'label: loop {
15+
break 'label
16+
//~^ WARNING: this labeled break expression is easy to confuse with an unlabeled break
17+
loop { break 42; };
18+
//~^ HELP: wrap this expression in parentheses
19+
};
20+
21+
'label2: loop {
22+
break 'label2 'inner: loop { break 42; };
23+
// no warnings or errors here
24+
}
925
}
1026

1127
pub fn main() {
12-
foo();
28+
// Regression test for issue #86948, as resolved in #87026:
29+
let a = 'first_loop: loop {
30+
break 'first_loop 1;
31+
};
32+
let b = loop {
33+
break 'inner_loop: loop {
34+
//~^ ERROR: parentheses are required around this expression to avoid confusion
35+
//~| HELP: wrap the expression in parentheses
36+
break 'inner_loop 1;
37+
};
38+
};
1339
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,45 @@
1-
error: expected identifier, found keyword `loop`
2-
--> $DIR/lifetime_starts_expressions.rs:6:26
1+
error: parentheses are required around this expression to avoid confusion with a labeled break expression
2+
--> $DIR/lifetime_starts_expressions.rs:8:18
33
|
44
LL | loop { break 'label: loop { break 'label 42; }; }
5-
| ^^^^ expected identifier, found keyword
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66
|
7-
help: you can escape reserved keywords to use them as identifiers
7+
help: wrap the expression in parentheses
88
|
9-
LL | loop { break 'label: r#loop { break 'label 42; }; }
10-
| ^^^^^^
9+
LL | loop { break ('label: loop { break 'label 42; }); }
10+
| ^ ^
1111

12-
error: expected type, found keyword `loop`
13-
--> $DIR/lifetime_starts_expressions.rs:6:26
12+
error: parentheses are required around this expression to avoid confusion with a labeled break expression
13+
--> $DIR/lifetime_starts_expressions.rs:33:15
1414
|
15-
LL | loop { break 'label: loop { break 'label 42; }; }
16-
| - ^^^^ expected type
17-
| |
18-
| help: maybe write a path separator here: `::`
15+
LL | break 'inner_loop: loop {
16+
| _______________^
17+
LL | |
18+
LL | |
19+
LL | | break 'inner_loop 1;
20+
LL | | };
21+
| |_________^
22+
|
23+
help: wrap the expression in parentheses
24+
|
25+
LL | break ('inner_loop: loop {
26+
LL |
27+
LL |
28+
LL | break 'inner_loop 1;
29+
LL | });
30+
|
31+
32+
warning: this labeled break expression is easy to confuse with an unlabeled break with a labeled value expression
33+
--> $DIR/lifetime_starts_expressions.rs:15:9
34+
|
35+
LL | / break 'label
36+
LL | |
37+
LL | | loop { break 42; };
38+
| |_____________-----------------^
39+
| |
40+
| help: wrap this expression in parentheses: `(loop { break 42; })`
1941
|
20-
= note: `#![feature(type_ascription)]` lets you annotate an expression with a type: `<expr>: <type>`
42+
= note: `#[warn(break_with_label_and_loop)]` on by default
2143

22-
error: aborting due to 2 previous errors
44+
error: aborting due to 2 previous errors; 1 warning emitted
2345

0 commit comments

Comments
 (0)