about summary refs log tree commit diff
diff options
context:
space:
mode:
authorZachary S <zasample18+github@gmail.com>2025-03-17 00:25:15 -0500
committerZachary S <zasample18+github@gmail.com>2025-03-17 01:59:37 -0500
commitf478853f425fc0207add653b48c49c937acaa94e (patch)
tree3f7fe7d662114e8b9f812c37cfe967072858265d
parent227690a258492c84ae9927d18289208d0180e62f (diff)
downloadrust-f478853f425fc0207add653b48c49c937acaa94e.tar.gz
rust-f478853f425fc0207add653b48c49c937acaa94e.zip
If a label is placed on the block of a loop instead of the header, suggest moving it to the header.
-rw-r--r--compiler/rustc_parse/src/parser/diagnostics.rs29
-rw-r--r--compiler/rustc_parse/src/parser/expr.rs36
-rw-r--r--compiler/rustc_parse/src/parser/item.rs2
-rw-r--r--compiler/rustc_parse/src/parser/mod.rs2
-rw-r--r--compiler/rustc_parse/src/parser/stmt.rs22
-rw-r--r--tests/ui/loops/label-on-block-suggest-move.rs90
-rw-r--r--tests/ui/loops/label-on-block-suggest-move.stderr140
7 files changed, 296 insertions, 25 deletions
diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs
index 716ababb008..c1cca1186af 100644
--- a/compiler/rustc_parse/src/parser/diagnostics.rs
+++ b/compiler/rustc_parse/src/parser/diagnostics.rs
@@ -2874,7 +2874,12 @@ impl<'a> Parser<'a> {
         first_pat
     }
 
-    pub(crate) fn maybe_recover_unexpected_block_label(&mut self) -> bool {
+    /// If `loop_header` is `Some` and an unexpected block label is encountered,
+    /// it is suggested to be moved just before `loop_header`, else it is suggested to be removed.
+    pub(crate) fn maybe_recover_unexpected_block_label(
+        &mut self,
+        loop_header: Option<Span>,
+    ) -> bool {
         // Check for `'a : {`
         if !(self.check_lifetime()
             && self.look_ahead(1, |t| *t == token::Colon)
@@ -2885,16 +2890,28 @@ impl<'a> Parser<'a> {
         let label = self.eat_label().expect("just checked if a label exists");
         self.bump(); // eat `:`
         let span = label.ident.span.to(self.prev_token.span);
-        self.dcx()
+        let mut diag = self
+            .dcx()
             .struct_span_err(span, "block label not supported here")
-            .with_span_label(span, "not supported here")
-            .with_tool_only_span_suggestion(
+            .with_span_label(span, "not supported here");
+        if let Some(loop_header) = loop_header {
+            diag.multipart_suggestion(
+                "if you meant to label the loop, move this label before the loop",
+                vec![
+                    (label.ident.span.until(self.token.span), String::from("")),
+                    (loop_header.shrink_to_lo(), format!("{}: ", label.ident)),
+                ],
+                Applicability::MachineApplicable,
+            );
+        } else {
+            diag.tool_only_span_suggestion(
                 label.ident.span.until(self.token.span),
                 "remove this block label",
                 "",
                 Applicability::MachineApplicable,
-            )
-            .emit();
+            );
+        }
+        diag.emit();
         true
     }
 
diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs
index 0774324eae7..cd931888fba 100644
--- a/compiler/rustc_parse/src/parser/expr.rs
+++ b/compiler/rustc_parse/src/parser/expr.rs
@@ -2286,7 +2286,7 @@ impl<'a> Parser<'a> {
             });
         }
 
-        let (attrs, blk) = self.parse_block_common(lo, blk_mode, true)?;
+        let (attrs, blk) = self.parse_block_common(lo, blk_mode, true, None)?;
         Ok(self.mk_expr_with_attrs(blk.span, ExprKind::Block(blk, opt_label), attrs))
     }
 
@@ -2851,7 +2851,11 @@ impl<'a> Parser<'a> {
             ));
         }
 
-        let (attrs, loop_block) = self.parse_inner_attrs_and_block()?;
+        let (attrs, loop_block) = self.parse_inner_attrs_and_block(
+            // Only suggest moving erroneous block label to the loop header
+            // if there is not already a label there
+            opt_label.is_none().then_some(lo),
+        )?;
 
         let kind = ExprKind::ForLoop { pat, iter: expr, body: loop_block, label: opt_label, kind };
 
@@ -2894,11 +2898,17 @@ impl<'a> Parser<'a> {
             err.span_label(lo, "while parsing the condition of this `while` expression");
             err
         })?;
-        let (attrs, body) = self.parse_inner_attrs_and_block().map_err(|mut err| {
-            err.span_label(lo, "while parsing the body of this `while` expression");
-            err.span_label(cond.span, "this `while` condition successfully parsed");
-            err
-        })?;
+        let (attrs, body) = self
+            .parse_inner_attrs_and_block(
+                // Only suggest moving erroneous block label to the loop header
+                // if there is not already a label there
+                opt_label.is_none().then_some(lo),
+            )
+            .map_err(|mut err| {
+                err.span_label(lo, "while parsing the body of this `while` expression");
+                err.span_label(cond.span, "this `while` condition successfully parsed");
+                err
+            })?;
 
         self.recover_loop_else("while", lo)?;
 
@@ -2912,7 +2922,11 @@ impl<'a> Parser<'a> {
     /// Parses `loop { ... }` (`loop` token already eaten).
     fn parse_expr_loop(&mut self, opt_label: Option<Label>, lo: Span) -> PResult<'a, P<Expr>> {
         let loop_span = self.prev_token.span;
-        let (attrs, body) = self.parse_inner_attrs_and_block()?;
+        let (attrs, body) = self.parse_inner_attrs_and_block(
+            // Only suggest moving erroneous block label to the loop header
+            // if there is not already a label there
+            opt_label.is_none().then_some(lo),
+        )?;
         self.recover_loop_else("loop", lo)?;
         Ok(self.mk_expr_with_attrs(
             lo.to(self.prev_token.span),
@@ -2962,7 +2976,7 @@ impl<'a> Parser<'a> {
                     Applicability::MaybeIncorrect, // speculative
                 );
             }
-            if self.maybe_recover_unexpected_block_label() {
+            if self.maybe_recover_unexpected_block_label(None) {
                 e.cancel();
                 self.bump();
             } else {
@@ -3376,7 +3390,7 @@ impl<'a> Parser<'a> {
 
     /// Parses a `try {...}` expression (`try` token already eaten).
     fn parse_try_block(&mut self, span_lo: Span) -> PResult<'a, P<Expr>> {
-        let (attrs, body) = self.parse_inner_attrs_and_block()?;
+        let (attrs, body) = self.parse_inner_attrs_and_block(None)?;
         if self.eat_keyword(exp!(Catch)) {
             Err(self.dcx().create_err(errors::CatchAfterTry { span: self.prev_token.span }))
         } else {
@@ -3424,7 +3438,7 @@ impl<'a> Parser<'a> {
         }
         let capture_clause = self.parse_capture_clause()?;
         let decl_span = lo.to(self.prev_token.span);
-        let (attrs, body) = self.parse_inner_attrs_and_block()?;
+        let (attrs, body) = self.parse_inner_attrs_and_block(None)?;
         let kind = ExprKind::Gen(capture_clause, body, kind, decl_span);
         Ok(self.mk_expr_with_attrs(lo.to(self.prev_token.span), kind, attrs))
     }
diff --git a/compiler/rustc_parse/src/parser/item.rs b/compiler/rustc_parse/src/parser/item.rs
index 2388463ecfc..f4df4044dd2 100644
--- a/compiler/rustc_parse/src/parser/item.rs
+++ b/compiler/rustc_parse/src/parser/item.rs
@@ -2529,7 +2529,7 @@ impl<'a> Parser<'a> {
             *sig_hi = self.prev_token.span;
             (AttrVec::new(), None)
         } else if self.check(exp!(OpenBrace)) || self.token.is_whole_block() {
-            self.parse_block_common(self.token.span, BlockCheckMode::Default, false)
+            self.parse_block_common(self.token.span, BlockCheckMode::Default, false, None)
                 .map(|(attrs, body)| (attrs, Some(body)))?
         } else if self.token == token::Eq {
             // Recover `fn foo() = $expr;`.
diff --git a/compiler/rustc_parse/src/parser/mod.rs b/compiler/rustc_parse/src/parser/mod.rs
index 323c0c1d6d0..a79b4048288 100644
--- a/compiler/rustc_parse/src/parser/mod.rs
+++ b/compiler/rustc_parse/src/parser/mod.rs
@@ -1374,7 +1374,7 @@ impl<'a> Parser<'a> {
             self.psess.gated_spans.gate(sym::inline_const_pat, span);
         }
         self.expect_keyword(exp!(Const))?;
-        let (attrs, blk) = self.parse_inner_attrs_and_block()?;
+        let (attrs, blk) = self.parse_inner_attrs_and_block(None)?;
         let anon_const = AnonConst {
             id: DUMMY_NODE_ID,
             value: self.mk_expr(blk.span, ExprKind::Block(blk, None)),
diff --git a/compiler/rustc_parse/src/parser/stmt.rs b/compiler/rustc_parse/src/parser/stmt.rs
index 0896bd88b4c..0fe247078d5 100644
--- a/compiler/rustc_parse/src/parser/stmt.rs
+++ b/compiler/rustc_parse/src/parser/stmt.rs
@@ -482,7 +482,7 @@ impl<'a> Parser<'a> {
 
     /// Parses a block. No inner attributes are allowed.
     pub fn parse_block(&mut self) -> PResult<'a, P<Block>> {
-        let (attrs, block) = self.parse_inner_attrs_and_block()?;
+        let (attrs, block) = self.parse_inner_attrs_and_block(None)?;
         if let [.., last] = &*attrs {
             let suggest_to_outer = match &last.kind {
                 ast::AttrKind::Normal(attr) => attr.item.is_valid_for_outer_style(),
@@ -660,22 +660,32 @@ impl<'a> Parser<'a> {
         Err(self.error_block_no_opening_brace_msg(Cow::from(msg)))
     }
 
-    /// Parses a block. Inner attributes are allowed.
-    pub(super) fn parse_inner_attrs_and_block(&mut self) -> PResult<'a, (AttrVec, P<Block>)> {
-        self.parse_block_common(self.token.span, BlockCheckMode::Default, true)
+    /// Parses a block. Inner attributes are allowed, block labels are not.
+    ///
+    /// If `loop_header` is `Some` and an unexpected block label is encountered,
+    /// it is suggested to be moved just before `loop_header`, else it is suggested to be removed.
+    pub(super) fn parse_inner_attrs_and_block(
+        &mut self,
+        loop_header: Option<Span>,
+    ) -> PResult<'a, (AttrVec, P<Block>)> {
+        self.parse_block_common(self.token.span, BlockCheckMode::Default, true, loop_header)
     }
 
-    /// Parses a block. Inner attributes are allowed.
+    /// Parses a block. Inner attributes are allowed, block labels are not.
+    ///
+    /// If `loop_header` is `Some` and an unexpected block label is encountered,
+    /// it is suggested to be moved just before `loop_header`, else it is suggested to be removed.
     pub(super) fn parse_block_common(
         &mut self,
         lo: Span,
         blk_mode: BlockCheckMode,
         can_be_struct_literal: bool,
+        loop_header: Option<Span>,
     ) -> PResult<'a, (AttrVec, P<Block>)> {
         maybe_whole!(self, NtBlock, |block| (AttrVec::new(), block));
 
         let maybe_ident = self.prev_token.clone();
-        self.maybe_recover_unexpected_block_label();
+        self.maybe_recover_unexpected_block_label(loop_header);
         if !self.eat(exp!(OpenBrace)) {
             return self.error_block_no_opening_brace();
         }
diff --git a/tests/ui/loops/label-on-block-suggest-move.rs b/tests/ui/loops/label-on-block-suggest-move.rs
new file mode 100644
index 00000000000..656034cd0e9
--- /dev/null
+++ b/tests/ui/loops/label-on-block-suggest-move.rs
@@ -0,0 +1,90 @@
+// see https://github.com/rust-lang/rust/issues/138585
+#![allow(break_with_label_and_loop)] // doesn't work locally
+
+fn main() {
+    loop 'a: {}
+    //~^ ERROR: block label not supported here
+    //~| HELP: if you meant to label the loop, move this label before the loop
+    while false 'a: {}
+    //~^ ERROR: block label not supported here
+    //~| HELP: if you meant to label the loop, move this label before the loop
+    for i in [0] 'a: {}
+    //~^ ERROR: block label not supported here
+    //~| HELP: if you meant to label the loop, move this label before the loop
+    'a: loop {
+        // first block is parsed as the break expr's value with or without parens
+        while break 'a 'b: {} 'c: {}
+        //~^ ERROR: block label not supported here
+        //~| HELP: if you meant to label the loop, move this label before the loop
+        while break 'a ('b: {}) 'c: {}
+        //~^ ERROR: block label not supported here
+        //~| HELP: if you meant to label the loop, move this label before the loop
+
+        // without the parens, the first block is parsed as the while-loop's body
+        // (see the 'no errors' section)
+        // #[allow(break_with_label_and_loop)] (doesn't work locally)
+        while (break 'a {}) 'c: {}
+        //~^ ERROR: block label not supported here
+        //~| HELP: if you meant to label the loop, move this label before the loop
+    }
+
+    // do not suggest moving the label if there is already a label on the loop
+    'a: loop 'b: {}
+    //~^ ERROR: block label not supported here
+    //~| HELP: remove this block label
+    'a: while false 'b: {}
+    //~^ ERROR: block label not supported here
+    //~| HELP: remove this block label
+    'a: for i in [0] 'b: {}
+    //~^ ERROR: block label not supported here
+    //~| HELP: remove this block label
+    'a: loop {
+        // first block is parsed as the break expr's value with or without parens
+        'd: while break 'a 'b: {} 'c: {}
+        //~^ ERROR: block label not supported here
+        //~| HELP: remove this block label
+        'd: while break 'a ('b: {}) 'c: {}
+        //~^ ERROR: block label not supported here
+        //~| HELP: remove this block label
+
+        // without the parens, the first block is parsed as the while-loop's body
+        // (see the 'no errors' section)
+        // #[allow(break_with_label_and_loop)] (doesn't work locally)
+        'd: while (break 'a {}) 'c: {}
+        //~^ ERROR: block label not supported here
+        //~| HELP: remove this block label
+    }
+
+    // no errors
+    loop { 'a: {} }
+    'a: loop { 'b: {} }
+    while false { 'a: {} }
+    'a: while false { 'b: {} }
+    for i in [0] { 'a: {} }
+    'a: for i in [0] { 'b: {} }
+    'a: {}
+    'a: { 'b: {} }
+    'a: loop {
+        // first block is parsed as the break expr's value if it is a labeled block
+        while break 'a 'b: {} {}
+        'd: while break 'a 'b: {} {}
+        while break 'a ('b: {}) {}
+        'd: while break 'a ('b: {}) {}
+        // first block is parsed as the while-loop's body if it has no label
+        // (the break expr is parsed as having no value),
+        // so the second block is a normal stmt-block, and the label is allowed
+        while break 'a {} 'c: {}
+        while break 'a {} {}
+        'd: while break 'a {} 'c: {}
+        'd: while break 'a {} {}
+    }
+
+    // unrelated errors that should not be affected
+    'a: 'b: {}
+    //~^ ERROR: expected `while`, `for`, `loop` or `{` after a label
+    //~| HELP: consider removing the label
+    loop { while break 'b: {} {} }
+    //~^ ERROR: parentheses are required around this expression to avoid confusion with a labeled break expression
+    //~| HELP: wrap the expression in parentheses
+    //~| ERROR: `break` or `continue` with no label in the condition of a `while` loop [E0590]
+}
diff --git a/tests/ui/loops/label-on-block-suggest-move.stderr b/tests/ui/loops/label-on-block-suggest-move.stderr
new file mode 100644
index 00000000000..66866703ca6
--- /dev/null
+++ b/tests/ui/loops/label-on-block-suggest-move.stderr
@@ -0,0 +1,140 @@
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:5:10
+   |
+LL |     loop 'a: {}
+   |          ^^^ not supported here
+   |
+help: if you meant to label the loop, move this label before the loop
+   |
+LL -     loop 'a: {}
+LL +     'a: loop {}
+   |
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:8:17
+   |
+LL |     while false 'a: {}
+   |                 ^^^ not supported here
+   |
+help: if you meant to label the loop, move this label before the loop
+   |
+LL -     while false 'a: {}
+LL +     'a: while false {}
+   |
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:11:18
+   |
+LL |     for i in [0] 'a: {}
+   |                  ^^^ not supported here
+   |
+help: if you meant to label the loop, move this label before the loop
+   |
+LL -     for i in [0] 'a: {}
+LL +     'a: for i in [0] {}
+   |
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:16:31
+   |
+LL |         while break 'a 'b: {} 'c: {}
+   |                               ^^^ not supported here
+   |
+help: if you meant to label the loop, move this label before the loop
+   |
+LL -         while break 'a 'b: {} 'c: {}
+LL +         'c: while break 'a 'b: {} {}
+   |
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:19:33
+   |
+LL |         while break 'a ('b: {}) 'c: {}
+   |                                 ^^^ not supported here
+   |
+help: if you meant to label the loop, move this label before the loop
+   |
+LL -         while break 'a ('b: {}) 'c: {}
+LL +         'c: while break 'a ('b: {}) {}
+   |
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:26:29
+   |
+LL |         while (break 'a {}) 'c: {}
+   |                             ^^^ not supported here
+   |
+help: if you meant to label the loop, move this label before the loop
+   |
+LL -         while (break 'a {}) 'c: {}
+LL +         'c: while (break 'a {}) {}
+   |
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:32:14
+   |
+LL |     'a: loop 'b: {}
+   |              ^^^ not supported here
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:35:21
+   |
+LL |     'a: while false 'b: {}
+   |                     ^^^ not supported here
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:38:22
+   |
+LL |     'a: for i in [0] 'b: {}
+   |                      ^^^ not supported here
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:43:35
+   |
+LL |         'd: while break 'a 'b: {} 'c: {}
+   |                                   ^^^ not supported here
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:46:37
+   |
+LL |         'd: while break 'a ('b: {}) 'c: {}
+   |                                     ^^^ not supported here
+
+error: block label not supported here
+  --> $DIR/label-on-block-suggest-move.rs:53:33
+   |
+LL |         'd: while (break 'a {}) 'c: {}
+   |                                 ^^^ not supported here
+
+error: expected `while`, `for`, `loop` or `{` after a label
+  --> $DIR/label-on-block-suggest-move.rs:83:9
+   |
+LL |     'a: 'b: {}
+   |         ^^ expected `while`, `for`, `loop` or `{` after a label
+   |
+help: consider removing the label
+   |
+LL -     'a: 'b: {}
+LL +     'b: {}
+   |
+
+error: parentheses are required around this expression to avoid confusion with a labeled break expression
+  --> $DIR/label-on-block-suggest-move.rs:86:24
+   |
+LL |     loop { while break 'b: {} {} }
+   |                        ^^^^^^
+   |
+help: wrap the expression in parentheses
+   |
+LL |     loop { while break ('b: {}) {} }
+   |                        +      +
+
+error[E0590]: `break` or `continue` with no label in the condition of a `while` loop
+  --> $DIR/label-on-block-suggest-move.rs:86:18
+   |
+LL |     loop { while break 'b: {} {} }
+   |                  ^^^^^^^^^^^^ unlabeled `break` in the condition of a `while` loop
+
+error: aborting due to 15 previous errors
+
+For more information about this error, try `rustc --explain E0590`.