diff --git a/src/librustc_lint/early.rs b/src/librustc_lint/early.rs
index 06987ffa3d569..d891466611ad3 100644
--- a/src/librustc_lint/early.rs
+++ b/src/librustc_lint/early.rs
@@ -55,7 +55,8 @@ impl<'a, T: EarlyLintPass> EarlyContextAndPass<'a, T> {
     where
         F: FnOnce(&mut Self),
     {
-        let push = self.context.builder.push(attrs, &self.context.lint_store);
+        let is_crate_node = id == ast::CRATE_NODE_ID;
+        let push = self.context.builder.push(attrs, &self.context.lint_store, is_crate_node);
         self.check_id(id);
         self.enter_attrs(attrs);
         f(self);
diff --git a/src/librustc_lint/levels.rs b/src/librustc_lint/levels.rs
index 05e7c9a0c780d..f875e2750a5c5 100644
--- a/src/librustc_lint/levels.rs
+++ b/src/librustc_lint/levels.rs
@@ -29,7 +29,7 @@ fn lint_levels(tcx: TyCtxt<'_>, cnum: CrateNum) -> LintLevelMap {
     let mut builder = LintLevelMapBuilder { levels, tcx, store };
     let krate = tcx.hir().krate();
 
-    let push = builder.levels.push(&krate.item.attrs, &store);
+    let push = builder.levels.push(&krate.item.attrs, &store, true);
     builder.levels.register_id(hir::CRATE_HIR_ID);
     for macro_def in krate.exported_macros {
         builder.levels.register_id(macro_def.hir_id);
@@ -109,7 +109,12 @@ impl<'s> LintLevelsBuilder<'s> {
     ///   `#[allow]`
     ///
     /// Don't forget to call `pop`!
-    pub fn push(&mut self, attrs: &[ast::Attribute], store: &LintStore) -> BuilderPush {
+    pub fn push(
+        &mut self,
+        attrs: &[ast::Attribute],
+        store: &LintStore,
+        is_crate_node: bool,
+    ) -> BuilderPush {
         let mut specs = FxHashMap::default();
         let sess = self.sess;
         let bad_attr = |span| struct_span_err!(sess, span, E0452, "malformed lint attribute input");
@@ -333,6 +338,40 @@ impl<'s> LintLevelsBuilder<'s> {
             }
         }
 
+        if !is_crate_node {
+            for (id, &(level, ref src)) in specs.iter() {
+                if !id.lint.crate_level_only {
+                    continue;
+                }
+
+                let (lint_attr_name, lint_attr_span) = match *src {
+                    LintSource::Node(name, span, _) => (name, span),
+                    _ => continue,
+                };
+
+                let lint = builtin::UNUSED_ATTRIBUTES;
+                let (lint_level, lint_src) =
+                    self.sets.get_lint_level(lint, self.cur, Some(&specs), self.sess);
+                struct_lint_level(
+                    self.sess,
+                    lint,
+                    lint_level,
+                    lint_src,
+                    Some(lint_attr_span.into()),
+                    |lint| {
+                        let mut db = lint.build(&format!(
+                            "{}({}) is ignored unless specified at crate level",
+                            level.as_str(),
+                            lint_attr_name
+                        ));
+                        db.emit();
+                    },
+                );
+                // don't set a separate error for every lint in the group
+                break;
+            }
+        }
+
         for (id, &(level, ref src)) in specs.iter() {
             if level == Level::Forbid {
                 continue;
@@ -449,7 +488,8 @@ impl LintLevelMapBuilder<'_, '_> {
     where
         F: FnOnce(&mut Self),
     {
-        let push = self.levels.push(attrs, self.store);
+        let is_crate_hir = id == hir::CRATE_HIR_ID;
+        let push = self.levels.push(attrs, self.store, is_crate_hir);
         if push.changed {
             self.levels.register_id(id);
         }
diff --git a/src/librustc_lint/non_ascii_idents.rs b/src/librustc_lint/non_ascii_idents.rs
index ad02b2637d22f..064b0255397ce 100644
--- a/src/librustc_lint/non_ascii_idents.rs
+++ b/src/librustc_lint/non_ascii_idents.rs
@@ -8,20 +8,23 @@ use std::ops::Deref;
 declare_lint! {
     pub NON_ASCII_IDENTS,
     Allow,
-    "detects non-ASCII identifiers"
+    "detects non-ASCII identifiers",
+    crate_level_only
 }
 
 declare_lint! {
     pub UNCOMMON_CODEPOINTS,
     Warn,
-    "detects uncommon Unicode codepoints in identifiers"
+    "detects uncommon Unicode codepoints in identifiers",
+    crate_level_only
 }
 
 // FIXME: Change this to warn.
 declare_lint! {
     pub CONFUSABLE_IDENTS,
     Allow,
-    "detects visually confusable pairs between identifiers"
+    "detects visually confusable pairs between identifiers",
+    crate_level_only
 }
 
 declare_lint_pass!(NonAsciiIdents => [NON_ASCII_IDENTS, UNCOMMON_CODEPOINTS, CONFUSABLE_IDENTS]);
diff --git a/src/librustc_session/lint.rs b/src/librustc_session/lint.rs
index ffb4579309075..0dcbee08abea1 100644
--- a/src/librustc_session/lint.rs
+++ b/src/librustc_session/lint.rs
@@ -88,6 +88,8 @@ pub struct Lint {
 
     /// `Some` if this lint is feature gated, otherwise `None`.
     pub feature_gate: Option<Symbol>,
+
+    pub crate_level_only: bool,
 }
 
 /// Extra information for a future incompatibility lint.
@@ -111,6 +113,7 @@ impl Lint {
             report_in_external_macro: false,
             future_incompatible: None,
             feature_gate: None,
+            crate_level_only: false,
         }
     }
 
@@ -336,6 +339,7 @@ macro_rules! declare_tool_lint {
             future_incompatible: None,
             is_plugin: true,
             feature_gate: None,
+            crate_level_only: false,
         };
     );
 }
diff --git a/src/librustc_session/lint/builtin.rs b/src/librustc_session/lint/builtin.rs
index bb0d6e1a47ead..e3444bb0e54ce 100644
--- a/src/librustc_session/lint/builtin.rs
+++ b/src/librustc_session/lint/builtin.rs
@@ -17,6 +17,7 @@ declare_lint! {
         reference: "issue #57571 <https://github.com/rust-lang/rust/issues/57571>",
         edition: None,
     };
+    crate_level_only
 }
 
 declare_lint! {
@@ -75,7 +76,8 @@ declare_lint! {
 declare_lint! {
     pub UNUSED_CRATE_DEPENDENCIES,
     Allow,
-    "crate dependencies that are never used"
+    "crate dependencies that are never used",
+    crate_level_only
 }
 
 declare_lint! {
@@ -166,7 +168,8 @@ declare_lint! {
 declare_lint! {
     pub UNKNOWN_CRATE_TYPES,
     Deny,
-    "unknown crate type found in `#[crate_type]` directive"
+    "unknown crate type found in `#[crate_type]` directive",
+    crate_level_only
 }
 
 declare_lint! {
@@ -339,7 +342,8 @@ declare_lint! {
 declare_lint! {
     pub ELIDED_LIFETIMES_IN_PATHS,
     Allow,
-    "hidden lifetime parameters in types are deprecated"
+    "hidden lifetime parameters in types are deprecated",
+    crate_level_only
 }
 
 declare_lint! {
@@ -459,6 +463,7 @@ declare_lint! {
         reference: "issue #52234 <https://github.com/rust-lang/rust/issues/52234>",
         edition: None,
     };
+    crate_level_only
 }
 
 declare_lint! {
diff --git a/src/test/ui/issues/issue-48508.rs b/src/test/ui/issues/issue-48508.rs
index 87965c204ada7..8dc9351260ebc 100644
--- a/src/test/ui/issues/issue-48508.rs
+++ b/src/test/ui/issues/issue-48508.rs
@@ -11,7 +11,7 @@
 // ignore-asmjs wasm2js does not support source maps yet
 
 #![feature(non_ascii_idents)]
-#[allow(uncommon_codepoints)]
+#![allow(uncommon_codepoints)]
 
 #[path = "issue-48508-aux.rs"]
 mod other_file;
diff --git a/src/test/ui/lint/crate_level_only_lint.rs b/src/test/ui/lint/crate_level_only_lint.rs
new file mode 100644
index 0000000000000..d9673faa2142e
--- /dev/null
+++ b/src/test/ui/lint/crate_level_only_lint.rs
@@ -0,0 +1,22 @@
+#![deny(uncommon_codepoints, unused_attributes)]
+
+mod foo {
+#![allow(uncommon_codepoints)]
+//~^ ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+//~| ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+//~| ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+
+#[allow(uncommon_codepoints)]
+//~^ ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+//~| ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+//~| ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+const BAR: f64 = 0.000001;
+
+}
+
+#[allow(uncommon_codepoints)]
+//~^ ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+//~| ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+//~| ERROR allow(uncommon_codepoints) is ignored unless specified at crate level [unused_attributes]
+fn main() {
+}
diff --git a/src/test/ui/lint/crate_level_only_lint.stderr b/src/test/ui/lint/crate_level_only_lint.stderr
new file mode 100644
index 0000000000000..8fb06df2a481a
--- /dev/null
+++ b/src/test/ui/lint/crate_level_only_lint.stderr
@@ -0,0 +1,62 @@
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:4:10
+   |
+LL | #![allow(uncommon_codepoints)]
+   |          ^^^^^^^^^^^^^^^^^^^
+   |
+note: the lint level is defined here
+  --> $DIR/crate_level_only_lint.rs:1:30
+   |
+LL | #![deny(uncommon_codepoints, unused_attributes)]
+   |                              ^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:9:9
+   |
+LL | #[allow(uncommon_codepoints)]
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:17:9
+   |
+LL | #[allow(uncommon_codepoints)]
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:4:10
+   |
+LL | #![allow(uncommon_codepoints)]
+   |          ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:9:9
+   |
+LL | #[allow(uncommon_codepoints)]
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:17:9
+   |
+LL | #[allow(uncommon_codepoints)]
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:4:10
+   |
+LL | #![allow(uncommon_codepoints)]
+   |          ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:9:9
+   |
+LL | #[allow(uncommon_codepoints)]
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: allow(uncommon_codepoints) is ignored unless specified at crate level
+  --> $DIR/crate_level_only_lint.rs:17:9
+   |
+LL | #[allow(uncommon_codepoints)]
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: aborting due to 9 previous errors
+