From 00942381575bc6c081835dbb38f68ce9603c032f Mon Sep 17 00:00:00 2001
From: sjwang05 <63834813+sjwang05@users.noreply.github.com>
Date: Thu, 9 Nov 2023 18:46:45 -0800
Subject: [PATCH] Catch stray { in let-chains

---
 compiler/rustc_parse/src/lexer/tokentrees.rs | 34 +++++++++++++++-
 tests/ui/parser/brace-in-let-chain.rs        | 28 +++++++++++++
 tests/ui/parser/brace-in-let-chain.stderr    | 42 ++++++++++++++++++++
 3 files changed, 103 insertions(+), 1 deletion(-)
 create mode 100644 tests/ui/parser/brace-in-let-chain.rs
 create mode 100644 tests/ui/parser/brace-in-let-chain.stderr

diff --git a/compiler/rustc_parse/src/lexer/tokentrees.rs b/compiler/rustc_parse/src/lexer/tokentrees.rs
index 31d91fe80bd..7aa4ac7c4cb 100644
--- a/compiler/rustc_parse/src/lexer/tokentrees.rs
+++ b/compiler/rustc_parse/src/lexer/tokentrees.rs
@@ -5,7 +5,8 @@ use super::{StringReader, UnmatchedDelim};
 use rustc_ast::token::{self, Delimiter, Token};
 use rustc_ast::tokenstream::{DelimSpan, Spacing, TokenStream, TokenTree};
 use rustc_ast_pretty::pprust::token_to_string;
-use rustc_errors::PErr;
+use rustc_errors::{Applicability, PErr};
+use rustc_span::symbol::kw;
 
 pub(super) struct TokenTreesReader<'a> {
     string_reader: StringReader<'a>,
@@ -121,9 +122,40 @@ impl<'a> TokenTreesReader<'a> {
             // out instead of complaining about the unclosed delims.
             let mut parser = crate::stream_to_parser(self.string_reader.sess, tts, None);
             let mut diff_errs = vec![];
+            // Suggest removing a `{` we think appears in an `if`/`while` condition
+            // We want to suggest removing a `{` only if we think we're in an `if`/`while` condition, but
+            // we have no way of tracking this in the lexer itself, so we piggyback on the parser
+            let mut in_cond = false;
             while parser.token != token::Eof {
                 if let Err(diff_err) = parser.err_diff_marker() {
                     diff_errs.push(diff_err);
+                } else if parser.token.is_keyword(kw::If) {
+                    in_cond = true;
+                } else if parser.token == token::CloseDelim(Delimiter::Brace) {
+                    in_cond = false;
+                } else if in_cond && parser.token == token::OpenDelim(Delimiter::Brace) {
+                    // Store the `&&` and `let` to use their spans later when creating the diagnostic
+                    let maybe_andand = parser.look_ahead(1, |t| t.clone());
+                    let maybe_let = parser.look_ahead(2, |t| t.clone());
+                    if maybe_andand == token::OpenDelim(Delimiter::Brace) {
+                        // This might be the beginning of the `if`/`while` body (i.e., the end of the condition)
+                        in_cond = false;
+                    } else if maybe_andand == token::AndAnd && maybe_let.is_keyword(kw::Let) {
+                        let mut err = parser.struct_span_err(
+                            parser.token.span,
+                            "found a `{` in the middle of a let-chain",
+                        );
+                        err.span_suggestion(
+                            parser.token.span,
+                            "consider removing this brace to parse the `let` as part of the same chain",
+                            "", Applicability::MachineApplicable
+                        );
+                        err.span_note(
+                            maybe_andand.span.to(maybe_let.span),
+                            "you might have meant to continue the let-chain here",
+                        );
+                        errs.push(err);
+                    }
                 }
                 parser.bump();
             }
diff --git a/tests/ui/parser/brace-in-let-chain.rs b/tests/ui/parser/brace-in-let-chain.rs
new file mode 100644
index 00000000000..4dc13fb3847
--- /dev/null
+++ b/tests/ui/parser/brace-in-let-chain.rs
@@ -0,0 +1,28 @@
+// issue #117766
+
+#![feature(let_chains)]
+fn main() {
+    if let () = ()
+        && let () = () { //~ERROR: found a `{` in the middle of a let-chain
+        && let () = ()
+    {
+    }
+}
+
+fn foo() {
+    {
+    && let () = ()
+}
+
+fn bar() {
+    if false {}
+    {
+        && let () = ()
+}
+
+fn baz() {
+    if false {
+        {
+            && let () = ()
+    }
+} //~ERROR: this file contains an unclosed delimiter
diff --git a/tests/ui/parser/brace-in-let-chain.stderr b/tests/ui/parser/brace-in-let-chain.stderr
new file mode 100644
index 00000000000..7550d5c43cf
--- /dev/null
+++ b/tests/ui/parser/brace-in-let-chain.stderr
@@ -0,0 +1,42 @@
+error: this file contains an unclosed delimiter
+  --> $DIR/brace-in-let-chain.rs:28:54
+   |
+LL | fn main() {
+   |           - unclosed delimiter
+...
+LL | fn foo() {
+   |          - unclosed delimiter
+...
+LL | fn bar() {
+   |          - unclosed delimiter
+...
+LL | fn baz() {
+   |          - unclosed delimiter
+LL |     if false {
+LL |         {
+   |         - this delimiter might not be properly closed...
+LL |             && let () = ()
+LL |     }
+   |     - ...as it matches this but it has different indentation
+LL | }
+   |                                                      ^
+
+error: found a `{` in the middle of a let-chain
+  --> $DIR/brace-in-let-chain.rs:6:24
+   |
+LL |         && let () = () {
+   |                        ^
+   |
+note: you might have meant to continue the let-chain here
+  --> $DIR/brace-in-let-chain.rs:7:9
+   |
+LL |         && let () = ()
+   |         ^^^^^^
+help: consider removing this brace to parse the `let` as part of the same chain
+   |
+LL -         && let () = () {
+LL +         && let () = ()
+   |
+
+error: aborting due to 2 previous errors
+