about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMatthias Krüger <matthias.krueger@famsik.de>2022-06-16 09:10:20 +0200
committerGitHub <noreply@github.com>2022-06-16 09:10:20 +0200
commit95be954af412e4473499a3745c84583c545f15e5 (patch)
tree0f3be1f8fa156aa48515f82ebb01fc0841f85da1
parent1b9daa69640970e7858b7c8e1c9f72c604985e11 (diff)
parent8527a3d36985bed55de1832c3c1f3d470720bb0b (diff)
Rollup merge of #97757 - xFrednet:rfc-2383-expect-with-force-warn, r=wesleywiser,flip1995
Support lint expectations for `--force-warn` lints (RFC 2383)

Rustc has a `--force-warn` flag, which overrides lint level attributes and forces the diagnostics to always be warn. This means, that for lint expectations, the diagnostic can't be suppressed as usual. This also means that the expectation would not be fulfilled, even if a lint had been triggered in the expected scope.

This PR now also tracks the expectation ID in the `ForceWarn` level. I've also made some minor adjustments, to possibly catch more bugs and make the whole implementation more robust.

This will probably conflict with https://github.com/rust-lang/rust/pull/97718. That PR should ideally be reviewed and merged first. The conflict itself will be trivial to fix.

---

r? `@wesleywiser`

cc: `@flip1995` since you've helped with the initial review and also discussed this topic with me. :upside_down_face:

Follow-up of: https://github.com/rust-lang/rust/pull/87835

Issue: https://github.com/rust-lang/rust/issues/85549

Yeah, and that's it.
-rw-r--r--compiler/rustc_codegen_llvm/src/back/write.rs2
-rw-r--r--compiler/rustc_codegen_ssa/src/back/write.rs2
-rw-r--r--compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs2
-rw-r--r--compiler/rustc_errors/src/diagnostic.rs10
-rw-r--r--compiler/rustc_errors/src/json.rs2
-rw-r--r--compiler/rustc_errors/src/lib.rs96
-rw-r--r--compiler/rustc_expand/src/proc_macro_server.rs2
-rw-r--r--compiler/rustc_lint/src/context.rs4
-rw-r--r--compiler/rustc_lint/src/expect.rs16
-rw-r--r--compiler/rustc_lint/src/levels.rs22
-rw-r--r--compiler/rustc_lint_defs/src/lib.rs14
-rw-r--r--compiler/rustc_middle/src/lint.rs16
-rw-r--r--compiler/rustc_session/src/config.rs2
-rw-r--r--compiler/rustc_session/src/session.rs15
-rw-r--r--src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.rs48
-rw-r--r--src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_fulfilled.stderr40
-rw-r--r--src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.rs49
-rw-r--r--src/test/ui/lint/rfc-2383-lint-reason/force_warn_expected_lints_unfulfilled.stderr38
-rw-r--r--src/tools/rustfmt/src/parse/session.rs8
19 files changed, 317 insertions, 71 deletions
diff --git a/compiler/rustc_codegen_llvm/src/back/write.rs b/compiler/rustc_codegen_llvm/src/back/write.rs
index d05d09a11ea..ab8874d796d 100644
--- a/compiler/rustc_codegen_llvm/src/back/write.rs
+++ b/compiler/rustc_codegen_llvm/src/back/write.rs
@@ -336,7 +336,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 00c0ff8bcaf..b8545139cec 100644
--- a/compiler/rustc_errors/src/diagnostic.rs
+++ b/compiler/rustc_errors/src/diagnostic.rs
@@ -209,7 +209,7 @@ impl Diagnostic {
             | Level::Error { .. }
             | Level::FailureNote => true,
 
-            Level::Warning
+            Level::Warning(_)
             | Level::Note
             | Level::OnceNote
             | Level::Help
@@ -222,7 +222,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;
             }
@@ -450,7 +452,7 @@ impl Diagnostic {
     /// Add a warning attached to this diagnostic.
     #[cfg_attr(not(bootstrap), rustc_lint_diagnostics)]
     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
     }
 
@@ -462,7 +464,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 8b6eba122f8..aa4ea82dffb 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -660,6 +660,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`.
     #[cfg_attr(not(bootstrap), rustc_lint_diagnostics)]
     pub fn struct_span_allow(
@@ -693,7 +710,21 @@ impl Handler {
     /// * `is_force_warn` was set in `DiagnosticId::Lint`
     #[cfg_attr(not(bootstrap), rustc_lint_diagnostics)]
     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`.
@@ -864,7 +895,7 @@ impl Handler {
 
     #[cfg_attr(not(bootstrap), rustc_lint_diagnostics)]
     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);
     }
 
     #[cfg_attr(not(bootstrap), rustc_lint_diagnostics)]
@@ -874,7 +905,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>) -> ! {
@@ -928,7 +959,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();
     }
 
@@ -1033,13 +1064,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);
             }
         }
 
@@ -1089,6 +1117,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`
@@ -1105,7 +1142,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()
         {
@@ -1115,22 +1157,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;
         }
 
@@ -1167,7 +1196,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;
             }
         }
@@ -1220,7 +1249,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) => {
@@ -1453,7 +1482,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,
@@ -1476,7 +1508,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 => {
@@ -1495,7 +1527,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",
@@ -1510,7 +1542,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 2084494f771..eeb66f2d738 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 7faac5b5296..4773feded12 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 2186456576a..f1814eebfa6 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -287,6 +287,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,
@@ -298,6 +306,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 a0f71c2b8ba..23db5421910 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);