Repository: dtolnay/syn
Pull request #1692 (2024-07-02 02:47:02)

Parenthesize break values containing leading label
Previously, it was possible for ExprBreak's ToTokens impl to produce `break 'inner: loop { break 'inner 1 } + 1` which is not valid Rust syntax.

Same bug in rustc: https://github.com/rust-lang/rust/pull/126883


Patch:
diff --git a/src/classify.rs b/src/classify.rs
index 1b0ff300..cc176d06 100644
--- a/src/classify.rs
+++ b/src/classify.rs
@@ -248,6 +248,59 @@ pub(crate) fn confusable_with_adjacent_lt(mut expr: &Expr) -> bool {
     }
 }
 
+/// Whether the expression's first token is the label of a loop/block.
+#[cfg(all(feature = "printing", feature = "full"))]
+pub(crate) fn expr_leading_label(mut expr: &Expr) -> bool {
+    loop {
+        match expr {
+            Expr::Block(e) => return e.label.is_some(),
+            Expr::ForLoop(e) => return e.label.is_some(),
+            Expr::Loop(e) => return e.label.is_some(),
+            Expr::While(e) => return e.label.is_some(),
+
+            Expr::Assign(e) => expr = &e.left,
+            Expr::Await(e) => expr = &e.base,
+            Expr::Binary(e) => expr = &e.left,
+            Expr::Call(e) => expr = &e.func,
+            Expr::Cast(e) => expr = &e.expr,
+            Expr::Field(e) => expr = &e.base,
+            Expr::Index(e) => expr = &e.expr,
+            Expr::MethodCall(e) => expr = &e.receiver,
+            Expr::Range(e) => match &e.start {
+                Some(start) => expr = start,
+                None => return false,
+            },
+            Expr::Try(e) => expr = &e.expr,
+
+            Expr::Array(_)
+            | Expr::Async(_)
+            | Expr::Break(_)
+            | Expr::Closure(_)
+            | Expr::Const(_)
+            | Expr::Continue(_)
+            | Expr::Group(_)
+            | Expr::If(_)
+            | Expr::Infer(_)
+            | Expr::Let(_)
+            | Expr::Lit(_)
+            | Expr::Macro(_)
+            | Expr::Match(_)
+            | Expr::Paren(_)
+            | Expr::Path(_)
+            | Expr::Reference(_)
+            | Expr::Repeat(_)
+            | Expr::Return(_)
+            | Expr::Struct(_)
+            | Expr::TryBlock(_)
+            | Expr::Tuple(_)
+            | Expr::Unary(_)
+            | Expr::Unsafe(_)
+            | Expr::Verbatim(_)
+            | Expr::Yield(_) => return false,
+        }
+    }
+}
+
 /// Whether the expression's last token is `}`.
 #[cfg(feature = "full")]
 pub(crate) fn expr_trailing_brace(mut expr: &Expr) -> bool {
diff --git a/src/expr.rs b/src/expr.rs
index 30bf9c76..e684068c 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -3286,8 +3286,15 @@ pub(crate) mod printing {
         outer_attrs_to_tokens(&e.attrs, tokens);
         e.break_token.to_tokens(tokens);
         e.label.to_tokens(tokens);
-        if let Some(expr) = &e.expr {
-            print_expr(expr, tokens, fixup.subsequent_subexpression());
+        if let Some(value) = &e.expr {
+            print_subexpression(
+                value,
+                // Parenthesize `break 'inner: loop { break 'inner 1 } + 1`
+                //                     ^---------------------------------^
+                e.label.is_none() && classify::expr_leading_label(value),
+                tokens,
+                fixup.subsequent_subexpression(),
+            );
         }
     }
 


Test patch:
diff --git a/tests/test_expr.rs b/tests/test_expr.rs
index 961e3c4e..364386c5 100644
--- a/tests/test_expr.rs
+++ b/tests/test_expr.rs
@@ -672,6 +672,7 @@ fn test_fixup() {
         quote! { match m { _ => ({}) - 1 } },
         quote! { if let _ = (a && b) && c {} },
         quote! { if let _ = (S {}) {} },
+        quote! { break ('a: loop { break 'a 1 } + 1) },
     ] {
         let original: Expr = syn::parse2(tokens).unwrap();