diff --git a/compiler/rustc_ast/src/attr/version.rs b/compiler/rustc_ast/src/attr/version.rs index 506a7a291813c..62755a2456ae1 100644 --- a/compiler/rustc_ast/src/attr/version.rs +++ b/compiler/rustc_ast/src/attr/version.rs @@ -24,14 +24,24 @@ impl RustcVersion { } }) } + + /// Parse a [`RustcVersion`] with an optional patch version, ignoring suffixes such as `-dev` or `-nightly`. fn parse_str(value: &str) -> Option { - // Ignore any suffixes such as "-dev" or "-nightly". let mut components = value.split('-').next().unwrap().splitn(3, '.'); let major = components.next()?.parse().ok()?; let minor = components.next()?.parse().ok()?; let patch = components.next().unwrap_or("0").parse().ok()?; Some(RustcVersion { major, minor, patch }) } + + /// Parse a [`RustcVersion`] which is exactly `..`, with no suffix. + pub fn parse_str_strict(value: &str) -> Option { + let mut components = value.splitn(3, '.'); + let major = components.next()?.parse().ok()?; + let minor = components.next()?.parse().ok()?; + let patch = components.next()?.parse().ok()?; + Some(RustcVersion { major, minor, patch }) + } } static CURRENT_OVERRIDABLE: OnceLock = OnceLock::new(); diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs index 2fbb90b770261..6f1aacc19246b 100644 --- a/compiler/rustc_errors/src/diagnostic.rs +++ b/compiler/rustc_errors/src/diagnostic.rs @@ -7,6 +7,7 @@ use std::panic; use std::path::PathBuf; use std::thread::panicking; +use rustc_ast::attr::version::RustcVersion; use rustc_data_structures::sync::{DynSend, DynSync}; use rustc_error_messages::{DiagArgMap, DiagArgName, DiagArgValue, IntoDiagArg}; use rustc_lint_defs::{Applicability, LintExpectationId}; @@ -256,6 +257,8 @@ pub struct DiagInner { /// With `-Ztrack_diagnostics` enabled, /// we print where in rustc this error was emitted. pub emitted_at: DiagLocation, + /// Used to avoid lints which would affect MSRV + pub rust_version: Option, } impl DiagInner { @@ -279,6 +282,7 @@ impl DiagInner { is_lint: None, long_ty_path: None, emitted_at: DiagLocation::caller(), + rust_version: None, } } @@ -372,6 +376,7 @@ impl DiagInner { // omit self.sort_span &self.is_lint, // omit self.emitted_at + // omit self.rust_version ) } } @@ -1164,6 +1169,13 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> { self } } + with_fn! { with_rust_version, + /// Add a minimum Rust version. + pub fn rust_version(&mut self, major: u16, minor: u16, patch: u16) -> &mut Self { + self.rust_version = Some(RustcVersion { major, minor, patch }); + self + } } + with_fn! { with_lint_id, /// Add an argument. pub fn lint_id( diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs index 55f10867c9321..3177278906bb4 100644 --- a/compiler/rustc_errors/src/lib.rs +++ b/compiler/rustc_errors/src/lib.rs @@ -45,6 +45,7 @@ pub use diagnostic_impls::{ }; pub use emitter::ColorConfig; use emitter::{DynEmitter, Emitter}; +use rustc_ast::attr::version::RustcVersion; use rustc_data_structures::AtomicRef; use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet}; use rustc_data_structures::stable_hash::StableHasher; @@ -363,6 +364,9 @@ struct DiagCtxtInner { /// The file where the ICE information is stored. This allows delayed_span_bug backtraces to be /// stored along side the main panic backtrace. ice_file: Option, + + /// Controlled by `-Z lint-rust-version`; this allows avoiding emitting lints which would raise MSRV. + msrv: Option, } /// A key denoting where from a diagnostic was stashed. @@ -477,6 +481,11 @@ impl DiagCtxt { self } + pub fn with_msrv(mut self, msrv: RustcVersion) -> Self { + self.inner.get_mut().msrv = Some(msrv); + self + } + pub fn new(emitter: Box) -> Self { Self { inner: Lock::new(DiagCtxtInner::new(emitter)) } } @@ -524,6 +533,7 @@ impl DiagCtxt { future_breakage_diagnostics, fulfilled_expectations, ice_file: _, + msrv: _, } = inner.deref_mut(); // For the `Vec`s and `HashMap`s, we overwrite with an empty container to free the @@ -1170,6 +1180,7 @@ impl DiagCtxtInner { future_breakage_diagnostics: Vec::new(), fulfilled_expectations: Default::default(), ice_file: None, + msrv: None, } } @@ -1284,6 +1295,12 @@ impl DiagCtxtInner { } } + if let (Some(msrv), Some(diag_msrv)) = (self.msrv, diagnostic.rust_version) + && diag_msrv > msrv + { + return None; + } + TRACK_DIAGNOSTIC(diagnostic, &mut |mut diagnostic| { if let Some(code) = diagnostic.code { self.emitted_diagnostic_codes.insert(code); diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs index cdff1280d69f9..748cd19614146 100644 --- a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs +++ b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs @@ -55,6 +55,7 @@ where message: None, code: None, used_fields: HashSet::new(), + msrv: None, }; f(builder, variant) }); @@ -87,6 +88,10 @@ pub(crate) struct DiagnosticDeriveVariantBuilder { /// multiple specifications. pub code: SpannedOption<()>, + /// MSRV is an optional part of the struct attribute - this is only set to detect + /// multiple specifications. + pub msrv: SpannedOption<()>, + pub used_fields: HashSet, } @@ -233,9 +238,29 @@ impl DiagnosticDeriveVariantBuilder { diag.code(#arg_value); }); } + "msrv" => { + let arg_span = arg_name.span().unwrap(); + self.msrv.set_once((), arg_span); + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(arg_value), + .. + }) = arg_value + && let arg_value = arg_value.value() + && let mut components = arg_value.splitn(3, '.') + && let Some(major) = components.next().and_then(|c| c.parse::().ok()) + && let Some(minor) = components.next().and_then(|c| c.parse::().ok()) + && let Some(patch) = components.next().and_then(|c| c.parse::().ok()) + { + tokens.extend(quote! { + diag.rust_version(#major, #minor, #patch); + }); + } else { + span_err(arg_span, "expected a string containing a rust version (xx.yy.zz)").emit(); + } + } _ => { span_err(arg_name.span().unwrap(), "unknown argument") - .note("only the `code` parameter is valid after the message") + .note("only the `code` or `msrv` parameters are valid after the message") .emit(); } } diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 1b3217ed0a030..e4e6daf703d49 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -3047,6 +3047,7 @@ pub(crate) mod dep_tracking { use std::path::PathBuf; use rustc_abi::Align; + use rustc_ast::attr::version::RustcVersion; use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::stable_hash::StableHasher; use rustc_errors::LanguageIdentifier; @@ -3172,7 +3173,8 @@ pub(crate) mod dep_tracking { InliningThreshold, FunctionReturn, Align, - CodegenRetagOptions + CodegenRetagOptions, + RustcVersion, ); impl DepTrackingHash for (T1, T2) diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 6ae0d9721bc00..4ba91285968e8 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::str; use rustc_abi::Align; +use rustc_ast::attr::version::RustcVersion; use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::profiling::TimePassesFormat; use rustc_data_structures::stable_hash::StableHasher; @@ -846,6 +847,7 @@ mod desc { super::mitigation_coverage::DeniedPartialMitigationKind::KINDS; pub(crate) const parse_deny_partial_mitigations: &str = super::mitigation_coverage::DeniedPartialMitigationKind::KINDS; + pub(crate) const parse_rust_version: &str = "a rust version (`xx.yy.zz`)"; } pub mod parse { @@ -2054,6 +2056,18 @@ pub mod parse { }; true } + + pub(crate) fn parse_rust_version(slot: &mut Option, v: Option<&str>) -> bool { + let Some(v) = v else { + return false; + }; + if let Some(version) = RustcVersion::parse_str_strict(v) { + *slot = Some(version); + true + } else { + false + } + } } options! { @@ -2446,6 +2460,8 @@ options! { "lint LLVM IR (default: no)"), lint_mir: bool = (false, parse_bool, [UNTRACKED], "lint MIR before and after each transformation"), + lint_rust_version: Option = (None, parse_rust_version, [TRACKED], + "control the minimum rust version for lints"), llvm_module_flag: Vec<(String, u32, String)> = (Vec::new(), parse_llvm_module_flag, [TRACKED], "a list of module flags to pass to LLVM (space separated)"), llvm_plugins: Vec = (Vec::new(), parse_list, [TRACKED], diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 64818f0114b61..83f9ca11d615d 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -1036,6 +1036,10 @@ pub fn build_session( dcx = dcx.with_ice_file(ice_file); } + if let Some(msrv) = sopts.unstable_opts.lint_rust_version { + dcx = dcx.with_msrv(msrv); + } + let host_triple = TargetTuple::from_tuple(config::host_tuple()); let (host, target_warnings) = Target::search(&host_triple, sopts.sysroot.path(), sopts.unstable_opts.unstable_options) diff --git a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.rs b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.rs index 0956556bf5ef2..b564b07862c87 100644 --- a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.rs +++ b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.rs @@ -753,3 +753,22 @@ struct VariableExists { struct VariableDoesNotExist { sub: String, } + +#[derive(Diagnostic)] +#[diag("this is an example message", msrv = "1.1.1")] +struct Msrv; + +#[derive(Diagnostic)] +#[diag("this is an example message", msrv = 1.1.1)] +//~^ ERROR expected a string containing a rust version +struct MsrvNoString; + +#[derive(Diagnostic)] +#[diag("this is an example message", msrv = "stable")] +//~^ ERROR expected a string containing a rust version +struct MsrvBadVersion; + +#[derive(Diagnostic)] +#[diag("this is an example message", msrv = foo())] +//~^ ERROR expected a string containing a rust version +struct MsrvBadVersionExpr; diff --git a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.stderr b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.stderr index 486ae9c28e84b..40d8d86892b30 100644 --- a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.stderr +++ b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive-inline.stderr @@ -78,7 +78,7 @@ error: derive(Diagnostic): unknown argument LL | #[diag("this is an example message", code = E0123, slug = "foo")] | ^^^^ | - = note: only the `code` parameter is valid after the message + = note: only the `code` or `msrv` parameters are valid after the message error: derive(Diagnostic): `#[suggestion = ...]` is not a valid attribute --> $DIR/diagnostic-derive-inline.rs:94:5 @@ -494,6 +494,24 @@ LL | #[diag("does not exist: {$nosub}")] | = help: Available fields: "sub" +error: derive(Diagnostic): expected a string containing a rust version (xx.yy.zz) + --> $DIR/diagnostic-derive-inline.rs:762:38 + | +LL | #[diag("this is an example message", msrv = 1.1.1)] + | ^^^^ + +error: derive(Diagnostic): expected a string containing a rust version (xx.yy.zz) + --> $DIR/diagnostic-derive-inline.rs:767:38 + | +LL | #[diag("this is an example message", msrv = "stable")] + | ^^^^ + +error: derive(Diagnostic): expected a string containing a rust version (xx.yy.zz) + --> $DIR/diagnostic-derive-inline.rs:772:38 + | +LL | #[diag("this is an example message", msrv = foo())] + | ^^^^ + error: cannot find attribute `nonsense` in this scope --> $DIR/diagnostic-derive-inline.rs:60:3 | @@ -574,5 +592,5 @@ LL | #[multipart_suggestion("with a suggestion")] | = note: `multipart_suggestion` is an attribute that can be used by the derive macro `Subdiagnostic`, you might be missing a `derive` attribute -error: aborting due to 75 previous errors +error: aborting due to 78 previous errors