about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDeadbeef <ent3rm4n@gmail.com>2025-04-12 15:53:46 +0000
committerDeadbeef <ent3rm4n@gmail.com>2025-05-05 23:10:08 +0800
commit662182637e100642d1e5de7cb193da837a14ae48 (patch)
tree28e183216387d8d8cd939404a0c2fc01efaf450f
parent0c33fe2c3d3eecadd17a84b110bb067288a64f1c (diff)
downloadrust-662182637e100642d1e5de7cb193da837a14ae48.tar.gz
rust-662182637e100642d1e5de7cb193da837a14ae48.zip
Implement RFC 3503: frontmatters
Supercedes #137193
-rw-r--r--compiler/rustc_ast_passes/src/feature_gate.rs1
-rw-r--r--compiler/rustc_feature/src/unstable.rs2
-rw-r--r--compiler/rustc_lexer/src/cursor.rs14
-rw-r--r--compiler/rustc_lexer/src/lib.rs155
-rw-r--r--compiler/rustc_lexer/src/tests.rs2
-rw-r--r--compiler/rustc_parse/messages.ftl13
-rw-r--r--compiler/rustc_parse/src/errors.rs55
-rw-r--r--compiler/rustc_parse/src/lexer/mod.rs106
-rw-r--r--compiler/rustc_span/src/symbol.rs1
-rw-r--r--src/doc/unstable-book/src/language-features/arbitrary-self-types.md2
-rw-r--r--src/doc/unstable-book/src/language-features/f128.md2
-rw-r--r--src/doc/unstable-book/src/language-features/f16.md2
-rw-r--r--src/doc/unstable-book/src/language-features/frontmatter.md25
-rw-r--r--src/librustdoc/html/highlight.rs6
-rw-r--r--src/tools/rust-analyzer/crates/parser/src/lexed_str.rs9
-rw-r--r--src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs4
-rw-r--r--src/tools/rust-analyzer/crates/syntax/rust.ungram1
-rw-r--r--src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs4
-rw-r--r--src/tools/rust-analyzer/xtask/src/codegen/grammar.rs1
-rw-r--r--tests/ui/feature-gates/feature-gate-frontmatter.rs5
-rw-r--r--tests/ui/feature-gates/feature-gate-frontmatter.stderr15
-rw-r--r--tests/ui/frontmatter/auxiliary/lib.rs6
-rw-r--r--tests/ui/frontmatter/auxiliary/makro.rs8
-rw-r--r--tests/ui/frontmatter/dot-in-infostring-leading.rs9
-rw-r--r--tests/ui/frontmatter/dot-in-infostring-leading.stderr10
-rw-r--r--tests/ui/frontmatter/dot-in-infostring-non-leading.rs9
-rw-r--r--tests/ui/frontmatter/escape.rs14
-rw-r--r--tests/ui/frontmatter/extra-after-end.rs7
-rw-r--r--tests/ui/frontmatter/extra-after-end.stderr8
-rw-r--r--tests/ui/frontmatter/frontmatter-after-tokens.rs10
-rw-r--r--tests/ui/frontmatter/frontmatter-after-tokens.stderr10
-rw-r--r--tests/ui/frontmatter/frontmatter-non-lexible-tokens.rs12
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-1.rs10
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-1.stderr26
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-2.rs15
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-2.stderr26
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-3.rs16
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-4.rs9
-rw-r--r--tests/ui/frontmatter/included-frontmatter.rs12
-rw-r--r--tests/ui/frontmatter/infostring-fail.rs9
-rw-r--r--tests/ui/frontmatter/infostring-fail.stderr10
-rw-r--r--tests/ui/frontmatter/mismatch-1.rs10
-rw-r--r--tests/ui/frontmatter/mismatch-1.stderr16
-rw-r--r--tests/ui/frontmatter/mismatch-2.rs8
-rw-r--r--tests/ui/frontmatter/mismatch-2.stderr22
-rw-r--r--tests/ui/frontmatter/multifrontmatter-2.rs12
-rw-r--r--tests/ui/frontmatter/multifrontmatter-2.stderr22
-rw-r--r--tests/ui/frontmatter/multifrontmatter.rs13
-rw-r--r--tests/ui/frontmatter/multifrontmatter.stderr10
-rw-r--r--tests/ui/frontmatter/proc-macro-observer.rs12
-rw-r--r--tests/ui/frontmatter/shebang.rs13
-rw-r--r--tests/ui/frontmatter/unclosed-1.rs10
-rw-r--r--tests/ui/frontmatter/unclosed-1.stderr16
-rw-r--r--tests/ui/frontmatter/unclosed-2.rs15
-rw-r--r--tests/ui/frontmatter/unclosed-2.stderr31
-rw-r--r--tests/ui/frontmatter/unclosed-3.rs16
-rw-r--r--tests/ui/frontmatter/unclosed-3.stderr41
-rw-r--r--tests/ui/frontmatter/unclosed-4.rs9
-rw-r--r--tests/ui/frontmatter/unclosed-4.stderr16
-rw-r--r--tests/ui/frontmatter/unclosed-5.rs10
-rw-r--r--tests/ui/frontmatter/unclosed-5.stderr29
61 files changed, 970 insertions, 22 deletions
diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs
index e312f15f05b..915613a3913 100644
--- a/compiler/rustc_ast_passes/src/feature_gate.rs
+++ b/compiler/rustc_ast_passes/src/feature_gate.rs
@@ -514,6 +514,7 @@ pub fn check_crate(krate: &ast::Crate, sess: &Session, features: &Features) {
     gate_all!(contracts_internals, "contract internal machinery is for internal use only");
     gate_all!(where_clause_attrs, "attributes in `where` clause are unstable");
     gate_all!(super_let, "`super let` is experimental");
+    gate_all!(frontmatter, "frontmatters are experimental");
 
     if !visitor.features.never_patterns() {
         if let Some(spans) = spans.get(&sym::never_patterns) {
diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs
index 75e09cacb1f..f6fddfb4d67 100644
--- a/compiler/rustc_feature/src/unstable.rs
+++ b/compiler/rustc_feature/src/unstable.rs
@@ -506,6 +506,8 @@ declare_features! (
     (incomplete, fn_delegation, "1.76.0", Some(118212)),
     /// Allows impls for the Freeze trait.
     (internal, freeze_impls, "1.78.0", Some(121675)),
+    /// Frontmatter `---` blocks for use by external tools.
+    (unstable, frontmatter, "CURRENT_RUSTC_VERSION", Some(136889)),
     /// Allows defining gen blocks and `gen fn`.
     (unstable, gen_blocks, "1.75.0", Some(117078)),
     /// Infer generic args for both consts and types.
diff --git a/compiler/rustc_lexer/src/cursor.rs b/compiler/rustc_lexer/src/cursor.rs
index e0e3bd0e30b..526693d3de1 100644
--- a/compiler/rustc_lexer/src/cursor.rs
+++ b/compiler/rustc_lexer/src/cursor.rs
@@ -1,5 +1,10 @@
 use std::str::Chars;
 
+pub enum FrontmatterAllowed {
+    Yes,
+    No,
+}
+
 /// Peekable iterator over a char sequence.
 ///
 /// Next characters can be peeked via `first` method,
@@ -8,6 +13,7 @@ pub struct Cursor<'a> {
     len_remaining: usize,
     /// Iterator over chars. Slightly faster than a &str.
     chars: Chars<'a>,
+    pub(crate) frontmatter_allowed: FrontmatterAllowed,
     #[cfg(debug_assertions)]
     prev: char,
 }
@@ -15,10 +21,11 @@ pub struct Cursor<'a> {
 pub(crate) const EOF_CHAR: char = '\0';
 
 impl<'a> Cursor<'a> {
-    pub fn new(input: &'a str) -> Cursor<'a> {
+    pub fn new(input: &'a str, frontmatter_allowed: FrontmatterAllowed) -> Cursor<'a> {
         Cursor {
             len_remaining: input.len(),
             chars: input.chars(),
+            frontmatter_allowed,
             #[cfg(debug_assertions)]
             prev: EOF_CHAR,
         }
@@ -95,6 +102,11 @@ impl<'a> Cursor<'a> {
         Some(c)
     }
 
+    /// Moves to a substring by a number of bytes.
+    pub(crate) fn bump_bytes(&mut self, n: usize) {
+        self.chars = self.as_str()[n..].chars();
+    }
+
     /// Eats symbols while predicate returns true or until the end of file is reached.
     pub(crate) fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) {
         // It was tried making optimized version of this for eg. line comments, but
diff --git a/compiler/rustc_lexer/src/lib.rs b/compiler/rustc_lexer/src/lib.rs
index f9c71b2fa65..2374f388250 100644
--- a/compiler/rustc_lexer/src/lib.rs
+++ b/compiler/rustc_lexer/src/lib.rs
@@ -35,8 +35,8 @@ pub use unicode_xid::UNICODE_VERSION as UNICODE_XID_VERSION;
 
 use self::LiteralKind::*;
 use self::TokenKind::*;
-pub use crate::cursor::Cursor;
 use crate::cursor::EOF_CHAR;
+pub use crate::cursor::{Cursor, FrontmatterAllowed};
 
 /// Parsed token.
 /// It doesn't contain information about data that has been parsed,
@@ -57,17 +57,27 @@ impl Token {
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum TokenKind {
     /// A line comment, e.g. `// comment`.
-    LineComment { doc_style: Option<DocStyle> },
+    LineComment {
+        doc_style: Option<DocStyle>,
+    },
 
     /// A block comment, e.g. `/* block comment */`.
     ///
     /// Block comments can be recursive, so a sequence like `/* /* */`
     /// will not be considered terminated and will result in a parsing error.
-    BlockComment { doc_style: Option<DocStyle>, terminated: bool },
+    BlockComment {
+        doc_style: Option<DocStyle>,
+        terminated: bool,
+    },
 
     /// Any whitespace character sequence.
     Whitespace,
 
+    Frontmatter {
+        has_invalid_preceding_whitespace: bool,
+        invalid_infostring: bool,
+    },
+
     /// An identifier or keyword, e.g. `ident` or `continue`.
     Ident,
 
@@ -109,10 +119,15 @@ pub enum TokenKind {
     /// this type will need to check for and reject that case.
     ///
     /// See [LiteralKind] for more details.
-    Literal { kind: LiteralKind, suffix_start: u32 },
+    Literal {
+        kind: LiteralKind,
+        suffix_start: u32,
+    },
 
     /// A lifetime, e.g. `'a`.
-    Lifetime { starts_with_number: bool },
+    Lifetime {
+        starts_with_number: bool,
+    },
 
     /// `;`
     Semi,
@@ -280,7 +295,7 @@ pub fn strip_shebang(input: &str) -> Option<usize> {
 #[inline]
 pub fn validate_raw_str(input: &str, prefix_len: u32) -> Result<(), RawStrError> {
     debug_assert!(!input.is_empty());
-    let mut cursor = Cursor::new(input);
+    let mut cursor = Cursor::new(input, FrontmatterAllowed::No);
     // Move past the leading `r` or `br`.
     for _ in 0..prefix_len {
         cursor.bump().unwrap();
@@ -290,7 +305,7 @@ pub fn validate_raw_str(input: &str, prefix_len: u32) -> Result<(), RawStrError>
 
 /// Creates an iterator that produces tokens from the input string.
 pub fn tokenize(input: &str) -> impl Iterator<Item = Token> {
-    let mut cursor = Cursor::new(input);
+    let mut cursor = Cursor::new(input, FrontmatterAllowed::No);
     std::iter::from_fn(move || {
         let token = cursor.advance_token();
         if token.kind != TokenKind::Eof { Some(token) } else { None }
@@ -361,7 +376,34 @@ impl Cursor<'_> {
             Some(c) => c,
             None => return Token::new(TokenKind::Eof, 0),
         };
+
         let token_kind = match first_char {
+            c if matches!(self.frontmatter_allowed, FrontmatterAllowed::Yes)
+                && is_whitespace(c) =>
+            {
+                let mut last = first_char;
+                while is_whitespace(self.first()) {
+                    let Some(c) = self.bump() else {
+                        break;
+                    };
+                    last = c;
+                }
+                // invalid frontmatter opening as whitespace preceding it isn't newline.
+                // combine the whitespace and the frontmatter to a single token as we shall
+                // error later.
+                if last != '\n' && self.as_str().starts_with("---") {
+                    self.bump();
+                    self.frontmatter(true)
+                } else {
+                    Whitespace
+                }
+            }
+            '-' if matches!(self.frontmatter_allowed, FrontmatterAllowed::Yes)
+                && self.as_str().starts_with("--") =>
+            {
+                // happy path
+                self.frontmatter(false)
+            }
             // Slash, comment or block comment.
             '/' => match self.first() {
                 '/' => self.line_comment(),
@@ -464,11 +506,110 @@ impl Cursor<'_> {
             c if !c.is_ascii() && c.is_emoji_char() => self.invalid_ident(),
             _ => Unknown,
         };
+        if matches!(self.frontmatter_allowed, FrontmatterAllowed::Yes)
+            && !matches!(token_kind, Whitespace)
+        {
+            // stop allowing frontmatters after first non-whitespace token
+            self.frontmatter_allowed = FrontmatterAllowed::No;
+        }
         let res = Token::new(token_kind, self.pos_within_token());
         self.reset_pos_within_token();
         res
     }
 
+    /// Given that one `-` was eaten, eat the rest of the frontmatter.
+    fn frontmatter(&mut self, has_invalid_preceding_whitespace: bool) -> TokenKind {
+        debug_assert_eq!('-', self.prev());
+
+        let pos = self.pos_within_token();
+        self.eat_while(|c| c == '-');
+
+        // one `-` is eaten by the caller.
+        let length_opening = self.pos_within_token() - pos + 1;
+
+        // must be ensured by the caller
+        debug_assert!(length_opening >= 3);
+
+        // whitespace between the opening and the infostring.
+        self.eat_while(|ch| ch != '\n' && is_whitespace(ch));
+
+        // copied from `eat_identifier`, but allows `.` in infostring to allow something like
+        // `---Cargo.toml` as a valid opener
+        if is_id_start(self.first()) {
+            self.bump();
+            self.eat_while(|c| is_id_continue(c) || c == '.');
+        }
+
+        self.eat_while(|ch| ch != '\n' && is_whitespace(ch));
+        let invalid_infostring = self.first() != '\n';
+
+        let mut s = self.as_str();
+        let mut found = false;
+        while let Some(closing) = s.find(&"-".repeat(length_opening as usize)) {
+            let preceding_chars_start = s[..closing].rfind("\n").map_or(0, |i| i + 1);
+            if s[preceding_chars_start..closing].chars().all(is_whitespace) {
+                // candidate found
+                self.bump_bytes(closing);
+                // in case like
+                // ---cargo
+                // --- blahblah
+                // or
+                // ---cargo
+                // ----
+                // combine those stuff into this frontmatter token such that it gets detected later.
+                self.eat_until(b'\n');
+                found = true;
+                break;
+            } else {
+                s = &s[closing + length_opening as usize..];
+            }
+        }
+
+        if !found {
+            // recovery strategy: a closing statement might have precending whitespace/newline
+            // but not have enough dashes to properly close. In this case, we eat until there,
+            // and report a mismatch in the parser.
+            let mut rest = self.as_str();
+            // We can look for a shorter closing (starting with four dashes but closing with three)
+            // and other indications that Rust has started and the infostring has ended.
+            let mut potential_closing = rest
+                .find("\n---")
+                // n.b. only in the case where there are dashes, we move the index to the line where
+                // the dashes start as we eat to include that line. For other cases those are Rust code
+                // and not included in the frontmatter.
+                .map(|x| x + 1)
+                .or_else(|| rest.find("\nuse "))
+                .or_else(|| rest.find("\n//!"))
+                .or_else(|| rest.find("\n#!["));
+
+            if potential_closing.is_none() {
+                // a less fortunate recovery if all else fails which finds any dashes preceded by whitespace
+                // on a standalone line. Might be wrong.
+                while let Some(closing) = rest.find("---") {
+                    let preceding_chars_start = rest[..closing].rfind("\n").map_or(0, |i| i + 1);
+                    if rest[preceding_chars_start..closing].chars().all(is_whitespace) {
+                        // candidate found
+                        potential_closing = Some(closing);
+                        break;
+                    } else {
+                        rest = &rest[closing + 3..];
+                    }
+                }
+            }
+
+            if let Some(potential_closing) = potential_closing {
+                // bump to the potential closing, and eat everything on that line.
+                self.bump_bytes(potential_closing);
+                self.eat_until(b'\n');
+            } else {
+                // eat everything. this will get reported as an unclosed frontmatter.
+                self.eat_while(|_| true);
+            }
+        }
+
+        Frontmatter { has_invalid_preceding_whitespace, invalid_infostring }
+    }
+
     fn line_comment(&mut self) -> TokenKind {
         debug_assert!(self.prev() == '/' && self.first() == '/');
         self.bump();
diff --git a/compiler/rustc_lexer/src/tests.rs b/compiler/rustc_lexer/src/tests.rs
index 8203ae70b07..fc8d9b9d57b 100644
--- a/compiler/rustc_lexer/src/tests.rs
+++ b/compiler/rustc_lexer/src/tests.rs
@@ -4,7 +4,7 @@ use super::*;
 
 fn check_raw_str(s: &str, expected: Result<u8, RawStrError>) {
     let s = &format!("r{}", s);
-    let mut cursor = Cursor::new(s);
+    let mut cursor = Cursor::new(s, FrontmatterAllowed::No);
     cursor.bump();
     let res = cursor.raw_double_quoted_string(0);
     assert_eq!(res, expected);
diff --git a/compiler/rustc_parse/messages.ftl b/compiler/rustc_parse/messages.ftl
index ac4f7ed64e2..3e953e6c855 100644
--- a/compiler/rustc_parse/messages.ftl
+++ b/compiler/rustc_parse/messages.ftl
@@ -297,6 +297,19 @@ parse_forgot_paren = perhaps you forgot parentheses?
 parse_found_expr_would_be_stmt = expected expression, found `{$token}`
     .label = expected expression
 
+parse_frontmatter_extra_characters_after_close = extra characters after frontmatter close are not allowed
+parse_frontmatter_invalid_close_preceding_whitespace = invalid preceding whitespace for frontmatter close
+    .note = frontmatter close should not be preceded by whitespace
+parse_frontmatter_invalid_infostring = invalid infostring for frontmatter
+    .note = frontmatter infostrings must be a single identifier immediately following the opening
+parse_frontmatter_invalid_opening_preceding_whitespace = invalid preceding whitespace for frontmatter opening
+    .note = frontmatter opening should not be preceded by whitespace
+parse_frontmatter_length_mismatch = frontmatter close does not match the opening
+    .label_opening = the opening here has {$len_opening} dashes...
+    .label_close = ...while the close has {$len_close} dashes
+parse_frontmatter_unclosed = unclosed frontmatter
+    .note = frontmatter opening here was not closed
+
 parse_function_body_equals_expr = function body cannot be `= expression;`
     .suggestion = surround the expression with `{"{"}` and `{"}"}` instead of `=` and `;`
 
diff --git a/compiler/rustc_parse/src/errors.rs b/compiler/rustc_parse/src/errors.rs
index 6a6fb0eb9b5..9e5c81d44a5 100644
--- a/compiler/rustc_parse/src/errors.rs
+++ b/compiler/rustc_parse/src/errors.rs
@@ -736,6 +736,61 @@ pub(crate) struct FoundExprWouldBeStmt {
 }
 
 #[derive(Diagnostic)]
+#[diag(parse_frontmatter_extra_characters_after_close)]
+pub(crate) struct FrontmatterExtraCharactersAfterClose {
+    #[primary_span]
+    pub span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(parse_frontmatter_invalid_infostring)]
+#[note]
+pub(crate) struct FrontmatterInvalidInfostring {
+    #[primary_span]
+    pub span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(parse_frontmatter_invalid_opening_preceding_whitespace)]
+pub(crate) struct FrontmatterInvalidOpeningPrecedingWhitespace {
+    #[primary_span]
+    pub span: Span,
+    #[note]
+    pub note_span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(parse_frontmatter_unclosed)]
+pub(crate) struct FrontmatterUnclosed {
+    #[primary_span]
+    pub span: Span,
+    #[note]
+    pub note_span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(parse_frontmatter_invalid_close_preceding_whitespace)]
+pub(crate) struct FrontmatterInvalidClosingPrecedingWhitespace {
+    #[primary_span]
+    pub span: Span,
+    #[note]
+    pub note_span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(parse_frontmatter_length_mismatch)]
+pub(crate) struct FrontmatterLengthMismatch {
+    #[primary_span]
+    pub span: Span,
+    #[label(parse_label_opening)]
+    pub opening: Span,
+    #[label(parse_label_close)]
+    pub close: Span,
+    pub len_opening: usize,
+    pub len_close: usize,
+}
+
+#[derive(Diagnostic)]
 #[diag(parse_leading_plus_not_supported)]
 pub(crate) struct LeadingPlusNotSupported {
     #[primary_span]
diff --git a/compiler/rustc_parse/src/lexer/mod.rs b/compiler/rustc_parse/src/lexer/mod.rs
index e8a5cae54cf..78c5742414b 100644
--- a/compiler/rustc_parse/src/lexer/mod.rs
+++ b/compiler/rustc_parse/src/lexer/mod.rs
@@ -7,7 +7,9 @@ use rustc_ast::tokenstream::TokenStream;
 use rustc_ast::util::unicode::contains_text_flow_control_chars;
 use rustc_errors::codes::*;
 use rustc_errors::{Applicability, Diag, DiagCtxtHandle, StashKey};
-use rustc_lexer::{Base, Cursor, DocStyle, LiteralKind, RawStrError};
+use rustc_lexer::{
+    Base, Cursor, DocStyle, FrontmatterAllowed, LiteralKind, RawStrError, is_whitespace,
+};
 use rustc_literal_escaper::{EscapeError, Mode, unescape_mixed, unescape_unicode};
 use rustc_session::lint::BuiltinLintDiag;
 use rustc_session::lint::builtin::{
@@ -15,7 +17,7 @@ use rustc_session::lint::builtin::{
     TEXT_DIRECTION_CODEPOINT_IN_COMMENT,
 };
 use rustc_session::parse::ParseSess;
-use rustc_span::{BytePos, Pos, Span, Symbol};
+use rustc_span::{BytePos, Pos, Span, Symbol, sym};
 use tracing::debug;
 
 use crate::errors;
@@ -56,7 +58,7 @@ pub(crate) fn lex_token_trees<'psess, 'src>(
         start_pos = start_pos + BytePos::from_usize(shebang_len);
     }
 
-    let cursor = Cursor::new(src);
+    let cursor = Cursor::new(src, FrontmatterAllowed::Yes);
     let mut lexer = Lexer {
         psess,
         start_pos,
@@ -193,6 +195,11 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
                     let content = self.str_from_to(content_start, content_end);
                     self.cook_doc_comment(content_start, content, CommentKind::Block, doc_style)
                 }
+                rustc_lexer::TokenKind::Frontmatter { has_invalid_preceding_whitespace, invalid_infostring } => {
+                    self.validate_frontmatter(start, has_invalid_preceding_whitespace, invalid_infostring);
+                    preceded_by_whitespace = true;
+                    continue;
+                }
                 rustc_lexer::TokenKind::Whitespace => {
                     preceded_by_whitespace = true;
                     continue;
@@ -256,7 +263,7 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
                     // was consumed.
                     let lit_start = start + BytePos(prefix_len);
                     self.pos = lit_start;
-                    self.cursor = Cursor::new(&str_before[prefix_len as usize..]);
+                    self.cursor = Cursor::new(&str_before[prefix_len as usize..], FrontmatterAllowed::No);
                     self.report_unknown_prefix(start);
                     let prefix_span = self.mk_sp(start, lit_start);
                     return (Token::new(self.ident(start), prefix_span), preceded_by_whitespace);
@@ -361,7 +368,7 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
                         // Reset the state so we just lex the `'r`.
                         let lt_start = start + BytePos(2);
                         self.pos = lt_start;
-                        self.cursor = Cursor::new(&str_before[2 as usize..]);
+                        self.cursor = Cursor::new(&str_before[2 as usize..], FrontmatterAllowed::No);
 
                         let lifetime_name = self.str_from(start);
                         let ident = Symbol::intern(lifetime_name);
@@ -474,6 +481,91 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
         }
     }
 
+    fn validate_frontmatter(
+        &self,
+        start: BytePos,
+        has_invalid_preceding_whitespace: bool,
+        invalid_infostring: bool,
+    ) {
+        let s = self.str_from(start);
+        let real_start = s.find("---").unwrap();
+        let frontmatter_opening_pos = BytePos(real_start as u32) + start;
+        let s_new = &s[real_start..];
+        let within = s_new.trim_start_matches('-');
+        let len_opening = s_new.len() - within.len();
+
+        let frontmatter_opening_end_pos = frontmatter_opening_pos + BytePos(len_opening as u32);
+        if has_invalid_preceding_whitespace {
+            let line_start =
+                BytePos(s[..real_start].rfind("\n").map_or(0, |i| i as u32 + 1)) + start;
+            let span = self.mk_sp(line_start, frontmatter_opening_end_pos);
+            let label_span = self.mk_sp(line_start, frontmatter_opening_pos);
+            self.dcx().emit_err(errors::FrontmatterInvalidOpeningPrecedingWhitespace {
+                span,
+                note_span: label_span,
+            });
+        }
+
+        if invalid_infostring {
+            let line_end = s[real_start..].find('\n').unwrap_or(s[real_start..].len());
+            let span = self.mk_sp(
+                frontmatter_opening_end_pos,
+                frontmatter_opening_pos + BytePos(line_end as u32),
+            );
+            self.dcx().emit_err(errors::FrontmatterInvalidInfostring { span });
+        }
+
+        let last_line_start = within.rfind('\n').map_or(0, |i| i + 1);
+        let last_line = &within[last_line_start..];
+        let last_line_trimmed = last_line.trim_start_matches(is_whitespace);
+        let last_line_start_pos = frontmatter_opening_end_pos + BytePos(last_line_start as u32);
+
+        let frontmatter_span = self.mk_sp(frontmatter_opening_pos, self.pos);
+        self.psess.gated_spans.gate(sym::frontmatter, frontmatter_span);
+
+        if !last_line_trimmed.starts_with("---") {
+            let label_span = self.mk_sp(frontmatter_opening_pos, frontmatter_opening_end_pos);
+            self.dcx().emit_err(errors::FrontmatterUnclosed {
+                span: frontmatter_span,
+                note_span: label_span,
+            });
+            return;
+        }
+
+        if last_line_trimmed.len() != last_line.len() {
+            let line_end = last_line_start_pos + BytePos(last_line.len() as u32);
+            let span = self.mk_sp(last_line_start_pos, line_end);
+            let whitespace_end =
+                last_line_start_pos + BytePos((last_line.len() - last_line_trimmed.len()) as u32);
+            let label_span = self.mk_sp(last_line_start_pos, whitespace_end);
+            self.dcx().emit_err(errors::FrontmatterInvalidClosingPrecedingWhitespace {
+                span,
+                note_span: label_span,
+            });
+        }
+
+        let rest = last_line_trimmed.trim_start_matches('-');
+        let len_close = last_line_trimmed.len() - rest.len();
+        if len_close != len_opening {
+            let span = self.mk_sp(frontmatter_opening_pos, self.pos);
+            let opening = self.mk_sp(frontmatter_opening_pos, frontmatter_opening_end_pos);
+            let last_line_close_pos = last_line_start_pos + BytePos(len_close as u32);
+            let close = self.mk_sp(last_line_start_pos, last_line_close_pos);
+            self.dcx().emit_err(errors::FrontmatterLengthMismatch {
+                span,
+                opening,
+                close,
+                len_opening,
+                len_close,
+            });
+        }
+
+        if !rest.trim_matches(is_whitespace).is_empty() {
+            let span = self.mk_sp(last_line_start_pos, self.pos);
+            self.dcx().emit_err(errors::FrontmatterExtraCharactersAfterClose { span });
+        }
+    }
+
     fn cook_doc_comment(
         &self,
         content_start: BytePos,
@@ -839,7 +931,7 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
         let space_pos = start + BytePos(1);
         let space_span = self.mk_sp(space_pos, space_pos);
 
-        let mut cursor = Cursor::new(str_before);
+        let mut cursor = Cursor::new(str_before, FrontmatterAllowed::No);
 
         let (is_string, span, unterminated) = match cursor.guarded_double_quoted_string() {
             Some(rustc_lexer::GuardedStr { n_hashes, terminated, token_len }) => {
@@ -905,7 +997,7 @@ impl<'psess, 'src> Lexer<'psess, 'src> {
             // For backwards compatibility, roll back to after just the first `#`
             // and return the `Pound` token.
             self.pos = start + BytePos(1);
-            self.cursor = Cursor::new(&str_before[1..]);
+            self.cursor = Cursor::new(&str_before[1..], FrontmatterAllowed::No);
             token::Pound
         }
     }
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index ba3e6d7ca82..d7dbdf04a8c 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -1047,6 +1047,7 @@ symbols! {
         from_u16,
         from_usize,
         from_yeet,
+        frontmatter,
         fs_create_dir,
         fsub_algebraic,
         fsub_fast,
diff --git a/src/doc/unstable-book/src/language-features/arbitrary-self-types.md b/src/doc/unstable-book/src/language-features/arbitrary-self-types.md
index 2f8b52d4043..d660dd13fe4 100644
--- a/src/doc/unstable-book/src/language-features/arbitrary-self-types.md
+++ b/src/doc/unstable-book/src/language-features/arbitrary-self-types.md
@@ -2,7 +2,7 @@
 
 The tracking issue for this feature is: [#44874]
 
-[#38788]: https://github.com/rust-lang/rust/issues/44874
+[#44874]: https://github.com/rust-lang/rust/issues/44874
 
 ------------------------
 
diff --git a/src/doc/unstable-book/src/language-features/f128.md b/src/doc/unstable-book/src/language-features/f128.md
index 0cc5f677230..b523ffe10f2 100644
--- a/src/doc/unstable-book/src/language-features/f128.md
+++ b/src/doc/unstable-book/src/language-features/f128.md
@@ -6,4 +6,4 @@ The tracking issue for this feature is: [#116909]
 
 ---
 
-Enable the `f128` type for  IEEE 128-bit floating numbers (quad precision).
+Enable the `f128` type for IEEE 128-bit floating numbers (quad precision).
diff --git a/src/doc/unstable-book/src/language-features/f16.md b/src/doc/unstable-book/src/language-features/f16.md
index efb07a5146d..5f31dcbb06c 100644
--- a/src/doc/unstable-book/src/language-features/f16.md
+++ b/src/doc/unstable-book/src/language-features/f16.md
@@ -6,4 +6,4 @@ The tracking issue for this feature is: [#116909]
 
 ---
 
-Enable the `f16` type for  IEEE 16-bit floating numbers (half precision).
+Enable the `f16` type for IEEE 16-bit floating numbers (half precision).
diff --git a/src/doc/unstable-book/src/language-features/frontmatter.md b/src/doc/unstable-book/src/language-features/frontmatter.md
new file mode 100644
index 00000000000..1d5b4feb6ac
--- /dev/null
+++ b/src/doc/unstable-book/src/language-features/frontmatter.md
@@ -0,0 +1,25 @@
+# `frontmatter`
+
+The tracking issue for this feature is: [#136889]
+
+------
+
+The `frontmatter` feature allows an extra metadata block at the top of files for consumption by
+external tools. For example, it can be used by [`cargo-script`] files to specify dependencies.
+
+```rust
+#!/usr/bin/env -S cargo -Zscript
+---
+[dependencies]
+libc = "0.2.172"
+---
+#![feature(frontmatter)]
+# mod libc { pub type c_int = i32; }
+
+fn main() {
+    let x: libc::c_int = 1i32;
+}
+```
+
+[#136889]: https://github.com/rust-lang/rust/issues/136889
+[`cargo-script`]: https://rust-lang.github.io/rfcs/3502-cargo-script.html
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs
index c943d3ad4d0..2db1ea8450c 100644
--- a/src/librustdoc/html/highlight.rs
+++ b/src/librustdoc/html/highlight.rs
@@ -9,7 +9,7 @@ use std::collections::VecDeque;
 use std::fmt::{Display, Write};
 
 use rustc_data_structures::fx::FxIndexMap;
-use rustc_lexer::{Cursor, LiteralKind, TokenKind};
+use rustc_lexer::{Cursor, FrontmatterAllowed, LiteralKind, TokenKind};
 use rustc_span::edition::Edition;
 use rustc_span::symbol::Symbol;
 use rustc_span::{BytePos, DUMMY_SP, Span};
@@ -638,7 +638,8 @@ impl<'src> Classifier<'src> {
     /// Takes as argument the source code to HTML-ify, the rust edition to use and the source code
     /// file span which will be used later on by the `span_correspondence_map`.
     fn new(src: &'src str, file_span: Span, decoration_info: Option<&DecorationInfo>) -> Self {
-        let tokens = PeekIter::new(TokenIter { src, cursor: Cursor::new(src) });
+        let tokens =
+            PeekIter::new(TokenIter { src, cursor: Cursor::new(src, FrontmatterAllowed::Yes) });
         let decorations = decoration_info.map(Decorations::new);
         Classifier {
             tokens,
@@ -884,6 +885,7 @@ impl<'src> Classifier<'src> {
             | TokenKind::At
             | TokenKind::Tilde
             | TokenKind::Colon
+            | TokenKind::Frontmatter { .. }
             | TokenKind::Unknown => return no_highlight(sink),
 
             TokenKind::Question => Class::QuestionMark,
diff --git a/src/tools/rust-analyzer/crates/parser/src/lexed_str.rs b/src/tools/rust-analyzer/crates/parser/src/lexed_str.rs
index 585e7ffb1ae..0a5c16dc4c4 100644
--- a/src/tools/rust-analyzer/crates/parser/src/lexed_str.rs
+++ b/src/tools/rust-analyzer/crates/parser/src/lexed_str.rs
@@ -179,6 +179,15 @@ impl<'a> Converter<'a> {
                     COMMENT
                 }
 
+                rustc_lexer::TokenKind::Frontmatter  { has_invalid_preceding_whitespace, invalid_infostring } => {
+                    if *has_invalid_preceding_whitespace {
+                        err = "invalid preceding whitespace for frontmatter opening"
+                    } else if *invalid_infostring {
+                        err = "invalid infostring for frontmatter"
+                    }
+                    FRONTMATTER
+                }
+
                 rustc_lexer::TokenKind::Whitespace => WHITESPACE,
 
                 rustc_lexer::TokenKind::Ident if token_text == "_" => UNDERSCORE,
diff --git a/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs b/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs
index e6f93a1fbda..b1727509b13 100644
--- a/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs
+++ b/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs
@@ -150,6 +150,7 @@ pub enum SyntaxKind {
     STRING,
     COMMENT,
     ERROR,
+    FRONTMATTER,
     IDENT,
     LIFETIME_IDENT,
     NEWLINE,
@@ -483,6 +484,7 @@ impl SyntaxKind {
             | YIELD_EXPR
             | COMMENT
             | ERROR
+            | FRONTMATTER
             | IDENT
             | LIFETIME_IDENT
             | NEWLINE
@@ -994,7 +996,7 @@ impl SyntaxKind {
     }
 }
 #[macro_export]
-macro_rules ! T_ { [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; [_] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [..] => { $ crate :: SyntaxKind :: DOT2 } ; [...] => { $ crate :: SyntaxKind :: DOT3 } ; [..=] => { $ crate :: SyntaxKind :: DOT2EQ } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [::] => { $ crate :: SyntaxKind :: COLON2 } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [=>] => { $ crate :: SyntaxKind :: FAT_ARROW } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [->] => { $ crate :: SyntaxKind :: THIN_ARROW } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [+=] => { $ crate :: SyntaxKind :: PLUSEQ } ; [-=] => { $ crate :: SyntaxKind :: MINUSEQ } ; [|=] => { $ crate :: SyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: SyntaxKind :: AMPEQ } ; [^=] => { $ crate :: SyntaxKind :: CARETEQ } ; [/=] => { $ crate :: SyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: SyntaxKind :: STAREQ } ; [%=] => { $ crate :: SyntaxKind :: PERCENTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; [<<=] => { $ crate :: SyntaxKind :: SHLEQ } ; [>>=] => { $ crate :: SyntaxKind :: SHREQ } ; [Self] => { $ crate :: SyntaxKind :: SELF_TYPE_KW } ; [abstract] => { $ crate :: SyntaxKind :: ABSTRACT_KW } ; [as] => { $ crate :: SyntaxKind :: AS_KW } ; [become] => { $ crate :: SyntaxKind :: BECOME_KW } ; [box] => { $ crate :: SyntaxKind :: BOX_KW } ; [break] => { $ crate :: SyntaxKind :: BREAK_KW } ; [const] => { $ crate :: SyntaxKind :: CONST_KW } ; [continue] => { $ crate :: SyntaxKind :: CONTINUE_KW } ; [crate] => { $ crate :: SyntaxKind :: CRATE_KW } ; [do] => { $ crate :: SyntaxKind :: DO_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [enum] => { $ crate :: SyntaxKind :: ENUM_KW } ; [extern] => { $ crate :: SyntaxKind :: EXTERN_KW } ; [false] => { $ crate :: SyntaxKind :: FALSE_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [fn] => { $ crate :: SyntaxKind :: FN_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [impl] => { $ crate :: SyntaxKind :: IMPL_KW } ; [in] => { $ crate :: SyntaxKind :: IN_KW } ; [let] => { $ crate :: SyntaxKind :: LET_KW } ; [loop] => { $ crate :: SyntaxKind :: LOOP_KW } ; [macro] => { $ crate :: SyntaxKind :: MACRO_KW } ; [match] => { $ crate :: SyntaxKind :: MATCH_KW } ; [mod] => { $ crate :: SyntaxKind :: MOD_KW } ; [move] => { $ crate :: SyntaxKind :: MOVE_KW } ; [mut] => { $ crate :: SyntaxKind :: MUT_KW } ; [override] => { $ crate :: SyntaxKind :: OVERRIDE_KW } ; [priv] => { $ crate :: SyntaxKind :: PRIV_KW } ; [pub] => { $ crate :: SyntaxKind :: PUB_KW } ; [ref] => { $ crate :: SyntaxKind :: REF_KW } ; [return] => { $ crate :: SyntaxKind :: RETURN_KW } ; [self] => { $ crate :: SyntaxKind :: SELF_KW } ; [static] => { $ crate :: SyntaxKind :: STATIC_KW } ; [struct] => { $ crate :: SyntaxKind :: STRUCT_KW } ; [super] => { $ crate :: SyntaxKind :: SUPER_KW } ; [trait] => { $ crate :: SyntaxKind :: TRAIT_KW } ; [true] => { $ crate :: SyntaxKind :: TRUE_KW } ; [type] => { $ crate :: SyntaxKind :: TYPE_KW } ; [typeof] => { $ crate :: SyntaxKind :: TYPEOF_KW } ; [unsafe] => { $ crate :: SyntaxKind :: UNSAFE_KW } ; [unsized] => { $ crate :: SyntaxKind :: UNSIZED_KW } ; [use] => { $ crate :: SyntaxKind :: USE_KW } ; [virtual] => { $ crate :: SyntaxKind :: VIRTUAL_KW } ; [where] => { $ crate :: SyntaxKind :: WHERE_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [yield] => { $ crate :: SyntaxKind :: YIELD_KW } ; [asm] => { $ crate :: SyntaxKind :: ASM_KW } ; [att_syntax] => { $ crate :: SyntaxKind :: ATT_SYNTAX_KW } ; [auto] => { $ crate :: SyntaxKind :: AUTO_KW } ; [builtin] => { $ crate :: SyntaxKind :: BUILTIN_KW } ; [clobber_abi] => { $ crate :: SyntaxKind :: CLOBBER_ABI_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [format_args] => { $ crate :: SyntaxKind :: FORMAT_ARGS_KW } ; [inlateout] => { $ crate :: SyntaxKind :: INLATEOUT_KW } ; [inout] => { $ crate :: SyntaxKind :: INOUT_KW } ; [label] => { $ crate :: SyntaxKind :: LABEL_KW } ; [lateout] => { $ crate :: SyntaxKind :: LATEOUT_KW } ; [macro_rules] => { $ crate :: SyntaxKind :: MACRO_RULES_KW } ; [may_unwind] => { $ crate :: SyntaxKind :: MAY_UNWIND_KW } ; [nomem] => { $ crate :: SyntaxKind :: NOMEM_KW } ; [noreturn] => { $ crate :: SyntaxKind :: NORETURN_KW } ; [nostack] => { $ crate :: SyntaxKind :: NOSTACK_KW } ; [offset_of] => { $ crate :: SyntaxKind :: OFFSET_OF_KW } ; [options] => { $ crate :: SyntaxKind :: OPTIONS_KW } ; [out] => { $ crate :: SyntaxKind :: OUT_KW } ; [preserves_flags] => { $ crate :: SyntaxKind :: PRESERVES_FLAGS_KW } ; [pure] => { $ crate :: SyntaxKind :: PURE_KW } ; [raw] => { $ crate :: SyntaxKind :: RAW_KW } ; [readonly] => { $ crate :: SyntaxKind :: READONLY_KW } ; [safe] => { $ crate :: SyntaxKind :: SAFE_KW } ; [sym] => { $ crate :: SyntaxKind :: SYM_KW } ; [union] => { $ crate :: SyntaxKind :: UNION_KW } ; [yeet] => { $ crate :: SyntaxKind :: YEET_KW } ; [async] => { $ crate :: SyntaxKind :: ASYNC_KW } ; [await] => { $ crate :: SyntaxKind :: AWAIT_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [gen] => { $ crate :: SyntaxKind :: GEN_KW } ; [try] => { $ crate :: SyntaxKind :: TRY_KW } ; [lifetime_ident] => { $ crate :: SyntaxKind :: LIFETIME_IDENT } ; [int_number] => { $ crate :: SyntaxKind :: INT_NUMBER } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [string] => { $ crate :: SyntaxKind :: STRING } ; [shebang] => { $ crate :: SyntaxKind :: SHEBANG } ; }
+macro_rules ! T_ { [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; [_] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [..] => { $ crate :: SyntaxKind :: DOT2 } ; [...] => { $ crate :: SyntaxKind :: DOT3 } ; [..=] => { $ crate :: SyntaxKind :: DOT2EQ } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [::] => { $ crate :: SyntaxKind :: COLON2 } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [=>] => { $ crate :: SyntaxKind :: FAT_ARROW } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [->] => { $ crate :: SyntaxKind :: THIN_ARROW } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [+=] => { $ crate :: SyntaxKind :: PLUSEQ } ; [-=] => { $ crate :: SyntaxKind :: MINUSEQ } ; [|=] => { $ crate :: SyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: SyntaxKind :: AMPEQ } ; [^=] => { $ crate :: SyntaxKind :: CARETEQ } ; [/=] => { $ crate :: SyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: SyntaxKind :: STAREQ } ; [%=] => { $ crate :: SyntaxKind :: PERCENTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; [<<=] => { $ crate :: SyntaxKind :: SHLEQ } ; [>>=] => { $ crate :: SyntaxKind :: SHREQ } ; [Self] => { $ crate :: SyntaxKind :: SELF_TYPE_KW } ; [abstract] => { $ crate :: SyntaxKind :: ABSTRACT_KW } ; [as] => { $ crate :: SyntaxKind :: AS_KW } ; [become] => { $ crate :: SyntaxKind :: BECOME_KW } ; [box] => { $ crate :: SyntaxKind :: BOX_KW } ; [break] => { $ crate :: SyntaxKind :: BREAK_KW } ; [const] => { $ crate :: SyntaxKind :: CONST_KW } ; [continue] => { $ crate :: SyntaxKind :: CONTINUE_KW } ; [crate] => { $ crate :: SyntaxKind :: CRATE_KW } ; [do] => { $ crate :: SyntaxKind :: DO_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [enum] => { $ crate :: SyntaxKind :: ENUM_KW } ; [extern] => { $ crate :: SyntaxKind :: EXTERN_KW } ; [false] => { $ crate :: SyntaxKind :: FALSE_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [fn] => { $ crate :: SyntaxKind :: FN_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [impl] => { $ crate :: SyntaxKind :: IMPL_KW } ; [in] => { $ crate :: SyntaxKind :: IN_KW } ; [let] => { $ crate :: SyntaxKind :: LET_KW } ; [loop] => { $ crate :: SyntaxKind :: LOOP_KW } ; [macro] => { $ crate :: SyntaxKind :: MACRO_KW } ; [match] => { $ crate :: SyntaxKind :: MATCH_KW } ; [mod] => { $ crate :: SyntaxKind :: MOD_KW } ; [move] => { $ crate :: SyntaxKind :: MOVE_KW } ; [mut] => { $ crate :: SyntaxKind :: MUT_KW } ; [override] => { $ crate :: SyntaxKind :: OVERRIDE_KW } ; [priv] => { $ crate :: SyntaxKind :: PRIV_KW } ; [pub] => { $ crate :: SyntaxKind :: PUB_KW } ; [ref] => { $ crate :: SyntaxKind :: REF_KW } ; [return] => { $ crate :: SyntaxKind :: RETURN_KW } ; [self] => { $ crate :: SyntaxKind :: SELF_KW } ; [static] => { $ crate :: SyntaxKind :: STATIC_KW } ; [struct] => { $ crate :: SyntaxKind :: STRUCT_KW } ; [super] => { $ crate :: SyntaxKind :: SUPER_KW } ; [trait] => { $ crate :: SyntaxKind :: TRAIT_KW } ; [true] => { $ crate :: SyntaxKind :: TRUE_KW } ; [type] => { $ crate :: SyntaxKind :: TYPE_KW } ; [typeof] => { $ crate :: SyntaxKind :: TYPEOF_KW } ; [unsafe] => { $ crate :: SyntaxKind :: UNSAFE_KW } ; [unsized] => { $ crate :: SyntaxKind :: UNSIZED_KW } ; [use] => { $ crate :: SyntaxKind :: USE_KW } ; [virtual] => { $ crate :: SyntaxKind :: VIRTUAL_KW } ; [where] => { $ crate :: SyntaxKind :: WHERE_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [yield] => { $ crate :: SyntaxKind :: YIELD_KW } ; [asm] => { $ crate :: SyntaxKind :: ASM_KW } ; [att_syntax] => { $ crate :: SyntaxKind :: ATT_SYNTAX_KW } ; [auto] => { $ crate :: SyntaxKind :: AUTO_KW } ; [builtin] => { $ crate :: SyntaxKind :: BUILTIN_KW } ; [clobber_abi] => { $ crate :: SyntaxKind :: CLOBBER_ABI_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [format_args] => { $ crate :: SyntaxKind :: FORMAT_ARGS_KW } ; [inlateout] => { $ crate :: SyntaxKind :: INLATEOUT_KW } ; [inout] => { $ crate :: SyntaxKind :: INOUT_KW } ; [label] => { $ crate :: SyntaxKind :: LABEL_KW } ; [lateout] => { $ crate :: SyntaxKind :: LATEOUT_KW } ; [macro_rules] => { $ crate :: SyntaxKind :: MACRO_RULES_KW } ; [may_unwind] => { $ crate :: SyntaxKind :: MAY_UNWIND_KW } ; [nomem] => { $ crate :: SyntaxKind :: NOMEM_KW } ; [noreturn] => { $ crate :: SyntaxKind :: NORETURN_KW } ; [nostack] => { $ crate :: SyntaxKind :: NOSTACK_KW } ; [offset_of] => { $ crate :: SyntaxKind :: OFFSET_OF_KW } ; [options] => { $ crate :: SyntaxKind :: OPTIONS_KW } ; [out] => { $ crate :: SyntaxKind :: OUT_KW } ; [preserves_flags] => { $ crate :: SyntaxKind :: PRESERVES_FLAGS_KW } ; [pure] => { $ crate :: SyntaxKind :: PURE_KW } ; [raw] => { $ crate :: SyntaxKind :: RAW_KW } ; [readonly] => { $ crate :: SyntaxKind :: READONLY_KW } ; [safe] => { $ crate :: SyntaxKind :: SAFE_KW } ; [sym] => { $ crate :: SyntaxKind :: SYM_KW } ; [union] => { $ crate :: SyntaxKind :: UNION_KW } ; [yeet] => { $ crate :: SyntaxKind :: YEET_KW } ; [async] => { $ crate :: SyntaxKind :: ASYNC_KW } ; [await] => { $ crate :: SyntaxKind :: AWAIT_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [gen] => { $ crate :: SyntaxKind :: GEN_KW } ; [try] => { $ crate :: SyntaxKind :: TRY_KW } ; [lifetime_ident] => { $ crate :: SyntaxKind :: LIFETIME_IDENT } ; [int_number] => { $ crate :: SyntaxKind :: INT_NUMBER } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [string] => { $ crate :: SyntaxKind :: STRING } ; [shebang] => { $ crate :: SyntaxKind :: SHEBANG } ; [frontmatter] => { $ crate :: SyntaxKind :: FRONTMATTER } ; }
 impl ::core::marker::Copy for SyntaxKind {}
 impl ::core::clone::Clone for SyntaxKind {
     #[inline]
diff --git a/src/tools/rust-analyzer/crates/syntax/rust.ungram b/src/tools/rust-analyzer/crates/syntax/rust.ungram
index a0ae0d68581..10abca7d35d 100644
--- a/src/tools/rust-analyzer/crates/syntax/rust.ungram
+++ b/src/tools/rust-analyzer/crates/syntax/rust.ungram
@@ -133,6 +133,7 @@ Meta =
 
 SourceFile =
   '#shebang'?
+  '#frontmatter'?
   Attr*
   Item*
 
diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs
index 1243f6418fe..cd9f4dba890 100644
--- a/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs
+++ b/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs
@@ -1525,6 +1525,10 @@ impl ast::HasDocComments for SourceFile {}
 impl ast::HasModuleItem for SourceFile {}
 impl SourceFile {
     #[inline]
+    pub fn frontmatter_token(&self) -> Option<SyntaxToken> {
+        support::token(&self.syntax, T![frontmatter])
+    }
+    #[inline]
     pub fn shebang_token(&self) -> Option<SyntaxToken> { support::token(&self.syntax, T![shebang]) }
 }
 pub struct Static {
diff --git a/src/tools/rust-analyzer/xtask/src/codegen/grammar.rs b/src/tools/rust-analyzer/xtask/src/codegen/grammar.rs
index 82df78c1a89..b5350de2b51 100644
--- a/src/tools/rust-analyzer/xtask/src/codegen/grammar.rs
+++ b/src/tools/rust-analyzer/xtask/src/codegen/grammar.rs
@@ -670,6 +670,7 @@ fn generate_syntax_kinds(grammar: KindsSrc) -> String {
             [ident] => { $crate::SyntaxKind::IDENT };
             [string] => { $crate::SyntaxKind::STRING };
             [shebang] => { $crate::SyntaxKind::SHEBANG };
+            [frontmatter] => { $crate::SyntaxKind::FRONTMATTER };
         }
 
         impl ::core::marker::Copy for SyntaxKind {}
diff --git a/tests/ui/feature-gates/feature-gate-frontmatter.rs b/tests/ui/feature-gates/feature-gate-frontmatter.rs
new file mode 100644
index 00000000000..58e1f57eec0
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-frontmatter.rs
@@ -0,0 +1,5 @@
+---cargo
+//~^ ERROR: frontmatters are experimental
+---
+
+fn main() {}
diff --git a/tests/ui/feature-gates/feature-gate-frontmatter.stderr b/tests/ui/feature-gates/feature-gate-frontmatter.stderr
new file mode 100644
index 00000000000..57d38db8e76
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-frontmatter.stderr
@@ -0,0 +1,15 @@
+error[E0658]: frontmatters are experimental
+  --> $DIR/feature-gate-frontmatter.rs:1:1
+   |
+LL | / ---cargo
+LL | |
+LL | | ---
+   | |___^
+   |
+   = note: see issue #136889 <https://github.com/rust-lang/rust/issues/136889> for more information
+   = help: add `#![feature(frontmatter)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0658`.
diff --git a/tests/ui/frontmatter/auxiliary/lib.rs b/tests/ui/frontmatter/auxiliary/lib.rs
new file mode 100644
index 00000000000..ff7094f410f
--- /dev/null
+++ b/tests/ui/frontmatter/auxiliary/lib.rs
@@ -0,0 +1,6 @@
+---something
+---
+
+pub fn foo(x: i32) -> i32 {
+    -x
+}
diff --git a/tests/ui/frontmatter/auxiliary/makro.rs b/tests/ui/frontmatter/auxiliary/makro.rs
new file mode 100644
index 00000000000..78e7417afb5
--- /dev/null
+++ b/tests/ui/frontmatter/auxiliary/makro.rs
@@ -0,0 +1,8 @@
+extern crate proc_macro;
+use proc_macro::TokenStream;
+
+#[proc_macro]
+pub fn check(_: TokenStream) -> TokenStream {
+    assert!("---\n---".parse::<TokenStream>().unwrap().is_empty());
+    Default::default()
+}
diff --git a/tests/ui/frontmatter/dot-in-infostring-leading.rs b/tests/ui/frontmatter/dot-in-infostring-leading.rs
new file mode 100644
index 00000000000..0d3d699644e
--- /dev/null
+++ b/tests/ui/frontmatter/dot-in-infostring-leading.rs
@@ -0,0 +1,9 @@
+---.toml
+//~^ ERROR: invalid infostring for frontmatter
+---
+
+// infostrings cannot have leading dots
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/dot-in-infostring-leading.stderr b/tests/ui/frontmatter/dot-in-infostring-leading.stderr
new file mode 100644
index 00000000000..bc86bd80eec
--- /dev/null
+++ b/tests/ui/frontmatter/dot-in-infostring-leading.stderr
@@ -0,0 +1,10 @@
+error: invalid infostring for frontmatter
+  --> $DIR/dot-in-infostring-leading.rs:1:4
+   |
+LL | ---.toml
+   |    ^^^^^
+   |
+   = note: frontmatter infostrings must be a single identifier immediately following the opening
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/dot-in-infostring-non-leading.rs b/tests/ui/frontmatter/dot-in-infostring-non-leading.rs
new file mode 100644
index 00000000000..a4d17bb6e81
--- /dev/null
+++ b/tests/ui/frontmatter/dot-in-infostring-non-leading.rs
@@ -0,0 +1,9 @@
+---Cargo.toml
+---
+
+// infostrings can contain dots as long as a dot isn't the first character.
+//@ check-pass
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/escape.rs b/tests/ui/frontmatter/escape.rs
new file mode 100644
index 00000000000..0e6f064607e
--- /dev/null
+++ b/tests/ui/frontmatter/escape.rs
@@ -0,0 +1,14 @@
+----
+
+---
+
+----
+
+//@ check-pass
+
+// This test checks that longer dashes for opening and closing can be used to
+// escape sequences such as three dashes inside the frontmatter block.
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/extra-after-end.rs b/tests/ui/frontmatter/extra-after-end.rs
new file mode 100644
index 00000000000..de2cf4cc85e
--- /dev/null
+++ b/tests/ui/frontmatter/extra-after-end.rs
@@ -0,0 +1,7 @@
+---
+---cargo
+//~^ ERROR: extra characters after frontmatter close are not allowed
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/extra-after-end.stderr b/tests/ui/frontmatter/extra-after-end.stderr
new file mode 100644
index 00000000000..c2770fdfd41
--- /dev/null
+++ b/tests/ui/frontmatter/extra-after-end.stderr
@@ -0,0 +1,8 @@
+error: extra characters after frontmatter close are not allowed
+  --> $DIR/extra-after-end.rs:2:1
+   |
+LL | ---cargo
+   | ^^^^^^^^
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/frontmatter-after-tokens.rs b/tests/ui/frontmatter/frontmatter-after-tokens.rs
new file mode 100644
index 00000000000..6683991dc4a
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-after-tokens.rs
@@ -0,0 +1,10 @@
+#![feature(frontmatter)]
+
+---
+//~^ ERROR: expected item, found `-`
+// FIXME(frontmatter): make this diagnostic better
+---
+
+// frontmatters must be at the start of a file. This test ensures that.
+
+fn main() {}
diff --git a/tests/ui/frontmatter/frontmatter-after-tokens.stderr b/tests/ui/frontmatter/frontmatter-after-tokens.stderr
new file mode 100644
index 00000000000..919456924d0
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-after-tokens.stderr
@@ -0,0 +1,10 @@
+error: expected item, found `-`
+  --> $DIR/frontmatter-after-tokens.rs:3:1
+   |
+LL | ---
+   | ^ expected item
+   |
+   = note: for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/frontmatter-non-lexible-tokens.rs b/tests/ui/frontmatter/frontmatter-non-lexible-tokens.rs
new file mode 100644
index 00000000000..ea042edef06
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-non-lexible-tokens.rs
@@ -0,0 +1,12 @@
+---uwu
+🏳️‍⚧️
+---
+
+//@ check-pass
+
+#![feature(frontmatter)]
+
+// check that frontmatter blocks can have tokens that are otherwise not accepted by
+// the lexer as Rust code.
+
+fn main() {}
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-1.rs b/tests/ui/frontmatter/frontmatter-whitespace-1.rs
new file mode 100644
index 00000000000..8b6e2d1af84
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-whitespace-1.rs
@@ -0,0 +1,10 @@
+  ---
+//~^ ERROR: invalid preceding whitespace for frontmatter opening
+  ---
+//~^ ERROR: invalid preceding whitespace for frontmatter close
+
+#![feature(frontmatter)]
+
+// check that whitespaces should not precede the frontmatter opening or close.
+
+fn main() {}
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-1.stderr b/tests/ui/frontmatter/frontmatter-whitespace-1.stderr
new file mode 100644
index 00000000000..37ece27acb2
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-whitespace-1.stderr
@@ -0,0 +1,26 @@
+error: invalid preceding whitespace for frontmatter opening
+  --> $DIR/frontmatter-whitespace-1.rs:1:1
+   |
+LL |   ---
+   | ^^^^^
+   |
+note: frontmatter opening should not be preceded by whitespace
+  --> $DIR/frontmatter-whitespace-1.rs:1:1
+   |
+LL |   ---
+   | ^^
+
+error: invalid preceding whitespace for frontmatter close
+  --> $DIR/frontmatter-whitespace-1.rs:3:1
+   |
+LL |   ---
+   | ^^^^^
+   |
+note: frontmatter close should not be preceded by whitespace
+  --> $DIR/frontmatter-whitespace-1.rs:3:1
+   |
+LL |   ---
+   | ^^
+
+error: aborting due to 2 previous errors
+
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-2.rs b/tests/ui/frontmatter/frontmatter-whitespace-2.rs
new file mode 100644
index 00000000000..e8c100849b4
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-whitespace-2.rs
@@ -0,0 +1,15 @@
+---cargo
+
+//@ compile-flags: --crate-type lib
+
+#![feature(frontmatter)]
+
+fn foo(x: i32) -> i32 {
+    ---x
+    //~^ ERROR: invalid preceding whitespace for frontmatter close
+    //~| ERROR: extra characters after frontmatter close are not allowed
+}
+//~^ ERROR: unexpected closing delimiter: `}`
+
+// this test is for the weird case that valid Rust code can have three dashes
+// within them and get treated as a frontmatter close.
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-2.stderr b/tests/ui/frontmatter/frontmatter-whitespace-2.stderr
new file mode 100644
index 00000000000..ada6af0ec04
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-whitespace-2.stderr
@@ -0,0 +1,26 @@
+error: invalid preceding whitespace for frontmatter close
+  --> $DIR/frontmatter-whitespace-2.rs:8:1
+   |
+LL |     ---x
+   | ^^^^^^^^
+   |
+note: frontmatter close should not be preceded by whitespace
+  --> $DIR/frontmatter-whitespace-2.rs:8:1
+   |
+LL |     ---x
+   | ^^^^
+
+error: extra characters after frontmatter close are not allowed
+  --> $DIR/frontmatter-whitespace-2.rs:8:1
+   |
+LL |     ---x
+   | ^^^^^^^^
+
+error: unexpected closing delimiter: `}`
+  --> $DIR/frontmatter-whitespace-2.rs:11:1
+   |
+LL | }
+   | ^ unexpected closing delimiter
+
+error: aborting due to 3 previous errors
+
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-3.rs b/tests/ui/frontmatter/frontmatter-whitespace-3.rs
new file mode 100644
index 00000000000..95e0981e2ae
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-whitespace-3.rs
@@ -0,0 +1,16 @@
+
+
+---cargo   
+---   
+
+// please note the whitespace characters after the first four lines.
+// This ensures that we accept whitespaces before the frontmatter, after
+// the frontmatter opening and the frontmatter close.
+
+//@ check-pass
+// ignore-tidy-end-whitespace
+// ignore-tidy-leading-newlines
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-4.rs b/tests/ui/frontmatter/frontmatter-whitespace-4.rs
new file mode 100644
index 00000000000..3bda3227838
--- /dev/null
+++ b/tests/ui/frontmatter/frontmatter-whitespace-4.rs
@@ -0,0 +1,9 @@
+--- cargo
+---
+
+//@ check-pass
+// A frontmatter infostring can have leading whitespace.
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/included-frontmatter.rs b/tests/ui/frontmatter/included-frontmatter.rs
new file mode 100644
index 00000000000..57616cd1228
--- /dev/null
+++ b/tests/ui/frontmatter/included-frontmatter.rs
@@ -0,0 +1,12 @@
+#![feature(frontmatter)]
+
+//@ check-pass
+
+include!("auxiliary/lib.rs");
+
+// auxiliary/lib.rs contains a frontmatter. Ensure that we can use them in an
+// `include!` macro.
+
+fn main() {
+    foo(1);
+}
diff --git a/tests/ui/frontmatter/infostring-fail.rs b/tests/ui/frontmatter/infostring-fail.rs
new file mode 100644
index 00000000000..91542f62f1a
--- /dev/null
+++ b/tests/ui/frontmatter/infostring-fail.rs
@@ -0,0 +1,9 @@
+---cargo,clippy
+//~^ ERROR: invalid infostring for frontmatter
+---
+
+// infostrings can only be a single identifier.
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/infostring-fail.stderr b/tests/ui/frontmatter/infostring-fail.stderr
new file mode 100644
index 00000000000..6b264abc90f
--- /dev/null
+++ b/tests/ui/frontmatter/infostring-fail.stderr
@@ -0,0 +1,10 @@
+error: invalid infostring for frontmatter
+  --> $DIR/infostring-fail.rs:1:4
+   |
+LL | ---cargo,clippy
+   |    ^^^^^^^^^^^^
+   |
+   = note: frontmatter infostrings must be a single identifier immediately following the opening
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/mismatch-1.rs b/tests/ui/frontmatter/mismatch-1.rs
new file mode 100644
index 00000000000..37e2b0af6b1
--- /dev/null
+++ b/tests/ui/frontmatter/mismatch-1.rs
@@ -0,0 +1,10 @@
+---cargo
+//~^ ERROR: frontmatter close does not match the opening
+----
+
+// there must be the same number of dashes for both the opening and the close
+// of the frontmatter.
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/mismatch-1.stderr b/tests/ui/frontmatter/mismatch-1.stderr
new file mode 100644
index 00000000000..b6e29294d9e
--- /dev/null
+++ b/tests/ui/frontmatter/mismatch-1.stderr
@@ -0,0 +1,16 @@
+error: frontmatter close does not match the opening
+  --> $DIR/mismatch-1.rs:1:1
+   |
+LL |   ---cargo
+   |   ^--
+   |   |
+   |  _the opening here has 3 dashes...
+   | |
+LL | |
+LL | | ----
+   | |_---^
+   |   |
+   |   ...while the close has 4 dashes
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/mismatch-2.rs b/tests/ui/frontmatter/mismatch-2.rs
new file mode 100644
index 00000000000..422abe1d6bc
--- /dev/null
+++ b/tests/ui/frontmatter/mismatch-2.rs
@@ -0,0 +1,8 @@
+----cargo
+//~^ ERROR: frontmatter close does not match the opening
+---cargo
+//~^ ERROR: extra characters after frontmatter close are not allowed
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/mismatch-2.stderr b/tests/ui/frontmatter/mismatch-2.stderr
new file mode 100644
index 00000000000..90bb7b80bce
--- /dev/null
+++ b/tests/ui/frontmatter/mismatch-2.stderr
@@ -0,0 +1,22 @@
+error: frontmatter close does not match the opening
+  --> $DIR/mismatch-2.rs:1:1
+   |
+LL |   ----cargo
+   |   ^---
+   |   |
+   |  _the opening here has 4 dashes...
+   | |
+LL | |
+LL | | ---cargo
+   | |_---____^
+   |   |
+   |   ...while the close has 3 dashes
+
+error: extra characters after frontmatter close are not allowed
+  --> $DIR/mismatch-2.rs:3:1
+   |
+LL | ---cargo
+   | ^^^^^^^^
+
+error: aborting due to 2 previous errors
+
diff --git a/tests/ui/frontmatter/multifrontmatter-2.rs b/tests/ui/frontmatter/multifrontmatter-2.rs
new file mode 100644
index 00000000000..33cc30cb465
--- /dev/null
+++ b/tests/ui/frontmatter/multifrontmatter-2.rs
@@ -0,0 +1,12 @@
+---
+ ---
+//~^ ERROR: invalid preceding whitespace for frontmatter close
+
+ ---
+//~^ ERROR: expected item, found `-`
+// FIXME(frontmatter): make this diagnostic better
+---
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/multifrontmatter-2.stderr b/tests/ui/frontmatter/multifrontmatter-2.stderr
new file mode 100644
index 00000000000..ed9ac4029e2
--- /dev/null
+++ b/tests/ui/frontmatter/multifrontmatter-2.stderr
@@ -0,0 +1,22 @@
+error: invalid preceding whitespace for frontmatter close
+  --> $DIR/multifrontmatter-2.rs:2:1
+   |
+LL |  ---
+   | ^^^^
+   |
+note: frontmatter close should not be preceded by whitespace
+  --> $DIR/multifrontmatter-2.rs:2:1
+   |
+LL |  ---
+   | ^
+
+error: expected item, found `-`
+  --> $DIR/multifrontmatter-2.rs:5:2
+   |
+LL |  ---
+   |  ^ expected item
+   |
+   = note: for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>
+
+error: aborting due to 2 previous errors
+
diff --git a/tests/ui/frontmatter/multifrontmatter.rs b/tests/ui/frontmatter/multifrontmatter.rs
new file mode 100644
index 00000000000..f3afa47bd38
--- /dev/null
+++ b/tests/ui/frontmatter/multifrontmatter.rs
@@ -0,0 +1,13 @@
+---
+---
+
+---
+//~^ ERROR: expected item, found `-`
+// FIXME(frontmatter): make this diagnostic better
+---
+
+// test that we do not parse another frontmatter block after the first one.
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/multifrontmatter.stderr b/tests/ui/frontmatter/multifrontmatter.stderr
new file mode 100644
index 00000000000..2e9d1cee9dd
--- /dev/null
+++ b/tests/ui/frontmatter/multifrontmatter.stderr
@@ -0,0 +1,10 @@
+error: expected item, found `-`
+  --> $DIR/multifrontmatter.rs:4:1
+   |
+LL | ---
+   | ^ expected item
+   |
+   = note: for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/proc-macro-observer.rs b/tests/ui/frontmatter/proc-macro-observer.rs
new file mode 100644
index 00000000000..bafbe912032
--- /dev/null
+++ b/tests/ui/frontmatter/proc-macro-observer.rs
@@ -0,0 +1,12 @@
+//@ check-pass
+//@ proc-macro: makro.rs
+//@ edition: 2021
+
+#![feature(frontmatter)]
+
+makro::check!();
+
+// checks that a proc-macro cannot observe frontmatter tokens.
+// see auxiliary/makro.rs for how it is tested.
+
+fn main() {}
diff --git a/tests/ui/frontmatter/shebang.rs b/tests/ui/frontmatter/shebang.rs
new file mode 100644
index 00000000000..abd983f219b
--- /dev/null
+++ b/tests/ui/frontmatter/shebang.rs
@@ -0,0 +1,13 @@
+#!/usr/bin/env -S cargo -Zscript
+---
+[dependencies]
+clap = "4"
+---
+
+//@ check-pass
+
+// Shebangs on a file can precede a frontmatter.
+
+#![feature(frontmatter)]
+
+fn main () {}
diff --git a/tests/ui/frontmatter/unclosed-1.rs b/tests/ui/frontmatter/unclosed-1.rs
new file mode 100644
index 00000000000..d8b52b3e69c
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-1.rs
@@ -0,0 +1,10 @@
+----cargo
+//~^ ERROR: unclosed frontmatter
+
+// This test checks that the #! characters can help us recover a frontmatter
+// close. There should not be a "missing `main` function" error as the rest
+// are properly parsed.
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/unclosed-1.stderr b/tests/ui/frontmatter/unclosed-1.stderr
new file mode 100644
index 00000000000..04031d12839
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-1.stderr
@@ -0,0 +1,16 @@
+error: unclosed frontmatter
+  --> $DIR/unclosed-1.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |
+   | |_^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-1.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/unclosed-2.rs b/tests/ui/frontmatter/unclosed-2.rs
new file mode 100644
index 00000000000..add266dce5b
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-2.rs
@@ -0,0 +1,15 @@
+----cargo
+//~^ ERROR: unclosed frontmatter
+//~| ERROR: frontmatters are experimental
+
+//@ compile-flags: --crate-type lib
+
+// Leading whitespace on the feature line prevents recovery. However
+// the dashes quoted will not be used for recovery and the entire file
+// should be treated as within the frontmatter block.
+
+ #![feature(frontmatter)]
+
+fn foo() -> &str {
+    "----"
+}
diff --git a/tests/ui/frontmatter/unclosed-2.stderr b/tests/ui/frontmatter/unclosed-2.stderr
new file mode 100644
index 00000000000..0a4022c1557
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-2.stderr
@@ -0,0 +1,31 @@
+error: unclosed frontmatter
+  --> $DIR/unclosed-2.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |     "----"
+LL | | }
+   | |__^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-2.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+
+error[E0658]: frontmatters are experimental
+  --> $DIR/unclosed-2.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |     "----"
+LL | | }
+   | |__^
+   |
+   = note: see issue #136889 <https://github.com/rust-lang/rust/issues/136889> for more information
+   = help: add `#![feature(frontmatter)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 2 previous errors
+
+For more information about this error, try `rustc --explain E0658`.
diff --git a/tests/ui/frontmatter/unclosed-3.rs b/tests/ui/frontmatter/unclosed-3.rs
new file mode 100644
index 00000000000..75f3fbda675
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-3.rs
@@ -0,0 +1,16 @@
+----cargo
+//~^ ERROR: frontmatter close does not match the opening
+
+//@ compile-flags: --crate-type lib
+
+// Unfortunate recovery situation. Not really preventable with improving the
+// recovery strategy, but this type of code is rare enough already.
+
+ #![feature(frontmatter)]
+
+fn foo(x: i32) -> i32 {
+    ---x
+    //~^ ERROR: invalid preceding whitespace for frontmatter close
+    //~| ERROR: extra characters after frontmatter close are not allowed
+}
+//~^ ERROR: unexpected closing delimiter: `}`
diff --git a/tests/ui/frontmatter/unclosed-3.stderr b/tests/ui/frontmatter/unclosed-3.stderr
new file mode 100644
index 00000000000..cd69cb00040
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-3.stderr
@@ -0,0 +1,41 @@
+error: invalid preceding whitespace for frontmatter close
+  --> $DIR/unclosed-3.rs:12:1
+   |
+LL |     ---x
+   | ^^^^^^^^
+   |
+note: frontmatter close should not be preceded by whitespace
+  --> $DIR/unclosed-3.rs:12:1
+   |
+LL |     ---x
+   | ^^^^
+
+error: frontmatter close does not match the opening
+  --> $DIR/unclosed-3.rs:1:1
+   |
+LL |   ----cargo
+   |   ^---
+   |   |
+   |  _the opening here has 4 dashes...
+   | |
+...  |
+LL | | fn foo(x: i32) -> i32 {
+LL | |     ---x
+   | |_---____^
+   |   |
+   |   ...while the close has 3 dashes
+
+error: extra characters after frontmatter close are not allowed
+  --> $DIR/unclosed-3.rs:12:1
+   |
+LL |     ---x
+   | ^^^^^^^^
+
+error: unexpected closing delimiter: `}`
+  --> $DIR/unclosed-3.rs:15:1
+   |
+LL | }
+   | ^ unexpected closing delimiter
+
+error: aborting due to 4 previous errors
+
diff --git a/tests/ui/frontmatter/unclosed-4.rs b/tests/ui/frontmatter/unclosed-4.rs
new file mode 100644
index 00000000000..41f6461db63
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-4.rs
@@ -0,0 +1,9 @@
+----cargo
+//~^ ERROR: unclosed frontmatter
+
+//! Similarly, a module-level content should allow for recovery as well (as
+//! per unclosed-1.rs)
+
+#![feature(frontmatter)]
+
+fn main() {}
diff --git a/tests/ui/frontmatter/unclosed-4.stderr b/tests/ui/frontmatter/unclosed-4.stderr
new file mode 100644
index 00000000000..b3ba56937bb
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-4.stderr
@@ -0,0 +1,16 @@
+error: unclosed frontmatter
+  --> $DIR/unclosed-4.rs:1:1
+   |
+LL | / ----cargo
+LL | |
+LL | |
+   | |_^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-4.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/frontmatter/unclosed-5.rs b/tests/ui/frontmatter/unclosed-5.rs
new file mode 100644
index 00000000000..9abbcfff94c
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-5.rs
@@ -0,0 +1,10 @@
+----cargo
+//~^ ERROR: unclosed frontmatter
+//~| ERROR: frontmatters are experimental
+
+// Similarly, a use statement should allow for recovery as well (as
+// per unclosed-1.rs)
+
+use std::env;
+
+fn main() {}
diff --git a/tests/ui/frontmatter/unclosed-5.stderr b/tests/ui/frontmatter/unclosed-5.stderr
new file mode 100644
index 00000000000..e904014a175
--- /dev/null
+++ b/tests/ui/frontmatter/unclosed-5.stderr
@@ -0,0 +1,29 @@
+error: unclosed frontmatter
+  --> $DIR/unclosed-5.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |
+   | |_^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-5.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+
+error[E0658]: frontmatters are experimental
+  --> $DIR/unclosed-5.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |
+   | |_^
+   |
+   = note: see issue #136889 <https://github.com/rust-lang/rust/issues/136889> for more information
+   = help: add `#![feature(frontmatter)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 2 previous errors
+
+For more information about this error, try `rustc --explain E0658`.