From 8527a3d36985bed55de1832c3c1f3d470720bb0b Mon Sep 17 00:00:00 2001
From: xFrednet <xFrednet@gmail.com>
Date: Sun, 5 Jun 2022 12:33:45 +0200
Subject: [PATCH] Support lint expectations for `--force-warn` lints (RFC 2383)

---
 compiler/rustc_codegen_llvm/src/back/write.rs |  2 +-
 compiler/rustc_codegen_ssa/src/back/write.rs  |  2 +-
 .../src/annotate_snippet_emitter_writer.rs    |  2 +-
 compiler/rustc_errors/src/diagnostic.rs       | 10 +-
 compiler/rustc_errors/src/json.rs             |  2 +-
 compiler/rustc_errors/src/lib.rs              | 96 ++++++++++++-------
 .../rustc_expand/src/proc_macro_server.rs     |  2 +-
 compiler/rustc_lint/src/context.rs            |  4 +-
 compiler/rustc_lint/src/expect.rs             | 16 ++--
 compiler/rustc_lint/src/levels.rs             | 22 +++--
 compiler/rustc_lint_defs/src/lib.rs           | 14 ++-
 compiler/rustc_middle/src/lint.rs             | 16 +++-
 compiler/rustc_session/src/config.rs          |  2 +-
 compiler/rustc_session/src/session.rs         | 15 +++
 .../force_warn_expected_lints_fulfilled.rs    | 48 ++++++++++
 ...force_warn_expected_lints_fulfilled.stderr | 40 ++++++++
 .../force_warn_expected_lints_unfulfilled.rs  | 49 ++++++++++
 ...rce_warn_expected_lints_unfulfilled.stderr | 38 ++++++++
 src/tools/rustfmt/src/parse/session.rs        |  8 +-
 19 files changed, 317 insertions(+), 71 deletions(-)
 create mode 100644 src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.rs
 create mode 100644 src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.stderr
 create mode 100644 src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.rs
 create mode 100644 src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.stderr

diff --git a/compiler/rustc_codegen_llvm/src/back/write.rs b/compiler/rustc_codegen_llvm/src/back/write.rs
index 99e30531c22..dce95e932dd 100644
--- a/compiler/rustc_codegen_llvm/src/back/write.rs
+++ b/compiler/rustc_codegen_llvm/src/back/write.rs
@@ -340,7 +340,7 @@ fn report_inline_asm(
     }
     let level = match level {
         llvm::DiagnosticLevel::Error => Level::Error { lint: false },
-        llvm::DiagnosticLevel::Warning => Level::Warning,
+        llvm::DiagnosticLevel::Warning => Level::Warning(None),
         llvm::DiagnosticLevel::Note | llvm::DiagnosticLevel::Remark => Level::Note,
     };
     cgcx.diag_emitter.inline_asm_error(cookie as u32, msg, level, source);
diff --git a/compiler/rustc_codegen_ssa/src/back/write.rs b/compiler/rustc_codegen_ssa/src/back/write.rs
index 02c7c1a435f..632f07c5c2d 100644
--- a/compiler/rustc_codegen_ssa/src/back/write.rs
+++ b/compiler/rustc_codegen_ssa/src/back/write.rs
@@ -1761,7 +1761,7 @@ impl SharedEmitterMain {
 
                     let mut err = match level {
                         Level::Error { lint: false } => sess.struct_err(msg).forget_guarantee(),
-                        Level::Warning => sess.struct_warn(msg),
+                        Level::Warning(_) => sess.struct_warn(msg),
                         Level::Note => sess.struct_note_without_error(msg),
                         _ => bug!("Invalid inline asm diagnostic level"),
                     };
diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs
index 1f270fcf56b..0fcd61d1e58 100644
--- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs
+++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs
@@ -87,7 +87,7 @@ fn annotation_type_for_level(level: Level) -> AnnotationType {
         Level::Bug | Level::DelayedBug | Level::Fatal | Level::Error { .. } => {
             AnnotationType::Error
         }
-        Level::Warning => AnnotationType::Warning,
+        Level::Warning(_) => AnnotationType::Warning,
         Level::Note | Level::OnceNote => AnnotationType::Note,
         Level::Help => AnnotationType::Help,
         // FIXME(#59346): Not sure how to map this level
diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs
index eaceecc1667..e559053bdfb 100644
--- a/compiler/rustc_errors/src/diagnostic.rs
+++ b/compiler/rustc_errors/src/diagnostic.rs
@@ -208,7 +208,7 @@ impl Diagnostic {
             | Level::Error { .. }
             | Level::FailureNote => true,
 
-            Level::Warning
+            Level::Warning(_)
             | Level::Note
             | Level::OnceNote
             | Level::Help
@@ -221,7 +221,9 @@ impl Diagnostic {
         &mut self,
         unstable_to_stable: &FxHashMap<LintExpectationId, LintExpectationId>,
     ) {
-        if let Level::Expect(expectation_id) = &mut self.level {
+        if let Level::Expect(expectation_id) | Level::Warning(Some(expectation_id)) =
+            &mut self.level
+        {
             if expectation_id.is_stable() {
                 return;
             }
@@ -445,7 +447,7 @@ impl Diagnostic {
 
     /// Add a warning attached to this diagnostic.
     pub fn warn(&mut self, msg: impl Into<SubdiagnosticMessage>) -> &mut Self {
-        self.sub(Level::Warning, msg, MultiSpan::new(), None);
+        self.sub(Level::Warning(None), msg, MultiSpan::new(), None);
         self
     }
 
@@ -456,7 +458,7 @@ impl Diagnostic {
         sp: S,
         msg: impl Into<SubdiagnosticMessage>,
     ) -> &mut Self {
-        self.sub(Level::Warning, msg, sp.into(), None);
+        self.sub(Level::Warning(None), msg, sp.into(), None);
         self
     }
 
diff --git a/compiler/rustc_errors/src/json.rs b/compiler/rustc_errors/src/json.rs
index fff35ac6ac8..d4d1491c169 100644
--- a/compiler/rustc_errors/src/json.rs
+++ b/compiler/rustc_errors/src/json.rs
@@ -154,7 +154,7 @@ impl Emitter for JsonEmitter {
             .into_iter()
             .map(|mut diag| {
                 if diag.level == crate::Level::Allow {
-                    diag.level = crate::Level::Warning;
+                    diag.level = crate::Level::Warning(None);
                 }
                 FutureBreakageItem { diagnostic: Diagnostic::from_errors_diagnostic(&diag, self) }
             })
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index 83fe2a2df89..1f6f271899d 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -658,6 +658,23 @@ impl Handler {
         result
     }
 
+    /// Construct a builder at the `Warning` level at the given `span` and with the `msg`.
+    /// The `id` is used for lint emissions which should also fulfill a lint expectation.
+    ///
+    /// Attempting to `.emit()` the builder will only emit if either:
+    /// * `can_emit_warnings` is `true`
+    /// * `is_force_warn` was set in `DiagnosticId::Lint`
+    pub fn struct_span_warn_with_expectation(
+        &self,
+        span: impl Into<MultiSpan>,
+        msg: impl Into<DiagnosticMessage>,
+        id: LintExpectationId,
+    ) -> DiagnosticBuilder<'_, ()> {
+        let mut result = self.struct_warn_with_expectation(msg, id);
+        result.set_span(span);
+        result
+    }
+
     /// Construct a builder at the `Allow` level at the given `span` and with the `msg`.
     pub fn struct_span_allow(
         &self,
@@ -688,7 +705,21 @@ impl Handler {
     /// * `can_emit_warnings` is `true`
     /// * `is_force_warn` was set in `DiagnosticId::Lint`
     pub fn struct_warn(&self, msg: impl Into<DiagnosticMessage>) -> DiagnosticBuilder<'_, ()> {
-        DiagnosticBuilder::new(self, Level::Warning, msg)
+        DiagnosticBuilder::new(self, Level::Warning(None), msg)
+    }
+
+    /// Construct a builder at the `Warning` level with the `msg`. The `id` is used for
+    /// lint emissions which should also fulfill a lint expectation.
+    ///
+    /// Attempting to `.emit()` the builder will only emit if either:
+    /// * `can_emit_warnings` is `true`
+    /// * `is_force_warn` was set in `DiagnosticId::Lint`
+    pub fn struct_warn_with_expectation(
+        &self,
+        msg: impl Into<DiagnosticMessage>,
+        id: LintExpectationId,
+    ) -> DiagnosticBuilder<'_, ()> {
+        DiagnosticBuilder::new(self, Level::Warning(Some(id)), msg)
     }
 
     /// Construct a builder at the `Allow` level with the `msg`.
@@ -842,7 +873,7 @@ impl Handler {
     }
 
     pub fn span_warn(&self, span: impl Into<MultiSpan>, msg: impl Into<DiagnosticMessage>) {
-        self.emit_diag_at_span(Diagnostic::new(Warning, msg), span);
+        self.emit_diag_at_span(Diagnostic::new(Warning(None), msg), span);
     }
 
     pub fn span_warn_with_code(
@@ -851,7 +882,7 @@ impl Handler {
         msg: impl Into<DiagnosticMessage>,
         code: DiagnosticId,
     ) {
-        self.emit_diag_at_span(Diagnostic::new_with_code(Warning, Some(code), msg), span);
+        self.emit_diag_at_span(Diagnostic::new_with_code(Warning(None), Some(code), msg), span);
     }
 
     pub fn span_bug(&self, span: impl Into<MultiSpan>, msg: impl Into<DiagnosticMessage>) -> ! {
@@ -905,7 +936,7 @@ impl Handler {
     }
 
     pub fn warn(&self, msg: impl Into<DiagnosticMessage>) {
-        let mut db = DiagnosticBuilder::new(self, Warning, msg);
+        let mut db = DiagnosticBuilder::new(self, Warning(None), msg);
         db.emit();
     }
 
@@ -1010,13 +1041,10 @@ impl Handler {
             for mut diag in diags.into_iter() {
                 diag.update_unstable_expectation_id(unstable_to_stable);
 
-                let stable_id = diag
-                    .level
-                    .get_expectation_id()
-                    .expect("all diagnostics inside `unstable_expect_diagnostics` must have a `LintExpectationId`");
-                inner.fulfilled_expectations.insert(stable_id);
-
-                (*TRACK_DIAGNOSTICS)(&diag);
+                // Here the diagnostic is given back to `emit_diagnostic` where it was first
+                // intercepted. Now it should be processed as usual, since the unstable expectation
+                // id is now stable.
+                inner.emit_diagnostic(&mut diag);
             }
         }
 
@@ -1066,6 +1094,15 @@ impl HandlerInner {
 
     // FIXME(eddyb) this should ideally take `diagnostic` by value.
     fn emit_diagnostic(&mut self, diagnostic: &mut Diagnostic) -> Option<ErrorGuaranteed> {
+        // The `LintExpectationId` can be stable or unstable depending on when it was created.
+        // Diagnostics created before the definition of `HirId`s are unstable and can not yet
+        // be stored. Instead, they are buffered until the `LintExpectationId` is replaced by
+        // a stable one by the `LintLevelsBuilder`.
+        if let Some(LintExpectationId::Unstable { .. }) = diagnostic.level.get_expectation_id() {
+            self.unstable_expect_diagnostics.push(diagnostic.clone());
+            return None;
+        }
+
         if diagnostic.level == Level::DelayedBug {
             // FIXME(eddyb) this should check for `has_errors` and stop pushing
             // once *any* errors were emitted (and truncate `delayed_span_bugs`
@@ -1082,7 +1119,12 @@ impl HandlerInner {
             self.future_breakage_diagnostics.push(diagnostic.clone());
         }
 
-        if diagnostic.level == Warning
+        if let Some(expectation_id) = diagnostic.level.get_expectation_id() {
+            self.suppressed_expected_diag = true;
+            self.fulfilled_expectations.insert(expectation_id);
+        }
+
+        if matches!(diagnostic.level, Warning(_))
             && !self.flags.can_emit_warnings
             && !diagnostic.is_force_warn()
         {
@@ -1092,22 +1134,9 @@ impl HandlerInner {
             return None;
         }
 
-        // The `LintExpectationId` can be stable or unstable depending on when it was created.
-        // Diagnostics created before the definition of `HirId`s are unstable and can not yet
-        // be stored. Instead, they are buffered until the `LintExpectationId` is replaced by
-        // a stable one by the `LintLevelsBuilder`.
-        if let Level::Expect(LintExpectationId::Unstable { .. }) = diagnostic.level {
-            self.unstable_expect_diagnostics.push(diagnostic.clone());
-            return None;
-        }
-
         (*TRACK_DIAGNOSTICS)(diagnostic);
 
-        if let Level::Expect(expectation_id) = diagnostic.level {
-            self.suppressed_expected_diag = true;
-            self.fulfilled_expectations.insert(expectation_id);
-            return None;
-        } else if diagnostic.level == Allow {
+        if matches!(diagnostic.level, Level::Expect(_) | Level::Allow) {
             return None;
         }
 
@@ -1144,7 +1173,7 @@ impl HandlerInner {
             self.emitter.emit_diagnostic(&diagnostic);
             if diagnostic.is_error() {
                 self.deduplicated_err_count += 1;
-            } else if diagnostic.level == Warning {
+            } else if let Warning(_) = diagnostic.level {
                 self.deduplicated_warn_count += 1;
             }
         }
@@ -1197,7 +1226,7 @@ impl HandlerInner {
         match (errors.len(), warnings.len()) {
             (0, 0) => return,
             (0, _) => self.emitter.emit_diagnostic(&Diagnostic::new(
-                Level::Warning,
+                Level::Warning(None),
                 DiagnosticMessage::Str(warnings),
             )),
             (_, 0) => {
@@ -1430,7 +1459,10 @@ pub enum Level {
         /// If this error comes from a lint, don't abort compilation even when abort_if_errors() is called.
         lint: bool,
     },
-    Warning,
+    /// This [`LintExpectationId`] is used for expected lint diagnostics, which should
+    /// also emit a warning due to the `force-warn` flag. In all other cases this should
+    /// be `None`.
+    Warning(Option<LintExpectationId>),
     Note,
     /// A note that is only emitted once.
     OnceNote,
@@ -1453,7 +1485,7 @@ impl Level {
             Bug | DelayedBug | Fatal | Error { .. } => {
                 spec.set_fg(Some(Color::Red)).set_intense(true);
             }
-            Warning => {
+            Warning(_) => {
                 spec.set_fg(Some(Color::Yellow)).set_intense(cfg!(windows));
             }
             Note | OnceNote => {
@@ -1472,7 +1504,7 @@ impl Level {
         match self {
             Bug | DelayedBug => "error: internal compiler error",
             Fatal | Error { .. } => "error",
-            Warning => "warning",
+            Warning(_) => "warning",
             Note | OnceNote => "note",
             Help => "help",
             FailureNote => "failure-note",
@@ -1487,7 +1519,7 @@ impl Level {
 
     pub fn get_expectation_id(&self) -> Option<LintExpectationId> {
         match self {
-            Level::Expect(id) => Some(*id),
+            Level::Expect(id) | Level::Warning(Some(id)) => Some(*id),
             _ => None,
         }
     }
diff --git a/compiler/rustc_expand/src/proc_macro_server.rs b/compiler/rustc_expand/src/proc_macro_server.rs
index d4407c03d03..e7ce9e7f1b7 100644
--- a/compiler/rustc_expand/src/proc_macro_server.rs
+++ b/compiler/rustc_expand/src/proc_macro_server.rs
@@ -267,7 +267,7 @@ impl ToInternal<rustc_errors::Level> for Level {
     fn to_internal(self) -> rustc_errors::Level {
         match self {
             Level::Error => rustc_errors::Level::Error { lint: false },
-            Level::Warning => rustc_errors::Level::Warning,
+            Level::Warning => rustc_errors::Level::Warning(None),
             Level::Note => rustc_errors::Level::Note,
             Level::Help => rustc_errors::Level::Help,
             _ => unreachable!("unknown proc_macro::Level variant: {:?}", self),
diff --git a/compiler/rustc_lint/src/context.rs b/compiler/rustc_lint/src/context.rs
index 2c6bdef361a..d1950ed9aee 100644
--- a/compiler/rustc_lint/src/context.rs
+++ b/compiler/rustc_lint/src/context.rs
@@ -324,7 +324,7 @@ impl LintStore {
         registered_tools: &RegisteredTools,
     ) {
         let (tool_name, lint_name_only) = parse_lint_and_tool_name(lint_name);
-        if lint_name_only == crate::WARNINGS.name_lower() && level == Level::ForceWarn {
+        if lint_name_only == crate::WARNINGS.name_lower() && matches!(level, Level::ForceWarn(_)) {
             struct_span_err!(
                 sess,
                 DUMMY_SP,
@@ -375,7 +375,7 @@ impl LintStore {
                 match level {
                     Level::Allow => "-A",
                     Level::Warn => "-W",
-                    Level::ForceWarn => "--force-warn",
+                    Level::ForceWarn(_) => "--force-warn",
                     Level::Deny => "-D",
                     Level::Forbid => "-F",
                     Level::Expect(_) => {
diff --git a/compiler/rustc_lint/src/expect.rs b/compiler/rustc_lint/src/expect.rs
index dc48ac0a618..95e3125045d 100644
--- a/compiler/rustc_lint/src/expect.rs
+++ b/compiler/rustc_lint/src/expect.rs
@@ -19,16 +19,16 @@ fn check_expectations(tcx: TyCtxt<'_>, tool_filter: Option<Symbol>) {
     let lint_expectations = &tcx.lint_levels(()).lint_expectations;
 
     for (id, expectation) in lint_expectations {
-        if !fulfilled_expectations.contains(id)
-            && tool_filter.map_or(true, |filter| expectation.lint_tool == Some(filter))
-        {
-            // This check will always be true, since `lint_expectations` only
-            // holds stable ids
-            if let LintExpectationId::Stable { hir_id, .. } = id {
+        // This check will always be true, since `lint_expectations` only
+        // holds stable ids
+        if let LintExpectationId::Stable { hir_id, .. } = id {
+            if !fulfilled_expectations.contains(&id)
+                && tool_filter.map_or(true, |filter| expectation.lint_tool == Some(filter))
+            {
                 emit_unfulfilled_expectation_lint(tcx, *hir_id, expectation);
-            } else {
-                unreachable!("at this stage all `LintExpectationId`s are stable");
             }
+        } else {
+            unreachable!("at this stage all `LintExpectationId`s are stable");
         }
     }
 }
diff --git a/compiler/rustc_lint/src/levels.rs b/compiler/rustc_lint/src/levels.rs
index 54f2c725279..0c7b9a7ccb0 100644
--- a/compiler/rustc_lint/src/levels.rs
+++ b/compiler/rustc_lint/src/levels.rs
@@ -117,7 +117,9 @@ impl<'s> LintLevelsBuilder<'s> {
             };
             for id in ids {
                 // ForceWarn and Forbid cannot be overridden
-                if let Some((Level::ForceWarn | Level::Forbid, _)) = self.current_specs().get(&id) {
+                if let Some((Level::ForceWarn(_) | Level::Forbid, _)) =
+                    self.current_specs().get(&id)
+                {
                     continue;
                 }
 
@@ -226,11 +228,18 @@ impl<'s> LintLevelsBuilder<'s> {
             return;
         }
 
-        if let Level::ForceWarn = old_level {
-            self.current_specs_mut().insert(id, (old_level, old_src));
-        } else {
-            self.current_specs_mut().insert(id, (level, src));
-        }
+        match (old_level, level) {
+            // If the new level is an expectation store it in `ForceWarn`
+            (Level::ForceWarn(_), Level::Expect(expectation_id)) => self
+                .current_specs_mut()
+                .insert(id, (Level::ForceWarn(Some(expectation_id)), old_src)),
+            // Keep `ForceWarn` level but drop the expectation
+            (Level::ForceWarn(_), _) => {
+                self.current_specs_mut().insert(id, (Level::ForceWarn(None), old_src))
+            }
+            // Set the lint level as normal
+            _ => self.current_specs_mut().insert(id, (level, src)),
+        };
     }
 
     /// Pushes a list of AST lint attributes onto this context.
@@ -269,6 +278,7 @@ impl<'s> LintLevelsBuilder<'s> {
 
             let level = match Level::from_attr(attr) {
                 None => continue,
+                // This is the only lint level with a `LintExpectationId` that can be created from an attribute
                 Some(Level::Expect(unstable_id)) if let Some(hir_id) = source_hir_id => {
                     let stable_id = self.create_stable_id(unstable_id, hir_id, attr_index);
 
diff --git a/compiler/rustc_lint_defs/src/lib.rs b/compiler/rustc_lint_defs/src/lib.rs
index 913dc58a102..cb1c6f40987 100644
--- a/compiler/rustc_lint_defs/src/lib.rs
+++ b/compiler/rustc_lint_defs/src/lib.rs
@@ -162,13 +162,19 @@ pub enum Level {
     ///
     /// See RFC 2383.
     ///
-    /// The `LintExpectationId` is used to later link a lint emission to the actual
+    /// The [`LintExpectationId`] is used to later link a lint emission to the actual
     /// expectation. It can be ignored in most cases.
     Expect(LintExpectationId),
     /// The `warn` level will produce a warning if the lint was violated, however the
     /// compiler will continue with its execution.
     Warn,
-    ForceWarn,
+    /// This lint level is a special case of [`Warn`], that can't be overridden. This is used
+    /// to ensure that a lint can't be suppressed. This lint level can currently only be set
+    /// via the console and is therefore session specific.
+    ///
+    /// The [`LintExpectationId`] is intended to fulfill expectations marked via the
+    /// `#[expect]` attribute, that will still be suppressed due to the level.
+    ForceWarn(Option<LintExpectationId>),
     /// The `deny` level will produce an error and stop further execution after the lint
     /// pass is complete.
     Deny,
@@ -184,7 +190,7 @@ impl Level {
             Level::Allow => "allow",
             Level::Expect(_) => "expect",
             Level::Warn => "warn",
-            Level::ForceWarn => "force-warn",
+            Level::ForceWarn(_) => "force-warn",
             Level::Deny => "deny",
             Level::Forbid => "forbid",
         }
@@ -219,7 +225,7 @@ impl Level {
 
     pub fn is_error(self) -> bool {
         match self {
-            Level::Allow | Level::Expect(_) | Level::Warn | Level::ForceWarn => false,
+            Level::Allow | Level::Expect(_) | Level::Warn | Level::ForceWarn(_) => false,
             Level::Deny | Level::Forbid => true,
         }
     }
diff --git a/compiler/rustc_middle/src/lint.rs b/compiler/rustc_middle/src/lint.rs
index c7c5f56867a..215d8decf2a 100644
--- a/compiler/rustc_middle/src/lint.rs
+++ b/compiler/rustc_middle/src/lint.rs
@@ -115,7 +115,7 @@ impl LintLevelSets {
 
         // Ensure that we never exceed the `--cap-lints` argument
         // unless the source is a --force-warn
-        level = if let LintLevelSource::CommandLine(_, Level::ForceWarn) = src {
+        level = if let LintLevelSource::CommandLine(_, Level::ForceWarn(_)) = src {
             level
         } else {
             cmp::min(level, self.lint_cap)
@@ -266,7 +266,7 @@ pub fn explain_lint_level_source(
                 Level::Deny => "-D",
                 Level::Forbid => "-F",
                 Level::Allow => "-A",
-                Level::ForceWarn => "--force-warn",
+                Level::ForceWarn(_) => "--force-warn",
                 Level::Expect(_) => {
                     unreachable!("the expect level does not have a commandline flag")
                 }
@@ -352,8 +352,14 @@ pub fn struct_lint_level<'s, 'd>(
                 // create a `DiagnosticBuilder` and continue as we would for warnings.
                 sess.struct_expect("", expect_id)
             }
-            (Level::Warn | Level::ForceWarn, Some(span)) => sess.struct_span_warn(span, ""),
-            (Level::Warn | Level::ForceWarn, None) => sess.struct_warn(""),
+            (Level::ForceWarn(Some(expect_id)), Some(span)) => {
+                sess.struct_span_warn_with_expectation(span, "", expect_id)
+            }
+            (Level::ForceWarn(Some(expect_id)), None) => {
+                sess.struct_warn_with_expectation("", expect_id)
+            }
+            (Level::Warn | Level::ForceWarn(None), Some(span)) => sess.struct_span_warn(span, ""),
+            (Level::Warn | Level::ForceWarn(None), None) => sess.struct_warn(""),
             (Level::Deny | Level::Forbid, Some(span)) => {
                 let mut builder = sess.diagnostic().struct_err_lint("");
                 builder.set_span(span);
@@ -398,7 +404,7 @@ pub fn struct_lint_level<'s, 'd>(
         explain_lint_level_source(lint, level, src, &mut err);
 
         let name = lint.name_lower();
-        let is_force_warn = matches!(level, Level::ForceWarn);
+        let is_force_warn = matches!(level, Level::ForceWarn(_));
         err.code(DiagnosticId::Lint { name, has_future_breakage, is_force_warn });
 
         if let Some(future_incompatible) = future_incompatible {
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index c4a67006504..89d724626cc 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -1432,7 +1432,7 @@ pub fn get_cmd_lint_options(
     let mut lint_opts_with_position = vec![];
     let mut describe_lints = false;
 
-    for level in [lint::Allow, lint::Warn, lint::ForceWarn, lint::Deny, lint::Forbid] {
+    for level in [lint::Allow, lint::Warn, lint::ForceWarn(None), lint::Deny, lint::Forbid] {
         for (arg_pos, lint_name) in matches.opt_strs_pos(level.as_str()) {
             if lint_name == "help" {
                 describe_lints = true;
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index b2c23cda6aa..06f3e59f9f7 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -286,6 +286,14 @@ impl Session {
     ) -> DiagnosticBuilder<'_, ()> {
         self.diagnostic().struct_span_warn(sp, msg)
     }
+    pub fn struct_span_warn_with_expectation<S: Into<MultiSpan>>(
+        &self,
+        sp: S,
+        msg: impl Into<DiagnosticMessage>,
+        id: lint::LintExpectationId,
+    ) -> DiagnosticBuilder<'_, ()> {
+        self.diagnostic().struct_span_warn_with_expectation(sp, msg, id)
+    }
     pub fn struct_span_warn_with_code<S: Into<MultiSpan>>(
         &self,
         sp: S,
@@ -297,6 +305,13 @@ impl Session {
     pub fn struct_warn(&self, msg: impl Into<DiagnosticMessage>) -> DiagnosticBuilder<'_, ()> {
         self.diagnostic().struct_warn(msg)
     }
+    pub fn struct_warn_with_expectation(
+        &self,
+        msg: impl Into<DiagnosticMessage>,
+        id: lint::LintExpectationId,
+    ) -> DiagnosticBuilder<'_, ()> {
+        self.diagnostic().struct_warn_with_expectation(msg, id)
+    }
     pub fn struct_span_allow<S: Into<MultiSpan>>(
         &self,
         sp: S,
diff --git a/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.rs b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.rs
new file mode 100644
index 00000000000..a3c3933d700
--- /dev/null
+++ b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.rs
@@ -0,0 +1,48 @@
+// compile-flags: --force-warn while_true
+// compile-flags: --force-warn unused_variables
+// compile-flags: --force-warn unused_mut
+// check-pass
+
+#![feature(lint_reasons)]
+
+fn expect_early_pass_lint() {
+    #[expect(while_true)]
+    while true {
+        //~^ WARNING denote infinite loops with `loop { ... }` [while_true]
+        //~| NOTE requested on the command line with `--force-warn while-true`
+        //~| HELP use `loop`
+        println!("I never stop")
+    }
+}
+
+#[expect(unused_variables, reason="<this should fail and display this reason>")]
+fn check_specific_lint() {
+    let x = 2;
+    //~^ WARNING unused variable: `x` [unused_variables]
+    //~| NOTE requested on the command line with `--force-warn unused-variables`
+    //~| HELP if this is intentional, prefix it with an underscore
+}
+
+#[expect(unused)]
+fn check_multiple_lints_with_lint_group() {
+    let fox_name = "Sir Nibbles";
+    //~^ WARNING unused variable: `fox_name` [unused_variables]
+    //~| HELP if this is intentional, prefix it with an underscore
+
+    let mut what_does_the_fox_say = "*ding* *deng* *dung*";
+    //~^ WARNING variable does not need to be mutable [unused_mut]
+    //~| NOTE requested on the command line with `--force-warn unused-mut`
+    //~| HELP remove this `mut`
+
+    println!("The fox says: {what_does_the_fox_say}");
+}
+
+#[allow(unused_variables)]
+fn check_expect_overrides_allow_lint_level() {
+    #[expect(unused_variables)]
+    let this_should_fulfill_the_expectation = "The `#[allow]` has no power here";
+    //~^ WARNING unused variable: `this_should_fulfill_the_expectation` [unused_variables]
+    //~| HELP if this is intentional, prefix it with an underscore
+}
+
+fn main() {}
diff --git a/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.stderr b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.stderr
new file mode 100644
index 00000000000..06befcbb511
--- /dev/null
+++ b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.stderr
@@ -0,0 +1,40 @@
+warning: denote infinite loops with `loop { ... }`
+  --> $DIR/force_warn_expected_lints_fulfilled.rs:10:5
+   |
+LL |     while true {
+   |     ^^^^^^^^^^ help: use `loop`
+   |
+   = note: requested on the command line with `--force-warn while-true`
+
+warning: unused variable: `x`
+  --> $DIR/force_warn_expected_lints_fulfilled.rs:20:9
+   |
+LL |     let x = 2;
+   |         ^ help: if this is intentional, prefix it with an underscore: `_x`
+   |
+   = note: requested on the command line with `--force-warn unused-variables`
+
+warning: unused variable: `fox_name`
+  --> $DIR/force_warn_expected_lints_fulfilled.rs:28:9
+   |
+LL |     let fox_name = "Sir Nibbles";
+   |         ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_fox_name`
+
+warning: unused variable: `this_should_fulfill_the_expectation`
+  --> $DIR/force_warn_expected_lints_fulfilled.rs:43:9
+   |
+LL |     let this_should_fulfill_the_expectation = "The `#[allow]` has no power here";
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_this_should_fulfill_the_expectation`
+
+warning: variable does not need to be mutable
+  --> $DIR/force_warn_expected_lints_fulfilled.rs:32:9
+   |
+LL |     let mut what_does_the_fox_say = "*ding* *deng* *dung*";
+   |         ----^^^^^^^^^^^^^^^^^^^^^
+   |         |
+   |         help: remove this `mut`
+   |
+   = note: requested on the command line with `--force-warn unused-mut`
+
+warning: 5 warnings emitted
+
diff --git a/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.rs b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.rs
new file mode 100644
index 00000000000..080e300232b
--- /dev/null
+++ b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.rs
@@ -0,0 +1,49 @@
+// compile-flags: --force-warn while_true
+// compile-flags: --force-warn unused_variables
+// compile-flags: --force-warn unused_mut
+// check-pass
+
+#![feature(lint_reasons)]
+
+fn expect_early_pass_lint(terminate: bool) {
+    #[expect(while_true)]
+    //~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations]
+    //~| NOTE `#[warn(unfulfilled_lint_expectations)]` on by default
+    while !terminate {
+        println!("Do you know what a spin lock is?")
+    }
+}
+
+#[expect(unused_variables, reason="<this should fail and display this reason>")]
+//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations]
+//~| NOTE <this should fail and display this reason>
+fn check_specific_lint() {
+    let _x = 2;
+}
+
+#[expect(unused)]
+//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations]
+fn check_multiple_lints_with_lint_group() {
+    let fox_name = "Sir Nibbles";
+
+    let what_does_the_fox_say = "*ding* *deng* *dung*";
+
+    println!("The fox says: {what_does_the_fox_say}");
+    println!("~ {fox_name}")
+}
+
+
+#[expect(unused)]
+//~^ WARNING this lint expectation is unfulfilled [unfulfilled_lint_expectations]
+fn check_overridden_expectation_lint_level() {
+    #[allow(unused_variables)]
+    let this_should_not_fulfill_the_expectation = "maybe";
+    //~^ WARNING unused variable: `this_should_not_fulfill_the_expectation` [unused_variables]
+    //~| NOTE requested on the command line with `--force-warn unused-variables`
+    //~| HELP if this is intentional, prefix it with an underscore
+}
+
+fn main() {
+    check_multiple_lints_with_lint_group();
+    check_overridden_expectation_lint_level();
+}
diff --git a/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.stderr b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.stderr
new file mode 100644
index 00000000000..c74fabe27dc
--- /dev/null
+++ b/src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.stderr
@@ -0,0 +1,38 @@
+warning: unused variable: `this_should_not_fulfill_the_expectation`
+  --> $DIR/force_warn_expected_lints_unfulfilled.rs:40:9
+   |
+LL |     let this_should_not_fulfill_the_expectation = "maybe";
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_this_should_not_fulfill_the_expectation`
+   |
+   = note: requested on the command line with `--force-warn unused-variables`
+
+warning: this lint expectation is unfulfilled
+  --> $DIR/force_warn_expected_lints_unfulfilled.rs:9:14
+   |
+LL |     #[expect(while_true)]
+   |              ^^^^^^^^^^
+   |
+   = note: `#[warn(unfulfilled_lint_expectations)]` on by default
+
+warning: this lint expectation is unfulfilled
+  --> $DIR/force_warn_expected_lints_unfulfilled.rs:17:10
+   |
+LL | #[expect(unused_variables, reason="<this should fail and display this reason>")]
+   |          ^^^^^^^^^^^^^^^^
+   |
+   = note: <this should fail and display this reason>
+
+warning: this lint expectation is unfulfilled
+  --> $DIR/force_warn_expected_lints_unfulfilled.rs:24:10
+   |
+LL | #[expect(unused)]
+   |          ^^^^^^
+
+warning: this lint expectation is unfulfilled
+  --> $DIR/force_warn_expected_lints_unfulfilled.rs:36:10
+   |
+LL | #[expect(unused)]
+   |          ^^^^^^
+
+warning: 5 warnings emitted
+
diff --git a/src/tools/rustfmt/src/parse/session.rs b/src/tools/rustfmt/src/parse/session.rs
index 4408e20747a..8b706b8bcb4 100644
--- a/src/tools/rustfmt/src/parse/session.rs
+++ b/src/tools/rustfmt/src/parse/session.rs
@@ -433,7 +433,7 @@ mod tests {
                 Some(ignore_list),
             );
             let span = MultiSpan::from_span(mk_sp(BytePos(0), BytePos(1)));
-            let non_fatal_diagnostic = build_diagnostic(DiagnosticLevel::Warning, Some(span));
+            let non_fatal_diagnostic = build_diagnostic(DiagnosticLevel::Warning(None), Some(span));
             emitter.emit_diagnostic(&non_fatal_diagnostic);
             assert_eq!(num_emitted_errors.load(Ordering::Acquire), 0);
             assert_eq!(can_reset_errors.load(Ordering::Acquire), true);
@@ -457,7 +457,7 @@ mod tests {
                 None,
             );
             let span = MultiSpan::from_span(mk_sp(BytePos(0), BytePos(1)));
-            let non_fatal_diagnostic = build_diagnostic(DiagnosticLevel::Warning, Some(span));
+            let non_fatal_diagnostic = build_diagnostic(DiagnosticLevel::Warning(None), Some(span));
             emitter.emit_diagnostic(&non_fatal_diagnostic);
             assert_eq!(num_emitted_errors.load(Ordering::Acquire), 1);
             assert_eq!(can_reset_errors.load(Ordering::Acquire), false);
@@ -494,8 +494,8 @@ mod tests {
             );
             let bar_span = MultiSpan::from_span(mk_sp(BytePos(0), BytePos(1)));
             let foo_span = MultiSpan::from_span(mk_sp(BytePos(21), BytePos(22)));
-            let bar_diagnostic = build_diagnostic(DiagnosticLevel::Warning, Some(bar_span));
-            let foo_diagnostic = build_diagnostic(DiagnosticLevel::Warning, Some(foo_span));
+            let bar_diagnostic = build_diagnostic(DiagnosticLevel::Warning(None), Some(bar_span));
+            let foo_diagnostic = build_diagnostic(DiagnosticLevel::Warning(None), Some(foo_span));
             let fatal_diagnostic = build_diagnostic(DiagnosticLevel::Fatal, None);
             emitter.emit_diagnostic(&bar_diagnostic);
             emitter.emit_diagnostic(&foo_diagnostic);