diff --git a/compiler/rustc_expand/src/mbe.rs b/compiler/rustc_expand/src/mbe.rs
index 9aed307ec93ae..da69b3260f694 100644
--- a/compiler/rustc_expand/src/mbe.rs
+++ b/compiler/rustc_expand/src/mbe.rs
@@ -84,7 +84,7 @@ enum TokenTree {
     /// e.g., `$var`
     MetaVar(Span, Ident),
     /// e.g., `$var:expr`. This is only used in the left hand side of MBE macros.
-    MetaVarDecl(Span, Ident /* name to bind */, NonterminalKind),
+    MetaVarDecl(Span, Ident /* name to bind */, Option<NonterminalKind>),
 }
 
 impl TokenTree {
diff --git a/compiler/rustc_expand/src/mbe/macro_parser.rs b/compiler/rustc_expand/src/mbe/macro_parser.rs
index 92a8f23112679..d2fe7fe10a830 100644
--- a/compiler/rustc_expand/src/mbe/macro_parser.rs
+++ b/compiler/rustc_expand/src/mbe/macro_parser.rs
@@ -378,6 +378,11 @@ fn nameize<I: Iterator<Item = NamedMatch>>(
                     n_rec(sess, next_m, res.by_ref(), ret_val)?;
                 }
             }
+            TokenTree::MetaVarDecl(span, _, None) => {
+                if sess.missing_fragment_specifiers.borrow_mut().remove(&span).is_some() {
+                    return Err((span, "missing fragment specifier".to_string()));
+                }
+            }
             TokenTree::MetaVarDecl(sp, bind_name, _) => match ret_val
                 .entry(MacroRulesNormalizedIdent::new(bind_name))
             {
@@ -437,6 +442,7 @@ fn token_name_eq(t1: &Token, t2: &Token) -> bool {
 ///
 /// A `ParseResult`. Note that matches are kept track of through the items generated.
 fn inner_parse_loop<'root, 'tt>(
+    sess: &ParseSess,
     cur_items: &mut SmallVec<[MatcherPosHandle<'root, 'tt>; 1]>,
     next_items: &mut Vec<MatcherPosHandle<'root, 'tt>>,
     eof_items: &mut SmallVec<[MatcherPosHandle<'root, 'tt>; 1]>,
@@ -554,9 +560,16 @@ fn inner_parse_loop<'root, 'tt>(
                     })));
                 }
 
+                // We need to match a metavar (but the identifier is invalid)... this is an error
+                TokenTree::MetaVarDecl(span, _, None) => {
+                    if sess.missing_fragment_specifiers.borrow_mut().remove(&span).is_some() {
+                        return Error(span, "missing fragment specifier".to_string());
+                    }
+                }
+
                 // We need to match a metavar with a valid ident... call out to the black-box
                 // parser by adding an item to `bb_items`.
-                TokenTree::MetaVarDecl(_, _, kind) => {
+                TokenTree::MetaVarDecl(_, _, Some(kind)) => {
                     // Built-in nonterminals never start with these tokens,
                     // so we can eliminate them from consideration.
                     if Parser::nonterminal_may_begin_with(kind, token) {
@@ -627,6 +640,7 @@ pub(super) fn parse_tt(parser: &mut Cow<'_, Parser<'_>>, ms: &[TokenTree]) -> Na
         // parsing from the black-box parser done. The result is that `next_items` will contain a
         // bunch of possible next matcher positions in `next_items`.
         match inner_parse_loop(
+            parser.sess,
             &mut cur_items,
             &mut next_items,
             &mut eof_items,
@@ -688,7 +702,7 @@ pub(super) fn parse_tt(parser: &mut Cow<'_, Parser<'_>>, ms: &[TokenTree]) -> Na
             let nts = bb_items
                 .iter()
                 .map(|item| match item.top_elts.get_tt(item.idx) {
-                    TokenTree::MetaVarDecl(_, bind, kind) => format!("{} ('{}')", kind, bind),
+                    TokenTree::MetaVarDecl(_, bind, Some(kind)) => format!("{} ('{}')", kind, bind),
                     _ => panic!(),
                 })
                 .collect::<Vec<String>>()
@@ -718,7 +732,7 @@ pub(super) fn parse_tt(parser: &mut Cow<'_, Parser<'_>>, ms: &[TokenTree]) -> Na
             assert_eq!(bb_items.len(), 1);
 
             let mut item = bb_items.pop().unwrap();
-            if let TokenTree::MetaVarDecl(span, _, kind) = item.top_elts.get_tt(item.idx) {
+            if let TokenTree::MetaVarDecl(span, _, Some(kind)) = item.top_elts.get_tt(item.idx) {
                 let match_cur = item.match_cur;
                 let nt = match parser.to_mut().parse_nonterminal(kind) {
                     Err(mut err) => {
diff --git a/compiler/rustc_expand/src/mbe/macro_rules.rs b/compiler/rustc_expand/src/mbe/macro_rules.rs
index f0e6fe39a3c7f..48a622d13efcb 100644
--- a/compiler/rustc_expand/src/mbe/macro_rules.rs
+++ b/compiler/rustc_expand/src/mbe/macro_rules.rs
@@ -400,7 +400,7 @@ pub fn compile_declarative_macro(
     let diag = &sess.parse_sess.span_diagnostic;
     let lhs_nm = Ident::new(sym::lhs, def.span);
     let rhs_nm = Ident::new(sym::rhs, def.span);
-    let tt_spec = NonterminalKind::TT;
+    let tt_spec = Some(NonterminalKind::TT);
 
     // Parse the macro_rules! invocation
     let (macro_rules, body) = match &def.kind {
@@ -577,7 +577,7 @@ fn check_lhs_no_empty_seq(sess: &ParseSess, tts: &[mbe::TokenTree]) -> bool {
             TokenTree::Sequence(span, ref seq) => {
                 if seq.separator.is_none()
                     && seq.tts.iter().all(|seq_tt| match *seq_tt {
-                        TokenTree::MetaVarDecl(_, _, NonterminalKind::Vis) => true,
+                        TokenTree::MetaVarDecl(_, _, Some(NonterminalKind::Vis)) => true,
                         TokenTree::Sequence(_, ref sub_seq) => {
                             sub_seq.kleene.op == mbe::KleeneOp::ZeroOrMore
                                 || sub_seq.kleene.op == mbe::KleeneOp::ZeroOrOne
@@ -960,7 +960,7 @@ fn check_matcher_core(
         // Now `last` holds the complete set of NT tokens that could
         // end the sequence before SUFFIX. Check that every one works with `suffix`.
         for token in &last.tokens {
-            if let TokenTree::MetaVarDecl(_, name, kind) = *token {
+            if let TokenTree::MetaVarDecl(_, name, Some(kind)) = *token {
                 for next_token in &suffix_first.tokens {
                     match is_in_follow(next_token, kind) {
                         IsInFollow::Yes => {}
@@ -1018,7 +1018,7 @@ fn check_matcher_core(
 }
 
 fn token_can_be_followed_by_any(tok: &mbe::TokenTree) -> bool {
-    if let mbe::TokenTree::MetaVarDecl(_, _, kind) = *tok {
+    if let mbe::TokenTree::MetaVarDecl(_, _, Some(kind)) = *tok {
         frag_can_be_followed_by_any(kind)
     } else {
         // (Non NT's can always be followed by anything in matchers.)
@@ -1123,7 +1123,7 @@ fn is_in_follow(tok: &mbe::TokenTree, kind: NonterminalKind) -> IsInFollow {
                         }
                         _ => IsInFollow::No(TOKENS),
                     },
-                    TokenTree::MetaVarDecl(_, _, NonterminalKind::Block) => IsInFollow::Yes,
+                    TokenTree::MetaVarDecl(_, _, Some(NonterminalKind::Block)) => IsInFollow::Yes,
                     _ => IsInFollow::No(TOKENS),
                 }
             }
@@ -1158,7 +1158,7 @@ fn is_in_follow(tok: &mbe::TokenTree, kind: NonterminalKind) -> IsInFollow {
                     TokenTree::MetaVarDecl(
                         _,
                         _,
-                        NonterminalKind::Ident | NonterminalKind::Ty | NonterminalKind::Path,
+                        Some(NonterminalKind::Ident | NonterminalKind::Ty | NonterminalKind::Path),
                     ) => IsInFollow::Yes,
                     _ => IsInFollow::No(TOKENS),
                 }
@@ -1171,7 +1171,8 @@ fn quoted_tt_to_string(tt: &mbe::TokenTree) -> String {
     match *tt {
         mbe::TokenTree::Token(ref token) => pprust::token_to_string(&token),
         mbe::TokenTree::MetaVar(_, name) => format!("${}", name),
-        mbe::TokenTree::MetaVarDecl(_, name, kind) => format!("${}:{}", name, kind),
+        mbe::TokenTree::MetaVarDecl(_, name, Some(kind)) => format!("${}:{}", name, kind),
+        mbe::TokenTree::MetaVarDecl(_, name, None) => format!("${}:", name),
         _ => panic!(
             "unexpected mbe::TokenTree::{{Sequence or Delimited}} \
              in follow set checker"
diff --git a/compiler/rustc_expand/src/mbe/quoted.rs b/compiler/rustc_expand/src/mbe/quoted.rs
index 48db532c78f30..01b11bb979d68 100644
--- a/compiler/rustc_expand/src/mbe/quoted.rs
+++ b/compiler/rustc_expand/src/mbe/quoted.rs
@@ -3,7 +3,7 @@ use crate::mbe::{Delimited, KleeneOp, KleeneToken, SequenceRepetition, TokenTree
 
 use rustc_ast::token::{self, Token};
 use rustc_ast::tokenstream;
-use rustc_ast::NodeId;
+use rustc_ast::{NodeId, DUMMY_NODE_ID};
 use rustc_ast_pretty::pprust;
 use rustc_session::parse::ParseSess;
 use rustc_span::symbol::{kw, Ident};
@@ -73,7 +73,7 @@ pub(super) fn parse(
                                                 .emit();
                                             token::NonterminalKind::Ident
                                         });
-                                    result.push(TokenTree::MetaVarDecl(span, ident, kind));
+                                    result.push(TokenTree::MetaVarDecl(span, ident, Some(kind)));
                                     continue;
                                 }
                                 _ => token.span,
@@ -83,8 +83,11 @@ pub(super) fn parse(
                     }
                     tree => tree.as_ref().map(tokenstream::TokenTree::span).unwrap_or(start_sp),
                 };
-                sess.span_diagnostic.struct_span_err(span, "missing fragment specifier").emit();
-                continue;
+                if node_id != DUMMY_NODE_ID {
+                    // Macros loaded from other crates have dummy node ids.
+                    sess.missing_fragment_specifiers.borrow_mut().insert(span, node_id);
+                }
+                result.push(TokenTree::MetaVarDecl(span, ident, None));
             }
 
             // Not a metavar or no matchers allowed, so just return the tree
diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs
index 66d3765d34739..77d80bb542f14 100644
--- a/compiler/rustc_interface/src/passes.rs
+++ b/compiler/rustc_interface/src/passes.rs
@@ -29,6 +29,7 @@ use rustc_passes::{self, hir_stats, layout_test};
 use rustc_plugin_impl as plugin;
 use rustc_resolve::{Resolver, ResolverArenas};
 use rustc_session::config::{CrateType, Input, OutputFilenames, OutputType, PpMode, PpSourceMode};
+use rustc_session::lint;
 use rustc_session::output::{filename_for_input, filename_for_metadata};
 use rustc_session::search_paths::PathKind;
 use rustc_session::Session;
@@ -307,11 +308,27 @@ fn configure_and_expand_inner<'a>(
             ecx.check_unused_macros();
         });
 
+        let mut missing_fragment_specifiers: Vec<_> = ecx
+            .sess
+            .parse_sess
+            .missing_fragment_specifiers
+            .borrow()
+            .iter()
+            .map(|(span, node_id)| (*span, *node_id))
+            .collect();
+        missing_fragment_specifiers.sort_unstable_by_key(|(span, _)| *span);
+
+        let recursion_limit_hit = ecx.reduced_recursion_limit.is_some();
+
+        for (span, node_id) in missing_fragment_specifiers {
+            let lint = lint::builtin::MISSING_FRAGMENT_SPECIFIER;
+            let msg = "missing fragment specifier";
+            resolver.lint_buffer().buffer_lint(lint, node_id, span, msg);
+        }
         if cfg!(windows) {
             env::set_var("PATH", &old_path);
         }
 
-        let recursion_limit_hit = ecx.reduced_recursion_limit.is_some();
         if recursion_limit_hit {
             // If we hit a recursion limit, exit early to avoid later passes getting overwhelmed
             // with a large AST
diff --git a/compiler/rustc_session/src/lint/builtin.rs b/compiler/rustc_session/src/lint/builtin.rs
index 0cc97fb4541d1..6133f4d94aad4 100644
--- a/compiler/rustc_session/src/lint/builtin.rs
+++ b/compiler/rustc_session/src/lint/builtin.rs
@@ -1229,6 +1229,38 @@ declare_lint! {
     };
 }
 
+declare_lint! {
+    /// The `missing_fragment_specifier` lint is issued when an unused pattern
+    /// in a `macro_rules!` macro definition has a meta-variable (e.g. `$e`)
+    /// that is not followed by a fragment specifier (e.g. `:expr`).
+    ///
+    /// This warning can always be fixed by removing the unused pattern in the
+    /// `macro_rules!` macro definition.
+    ///
+    /// ### Example
+    ///
+    /// ```rust,compile_fail
+    /// macro_rules! foo {
+    ///    ($e) => {}
+    /// }
+    /// ```
+    ///
+    /// {{produces}}
+    ///
+    ///
+    /// ### Explanation
+    ///
+    /// The meta-variable (`$e` above) lacks a fragment specifier, which is a
+    /// malformed input. It can be fixed by adding a fragment specifier.
+    pub MISSING_FRAGMENT_SPECIFIER,
+    Deny,
+    "detects missing fragment specifiers in unused `macro_rules!` patterns",
+    @future_incompatible = FutureIncompatibleInfo {
+        reference: "issue #40107 <https://github.com/rust-lang/rust/issues/40107>",
+        edition: None,
+    };
+}
+
 declare_lint! {
     /// The `late_bound_lifetime_arguments` lint detects generic lifetime
     /// arguments in path segments with late bound lifetime parameters.
@@ -2721,6 +2753,7 @@ declare_lint_pass! {
         CENUM_IMPL_DROP_CAST,
         CONST_EVALUATABLE_UNCHECKED,
         INEFFECTIVE_UNSTABLE_TRAIT_IMPL,
+        MISSING_FRAGMENT_SPECIFIER,
     ]
 }
 
diff --git a/compiler/rustc_session/src/parse.rs b/compiler/rustc_session/src/parse.rs
index 6f10d0c4b89ea..a2bb8c4f91ff4 100644
--- a/compiler/rustc_session/src/parse.rs
+++ b/compiler/rustc_session/src/parse.rs
@@ -119,6 +119,7 @@ pub struct ParseSess {
     pub unstable_features: UnstableFeatures,
     pub config: CrateConfig,
     pub edition: Edition,
+    pub missing_fragment_specifiers: Lock<FxHashMap<Span, NodeId>>,
     /// Places where raw identifiers were used. This is used for feature-gating raw identifiers.
     pub raw_identifier_spans: Lock<Vec<Span>>,
     /// Used to determine and report recursive module inclusions.
@@ -153,6 +154,7 @@ impl ParseSess {
             unstable_features: UnstableFeatures::from_environment(),
             config: FxHashSet::default(),
             edition: ExpnId::root().expn_data().edition,
+            missing_fragment_specifiers: Default::default(),
             raw_identifier_spans: Lock::new(Vec::new()),
             included_mod_stack: Lock::new(vec![]),
             source_map,
diff --git a/src/test/ui/macros/issue-39404.rs b/src/test/ui/issues/issue-39404.rs
similarity index 77%
rename from src/test/ui/macros/issue-39404.rs
rename to src/test/ui/issues/issue-39404.rs
index 054958ba00b8d..2229f2c3900c3 100644
--- a/src/test/ui/macros/issue-39404.rs
+++ b/src/test/ui/issues/issue-39404.rs
@@ -2,5 +2,6 @@
 
 macro_rules! m { ($i) => {} }
 //~^ ERROR missing fragment specifier
+//~| WARN previously accepted
 
 fn main() {}
diff --git a/src/test/ui/issues/issue-39404.stderr b/src/test/ui/issues/issue-39404.stderr
new file mode 100644
index 0000000000000..d2f2a823c2a6b
--- /dev/null
+++ b/src/test/ui/issues/issue-39404.stderr
@@ -0,0 +1,12 @@
+error: missing fragment specifier
+  --> $DIR/issue-39404.rs:3:19
+   |
+LL | macro_rules! m { ($i) => {} }
+   |                   ^^
+   |
+   = note: `#[deny(missing_fragment_specifier)]` on by default
+   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
+   = note: for more information, see issue #40107 <https://github.com/rust-lang/rust/issues/40107>
+
+error: aborting due to previous error
+
diff --git a/src/test/ui/lint/expansion-time.rs b/src/test/ui/lint/expansion-time.rs
index c98ecc980dd3d..6e420c51f0a7f 100644
--- a/src/test/ui/lint/expansion-time.rs
+++ b/src/test/ui/lint/expansion-time.rs
@@ -5,6 +5,10 @@ macro_rules! foo {
     ( $($i:ident)* ) => { $($i)+ }; //~ WARN meta-variable repeats with different Kleene operator
 }
 
+#[warn(missing_fragment_specifier)]
+macro_rules! m { ($i) => {} } //~ WARN missing fragment specifier
+                              //~| WARN this was previously accepted
+
 #[warn(soft_unstable)]
 mod benches {
     #[bench] //~ WARN use of unstable library feature 'test'
diff --git a/src/test/ui/lint/expansion-time.stderr b/src/test/ui/lint/expansion-time.stderr
index bc48d64e7e6b7..e6b5cf67e3904 100644
--- a/src/test/ui/lint/expansion-time.stderr
+++ b/src/test/ui/lint/expansion-time.stderr
@@ -12,14 +12,28 @@ note: the lint level is defined here
 LL | #[warn(meta_variable_misuse)]
    |        ^^^^^^^^^^^^^^^^^^^^
 
+warning: missing fragment specifier
+  --> $DIR/expansion-time.rs:9:19
+   |
+LL | macro_rules! m { ($i) => {} }
+   |                   ^^
+   |
+note: the lint level is defined here
+  --> $DIR/expansion-time.rs:8:8
+   |
+LL | #[warn(missing_fragment_specifier)]
+   |        ^^^^^^^^^^^^^^^^^^^^^^^^^^
+   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
+   = note: for more information, see issue #40107 <https://github.com/rust-lang/rust/issues/40107>
+
 warning: use of unstable library feature 'test': `bench` is a part of custom test frameworks which are unstable
-  --> $DIR/expansion-time.rs:10:7
+  --> $DIR/expansion-time.rs:14:7
    |
 LL |     #[bench]
    |       ^^^^^
    |
 note: the lint level is defined here
-  --> $DIR/expansion-time.rs:8:8
+  --> $DIR/expansion-time.rs:12:8
    |
 LL | #[warn(soft_unstable)]
    |        ^^^^^^^^^^^^^
@@ -33,10 +47,10 @@ LL | 2
    | ^
    |
 note: the lint level is defined here
-  --> $DIR/expansion-time.rs:15:8
+  --> $DIR/expansion-time.rs:19:8
    |
 LL | #[warn(incomplete_include)]
    |        ^^^^^^^^^^^^^^^^^^
 
-warning: 3 warnings emitted
+warning: 4 warnings emitted
 
diff --git a/src/test/ui/macros/issue-39404.stderr b/src/test/ui/macros/issue-39404.stderr
deleted file mode 100644
index 645f06e59d817..0000000000000
--- a/src/test/ui/macros/issue-39404.stderr
+++ /dev/null
@@ -1,8 +0,0 @@
-error: missing fragment specifier
-  --> $DIR/issue-39404.rs:3:19
-   |
-LL | macro_rules! m { ($i) => {} }
-   |                   ^^
-
-error: aborting due to previous error
-
diff --git a/src/test/ui/macros/macro-match-nonterminal.rs b/src/test/ui/macros/macro-match-nonterminal.rs
index 6b023e4137274..b23e5c71c03f0 100644
--- a/src/test/ui/macros/macro-match-nonterminal.rs
+++ b/src/test/ui/macros/macro-match-nonterminal.rs
@@ -2,6 +2,7 @@ macro_rules! test {
     ($a, $b) => {
         //~^ ERROR missing fragment
         //~| ERROR missing fragment
+        //~| WARN this was previously accepted
         ()
     };
 }
diff --git a/src/test/ui/macros/macro-match-nonterminal.stderr b/src/test/ui/macros/macro-match-nonterminal.stderr
index 334d62812cdab..674ce3434aac6 100644
--- a/src/test/ui/macros/macro-match-nonterminal.stderr
+++ b/src/test/ui/macros/macro-match-nonterminal.stderr
@@ -9,6 +9,10 @@ error: missing fragment specifier
    |
 LL |     ($a, $b) => {
    |          ^^
+   |
+   = note: `#[deny(missing_fragment_specifier)]` on by default
+   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
+   = note: for more information, see issue #40107 <https://github.com/rust-lang/rust/issues/40107>
 
 error: aborting due to 2 previous errors
 
diff --git a/src/test/ui/parser/macro/issue-33569.rs b/src/test/ui/parser/macro/issue-33569.rs
index cf81f0480a2a7..80e2d7c6545ba 100644
--- a/src/test/ui/parser/macro/issue-33569.rs
+++ b/src/test/ui/parser/macro/issue-33569.rs
@@ -2,7 +2,6 @@ macro_rules! foo {
     { $+ } => { //~ ERROR expected identifier, found `+`
                 //~^ ERROR missing fragment specifier
         $(x)(y) //~ ERROR expected one of: `*`, `+`, or `?`
-       //~^ ERROR attempted to repeat an expression containing no syntax variables
     }
 }
 
diff --git a/src/test/ui/parser/macro/issue-33569.stderr b/src/test/ui/parser/macro/issue-33569.stderr
index f54efaa6996f2..b4d38d3ce4806 100644
--- a/src/test/ui/parser/macro/issue-33569.stderr
+++ b/src/test/ui/parser/macro/issue-33569.stderr
@@ -4,23 +4,17 @@ error: expected identifier, found `+`
 LL |     { $+ } => {
    |        ^
 
-error: missing fragment specifier
-  --> $DIR/issue-33569.rs:2:8
-   |
-LL |     { $+ } => {
-   |        ^
-
 error: expected one of: `*`, `+`, or `?`
   --> $DIR/issue-33569.rs:4:13
    |
 LL |         $(x)(y)
    |             ^^^
 
-error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
-  --> $DIR/issue-33569.rs:4:10
+error: missing fragment specifier
+  --> $DIR/issue-33569.rs:2:8
    |
-LL |         $(x)(y)
-   |          ^^^
+LL |     { $+ } => {
+   |        ^
 
-error: aborting due to 4 previous errors
+error: aborting due to 3 previous errors